You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何判断对象是否为方法定义?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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 08:41:25