@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 只是调用底层函数的 .specialclassmethod 不知道如何绑定它) - 所以 Foo.qux.special() 正在调用一个未绑定的函数,因此是 TypeError

有没有办法让Foo.qux.special 知道它是从classmethod 调用的?或者其他解决这个问题的方法?

【问题讨论】:

【参考方案1】:

classmethod 是返回绑定方法的描述符。它不会在此过程中调用您的 __get__ 方法,因为它无法在不破坏描述符协议的某些合同的情况下这样做。 (也就是说,instance 应该是一个实例,而不是一个类。)所以你的 __get__ 方法没有被调用是完全可以预料的。

那么你是如何让它发挥作用的呢?好吧,考虑一下:您希望 some_instance.barSomeClass.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 classmethod 修饰符

python中self和cls @classmethod修饰符

python中self和cls @classmethod修饰符

Python classmethod 修饰符

为啥我的自定义函数调用方法中的 println 语句没有出现在日志中?

为啥在我的自定义 UIView 类中没有调用 init(frame: CGRect)?