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

时间:2018-06-13 22:47:19

标签: python python-decorators class-method

我有一个名为Special的装饰器,它将一个函数转换为自身的两个版本:一个可以直接调用,结果前缀为'regular ',一个可以用{{1}调用并使用.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__的新实例 - 这就是SpecialFoo().bar() Foo().bar.special()工作。

  • Foo.baz只是返回原始Special实例 - 常规和特殊调用都很简单。

  • Foo.qux绑定时不会调用我的__get__

    • 新绑定对象知道在直接调用时将类作为第一个参数传递 - 所以Foo.qux()可以工作。
    • Foo.qux.special只是调用底层函数的.specialclassmethod不知道如何绑定它) - 所以Foo.qux.special()正在调用一个未绑定的函数,因此TypeError

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

2 个答案:

答案 0 :(得分:4)

classmethod是一个返回绑定方法的描述符。它不会在此过程中调用您的__get__方法,因为如果不破坏描述符协议的某些协定,它就无法执行此操作。 (即,instance应该是一个实例,而不是一个类。)因此,未完全预期未调用__get__方法。

那么你如何让它发挥作用?好吧,想一想:您希望some_instance.barSomeClass.bar都返回Special个实例。为了实现这一目标,您只需应用@Special decorator last

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

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

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

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

现在你所有的断言都会过去。

答案 1 :(得分:3)

问题是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.