为什么非函数可调用对象不绑定到 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
是可调用的,但它不会像 bar
和 wrapped_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
。绑定方法是一个函数,其中实例作为第一个参数传递。我不确定这是语言规范的一部分(可能是,但我找不到参考)。
所以您的 bar
和 wrapped_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'>]
假设我们有一个 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
是可调用的,但它不会像 bar
和 wrapped_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
。绑定方法是一个函数,其中实例作为第一个参数传递。我不确定这是语言规范的一部分(可能是,但我找不到参考)。
所以您的 bar
和 wrapped_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'>]