如何利用父类定义的装饰器保存函数每次调用的参数
现有A、B父子类结构,用户可继承任意类定义子类Z,其initialize方法可继承父类实现或自行重写。目前每次调用initialize时需自动保存参数信息——原来的做法是在Z.initialize()里显式调用save_last_arginfo(),现在要改成用A类中定义的装饰器自动实现,有以下几个问题:
- 如何编写该装饰器;
- 装饰器定义及
@语法中self的处理方式; - 如何引用该装饰器(是否需指定
A.decorator); - 使用装饰器后,
inspect.getouterframes的帧层级是否会发生变化。
1. 装饰器的正确写法
不能把装饰器写成普通实例方法——因为装饰类方法时,类的实例还没创建呢。得把它写成类方法,这样既能访问类结构,又能在装饰阶段被正常调用。
装饰器核心逻辑是包装目标方法:先调用原方法,再触发save_last_arginfo保存参数(你也可以根据需求反过来,先保存再调用)。一定要用functools.wraps把原方法的元信息拷贝到包装函数里,不然inspect获取参数时会拿到包装函数的信息,而非原方法的。
2. self的处理方式
装饰器作为类方法,第一个参数是cls(代表类本身);而被装饰的initialize是实例方法,第一个参数是self(代表实例对象)。在装饰器的包装函数里,我们能直接拿到这个self,直接调用它的save_last_arginfo方法即可,不需要额外的特殊处理。
3. 装饰器的引用方式
因为装饰器定义在父类A中,子类B、Z直接使用@A.decorator是最稳妥的方式,不会出现作用域查找问题。如果子类继承了A,理论上用@decorator也能生效,但万一子类中出现同名方法就会出问题,所以推荐写全A.decorator。
4. 帧层级的变化
使用装饰器后,调用栈会多一层包装函数的调用,所以inspect.getouterframes的帧层级确实会变。原代码中用[1]获取外层帧,现在需要改成[2]才能跳过装饰器的包装层,拿到真正调用initialize的原始帧。
import inspect import functools class A(): lastargsinfo = None def initialize(self, *args, **kwargs): pass def save_last_arginfo(self): # 获取initialize方法的参数规范 argspec = inspect.getfullargspec(self.initialize) args1, varargs1, varkw1, defaults1 = argspec[:4] # 装饰器增加了一层调用栈,所以取索引[2]而非原[1] frame = inspect.getouterframes(inspect.currentframe())[2][0] args2, varargs2, keywords2, locals2 = inspect.getargvalues(frame) self.lastargsinfo = [args1, defaults1, locals2] @classmethod def decorator(cls, func): @functools.wraps(func) def wrapper(self, *args, **kwargs): # 先执行原initialize方法,再保存参数信息 result = func(self, *args, **kwargs) self.save_last_arginfo() return result return wrapper class B(A): @A.decorator def initialize(self, a, b=1): pass class Z(B): @A.decorator def initialize(self, a, b=1, c=2, **kwargs): # 移除显式调用,由装饰器自动处理参数保存 return (a + b) * c # 测试代码 myZ = Z() myZ.initialize(0, b=3, c=8, d=12) print(myZ.lastargsinfo) # 输出示例:[['self', 'a', 'b', 'c'], (1, 2), {'self': <__main__.Z object at 0x000001BBBF135340>, 'a': 0, 'b': 3, 'c': 8, 'kwargs': {'d': 12}}]
内容的提问来源于stack exchange,提问作者user3650925

