Python方法如何自动接收'self'作为第一个参数?
下面是一个关于策略模式的例子,使用的是Python语言(这个例子改编自这里)。在这个例子中,另一种策略是一个函数。
class StrategyExample(object):
def __init__(self, strategy=None) :
if strategy:
self.execute = strategy
def execute(*args):
# I know that the first argument for a method
# must be 'self'. This is just for the sake of
# demonstration
print locals()
#alternate strategy is a function
def alt_strategy(*args):
print locals()
这是默认策略的结果。
>>> s0 = StrategyExample()
>>> print s0
<__main__.StrategyExample object at 0x100460d90>
>>> s0.execute()
{'args': (<__main__.StrategyExample object at 0x100460d90>,)}
在上面的例子中,s0.execute
是一个方法(不是普通的函数),所以在 args
中的第一个参数是 self
,这是预期的结果。
这是替代策略的结果。
>>> s1 = StrategyExample(alt_strategy)
>>> s1.execute()
{'args': ()}
在这种情况下,s1.execute
是一个普通的函数,因此它没有接收到 self
。所以 args
是空的。等一下!这是怎么发生的呢?
这两个方法和函数都是以相同的方式调用的。为什么方法会自动将 self
作为第一个参数?而当方法被普通函数替代时,为什么它却没有 self
作为第一个参数呢?
我能找到的唯一不同之处是,当我检查默认策略和替代策略的属性时发现的。
>>> print dir(s0.execute)
['__cmp__', '__func__', '__self__', ...]
>>> print dir(s1.execute)
# does not have __self__ attribute
在 s0.execute
(方法)上有 __self__
属性,而在 s1.execute
(函数)上没有,这是否能解释这种行为的差异?这一切是如何在内部运作的呢?
4 个回答
这个方法其实是一个函数的包装器,它会把实例作为第一个参数来调用这个函数。没错,它有一个叫 __self__
的属性(在 Python 3.x 之前叫 im_self
),这个属性用来记录这个方法是和哪个实例绑定在一起的。不过,单纯给一个普通函数加上这个属性并不能把它变成一个方法;你需要用包装器来实现。这里有个方法(不过你可能想用 types
模块里的 MethodType
来获取构造函数,而不是用 type(some_obj.some_method)
)。
顺便提一下,被包装的函数可以通过方法的 __func__
(或者 im_func
)属性来访问。
你需要把一个没有绑定的方法(也就是带有 self
参数的方法)分配给类,或者把一个绑定的方法分配给对象。
通过描述符机制,你可以自己创建绑定的方法,这也是为什么当你把(未绑定的)函数分配给类时,它能够正常工作的原因:
my_instance = MyClass()
MyClass.my_method = my_method
当你调用 my_instance.my_method()
时,系统会在 my_instance
上查找这个方法,但找不到,所以它会在后面执行这个操作:MyClass.my_method.__get__(my_instance, MyClass)
- 这就是描述符协议。这个操作会返回一个新的方法,它是绑定到 my_instance
的,然后你可以用 ()
操作符来执行它。
这样的话,所有的 MyClass
的实例都可以共享这个方法,不管它们是什么时候创建的。不过,它们可能在你分配这个属性之前就“隐藏”了这个方法。
如果你只想让特定的对象拥有这个方法,可以手动创建一个绑定的方法:
my_instance.my_method = my_method.__get__(my_instance, MyClass)
想了解更多关于描述符的细节(指南),可以查看这里。
你可以在Python的官方文档中找到完整的解释,具体在“用户定义的方法”部分,链接在这里:这里。如果想要更简单易懂的解释,可以查看Python教程中关于方法对象的描述:
如果你还是不明白方法是怎么工作的,可以看看它的实现,这可能会让事情变得更清楚。当你引用一个实例属性,但这个属性不是数据属性时,系统会去查找它的类。如果这个名字对应的是一个有效的类属性,并且是一个函数对象,那么就会创建一个方法对象,这个对象把实例对象和刚找到的函数对象打包在一起。这个就是方法对象。当调用这个方法对象并传入参数时,会根据实例对象和参数列表构建一个新的参数列表,然后用这个新的参数列表来调用函数对象。
简单来说,你的例子中发生的事情是这样的:
- 一个分配给类的函数(比如在类体内声明的方法)就是...一个方法。
- 当你通过类访问这个方法,比如说
StrategyExample.execute
,你得到的是一个“未绑定的方法”:它不知道属于哪个实例,所以如果你想在某个实例上使用它,你需要自己把实例作为第一个参数传进去,比如StrategyExample.execute(s0)
。 - 当你通过实例访问这个方法,比如
self.execute
或s0.execute
,你得到的是一个“绑定的方法”:它知道自己属于哪个对象,并且会把这个实例作为第一个参数传入。
- 当你通过类访问这个方法,比如说
- 而如果你直接把一个函数分配给实例属性,比如
self.execute = strategy
或者s0.execute = strategy
,那么它就是...一个普通的函数(与方法不同,它不经过类)。
要让你的例子在这两种情况下都能正常工作:
你可以把这个函数变成一个“真正的”方法:你可以使用
types.MethodType
来做到这一点:self.execute = types.MethodType(strategy, self, StrategyExample)
(你基本上是在告诉类,当请求这个特定实例的
execute
时,它应该把strategy
变成一个绑定的方法)或者 - 如果你的策略其实不需要访问实例 - 你可以反过来,把原来的
execute
方法变成一个静态方法(让它变回普通函数:这样调用s0.execute()
和StrategyExample.execute()
会完全一样):@staticmethod def execute(*args): print locals()