Python方法如何自动接收'self'作为第一个参数?

14 投票
4 回答
3398 浏览
提问于 2025-04-16 21:15

下面是一个关于策略模式的例子,使用的是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 个回答

6

这个方法其实是一个函数的包装器,它会把实例作为第一个参数来调用这个函数。没错,它有一个叫 __self__ 的属性(在 Python 3.x 之前叫 im_self),这个属性用来记录这个方法是和哪个实例绑定在一起的。不过,单纯给一个普通函数加上这个属性并不能把它变成一个方法;你需要用包装器来实现。这里有个方法(不过你可能想用 types 模块里的 MethodType 来获取构造函数,而不是用 type(some_obj.some_method))。

顺便提一下,被包装的函数可以通过方法的 __func__(或者 im_func)属性来访问。

8

你需要把一个没有绑定的方法(也就是带有 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)

想了解更多关于描述符的细节(指南),可以查看这里

8

你可以在Python的官方文档中找到完整的解释,具体在“用户定义的方法”部分,链接在这里:这里。如果想要更简单易懂的解释,可以查看Python教程中关于方法对象的描述:

如果你还是不明白方法是怎么工作的,可以看看它的实现,这可能会让事情变得更清楚。当你引用一个实例属性,但这个属性不是数据属性时,系统会去查找它的类。如果这个名字对应的是一个有效的类属性,并且是一个函数对象,那么就会创建一个方法对象,这个对象把实例对象和刚找到的函数对象打包在一起。这个就是方法对象。当调用这个方法对象并传入参数时,会根据实例对象和参数列表构建一个新的参数列表,然后用这个新的参数列表来调用函数对象。

简单来说,你的例子中发生的事情是这样的:

  • 一个分配给类的函数(比如在类体内声明的方法)就是...一个方法。
    • 当你通过类访问这个方法,比如说StrategyExample.execute,你得到的是一个“未绑定的方法”:它不知道属于哪个实例,所以如果你想在某个实例上使用它,你需要自己把实例作为第一个参数传进去,比如StrategyExample.execute(s0)
    • 当你通过实例访问这个方法,比如self.executes0.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()
    

撰写回答