4

从Python 文档中“数据模型”一章的“新式类的特殊方法查找”部分(粗体强调我的):

对于新式类,特殊方法的隐式调用只有在对象类型上定义时才能保证正常工作,而不是在对象的实例字典中。这种行为是以下代码引发异常的原因(与旧式类的等效示例不同):

>>> 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

粗体字我听不懂……</p>

4

3 回答 3

2

要了解这里发生了什么,您需要对常规属性查找过程有一个(基本的)了解。举一个典型的面向对象的介绍性编程示例 -fidoDog

class Dog(object):
    pass

fido = Dog()

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

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

fido.walk = walk

并将fido.walk()工作。如果我们没有这样做,它会walktype(fido)(which is Dog) 中查找一个属性,并使用实例作为第一个参数 (ie, 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__assigned to 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__withDog作为它的self参数:

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

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

type(foo).__repr__(foo) 
于 2012-09-01T14:08:25.400 回答
1

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

class MC(type):
  def foo(self):
    print 'foo'

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

C.foo()
C().bar()
C.bar()
于 2012-09-01T12:36:15.187 回答
1

普通属性检索在 的实例属性和属性中obj.attr查找。它在和中定义。attrobjobject.__getattribute__type.__getattribute__

隐式特殊方法调用special(obj, *args, **kwargs)(例如)在(例如)的类属性中hash(1)查找(例如),绕过实例属性而不是执行正常的属性检索,并调用它。理由是实例属性可能需要一个接收器参数(通常称为called ) 这是要调用的类的一个实例(例如函数属性),并提供一个:__special____hash__obj1objobj.__special__objselfobj special(obj, *args, **kwargs)objselftype(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>
  • 第一个表达式从属性中检索vars(type(1))['__hash__'].__get__(1)绑定到的方法。所以类属性需要一个接收器参数,它是一个被调用的实例,我们已经提供了一个:1vars(type(1))['__hash__']type(1)1
  • 第二个表达式vars(int)['__hash__'].__get__(None, int)实例属性中检索函数vars(int)['__hash__']。所以 instance 属性需要一个接收器参数,它是一个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
于 2021-05-12T23:28:29.583 回答