
讲个真事儿。
上个月我写一个内部工具,要给十几个函数统一加日志。心想,用装饰器呗,优雅又省事。于是写了个带日志级别的装饰器,让每个函数自己指定日志等级:
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运行没问题,完美。我心里还美滋滋的。
后来有个函数不需要指定级别,我就想:那简单,把括号去掉用默认值不就完了?于是改成了这样:
@log
def test_func():
return "hello"结果,程序直接报错,说log函数缺少参数。我盯着屏幕看了十分钟,就多了对括号的事,怎么差别这么大?
那一刻我才真正搞明白,装饰器加不加括号,背后是两种完全不同的逻辑。
在聊括号之前,咱们先把装饰器的本质搞清楚。
一句话总结:装饰器就是一个函数,它接收一个函数作为参数,返回一个新的函数。
语法糖@decorator等价于:
@decorator
def func():
pass
# 等价于
def func():
pass
func = decorator(func)看到了吗?@decorator就是**把func传进decorator()这个函数里,然后把返回值重新赋值给func**。
关键点:装饰器在定义函数的时候就已经执行了,不是调用的时候才执行。
搞明白这个,咱们再来看加括号和不加括号到底差在哪。
用一句话说清楚:
@log —— 把log本身当作装饰器,直接传入被装饰的函数@log() —— 先调用log(),把它的返回值当作装饰器你看,加括号是先执行函数调用,把结果当作装饰器。不加括号是直接把函数本身当作装饰器。
这两个返回的东西完全不一样:
log是一个函数,它接收一个函数作为参数log()是调用了log,它返回的是另一个函数形状一样,但一个是原函数,一个是返回值。
最常见的装饰器写法:
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():
@timer() # ❌ 报错
def slow_function():
time.sleep(1)为什么报错?因为timer()先被执行,返回的是wrapper这个函数。然后Python把slow_function传给了wrapper。但wrapper接收的参数格式是(*args, **kwargs),它不接收一个函数作为参数,所以报错。
这就好比你本来应该把门牌号告诉快递员,结果你把门牌号告诉了物业,物业又说“我不收快递”,快递就送不到了。
除了用函数,还可以用类来实现无参装饰器:
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")输出:
进入到 __init__
进入 __call__
myFunction arguments: say hello
结束 __call__注意,__init__只被调用了一次。因为用类作为装饰器,会先创建类实例,然后再去执行这个实例。执行过程等价于:
myDecorate = my_decorate(myFunction)
myDecorate('say', 'hello')被装饰器修饰的函数,最终类型实际上是装饰器本身。
这就是我踩坑的场景。当装饰器需要参数时,你必须多包一层:
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("INFO"),返回decorator函数get_user传进decorator,返回wrapperget_user被替换成wrapper整个过程等价于:
get_user = log("INFO")(get_user)先执行log("INFO"),返回的是decorator函数,再调用返回的函数,参数是get_user函数,返回值最终是wrapper函数。
用类实现带参装饰器,结构会发生较大变化:
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')输出:
进入 __init__()
进入 __call__()
进入 wrapped_f()
装饰器参数: hello world 1
this is myFunction args: ('hello', 'myfunction')
执行完函数myfunction关键区别:装饰器带参数时,不能在__init__中接收函数作为参数,因为__init__的参数是装饰器自己的参数。函数是在__call__方法中接收的。
回到开头那个场景。不带参数的装饰器只能打印固定的字符串:
def decorator(fun):
def wrapper(*args, **kwargs):
print("Method is running")
return fun()
return wrapper但如果我们想打印不同的信息,就需要带参数的装饰器:
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可以看到,在不改变方法调用代码的情况下,实现了在不同方法之前打印特定的信息。
带参数的装饰器,本质上就是在不带参数的装饰器外面再封装一层。这多出来的一层,就是为了接收装饰器自己的参数。
有些场景下,你可能希望同一个装饰器既能不带参数使用,也能带参数使用。比如:
@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 删除。