@classmethod 没有调用我的自定义描述符 __get__

@classmethod not invoking my custom descriptor's __get__

我有一个名为 Special 的装饰器,它可以将一个函数转换为自身的两个版本:一个可以直接调用并在结果前加上 'regular ' 前缀,另一个可以用 [= 调用15=] 并在结果前加上前缀 '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 方法:

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.qux.special 知道它是从 classmethod 调用的吗?或者解决这个问题的其他方法?

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

那么你是如何让它发挥作用的呢?好吧,想想看:你想要 some_instance.barSomeClass.bar 到 return 一个 Special 实例。为了实现这一点,您只需应用 @Special 装饰器 last:

class Foo:
    @Special
    @staticmethod
    def baz():
        return 'baz'

    @Special
    @classmethod
    def qux(cls):
        return 'qux'

这使您可以完全控制 if/when/how 调用修饰函数的描述符协议。现在您只需删除 __get__ 方法中的 if instance is None: 特殊情况,因为它会阻止 class 方法正常工作。 (原因是class方法对象是不可调用的,必须调用描述符协议将class方法对象变成可以调用的函数。)换句话说,Special.__get__ 方法必须无条件调用修饰函数的 __get__ 方法,如下所示:

def __get__(self, instance=None, owner=None):
    return Special(self.func.__get__(instance, owner))

现在你所有的断言都将通过。

问题是 classmethod.__get__ 没有调用包装函数的 __get__——因为这基本上就是 @classmethod 的全部要点。您可以查看 the Descriptors HOWTO, or the actual CPython C source in funcobject.c 中等同于 classmethod 的纯 Python 以获取详细信息,但您会发现确实没有办法解决这个问题。


当然,如果你只是 @Special 一个 @classmethod,而不是反过来,在实例上调用时一切都会正常工作:

class Foo:
    @Special
    @classmethod
    def spam(cls):
        return 'spam'

assert Foo().spam() == 'regular spam'
assert Foo().spam.special() == 'special spam'

… 但是现在在 class:

上调用时它不会工作
assert Foo.spam() == 'regular spam'
assert Foo.spam.special() == 'special spam'

… 因为你试图调用一个 classmethod 对象,它是不可调用的。


但与上一个问题不同的是,这个问题是可以解决的。事实上,失败的唯一原因是这部分:

if instance is None:
    return self

当您尝试将 Special 实例绑定到 class 时,它只是 returns self 而不是绑定其包装对象。这意味着它最终只是 classmethod 对象的包装器,而不是绑定 class 方法的包装器,当然你不能调用 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 .