Python如何知道何时传入`self`?

4
考虑一个简单的例子:
class C:
    @staticmethod
    def my_static_method():
        print("static")

    def my_instance_method(self):
        print("self")

当我调用 C().my_static_method() 时,Python 不会将 C 的实例传递给 my_static_method,而 my_static_method 引用的 descriptor 也不需要 C 的实例。
这是有道理的。
但是当我调用 C().my_instance_method() 时,Python 如何知道将我从中调用 my_instance_methodC 实例作为参数传递进去,而我没有指定任何内容?

3
因为函数对象是描述符。请阅读您自己链接中的工作原理。 - juanpa.arrivillaga
“方法对象”文档(https://docs.python.org/3/tutorial/classes.html#method-objects):“调用`x.f()`与`MyClass.f(x)`完全等价。” - Amadan
2
记录一下,我没有给你的问题投反对票。而且我认为这些反对票有点过分了。这并不是一个糟糕的问题。 - juanpa.arrivillaga
很难写出我想要的答案,因为我无法决定在各种技术方面应该包含多少细节。 - Karl Knechtel
3个回答

2
正如链接所解释的那样,函数对象是描述符!就像staticmethod对象一样。
它们有一个__get__方法,该方法返回一个绑定的方法对象,本质上只是部分地将实例本身作为第一个位置参数应用。考虑以下示例:
>>> def foo(self):
...     return self.bar
...
>>> class Baz:
...     bar = 42
...
>>> baz = Baz()
>>> bound_method = foo.__get__(baz, Baz)
>>> bound_method
<bound method foo of <__main__.Baz object at 0x7ffcd001c7f0>>
>>> method()
42

1
这里的关键是,方法只是类属性的函数。在Python中,实际的魔法发生在属性查找过程中。你提供的链接解释了每当在Python中发生`x.y`时会发生多少事情(记住,所有东西都是对象;包括函数、类、模块和类型(它是自身的一个实例)......)。
这个过程是描述符能够工作的原因,也是为什么我们需要明确的self,以及为什么我们可以用普通函数调用语法调用一个方法(只要我们从类中查找它而不是实例),别名它,用functools.partial模仿方法绑定过程等等有趣的事情。
假设我们有 c = C()。当你执行 c.my_instance_method(暂时不考虑调用),Python 会在 type(c) 中查找 my_instance_method(即在 C 类中),并检查它是否是描述符,以及它是否特别是数据描述符。函数是非数据描述符;即使在类外部,你也可以编写。
>>> def x(spam): return spam
...
>>> x.__get__
<method-wrapper '__get__' of function object at 0x...>

由于优先级规则,只要c没有直接附加相同名称的属性,函数就会在C中找到,并使用其__get__。请注意,所讨论的__get__来自类 - 但它不使用与上面的x.__get__相同的过程。该代码查找类,因为这是一个属性查找的检查位置之一;但是当c.my_instance_method重定向到C.my_instance_method.__get__时,它直接查找那里 - 直接将__get__属性附加到函数上不会改变任何内容(这就是为什么staticmethod被实现为一个类而不是一个函数)。 __get__实现了实际的方法绑定。假设我们在str类中找到了x作为一个方法:
>>> x.__get__('spam', str)
<bound method x of 'spam'>
>>> x.__get__('spam', str)()
'spam'

请记住,虽然所讨论的函数需要三个参数,但我们正在调用__get__本身,作为一个方法 - 因此x以相同的方式绑定到它。同样地,更忠实于实际过程:

>>> type(x).__get__(x, 'spam', str)
<bound method x of 'spam'>
>>> type(x).__get__(x, 'spam', str)()
'spam'

那么,“bound method”究竟是什么呢?

>>> bound = type(x).__get__(x, 'spam', str)
>>> type(bound)
<class 'method'>
>>> bound.__call__
<method-wrapper '__call__' of method object at 0x...>
>>> bound.__func__
<function x at 0x...>
>>> bound.__self__
'spam'
>>> type(bound)(x, 'eggs')
<bound method x of 'eggs'>

基本上就是你所期望的:它是一个可调用对象,它存储和使用原始函数和self值,并在__call__中执行明显的操作。

0
通过将@staticmethod装饰器添加到my_static_method中,您告诉Python不要将C的调用实例传递给函数。因此,您可以将此函数称为C.my_static_method()。
通过调用C(),您创建了C的一个实例。然后,您调用了非静态函数my_instance_method(),Python高兴地将您的新C实例作为第一个参数传递。
当您调用C.my_instance_method()时会发生什么?
反问:您将获得“缺少一个必需的self参数”的异常--因为my_instance_method仅在从实例调用时才起作用,除非您将其装饰为静态。
当然,您仍然可以从实例C().my_static_method()调用静态成员,但是您没有self参数,因此无法访问实例。

2
你似乎没有理解重点。问题是关于Python如何将实例作为参数传递给非静态方法的。 - Karl Knechtel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接