如何在Python运行时为类动态添加方法并保留其类型注解?
我懂你碰到的这个坑了——用setattr给类动态加完方法,调用起来一切正常,但用inspect.signature去查方法签名的时候,之前写的类型注解好像凭空消失了似的。其实这背后的原因很简单,咱们一步步来解决:
首先给你吃个定心丸:你写的类型注解根本没丢!你可以试试直接打印MyClass.dynamic_method.__annotations__,会发现x、y和返回值的注解都好好存在里面。那为什么inspect.signature看起来“没显示”?哦,因为你定义dynamic_method的时候,self参数没有加类型注解,所以signature输出里self就没有类型标注。如果你希望self的类型被识别为MyClass,那咱们手动补上就行。
下面给你几个实用的解决办法:
方法一:手动补全self的类型注解
这是最直接的方式,在把函数绑定到类之前,给函数的__annotations__字典添加self对应的类型:from typing import Callable from inspect import signature class MyClass: pass def dynamic_method(self, x: int, y: str) -> str: return f"{x} - {y}" # 补上self的类型为MyClass dynamic_method.__annotations__['self'] = MyClass setattr(MyClass, 'dynamic_method', dynamic_method) # 现在再看签名就正常了 print(signature(MyClass.dynamic_method)) # 输出: (self: MyClass, x: int, y: str) -> str方法二:用装饰器加方法?记得用@wraps保注解
如果你习惯用装饰器的方式批量给类添加方法,可别忘用functools.wraps把原函数的元数据(包括类型注解)都保留下来,不然装饰器会把原函数的注解覆盖掉:from typing import Callable from inspect import signature from functools import wraps class MyClass: pass def add_to_myclass(func): @wraps(func) # 关键!把原函数的注解、函数名等元数据都复制过来 def wrapped(*args, **kwargs): return func(*args, **kwargs) setattr(MyClass, func.__name__, wrapped) return func # 用装饰器添加方法 @add_to_myclass def dynamic_method(self, x: int, y: str) -> str: return f"{x} - {y}" print(signature(MyClass.dynamic_method)) # 输出同样能看到完整的类型注解方法三:进阶操作——动态构造函数(可选)
要是你需要更灵活地修改函数的签名或者注解,可以用types.FunctionType重新构造函数,把原函数的代码对象、注解、全局变量等都传进去。不过大部分情况下,前面两种方法就足够用了,这个属于进阶场景的备用方案:import types from inspect import signature class MyClass: pass def dynamic_method(self, x: int, y: str) -> str: return f"{x} - {y}" # 重新构造函数,保留原注解并补充self类型 new_func = types.FunctionType( dynamic_method.__code__, dynamic_method.__globals__, name=dynamic_method.__name__, argdefs=dynamic_method.__defaults__, closure=dynamic_method.__closure__ ) new_func.__annotations__ = dynamic_method.__annotations__.copy() new_func.__annotations__['self'] = MyClass setattr(MyClass, 'dynamic_method', new_func) print(signature(MyClass.dynamic_method))
最后再总结一下关键点:
- 直接用
setattr添加函数到类,原函数的类型注解是不会丢失的,只是self参数默认没类型,所以看起来像“消失”了 - 要让
self的类型被识别,手动给函数的__annotations__加个'self'键就行 - 用装饰器的话,务必要加
@wraps(func),不然装饰器会覆盖原函数的注解和元数据
备注:内容来源于stack exchange,提问作者Visva Dheeran Balasubramaniam

