【问题标题】:Decorating a method that's already a classmethod?装饰一个已经是类方法的方法?
【发布时间】:2012-01-23 19:22:25
【问题描述】:

今天早上我遇到了一个有趣的问题。我有一个看起来像这样的基类:

# base.py
class Base(object):

    @classmethod
    def exists(cls, **kwargs):
        # do some work
        pass

还有一个看起来像这样的装饰器模块:

# caching.py

# actual caching decorator
def cached(ttl):
    # complicated

def cached_model(ttl=300):
    def closure(model_class):
        # ...
        # eventually:
        exists_decorator = cached(ttl=ttl)
        model_class.exists = exists_decorator(model_class.exists))

        return model_class
    return closure

这是我的子类模型:

@cached_model(ttl=300)
class Model(Base):
    pass

问题是,当我实际调用 Model.exists 时,我收到了关于参数数量错误的投诉!检查装饰器中的参数显示没有什么奇怪的事情发生 - 参数正是我所期望的,它们与方法签名相匹配。如何向已经使用 classmethod 装饰的方法添加更多装饰器?

并非所有模型都被缓存,但是 exists() 方法作为类方法存在于每个模型上,因此重新排序装饰器不是一种选择:cached_model 可以将类方法添加到 exists(),但是接下来呢使exists() 成为未缓存模型上的类方法?

【问题讨论】:

  • 那么,解决方案是什么?目前还不清楚。如果您将问题保持原样并发布答案会更好。
  • 可以发布问题并自己回答,但请将问题和回答分开。见meta.stackexchange.com/questions/17463/…
  • 我想你在课堂上忘记了@classmethod Base
  • @RikPoggi,提出了明确的问题,修复了缺少的装饰器 - 谢谢

标签: python decorator chaining


【解决方案1】:

在 Python 中,当一个方法被声明时,在一个函数体中,它就像一个函数—— 一旦类被解析并存在,通过“。”检索方法。运算符将该函数动态转换为方法。此转换确实将第一个参数添加到方法(如果它不是静态方法) -

所以:

>>> class A(object):
...    def b(self):
...        pass
... 
>>> A.b is A.b
False

因为每次检索“A”的“b”属性都会产生“方法对象 b”的不同实例

>>> A.b
<unbound method A.b>

如果有的话,可以不用任何转换就可以检索到原始函数“b”

>>> A.__dict__["b"]
<function b at 0xe36230>

对于用@classmethod 修饰的函数,情况也一样,当从 A 中检索到值“class”时,会将其添加到参数列表中。

@classmethod@staticmethod 装饰器会将底层函数包装在与普通实例方法不同的描述符中。一个 classmethod 对象——当它被 classmethod 包装时,函数变成什么是一个描述符对象,它有一个 '__get__' 方法,它将返回一个包装底层函数的函数 - 并在所有之前添加“cls”参数其他的。

@classmethod 的任何进一步装饰器必须“知道”它实际上是在处理描述符对象,而不是函数。 -

>>> class A(object):
...    @classmethod
...    def b(cls):
...       print b
... 
>>> A.__dict__["b"]
<classmethod object at 0xd97a28>

因此,让 @classmethod 装饰器成为最后一个应用于方法的装饰器(堆栈上的第一个装饰器)要容易得多 - 这样其他装饰器就可以处理一个简单的函数(知道"cls" 参数将作为第一个参数插入)。

【讨论】:

  • 没错,但有时您无法控制装饰器的顺序。在我的例子中,该方法始终是一个类方法,但只有一些类具有额外的缓存装饰器——它是特定于子类的。在这种情况下,您需要一种方法来“撤消”第一个 classmethod 装饰器。请参阅我的答案以了解我最终的结果。
【解决方案2】:

感谢jsbueno 提供有关 Python 的信息。我正在根据decorating all methods of a class 的案例寻找这个问题的答案。基于寻找这个问题的答案和 jsbueno 的回应,我能够收集到以下内容:

def for_all_methods(decorator):

    def decorate(cls):

        for attr in dir(cls):
            possible_method = getattr(cls, attr)
            if not callable(possible_method):
                continue

            # staticmethod
            if not hasattr(possible_method, "__self__"):
                raw_function = cls.__dict__[attr].__func__
                decorated_method = decorator(raw_function)
                decorated_method = staticmethod(decorated_method)

            # classmethod
            elif type(possible_method.__self__) == type:
                raw_function = cls.__dict__[attr].__func__
                decorated_method = decorator(raw_function)
                decorated_method = classmethod(decorated_method)

            # instance method
            elif possible_method.__self__ is None:
                decorated_method = decorator(possible_method)

            setattr(cls, attr, decorated_method)

        return cls
    return decorate

您可以使用一些冗余和一些变体来减少它。

【讨论】:

    【解决方案3】:

    classmethod 装饰器实际上在调用方法之前添加了一个class 参数,据我所知,在某些情况下,除了将方法绑定到类之外。解决方案是编辑我的班级装饰闭幕:

    def cached_model(ttl=300):
        def closure(model_class):
            # ...
            # eventually:
            exists_decorator = cached(ttl=ttl, cache_key=exists_cache_key)
            model_class.exists = classmethod(exists_decorator(model_class.exists.im_func))
    
            return model_class
        return closure
    

    im_func 属性似乎获得了对原始函数的引用,这使我可以使用缓存装饰器访问并装饰原始函数,然后将整个混乱包裹在 classmethod 调用中。总结,classmethod 装饰不可堆叠,因为参数似乎被注入了。

    【讨论】:

      【解决方案4】:

      只是添加到 Scott Lobdell 出色答案中的一个功能示例......

      messages.py

      from distutils.cmd import Command
      
      import functools
      import unittest
      
      def for_all_methods(decorator):
      
          def decorate(cls):
      
              for attr in cls.__dict__:
                  possible_method = getattr(cls, attr)
                  if not callable(possible_method):
                      continue
      
                  # staticmethod
                  if not hasattr(possible_method, "__self__"):
                      raw_function = cls.__dict__[attr].__func__
                      decorated_method = decorator(raw_function)
                      decorated_method = staticmethod(decorated_method)
      
                  # classmethod
                  if type(possible_method.__self__) == type:
                      raw_function = cls.__dict__[attr].__func__
                      decorated_method = decorator(raw_function)
                      decorated_method = classmethod(decorated_method)
      
      
                  # instance method
                  elif possible_method.__self__ is None:
                      decorated_method = decorator(possible_method)
      
                  setattr(cls, attr, decorated_method)
      
              return cls
      
          return decorate
      
      def add_arguments(func):
          """
          The add_arguments decorator simply add the passed in arguments
          (args and kwargs) the returned error message.
          """    
          @functools.wraps(func)
          def wrapped(self, *args, **kwargs):
              try:
                  message = func(self, *args, **kwargs)
                  message = ''.join([message, 
                                     "[ args:'", str(args), "'] ", 
                                     "[ kwargs:'", str(kwargs), "' ] " 
                                     ])
                  return message
      
              except Exception as e:
                  err_message = ''.join(["errorhandler.messages.MESSAGE: '",
                                         str(func), 
                                         "(", str(args), str(kwargs), ")' ", 
                                         "FAILED FOR UNKNOWN REASON. ",
                                         " [ ORIGINAL ERROR: ", str(e), " ] "
                                         ])
                  return err_message
      
          return wrapped
      
      
      
      @for_all_methods(add_arguments)    
      class MESSAGE(object):
          """
                  log.error(MSG.triggerPhrase(args, kwargs))
      
          """    
          @classmethod
          def TEMPLATE(self, *args, **kwargs):
              message = "This is a template of a pre-digested message."
              return message
      

      用法

      from messages import MESSAGE
      
      if __name__ == '__main__':
          result = MESSAGE.TEMPLATE(1,2,test=3)
          print result
      

      输出

      This is a template of a pre-digested message.[ args:'(1, 2)'] [ kwargs:'{'test': 3}' ] 
      

      【讨论】:

        猜你喜欢
        • 2016-03-17
        • 1970-01-01
        • 2021-10-23
        • 2019-10-02
        • 2015-12-08
        • 1970-01-01
        • 2012-07-29
        • 1970-01-01
        • 2021-12-26
        相关资源
        最近更新 更多