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

Han*_*ave 5 python methods class python-3.x python-internals

假设我们有一个要进行猴子补丁的类以及一些我们想对其进行猴子补丁的可调用对象。

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'>]
Run Code Online (Sandbox Code Playgroud)

即使baz是可调用的,它也不会绑定到Foo()两个函数bar和的实例wrapped_baz。由于Python是鸭子式语言,因此给定可调用类型在对象机制的行为中起着举足轻重的作用,这似乎很奇怪。

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

MSe*_*ert 3

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

当您查找实例或类上的属性时,它将检查类上的属性是否是描述符,即它是否具有__get____set____delete__。如果它是一个描述符,那么属性查找(获取、设置和删除)将通过这些方法。如果您想了解更多有关描述符如何工作的信息,可以查看官方 Python 文档或 StackOverflow 上的其他答案,例如“理解__get__and__set__和 Python 描述符”

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

所以你的barwrapped_baz函数是描述符,但你的Baz类不是。因此bar(和wrapped_baz) 函数将被查找为“绑定方法”,其中实例在调用时隐式传递到参数中。然而,baz实例按原样返回,因此调用时没有隐式参数。

让你的Baz类更像方法

根据您的需要,您可以Baz通过实现以下方法使您的行为像方法一样__get__

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'>]
Run Code Online (Sandbox Code Playgroud)

减少wrapped_baz方法性

或者,如果您不想要Foo(类似于您的Baz课程),则只需将其包装wrapped_bazstaticmethod

# ...

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'>]
Run Code Online (Sandbox Code Playgroud)