在另一个 class 中使用实例方法作为装饰器

Use an instance method as a decorator within another class

我正在尝试创建一个实例化串行对象的 class (MySerial),以便我可以 write/read 到串行设备 (UART)。有一个实例方法是一个装饰器,它包装了一个属于完全不同的 class (App) 的函数。所以装饰器负责写入和读取串行缓冲区。

如果我在 App class 中创建 MySerial 的实例,我将无法使用从 MySerial 创建的装饰器实例方法。 我已经尝试过上述实例方法并使用 class 方法,如 this second answer 中所述,但我确实需要实例化 MySerial,因此使用 __init__.[=20= 创建实例]

如何实现?不可能吗?


class MySerial():
    def __init__(self):
        pass # I have to have an __init__
    def write(self):
        pass # write to buffer
    def read(self):
        pass # read to buffer
    def decorator(self, func):
        def func_wrap(*args, **kwargs):
            self.write(func(*args, **kwars))
            return self.read()
        return func_wrap

class App():
    def __init__(self):
        self.ser = MySerial()

    @self.ser.decorator  # <-- does not work here.
    def myfunc(self):
        # 'yummy_bytes' is written to the serial buffer via 
        # MySerial's decorator method
        return 'yummy_bytes'

if __name__ == '__main__':
    app = App()

您可以使用 staticmethod 来包裹 decoratordecorator 的内部 func_wrap 函数在其签名中包含一个附加参数:clscls 可以用来访问 App 实例的 ser 属性,然后可以从 [=21] 调用所需的方法 writeread =].另外,请注意,在您的声明中, MySerial.write 不接受任何参数,但会传递包装函数的结果。下面的代码使用 *args 来防止 TypeError 否则会被引发:

class MySerial():
   def __init__(self):
     pass # I have to have an __init__
   def write(self, *args):
     pass # write to buffer
   def read(self):
     pass # read to buffer
   @staticmethod
   def decorator(func):
     def func_wrap(cls, *args, **kwargs):
        cls.ser.write(func(cls, *args, **kwargs))
        return cls.ser.read()
     return func_wrap

class App():
  def __init__(self):
     self.ser = MySerial()
  @MySerial.decorator 
  def myfunc(self):
    # 'yummy_bytes' is written to the serial buffer via 
    # MySerial's decorator method
    return 'yummy_bytes'

App().myfunc()

这不起作用的原因是因为您在 class 正文中引用 self,但未定义它。这里有两个解决方案。

将串行对象存储为 class 属性

如果您将 MySerial 实例存储为 class 属性,那么它仍然可以在 class 正文中访问:

class App():
    ser = MySerial()

    @ser.decorator
    def myfunc(self):
        return 'yummy_bytes'

在每个实例化时装饰

或者如果每个 App 实例都需要一个不同的 MySerial 实例,那么您将需要等待创建实例以定义 实例 属性my_func。这意味着该函数在每个实例创建时都被动态装饰,在这种情况下,必须将 @ 装饰器语法替换为函数调用。

class App():
    def __init__(self):
        self.ser = MySerial()
        self.my_func = self.ser.decorator(self.myfunc)

    def myfunc(self):
        return 'yummy_bytes'

此解决方案概括为装饰多个方法或有条件地停用序列化,例如在测试环境中。

import env

class App():
    def __init__(self):
        self.ser = MySerial()

        to_decorate = [] if env.test else ['myfunc']

        for fn_name in to_decorate:
            fn = getattr(self, fn_name)
            setattr(self, fn_name, self.ser.decorator(fn))

有很多隐藏的陷阱使这个设计具有风险,但它是一个很好的学习示例。

首先,装饰时对 'self' 的调用失败,因为在该范围内没有 self。它只存在于方法内部。现在简单的已经不在了...

myfunc 是 App class 的一个属性。当你创建 App 的一个实例时,它总是被调用的一个函数。即使它变得方法化,也只会发生一次。

a1 = App()
a2 = App()
assert a1.myfunc.__func__ is a2.myfunc.__func__
assert id(a1.myfunc) is id(a2.myfunc)  # Methods have some weirdness that means that won't equate but id's show they are the same 

这就是为什么需要 self 来为实例获取唯一的命名空间。这也是为什么您无法通过这种方式获得实例独有的装饰器的原因。 另一种思考方式是必须先定义 Class 才能生成实例。因此,您不能在 Class.

的定义中使用实例

解决方案

装饰器需要以不存储任何实例属性的方式编写。它将改为访问 App 实例属性。

class MySerial():
    def __init__(self):
        pass # Possibly don't need to have an __init__
    def write(self, serial_config):
        pass # write to buffer
    def read(self, serial_config):
        pass # read to buffer
    def decorator(self, func):
        def func_wrap(self_app: App, *args, **kwargs):
            self.write(func(self_app, *args, **kwars), self_app.serial_config)
            return self.read(self_app.serial_config)
        return func_wrap

ser = MySerial()

class App():
    def __init__(self, serial_config):
        self.serial_config = serial_config  # This is the instance data for     MySerial

    @ser.decorator
    def myfunc(self):
        # 'yummy_bytes' is written to the serial buffer via 
        # MySerial's decorator method
        return 'yummy_bytes'

if __name__ == '__main__':
    app = App()

现在我假设 MySerial 将有一个唯一的文件、端口或每个 App 实例。这就是 serial_config 中要记录的内容。如果流正在打开关闭,这可能不太优雅,但您应该能够针对您的具体应用改进它。