代码之家  ›  专栏  ›  技术社区  ›  Idan Arye

@ClassMethod没有调用自定义描述符的__

  •  3
  • Idan Arye  · 技术社区  · 7 年前

    我有个装修师 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)
    

    它可以与常规方法和静态方法一起工作,但是 特殊的 不适用于类方法:

    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__ ,它绑定底层函数并将绑定方法传递给 特殊的 -这就是为什么 Foo().bar() Foo().bar.special() 工作。

    • Foo.baz 只是把原稿还回去 特殊的 实例-其中常规调用和特殊调用都很简单。

    • Foo.qux 绑定而不调用我的 第二代 .

      • 新绑定对象知道在被直接调用时将类作为第一个参数传递-so Foo.qux() 作品。
      • Foo.qux.special 只是打电话给 特殊的 基本功能的( classmethod 不知道如何绑定)-所以 Foo.qux.special() 正在调用未绑定的函数,因此 TypeError .

    有办法吗 foo.qux.特殊 知道它是从一个 分类法 ?或者其他办法来解决这个问题?

    2 回复  |  直到 7 年前
        1
  •  4
  •   Aran-Fey Kevin    7 年前

    classmethod 是返回绑定方法的描述符。它不会引起你的 __get__ 方法,因为它不能在不破坏描述符协议的某些协定的情况下这样做。(即 instance 应该是一个实例,而不是一个类)。 第二代 完全需要未调用的方法。

    那你是怎么做到的?好吧,想想看:你两个都想要 some_instance.bar SomeClass.bar 归还 Special 实例。为了实现这一点,您只需应用 @Special 装饰者 最后的 :

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

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

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

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

        2
  •  3
  •   abarnert    7 年前

    问题是 classmethod.__get__ 不调用包装函数的 __get__ 因为这基本上就是 @classmethod . 您可以看到纯python等价于 classmethod 在里面 the Descriptors HOWTO ,或中的实际cpython c源 funcobject.c ,了解详细信息,但你会发现这真的没有办法。


    当然如果你只是 @Special @类方法 ,而不是相反,当对实例调用时,一切都将正常工作:

    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'
    

    因为你想打电话给 分类法 对象,它不可调用。


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

    if instance is None:
        return self
    

    当你试图绑定 Special 实例到类,它只返回 self 而不是绑定其包装的对象。也就是说它最终只是一个包裹 分类法 对象而不是绑定类方法的包装器,当然不能调用 分类法 对象。

    但如果你不考虑这一点 分类法 绑定的方式与普通函数相同,后者的作用完全正确,现在一切正常:

    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'
    

    当然,这会导致在Python2.7中包装未绑定方法对象时出现问题,但我认为您的设计已经打破了2.7中的常规方法,希望您在这里只关心3.x。