首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python的装饰器把我绕晕了,原来带参和不带参区别这么大

Python的装饰器把我绕晕了,原来带参和不带参区别这么大

原创
作者头像
风一样的男子
发布2026-09-03 14:19:31
发布2026-09-03 14:19:31
120
举报
文章被收录于专栏:编程教程编程教程

讲个真事儿。

上个月我写一个内部工具,要给十几个函数统一加日志。心想,用装饰器呗,优雅又省事。于是写了个带日志级别的装饰器,让每个函数自己指定日志等级:

代码语言:javascript
复制
def log(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"[{level}] 调用 {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@log("INFO")
def get_user():
    return {"name": "张三"}

@log("DEBUG")
def calculate():
    return 42

运行没问题,完美。我心里还美滋滋的。

后来有个函数不需要指定级别,我就想:那简单,把括号去掉用默认值不就完了?于是改成了这样:

代码语言:javascript
复制
@log
def test_func():
    return "hello"

结果,程序直接报错,说log函数缺少参数。我盯着屏幕看了十分钟,就多了对括号的事,怎么差别这么大?

那一刻我才真正搞明白,装饰器加不加括号,背后是两种完全不同的逻辑。

先搞明白装饰器本质是什么

在聊括号之前,咱们先把装饰器的本质搞清楚。

一句话总结:装饰器就是一个函数,它接收一个函数作为参数,返回一个新的函数

语法糖@decorator等价于:

代码语言:javascript
复制
@decorator
def func():
    pass

# 等价于
def func():
    pass
func = decorator(func)

看到了吗?@decorator就是**把func传进decorator()这个函数里,然后把返回值重新赋值给func**。

关键点:装饰器在定义函数的时候就已经执行了,不是调用的时候才执行

搞明白这个,咱们再来看加括号和不加括号到底差在哪。

加括号 vs 不加括号,到底差在哪儿

用一句话说清楚:

  • @log —— 把log本身当作装饰器,直接传入被装饰的函数
  • @log() —— 先调用log(),把它的返回值当作装饰器

你看,加括号是先执行函数调用,把结果当作装饰器。不加括号是直接把函数本身当作装饰器。

这两个返回的东西完全不一样:

  • log是一个函数,它接收一个函数作为参数
  • log()是调用了log,它返回的是另一个函数

形状一样,但一个是原函数,一个是返回值。

场景一:无参装饰器——千万别加括号

最常见的装饰器写法:

代码语言:javascript
复制
def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print(f"耗时: {time.time() - start}")
        return result
    return wrapper

@timer  # ✅ 正确,不加括号
def slow_function():
    time.sleep(1)

如果你手贱写成@timer()

代码语言:javascript
复制
@timer()  # ❌ 报错
def slow_function():
    time.sleep(1)

为什么报错?因为timer()先被执行,返回的是wrapper这个函数。然后Python把slow_function传给了wrapper。但wrapper接收的参数格式是(*args, **kwargs),它不接收一个函数作为参数,所以报错。

这就好比你本来应该把门牌号告诉快递员,结果你把门牌号告诉了物业,物业又说“我不收快递”,快递就送不到了。

无参装饰器的正确写法(用类实现)

除了用函数,还可以用类来实现无参装饰器:

代码语言:javascript
复制
class my_decorate(object):
    def __init__(self, f):
        """装饰器不带参数时,函数作为参数传给构造器"""
        print("进入到 __init__")
        self.f = f
    
    def __call__(self, *args):
        print("进入 __call__")
        self.f(*args)
        print("结束 __call__")

@my_decorate
def myFunction(arg1, arg2):
    print('myFunction arguments:', arg1, arg2)

myFunction("say", "hello")

输出:

代码语言:javascript
复制
进入到 __init__
进入 __call__
myFunction arguments: say hello
结束 __call__

注意,__init__只被调用了一次。因为用类作为装饰器,会先创建类实例,然后再去执行这个实例。执行过程等价于:

代码语言:javascript
复制
myDecorate = my_decorate(myFunction)
myDecorate('say', 'hello')

被装饰器修饰的函数,最终类型实际上是装饰器本身。

场景二:带参装饰器——必须加括号

这就是我踩坑的场景。当装饰器需要参数时,你必须多包一层:

代码语言:javascript
复制
def log(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"[{level}] 调用 {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

# 使用
@log("INFO")  # ✅ 加括号,传参数
def get_user():
    return {"name": "张三"}

执行顺序是这样的:

  1. 先执行log("INFO"),返回decorator函数
  2. Python把get_user传进decorator,返回wrapper
  3. 最终get_user被替换成wrapper

整个过程等价于:

代码语言:javascript
复制
get_user = log("INFO")(get_user)

先执行log("INFO"),返回的是decorator函数,再调用返回的函数,参数是get_user函数,返回值最终是wrapper函数。

带参装饰器的正确写法(用类实现)

用类实现带参装饰器,结构会发生较大变化:

代码语言:javascript
复制
class my_decorate(object):
    def __init__(self, arg1, arg2, arg3):
        """带参数时,__init__接收的是装饰器的参数,不是函数"""
        print("进入 __init__()")
        self.arg1 = arg1
        self.arg2 = arg2
        self.arg3 = arg3
    
    def __call__(self, f):
        """函数作为参数传给__call__方法"""
        print("进入 __call__()")
        def wrapped_f(*args):
            print("进入 wrapped_f()")
            print("装饰器参数:", self.arg1, self.arg2, self.arg3)
            f(*args)
            print('执行完函数myfunction')
        return wrapped_f

@my_decorate("hello", "world", 1)
def myFunction(*args):
    if len(args) > 0:
        print('this is myFunction args: %s' % str(args))
    else:
        print('this is myFunctions')

myFunction('hello', 'myfunction')

输出:

代码语言:javascript
复制
进入 __init__()
进入 __call__()
进入 wrapped_f()
装饰器参数: hello world 1
this is myFunction args: ('hello', 'myfunction')
执行完函数myfunction

关键区别:装饰器带参数时,不能在__init__中接收函数作为参数,因为__init__的参数是装饰器自己的参数。函数是在__call__方法中接收的。

场景三:带参装饰器,解决实际问题

回到开头那个场景。不带参数的装饰器只能打印固定的字符串:

代码语言:javascript
复制
def decorator(fun):
    def wrapper(*args, **kwargs):
        print("Method is running")
        return fun()
    return wrapper

但如果我们想打印不同的信息,就需要带参数的装饰器:

代码语言:javascript
复制
def decorator_par(name):
    def decorator(fun):
        def wrapper(*args, **kwargs):
            print("Method {} is running".format(name))
            return fun()
        return wrapper
    return decorator

@decorator_par("A")
def A():
    print("I am A")

@decorator_par("B")
def B():
    print("I am B")

A()  # 输出: Method A is running \n I am A
B()  # 输出: Method B is running \n I am B

可以看到,在不改变方法调用代码的情况下,实现了在不同方法之前打印特定的信息。

带参数的装饰器,本质上就是在不带参数的装饰器外面再封装一层。这多出来的一层,就是为了接收装饰器自己的参数。

场景四:装饰器既要带参又要无参——能兼容吗?

有些场景下,你可能希望同一个装饰器既能不带参数使用,也能带参数使用。比如:

代码语言:javascript
复制
@logged
def f1():
    pass

@logged(level=logging.CRITICAL)
def f2():
    pass

这时候就需要在装饰器内部做判断。但说实话,这种写法比较复杂,而且容易引入歧义。一般情况下,建议明确区分:需要参数就用带参版本,不需要就用无参版本,不要试图让一个装饰器同时兼容两种用法。

总结

对比项

无参装饰器 @log

带参装饰器 @log("INFO")

嵌套层数

2层(decorator + wrapper)

3层(outer + decorator + wrapper)

执行时机

定义时直接装饰

先执行外层函数获取装饰器,再装饰

适用场景

功能固定,不需要额外配置

需要根据不同情况调整行为

记住最核心的一句话:

  • @log —— 把log本身当作装饰器
  • @log() —— 先调用log(),把返回值当作装饰器

加不加括号,决定了Python把当作装饰器。这个区别,搞明白了就不会再踩坑了。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 先搞明白装饰器本质是什么
  • 加括号 vs 不加括号,到底差在哪儿
  • 场景一:无参装饰器——千万别加括号
    • 无参装饰器的正确写法(用类实现)
  • 场景二:带参装饰器——必须加括号
    • 带参装饰器的正确写法(用类实现)
  • 场景三:带参装饰器,解决实际问题
  • 场景四:装饰器既要带参又要无参——能兼容吗?
  • 总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档