【问题标题】:Why do we bypass instance attributes during implicit lookup of special methods?为什么我们在隐式查找特殊方法时会绕过实例属性?
【发布时间】:2012-08-26 23:56:22
【问题描述】:

来自 Python 文档中“数据模型”一章的‘Special method lookup for new-style classes’ section(我的粗体强调):

对于新式类,特殊方法的隐式调用只有在定义于 对象的类型,不在对象的实例字典中。这种行为是以下代码引发的原因 一个例外(与旧式类的等效示例不同):

>>> class C(object):
...     pass
...
>>> c = C()
>>> c.__len__ = lambda: 5
>>> len(c)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'C' has no len()

这种行为背后的基本原理在于许多特殊方法,例如__hash__()__repr__() 由所有对象实现,包括类型对象。如果这些方法的隐式查找 使用传统的查找过程,在类型对象本身上调用它们会失败:

>>> 1 .__hash__() == hash(1)
True
>>> int.__hash__() == hash(int)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: descriptor ’__hash__’ of ’int’ object needs an argument

以这种方式错误地尝试调用类的未绑定方法有时被称为“元类” 混淆”,并通过在查找特殊方法时绕过实例来避免:

>>> type(1).__hash__(1) == hash(1)
True
>>> type(int).__hash__(int) == hash(int)
True

粗体字我听不懂……

【问题讨论】:

    标签: python function methods method-call


    【解决方案1】:

    要了解这里发生了什么,您需要对常规属性查找过程有一个(基本的)了解。举一个典型的面向对象编程入门示例——fido 是一个Dog

    class Dog(object):
        pass
    
    fido = Dog()
    

    如果我们说fido.walk(),Python 做的第一件事就是在fido 中寻找一个名为walk 的函数(作为fido.__dict__ 中的一个条目)并在不带参数的情况下调用它——所以,一个已经定义如下:

    def walk():
       print "Yay! Walking! My favourite thing!"
    
    fido.walk = walk
    

    fido.walk() 将起作用。如果我们没有这样做,它将在type(fido)(即Dog)中查找属性walk,并将实例作为第一个参数(即self)调用它——由我们在 Python 中定义方法的常用方式:

    class Dog:
        def walk(self):
             print "Yay! Walking! My favourite thing!"
    

    现在,当您调用 repr(fido) 时,它最终会调用特殊方法 __repr__。它可能(不好,但说明性地)定义如下:

    class Dog:
        def __repr__(self):
              return 'Dog()'
    

    但是,粗体字表示这样做也很有意义:

     repr(Dog)
    

    在我刚刚描述的查找过程中,它首先查找的是一个名为 __repr__ 的方法,它分配给了 Dog... 嘿,看,有一个,因为我们只是很差地但说明性地定义了它。所以,Python 调用:

    Dog.__repr__()
    

    它在我们的脸上爆炸:

    >>> Dog.__repr__()
    Traceback (most recent call last):
      File "<pyshell#38>", line 1, in <module>
        Dog.__repr__()
    TypeError: __repr__() takes exactly 1 argument (0 given)
    

    因为__repr__() 期望将Dog 实例作为self 参数传递给它。我们可以这样做以使其工作:

    class Dog:
        def __repr__(self=None):
           if self is None:
               # return repr of Dog
           # return repr of self
    

    但是,我们需要这样做每次我们编写一个自定义的__repr__ 函数。它需要知道如何找到类的__repr__ 是一个问题,但问题不大——它可以委托给Dog 自己的类(type(Dog))并调用它的__repr__ Dog 作为它的self-argument:

     if self is None:
       return type(Dog).__repr__(Dog)
    

    但首先,如果类名在未来发生变化,这将中断,因为我们需要在同一行中提到它两次。但更大的问题是,这基本上是样板文件:99% 的实现将只是委托链上,或者忘记委托,因此是错误的。因此,Python 采用了那些段落中描述的方法 - repr(foo) 跳过查找附加到 foo__repr__,并直接转到:

    type(foo).__repr__(foo) 
    

    【讨论】:

    • 好!但是如何理解1 .__hash__() 可以而int.__hash__() 不行呢?我认为1int 都是它们类型的实例(1 来自intint 来自type)。这种不一致让我抓狂...x x
    • @ymfoi 与 Dog.__hash__() 不起作用的原因相同:int.__hash__ 不知道如何散列 int 本身,只知道如何散列 int 实例,它希望给定一个 int 的实例为self。要哈希 int 本身,我们需要 type.__hash__(int)
    • 好吧,奇怪的是1 .__hash__() 确实知道如何做哈希——调用int.__hash__。但是为什么int.__hash__() 不知道它可以使用type.__hash__(int)
    • @ymfoi 因为1 .__hash__ 的查找规则表明__hash__ 直接在1 上如果存在则优先 - 它不存在,因此 Python 在 @ 上查找 __hash__ 987654380@,确实存在,所以它使用它。对于int.__hash__int 上的那个确实存在,所以它选择那个。
    【解决方案2】:

    您必须记住的是,类是它们的元类的实例。一些操作不仅需要在实例上执行,还需要在类型上执行。如果实例上的方法被运行,那么它将失败,因为实例上的方法(在这种情况下实际上是一个类)需要类的实例而不是元类。

    class MC(type):
      def foo(self):
        print 'foo'
    
    class C(object):
      __metaclass__ = MC
      def bar(self):
        print 'bar'
    
    C.foo()
    C().bar()
    C.bar()
    

    【讨论】:

    • ..hmm..你能用那个 int 和 type(int) 做一个例子吗?
    【解决方案3】:

    普通属性检索obj.attrobjinstance 属性和class 属性中查找attr。它在object.__getattribute__type.__getattribute__ 中定义。

    隐式特殊方法调用special(obj, *args, **kwargs)(如hash(1))在obj(如1)的类属性中查找__special__(如__hash__),绕过实例属性 的obj 而不是执行正常的属性检索obj.__special__,并调用它。理由是obj 的实例属性可能需要一个接收器参数(通常称为self),它是obj 的一个实例要被调用(例如函数属性)而special(obj, *args, **kwargs)不提供一个,与obj 的类属性相反,它可能需要一个接收器参数(通常称为self),它是type(obj) 类的实例要调用(例如函数属性)和special(obj, *args, **kwargs) 提供一个:obj

    示例

    特殊方法__hash__ 采用单个参数。比较这两个表达式:

    >>> 1 .__hash__
    <method-wrapper '__hash__' of int object at 0x103c1f930>
    >>> int.__hash__
    <slot wrapper '__hash__' of 'int' objects>
    
    • 第一个表达式从 class 属性 vars(type(1))['__hash__'] 检索绑定到 1 的方法 vars(type(1))['__hash__'].__get__(1)。因此,类属性需要一个接收器参数,它是要调用的type(1) 的一个实例,我们已经提供了一个:1
    • 第二个表达式从 instance 属性 vars(int)['__hash__'] 中检索函数 vars(int)['__hash__'].__get__(None, int)。所以实例属性需要一个接收器参数,它是一个int 的实例,我们还没有提供。
    >>> 1 .__hash__()
    1
    >>> int.__hash__(1)
    1
    

    由于内置函数hash接受单个参数,hash(1)可以提供第一次调用(类属性调用)所需的1,而hash(int)不能提供第一次调用所需的1第二次调用(实例属性调用)。因此,hash(obj) 应该绕过实例属性vars(obj)['__hash__'],直接访问类属性vars(type(obj))['__hash__']

    >>> hash(1) == vars(type(1))['__hash__'].__get__(1)()
    True
    >>> hash(int) == vars(type(int))['__hash__'].__get__(int)()
    True
    

    【讨论】:

      猜你喜欢
      • 2023-03-22
      • 1970-01-01
      • 1970-01-01
      • 2016-12-01
      • 2018-08-25
      • 1970-01-01
      • 2017-12-09
      • 2012-10-08
      • 2017-06-25
      相关资源
      最近更新 更多