装饰器和继承:装饰器应用于父级时会发生什么class

Decorator and inheritance: what happens when the decorator is applied to parent class

需要一些帮助来理解装饰器的行为...

这是一些代码:

import random

class MyDecorator(object):
    """ Logger decorator, adds a 'logger' attribute to the class """
    def __init__(self, *args, **kwargs):
      print(random.randint(1, 100), *args, **kwargs)
      
      self.cls = args[0]

    def __call__(self, *args, **kwargs):
      # Do something, inject some attribute
      setattr(self.cls, 'x', 1234)
      
      # Return an instance of the class
      return self.cls(*args[1:], **kwargs)

@MyDecorator
class A:
  def __init__(self):
    print(f'A {self.x}')
  
@MyDecorator
class B:
  """ Parent class """
  def __init__(self):
    print(f'B {self.x}')


class B1(B):
  """ Child class """
  def __init__(self):
    super().__init__()
    
    self.logger.info('Class B1 __init__()')
    

# Here the decorator is applied directly to the class that is going to be instantiated
# Decorator's __init__() receives the class as arg[0], so I can store it and use it when __call__()'ed
a = A()

# Here the decorator is not applied to the class being instantiated, rather to its parent class
# It looks like the decorator's __init__() is being called twice:
# - first time it do recceives the class to which it is applied (in this case, B)
# - second time it receives 3 arguments: a string containing the name of the child class, a tuple containing an instance of the decorator class itself and then some dict containing internal Python controls (I think)
b1 = B1()
    

输出:

82 <class '__main__.A'>
47 <class '__main__.B'>
52 B1 (<__main__.MyDecorator object at 0x7fd4ea9b0860>,) {'__module__': '__main__', '__qualname__': 'B1', '__doc__': ' Child class ', '__init__': <function B1.__init__ at 0x7fd4e9785a60>, '__classcell__': <cell at 0x7fd4eaa0f828: empty>}
A 1234
Traceback (most recent call last):
  File "main.py", line 39, in <module>
    b1 = B1()
  File "main.py", line 12, in __call__
    setattr(self.cls, 'x', 1234)
AttributeError: 'str' object has no attribute 'x'

所以,我的问题是:

  1. 当我将装饰器应用到父 class 而不是它的子时会发生什么?看起来装饰器正在为两者调用 parent/child,并且在每种情况下传递不同的参数集
  2. 在这种情况下,我将如何解决我的“class 通过装饰器实例化”? (在像 A 这样的情况下一切正常,装饰器直接应用于正在实例化的 class)
  3. 我真的应该返回 class 的实例吗?如果我需要链接一些装饰器会发生什么?在这种情况下,我的 __call__ 方法应该是什么样的
@MyDecorator1
@MyDecorator2
class A():

感谢您的帮助!

所以,当你这样做时:

@MyDecorator
class A:
  def __init__(self):
    print(f'A {self.x}')

你可以认为这是

的语法糖
class B:
  def __init__(self):
    print(f'B {self.x}')

B = MyDecorator(B)

问题是,现在 B MyDecorator 的一个实例,即现在 B 不引用到 class。所以,当你继承自 B in:

class B1(B):
    pass

奇怪的事情发生了。相关的奇怪事情——最终导致错误的事情——是 when deciding on the metaclass to use:

3.3.3.3. Determining the appropriate metaclass

  • if no bases and no explicit metaclass are given, then type() is used;

  • if an explicit metaclass is given and it is not an instance of type(), then it is used directly as the metaclass;

  • if an instance of type() is given as the explicit metaclass, or bases are defined, then the most derived metaclass is used.

The most derived metaclass is selected from the explicitly specified metaclass (if any) and the metaclasses (i.e. type(cls)) of all specified base classes. The most derived metaclass is one which is a subtype of all of these candidate metaclasses.

即第三个要点发生了,在这种情况下,type(instance_of_my_decorator) 被用作 metaclass,即 MyDecorator。所以最终,通过不同的途径,B1 现在也指代 MyDecorator 的(另一个)实例,它把字符串 "B1" 作为第一个参数传递给构造函数,因此在MyDecorator.__init__:

 self.cls = args[0]

正在分配该字符串,所以当您到达:

setattr(self.cls, 'x', 1234)

__call__ 中,它失败并出现上述错误。

因为您只想为 class 分配一个属性,最简单的解决方案是不使用基于 class 的方法,而是使用一个函数:

def MyDecorator(cls):
    setattr(self.cls, 'x', 1234)
    return cls # important

当然,这似乎有点矫枉过正。您可能应该像往常一样将属性设置为 class 属性,作为 class 定义语句的一部分,但是如果您坚持使用装饰器,上述方法 工作。当然,这不会被继承。