函数作为类属性如何成为Python中的方法?
>>> class A(object): pass
>>> def func(cls): pass
>>> A.func = func
>>> A.func
<unbound method A.func>
这个赋值是怎么创建一个方法的呢?把赋值和类联系在一起似乎有点不太直观:
- 把函数变成了没有绑定的实例方法
- 把用
classmethod()
包裹的函数变成了类方法(其实,这个还挺直观的) - 把用
staticmethod()
包裹的函数变成了普通函数
对于第一个,我觉得应该有一个 instancemethod()
,而对于最后一个,我觉得根本就不需要包裹函数。我明白这些是在 class
块内部使用的,但为什么它们在外部也适用呢?
更重要的是,把函数赋值到类里面到底是怎么回事?发生了什么神奇的事情,让这三件事得以实现?
这让人更困惑的是:
>>> A.func
<unbound method A.func>
>>> A.__dict__['func']
<function func at 0x...>
但我觉得这和描述符有关,特别是在 获取 属性的时候。我认为这和这里的 设置 属性关系不大。
5 个回答
你需要明白的是,在Python中一切都是对象。这个概念能帮助你更好地理解发生了什么。如果你有一个函数def foo(bar): print bar
,你可以把它赋值给spam = foo
,然后调用spam(1)
,结果当然是1
。
在Python中,对象会把它们的实例属性保存在一个叫__dict__
的字典里,这个字典里有指向其他对象的“指针”。因为函数在Python中也是对象,所以它们可以像普通变量一样被赋值和操作,可以传递给其他函数等等。Python的面向对象实现利用了这一点,把方法当作属性,视为在对象的__dict__
中的函数。
实例方法的第一个参数总是实例对象本身,通常叫self
(当然你也可以叫它this
或者banana
)。当你直接在class
上调用一个方法时,它并没有绑定到任何实例上,所以你需要把一个实例对象作为第一个参数传进去(比如A.func(A())
)。当你调用一个绑定方法(A().func()
)时,方法的第一个参数self
是隐式的,但实际上Python在后台做的事情和直接调用未绑定函数并把实例对象作为第一个参数传入是一样的。
如果你理解了这一点,那么A.func = func
(在后台实际上是A.__dict__["func"] = func
)留下一个未绑定的方法就不奇怪了。
在你的例子中,def func(cls): pass
中的cls
实际上会传入的是类型A
的实例(self
)。当你使用classmethod
或staticmethod
的装饰器时,它们的作用就是在调用函数/方法时获取第一个参数,并把它转化成其他东西,然后再调用这个函数。
classmethod
会获取第一个参数,得到实例的class
对象,并把它作为第一个参数传给函数调用,而staticmethod
则简单地丢掉第一个参数,直接调用函数。
描述符是让普通函数变成绑定方法或非绑定方法的魔法1,当你从一个实例或类中获取它时,它们其实都是函数,只是需要不同的绑定方式。classmethod
和 staticmethod
装饰器实现了其他的绑定策略,而 staticmethod
实际上只是返回原始函数,这和非函数的 callable
对象的行为是一样的。
想了解更多细节,可以查看“用户定义的方法”,但请注意以下几点:
还要注意,这种转换只发生在用户定义的函数上;其他可调用对象(以及所有不可调用对象)在获取时不会发生转换。
所以,如果你想让自己的可调用对象也进行这种转换,可以把它包裹在一个函数里,或者你也可以写一个描述符来实现自己的绑定策略。
下面是 staticmethod
装饰器的实际应用,当它被访问时返回底层函数。
>>> @staticmethod
... def f(): pass
>>> class A(object): pass
>>> A.f = f
>>> A.f
<function f at 0x100479398>
>>> f
<staticmethod object at 0x100492750>
而一个普通对象如果有 __call__
方法,则不会被转换:
>>> class C(object):
... def __call__(self): pass
>>> c = C()
>>> A.c = c
>>> A.c
<__main__.C object at 0x10048b890>
>>> c
<__main__.C object at 0x10048b890>
1 具体的函数是 func_descr_get
,详细信息可以查看Objects/funcobject.c。
你说得对,这和描述符协议有关系。描述符是Python中实现将接收对象作为方法第一个参数的方式。想了解更多关于Python属性查找的细节,可以点击这里。下面的内容展示了当你执行A.func = func; A.func时,发生了什么。
# A.func = func
A.__dict__['func'] = func # This just sets the attribute
# A.func
# The __getattribute__ method of a type object calls the __get__ method with
# None as the first parameter and the type as the second.
A.__dict__['func'].__get__(None, A) # The __get__ method of a function object
# returns an unbound method object if the
# first parameter is None.
a = A()
# a.func()
# The __getattribute__ method of object finds an attribute on the type object
# and calls the __get__ method of it with the instance as its first parameter.
a.__class__.__dict__['func'].__get__(a, a.__class__)
# This returns a bound method object that is actually just a proxy for
# inserting the object as the first parameter to the function call.
所以,把函数查找在类或实例上,才会把它变成一个方法,而不是简单地将它赋值给类的属性。
classmethod
和staticmethod
只是稍微不同的描述符,classmethod返回一个绑定到类型对象的绑定方法对象,而staticmethod则只是返回原始的函数。