@classmethod 没有调用我的自定义描述符的 __get__
Posted
技术标签:
【中文标题】@classmethod 没有调用我的自定义描述符的 __get__【英文标题】:@classmethod not invoking my custom descriptor's __get__ 【发布时间】:2018-11-23 15:12:14 【问题描述】:我有一个名为Special
的装饰器,它将一个函数转换为它自己的两个版本:一个可以直接调用并在结果前面加上'regular '
,另一个可以用.special
调用并在结果前面加上前缀'special '
:
class Special:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner=None):
if instance is None:
return self
return Special(self.func.__get__(instance, owner))
def special(self, *args, **kwargs):
return 'special ' + self.func(*args, **kwargs)
def __call__(self, *args, **kwargs):
return 'regular ' + self.func(*args, **kwargs)
它适用于常规方法和静态方法 - 但 .special
不适用于类方法:
class Foo:
@Special
def bar(self):
return 'bar'
@staticmethod
@Special
def baz():
return 'baz'
@classmethod
@Special
def qux(cls):
return 'qux'
assert Foo().bar() == 'regular bar'
assert Foo().bar.special() == 'special bar'
assert Foo.baz() == 'regular baz'
assert Foo.baz.special() == 'special baz'
assert Foo.qux() == 'regular qux'
assert Foo.qux.special() == 'special qux' # TypeError: qux() missing 1 required positional argument: 'cls'
Foo().bar
正在调用 __get__
,它绑定底层函数并将绑定的方法传递给 Special
的新实例 - 这就是 Foo().bar()
和 Foo().bar.special()
都工作的原因。
Foo.baz
只是返回原始的 Special
实例 - 其中常规和特殊调用都很简单。
Foo.qux
绑定而不调用我的__get__
。
Foo.qux()
有效。
Foo.qux.special
只是调用底层函数的 .special
(classmethod
不知道如何绑定它) - 所以 Foo.qux.special()
正在调用一个未绑定的函数,因此是 TypeError
。
有没有办法让Foo.qux.special
知道它是从classmethod
调用的?或者其他解决这个问题的方法?
【问题讨论】:
【参考方案1】:classmethod
是返回绑定方法的描述符。它不会在此过程中调用您的 __get__
方法,因为它无法在不破坏描述符协议的某些合同的情况下这样做。 (也就是说,instance
应该是一个实例,而不是一个类。)所以你的 __get__
方法没有被调用是完全可以预料的。
那么你是如何让它发挥作用的呢?好吧,考虑一下:您希望 some_instance.bar
和 SomeClass.bar
返回一个 Special
实例。为了实现这一点,您只需应用 @Special
装饰器last:
class Foo:
@Special
@staticmethod
def baz():
return 'baz'
@Special
@classmethod
def qux(cls):
return 'qux'
这使您可以完全控制是否/何时/如何调用装饰函数的描述符协议。现在您只需要删除 __get__
方法中的 if instance is None:
特殊情况,因为它会阻止类方法正常工作。 (原因是classmethod对象是不可调用的;你必须调用描述符协议才能将classmethod对象变成可以调用的函数。)换句话说,Special.__get__
方法必须无条件调用装饰函数的@987654334 @方法,像这样:
def __get__(self, instance=None, owner=None):
return Special(self.func.__get__(instance, owner))
现在你所有的断言都会通过。
【讨论】:
谢谢。我希望能够最后申请@classmethod
(这更有意义),但我想那必须这样做......
这确实通过了他的所有测试,但它会在Foo().qux
上失败。换句话说,一个特殊的类方法不像一个类方法。
@abarnert 不确定你的意思。 Foo().qux()
返回“常规 qux”,Foo().qux.special()
返回“特殊 qux”。什么不起作用?
@Aran-Fey 对不起,弄错了。它适用于Foo().qux
,但适用于it doesn't work for Foo.qux()
。请参阅我的答案以了解原因。
@abarnert 是的,这是因为您没有删除 __get__
中的 if instance is None: return self
。我想我应该尝试澄清这部分答案。【参考方案2】:
问题在于classmethod.__get__
没有调用包装函数的__get__
——因为这基本上就是@classmethod
的全部意义所在。您可以在the Descriptors HOWTO 中查看与classmethod
等效的纯Python,或在funcobject.c
中查看实际的CPython C 源代码,以了解详细信息,但您会发现确实没有办法解决这个问题。
当然,如果您只是 @Special
和 @classmethod
,而不是相反,在实例上调用时一切都会正常工作:
class Foo:
@Special
@classmethod
def spam(cls):
return 'spam'
assert Foo().spam() == 'regular spam'
assert Foo().spam.special() == 'special spam'
…但现在在类上调用时它不起作用:
assert Foo.spam() == 'regular spam'
assert Foo.spam.special() == 'special spam'
...因为您正在尝试调用一个不可调用的 classmethod
对象。
但与前一个不同,这个问题是可以解决的。事实上,这失败的唯一原因是这部分:
if instance is None:
return self
当您尝试将Special
实例绑定到类时,它只会返回self
,而不是绑定其包装对象。这意味着它最终只是 classmethod
对象的包装器,而不是绑定类方法的包装器,当然你不能调用 classmethod
对象。
但是,如果您忽略它,它将让底层 classmethod
以与普通函数相同的方式绑定,这完全正确,现在一切正常:
class Special:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner=None):
return Special(self.func.__get__(instance, owner))
def special(self, *args, **kwargs):
return 'special ' + self.func(*args, **kwargs)
def __call__(self, *args, **kwargs):
return 'regular ' + self.func(*args, **kwargs)
class Foo:
@Special
def bar(self):
return 'bar'
@Special
@staticmethod
def baz():
return 'baz'
@Special
@classmethod
def qux(cls):
return 'qux'
assert Foo().bar() == 'regular bar'
assert Foo().bar.special() == 'special bar'
assert Foo.baz() == 'regular baz'
assert Foo.baz.special() == 'special baz'
assert Foo().baz() == 'regular baz'
assert Foo().baz.special() == 'special baz'
assert Foo.qux() == 'regular qux'
assert Foo.qux.special() == 'special qux'
assert Foo().qux() == 'regular qux'
assert Foo().qux.special() == 'special qux'
当然,这会导致在 Python 2.7 中包装未绑定的方法对象时出现问题,但我认为您的设计对于 2.7 中的普通方法已经失效,希望您在这里只关心 3.x。
【讨论】:
以上是关于@classmethod 没有调用我的自定义描述符的 __get__的主要内容,如果未能解决你的问题,请参考以下文章
python中self和cls @classmethod修饰符
python中self和cls @classmethod修饰符