Python - 元类装饰器 - 如何使用 @classmethod

Posted

技术标签:

【中文标题】Python - 元类装饰器 - 如何使用 @classmethod【英文标题】:Python - Metaclass decorator - How to use @classmethod 【发布时间】:2019-08-08 00:30:54 【问题描述】:

我有以下 Python 元类,它为每个类添加了一个 deco_with_args 装饰器:

def deco_with_args(baz):
    def decorator(func):
        ...
        return func
    return decorator

class Foo(type):
    def __prepare__(name, bases):    
        return 'deco_with_args': deco_with_args

这让我可以像这样使用装饰器:

class Bar(metaclass=Foo):
    @deco_with_args('baz')
    def some_function(self):
        ...

如何使deco_with_args 装饰器的行为类似于@classmethod,以便我可以从decorator 函数中访问Bar 类(或任何其他类)?

我尝试在deco_with_args 函数上使用@classmethod,但没有成功。

【问题讨论】:

您确定要使用元类来定义装饰器吗? @DeepSpace 是有原因的。这是this question的后续行动。 Welp,我刚刚意识到这是不可能的。是时候重写我完成了 60% 的答案了…… 澄清一下:您需要访问testdecorator 中的课程吗? 就在decorator里面。 【参考方案1】:

您可以使用descriptor protocol 来捕获您对方法的调用并将类作为参数添加:

def another_classmethod(baz):

  class decorator:
    def __init__(self, func):
      self.func = func
    def __get__(self, instance, owner):
      def new_call(*args, **kwargs):
        print(baz, self.func(owner, *args, **kwargs))
      return new_call

  return decorator


class Bar():
    @another_classmethod('baz')
    def some_function(cls):
        return f"test cls.__name__"

Bar.some_function()

这打印:

baz test Bar

这里的主要“技巧”是调用Bar.some_function()时的协议是先调用__get__,然后在__get__返回的函数上调用__call__

请注意,__get__ 也会在您执行 Bar.some_function 时被调用,这就是在像 @property 这样的装饰器中使用的。

有一点需要注意,在使用 classmethod 时,您不应该将第一个参数命名为 self,因为它会造成混淆(这会使人们认为第一个参数是实例而不是类对象/类型)。

【讨论】:

我不认为 OP 想把 some_function 变成一个类方法。那很容易。只需拍一下@classmethod 就可以了。为此实现自己的描述符对我来说似乎有点过头了。 这就是问题所说的,我什至没有在字里行间阅读“如何使 deco_with_args 装饰器表现得像@classmethod” 我的观点完全正确。它没有说“我如何让some_function 表现得像一个类方法”。 我不明白你的意思,这里的目标是有一个装饰器,其行为类似于类方法,并在装饰器中添加参数。 这并不过分,因为classmethod 装饰器不带参数,即使它这样做也不会调用类似print(baz, func()) 的东西。【参考方案2】:

@classmethod 对装饰器没有任何用处,因为它不是通过类或实例调用的。 classmethod 是descriptor,描述符只对属性访问有效。换句话说,只有像 @Bar.deco_with_args('baz') 这样调用装饰器才会有帮助。

下一个问题是在执行装饰器时类还不存在。 Python 在创建类之前 执行函数体中的所有代码。所以无法访问deco_with_argsdecorator中的类。

【讨论】:

“不可能”的事情正是元类旨在克服的问题。 @jsbueno 好吧,确实,OP 试图实现的任何目标都可以通过元类来完成。但不可否认的是,没有办法在这个装饰器中访问类。我不确定我是否要扩展我的答案以包含一个通用的“您可以编写一个标记函数的装饰器,然后在元类的__init__ 中对标记的函数进行后处理”黑客可能会用来创建可怕的代码. 10 次中有 9 次,没有真正需要这样的东西。 @Aran-Fey 我实际上想访问该类以标记函数:cls.marked_functions.append(func)。没有课你会怎么做? @DavidCallanan 我只需设置一个属性,如func.marked = True【参考方案3】:

您的问题有两种解释 - 如果您需要 cls 在您的示例中名为 decorator 的函数被调用时可用(即您需要您的修饰方法成为类方法),它本身就足够了转化为类方法:

def deco_with_args(baz):
    def decorator(func):
        ...
        return classmethod(func)
    return decorator

第二个是如果你需要clsdeco_with_args 本身被调用时可用,在创建装饰函数本身时,在类创建时。现在被列为接受的答案列出了一个简单的问题:运行类主体时该类还不存在,因此,在解析类结束时没有办法你可以拥有知道类本身的方法。

但是,与该答案试图暗示的不同,这不是真正的交易。您所要做的就是在类创建过程结束时懒惰地运行您的装饰器代码(需要cls 的代码)。你已经有了一个元类设置,所以这样做几乎是微不足道的,只需在你的装饰器代码周围添加另一个可调用层:

def deco_with_args(baz):
    def outter_decorator(func):
        def decorator(cls):
            # Code that needs cls at class creation time goes here
            ...

            return func
        return decorator
    outter_decorator._deco_with_args = True
    return outter_decorator

class Foo(type):
    def __prepare__(name, bases):    
        return 'deco_with_args': deco_with_args

    def __init__(cls, cls_name, bases, namespace, **kwds):
        for name, method in cls.__dict__.items():
            if getattr(method, '_deco_with_args', False):
                cls.__dict__[name] = method(cls)

        super().__init__(cls_name, bases, namespace, **kwds)

当然,这将在类主体执行完成之后运行,但在运行 class 之后的任何其他 Python 语句之前。 如果您的装饰器会影响在类主体内部执行的其他元素,您需要做的就是将它们包装起来以保证延迟执行。

【讨论】:

还是坏了。 decorator 永远不会被执行。 outter_decorator._deco_with_args = True 需要改为decorator._deco_with_args = Truecls.__dict__[name] = method(cls) 需要改为setattr(cls, name, method(cls)) 这正是我想要的。我是否可以更改已接受的答案? 我认为是这样 - 我什至与原始答案的作者评论了这种可能性,他表示他更愿意保持这种方式。因此,如果这种方法效果更好,那么接受它是有意义的。 我知道我现在可能让你很紧张,但你为什么不修复最后一个错误? 这是关于有两个“名称”变量,对吧?当你发现它时我没有修复它,但是是的,它现在已经修复了。

以上是关于Python - 元类装饰器 - 如何使用 @classmethod的主要内容,如果未能解决你的问题,请参考以下文章

Python之路:描述符,类装饰器,元类

Python 中的纯静态类 - 使用元类、类装饰器还是其他东西?

Python基础(30)——上下文管理,描述符,类装饰器,元类

使用两种不同的装饰器实现来装饰所有类方法的元类

使用装饰器设置类的元类

Ruby 与 Python 元类的类比是啥?