【问题标题】:Why don't non-function callables get bound to class instances?为什么非函数可调用对象不绑定到类实例?
【发布时间】:2020-01-10 08:28:45
【问题描述】:

假设我们有一个我们想要对其进行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 实现的怪癖,还是语言规范的一部分描述了观察到的行为?

【问题讨论】:

    标签: python python-3.x class methods python-internals


    【解决方案1】:

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

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

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

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

    让你的 Baz 类更像方法

    根据您的需要,您可以通过实现__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 类),那么只需将 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'>]
    

    【讨论】:

      猜你喜欢
      • 2012-06-17
      • 1970-01-01
      • 1970-01-01
      • 2022-01-01
      相关资源
      最近更新 更多