如何判断对象是否为方法定义?Python装饰器区分函数与方法的问题
你说得太对了——当装饰器在类定义阶段被应用时,类里的方法还只是普通的function对象,根本没法用inspect.ismethod()直接判断,因为方法的绑定(变成method对象)是在实例化之后才发生的。这确实是个坑,但我们有几种可靠的方案来解决这个问题:
方案一:在调用时动态判断(最可靠)
核心思路是:不在装饰器初始化时判断,而是在方法/函数被实际调用时检查当前是否是绑定方法调用。具体来说,我们可以检查第一个参数是否是实例或类,并且该对象的对应方法底层就是我们装饰的函数。
这里用到inspect模块来辅助判断,同时结合functools.wraps保留原函数的元信息:
import inspect from functools import wraps def create_key(*args, **kwargs): # 简单实现:将参数转为可哈希的缓存键 return (args, frozenset(kwargs.items())) def actual_cache_mechanism(key, f, *args, **kwargs): # 模拟缓存逻辑,用全局字典存储 if not hasattr(actual_cache_mechanism, "cache"): actual_cache_mechanism.cache = {} if key not in actual_cache_mechanism.cache: actual_cache_mechanism.cache[key] = f(*args, **kwargs) return actual_cache_mechanism.cache[key] def cached(f): @wraps(f) def _internal(*args, **kwargs): is_method_call = False if args: first_arg = args[0] # 检查第一个参数是否是实例/类,且该对象拥有当前函数作为方法 if hasattr(first_arg, f.__name__): bound_member = getattr(first_arg, f.__name__) # 验证绑定方法的底层函数就是我们装饰的f if inspect.ismethod(bound_member) and bound_member.__func__ is f: is_method_call = True # 根据是否为方法调用生成不同的缓存键 if is_method_call: key = create_key(*args[1:], **kwargs) else: key = create_key(*args, **kwargs) return actual_cache_mechanism(key, f, *args, **kwargs) return _internal
测试代码
class A: @cached def b(self, something): print(f"执行A.b,参数:{something}") return something * 2 @classmethod @cached def c(cls, something): print(f"执行A.c,参数:{something}") return something * 3 # 普通函数测试 @cached def regular_func(x): print(f"执行普通函数,参数:{x}") return x + 1 # 实例方法:不同实例调用相同参数会复用缓存 a1 = A() print(a1.b(5)) # 打印执行信息,返回10 print(a1.b(5)) # 复用缓存,不打印执行信息 a2 = A() print(a2.b(5)) # 复用缓存,返回10 # 类方法测试 print(A.c(10)) # 打印执行信息,返回30 print(A.c(10)) # 复用缓存 # 普通函数测试 print(regular_func(3)) # 打印执行信息,返回4 print(regular_func(3)) # 复用缓存
这个方案的优势是完全不依赖命名规范(比如不用强制参数叫self/cls),也不依赖函数的命名空间,能准确识别实例方法、类方法,甚至继承来的方法。
方案二:利用__qualname__提前判断(简单但有局限性)
Python中,类内定义的函数的__qualname__会包含类名(比如A.b),而普通函数的__qualname__就是函数名。我们可以利用这一点在装饰器初始化时提前标记:
from functools import wraps def cached(f): # 通过__qualname__判断是否为类内方法(包含.) is_class_method = "." in f.__qualname__ @wraps(f) def _internal(*args, **kwargs): if is_class_method: key = create_key(*args[1:], **kwargs) else: key = create_key(*args, **kwargs) return actual_cache_mechanism(key, f, *args, **kwargs) return _internal
这个方法简单高效,但局限性明显:如果是嵌套函数(比如def outer(): def inner(): ...),inner的__qualname__是outer.inner,会被误判为类方法。所以只适合没有嵌套函数的场景。
方案三:依赖参数命名规范(不够健壮)
如果你的代码严格遵循PEP8规范,实例方法第一个参数叫self,类方法叫cls,可以通过检查函数签名的第一个参数名来判断:
import inspect from functools import wraps def cached(f): sig = inspect.signature(f) params = list(sig.parameters.keys()) # 判断第一个参数是否是约定的self/cls is_method_by_param = len(params) > 0 and params[0] in ("self", "cls") @wraps(f) def _internal(*args, **kwargs): if is_method_by_param: key = create_key(*args[1:], **kwargs) else: key = create_key(*args, **kwargs) return actual_cache_mechanism(key, f, *args, **kwargs) return _internal
这个方法的缺点是依赖命名规范,如果有人把实例参数命名为this或者其他名字,就会失效,所以不推荐在通用场景使用。
总结
如果追求准确性和通用性,方案一是最佳选择;如果你的代码场景简单,没有嵌套函数,方案二足够高效;方案三只适合严格遵循命名规范的封闭项目。
内容的提问来源于stack exchange,提问作者Nadav

