为什么非函数可调用对象不绑定到 class 个实例?

Why don't non-function callables get bound to class instances?

假设我们有一个 class 我们想要 monkeypatch 和一些我们想要 monkeypatch 的可调用对象。

class Foo:
  pass

def bar(*args):
  print(list(map(type, args)))

class Baz:
  def __call__(*args):
    print(list(map(type, args)))
baz = Baz()

def wrapped_baz(*args):
  return baz(*args)

Foo.bar = bar
Foo.baz = baz
Foo.biz = wrapped_baz

Foo().bar()  # [<class '__main__.Foo'>]
Foo().baz()  # [<class '__main__.Baz'>]
Foo().biz()  # [<class '__main__.Baz'>, <class '__main__.Foo'>]

尽管 baz 是可调用的,但它不会像 barwrapped_baz 这两个函数那样绑定到 Foo() 实例。由于 Python 是一种鸭子类型的语言,因此给定可调用对象的类型在对象机制的行为中扮演如此重要的角色似乎很奇怪。

并不是说包装可调用项一定是一种糟糕的方法,还有其他方法可以将可调用项适当地绑定到 Foo 实例吗?这是 CPython 实现的怪癖,还是语言规范的一部分描述了观察到的行为?

差异的原因是函数实现了 descriptor protocol,但您的可调用 class 没有。描述符协议是语言规范的一部分。

当您在实例或 class 上查找属性时,它将检查 class 上的属性是否为描述符,即它是否具有 __get____set__,或 __delete__。如果它是一个描述符,那么属性查找(获取、设置和删除)将通过这些方法。如果您想了解更多关于描述符如何工作的信息,您可以查看官方 Python 文档或 Whosebug 上的其他答案,例如 "Understanding __get__ and __set__ and Python descriptors".

函数有一个 __get__,因此如果你查找它们,它们 return 一个 bound method。绑定方法是一个函数,其中实例作为第一个参数传递。我不确定这是语言规范的一部分(可能是,但我找不到参考)。

所以您的 barwrapped_baz 函数是描述符,但您的 Baz class 不是。因此 bar(和 wrapped_baz)函数将被查找为 "bound method",其中实例在调用时隐式传递到参数中。但是 baz 实例是 return 按原样编辑的,因此调用时没有隐式参数。

让您的 Baz class 更像方法

根据您的需要,您可以通过实施 __get__:

让您的 Baz 像一个方法一样工作
import types

# ...

class Baz:
    def __get__(self, instance, cls):
        """Makes Baz a descriptor and when looked up on an instance returns a
        "bound baz" similar to normal methods."""
        if instance is None:
            return self
        return types.MethodType(self, instance)
    def __call__(*args):
        print(list(map(type, args)))

# ...

Foo().baz()  # [<class '__main__.Baz'>, <class '__main__.Foo'>]

减少 wrapped_baz 方法

或者,如果您不想要 Foo(类似于您的 Baz class),则只需将 wrapped_baz 包装为 staticmethod

# ...

class Baz:
  def __call__(*args):
    print(list(map(type, args)))

baz = Baz()

@staticmethod
def wrapped_baz(*args):
    return baz(*args)

# ...

Foo().biz()  # [<class '__main__.Baz'>]