首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python的装饰器把我坑惨了,原来加不加括号完全是两码事

Python的装饰器把我坑惨了,原来加不加括号完全是两码事

原创
作者头像
风一样的男子
发布2026-08-21 14:42:42
发布2026-08-21 14:42:42
1020
举报
文章被收录于专栏:编程教程编程教程

讲个真事儿,上个月我写一个内部工具,需要给十几个函数统一加日志。

我心想,用装饰器呗,优雅又省事。于是写了个带日志级别的装饰器,打算让每个函数自己指定日志等级。

代码是这样的:

代码语言: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
复制
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. Python先执行log("INFO"),返回decorator函数
  2. 然后把get_user传给decorator,返回wrapper
  3. wrapper替换原来的get_user

所以带参装饰器其实是个"装饰器工厂"——你调用它传入参数,它生产出一个真正的装饰器。

场景三:最让人懵的——带默认参数的装饰器

有时候你想让装饰器既支持无参调用,又支持带参调用,比如:

代码语言:javascript
复制
@log
def func1():
    pass

@log("INFO")
def func2():
    pass

这能做到吗?可以,但写法很巧妙,需要判断第一个参数是不是一个可调用的函数:

代码语言:javascript
复制
def log(arg):
    if callable(arg):
        # 没加括号,arg就是被装饰的函数
        def wrapper(*args, **kwargs):
            print(f"调用 {arg.__name__}")
            return arg(*args, **kwargs)
        return wrapper
    else:
        # 加了括号,arg是传入的级别参数
        def decorator(func):
            def wrapper(*args, **kwargs):
                print(f"[{arg}] 调用 {func.__name__}")
                return func(*args, **kwargs)
            return wrapper
        return decorator

这样两种写法都能工作。callable()判断参数是不是函数。但说实话,这种写法对新手不太友好,容易造成混乱。大多数实际项目中,要么只支持无参,要么只支持带参,很少混用。

一个小实验:看看执行顺序

为了让理解更直观,做个小实验:

代码语言:javascript
复制
def outer(msg):
    print(f"1. outer 被调用,参数: {msg}")
    def decorator(func):
        print(f"2. decorator 被调用,函数: {func.__name__}")
        def wrapper(*args, **kwargs):
            print(f"3. wrapper 被调用,执行 {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

print("开始定义函数...")

@outer("测试")
def test():
    print("4. 函数主体执行")

print("定义完成,现在调用函数...")

test()

猜猜打印顺序:

代码语言:javascript
复制
开始定义函数...
1. outer 被调用,参数: 测试
2. decorator 被调用,函数: test
定义完成,现在调用函数...
3. wrapper 被调用,执行 test
4. 函数主体执行

看到了吗?装饰器在函数定义时就执行了,而不是在调用时

outertest定义的那一刻就运行了,返回decoratordecorator也立刻执行,返回wrapper替换test。等你真正调用test()时,执行的是wrapper里的逻辑。

常见错误TOP3

错误1:忘记return内层函数

代码语言:javascript
复制
def decorator(func):
    func()  # ❌ 没返回wrapper
    # 忘记return了

@decorator
def test():
    print("hello")
# test变成了None,调用报错

错误2:带参装饰器少了一层

代码语言:javascript
复制
def log(level):
    def wrapper(func):
        print(f"[{level}] {func.__name__}")  # 只有两层
        return func

@log("INFO")  # 报错,log("INFO")返回的是wrapper
def test():
    pass
# wrapper接收func,但log("INFO")返回的wrapper被当作装饰器
# 然后Python把test传给wrapper,wrapper返回了func
# 看起来没问题?但wrapper的参数是func,一旦test被调用,实际上调用的是原始test,没被包装

错误3:被装饰函数有参数时wrapper忘记传参

代码语言:javascript
复制
def timer(func):
    def wrapper():  # ❌ 没接收参数
        start = time.time()
        func()  # ❌ 没传参
        print(time.time() - start)

@timer
def add(a, b):
    return a + b

add(1, 2)  # 报错,wrapper不接收参数

正确写法:

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

实战:一个完整的带参装饰器

给你看一个我项目中真实在用的日志装饰器,带参数控制日志级别和是否记录返回值:

代码语言:javascript
复制
import time
from functools import wraps

def log(level="INFO", record_return=False):
    def decorator(func):
        @wraps(func)  # 保留原函数的元信息
        def wrapper(*args, **kwargs):
            print(f"[{level}] 执行 {func.__name__}")
            start = time.time()
            result = func(*args, **kwargs)
            cost = time.time() - start
            if record_return:
                print(f"[{level}] {func.__name__} 返回值: {result}")
            print(f"[{level}] {func.__name__} 耗时: {cost:.3f}s")
            return result
        return wrapper
    return decorator

# 使用
@log("DEBUG", record_return=True)
def calculate(a, b):
    return a + b

calculate(3, 5)
# 输出:
# [DEBUG] 执行 calculate
# [DEBUG] calculate 返回值: 8
# [DEBUG] calculate 耗时: 0.000s

注意到@wraps(func)了吗?这是个好习惯,它能保留原函数的__name____doc__等属性。不然被装饰后,函数名就变成了wrapper,调试时很麻烦。

总结:一句话记住

写法

含义

适用场景

@decorator

func = decorator(func)

无参装饰器

@decorator()

func = decorator()(func)

带参装饰器

@decorator("arg")

func = decorator("arg")(func)

带参装饰器传参

加括号,就是在调用。调用完的结果,才拿去当装饰器。

不加括号,就是把函数本身当装饰器。

就这么简单。记住这条,你就能避开我踩过的那个坑。

最后送你两句话:

无参装饰器不加括号,带参装饰器必须加括号。

如果你不确定,就看看这个装饰器定义的时候需不需要传参数。

那次之后,我每次写带参装饰器都会停顿一下,确认括号的位置对不对。这个习惯帮我省了不少事。

希望你看了这篇文章,能在写装饰器的时候多留个心眼。括号虽小,搞错了,程序可一点都不跟你客气。

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

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

目录
  • 先搞明白装饰器本质是什么
  • 加括号 vs 不加括号,到底差在哪儿
  • 场景一:无参装饰器——千万别加括号
  • 场景二:带参装饰器——必须加括号
  • 场景三:最让人懵的——带默认参数的装饰器
  • 一个小实验:看看执行顺序
  • 常见错误TOP3
    • 错误1:忘记return内层函数
    • 错误2:带参装饰器少了一层
    • 错误3:被装饰函数有参数时wrapper忘记传参
  • 实战:一个完整的带参装饰器
  • 总结:一句话记住
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档