装饰器是 Python 中一个强大且灵活的特性,允许用户在不修改原有函数或类定义的基础上,为其增加额外功能。
今天在尝试自定义 Python 装饰器的时候遇到了一个问题,因为以前一直是使用装饰器,基本没有自定义过装饰器,所以写了一个不是常见的写法(符合装饰器的语法,但是用法上是错误的)
一、Python 装饰器的简单介绍
装饰器本质上是一个可调用对象(通常是函数),它接受一个函数作为输入,并返回一个新的函数作为输出。这个新函数通常会在执行原始函数前后添加额外的操作,从而扩展或改变原始函数的行为。
Python 提供了简洁的语法糖来应用装饰器,即在函数定义之前使用 @my_decorator
的格式。例如:
@my_decorator
def my_function():
pass
这里的 @my_decorator
实际上是一个语法糖,会将 my_function
传递给 my_decorator
函数,并将返回的结果重新绑定到 my_function
上。因此上诉调用等同于:
def my_function():
pass
my_function = my_decorator(my_function)
这里需要强调一下,是将 my_function
传递给 @ 后面的整个部分,可以在看完文章之后再返回来理解一下这句话。
二、不带括号和带括号的 Python 装饰器
然后我就写了一个装饰器,不过我犯了一个错误,现在我把代码整理了一下贴了出来。第一种是通常的写法,第二种是我的写法,不过它的调用会有问题。不过这里目前可以看出区别就是一个不带括号,另一个带括号。
import time
def log1(func):
def wrapper1():
start = time.time()
res = func()
print("exec time: %.2f" % (time.time()-start))
return res
print("log1 wrapper1")
return wrapper1
@log1
def func1():
time.sleep(0.15)
print("call func1")
def log2():
def wrapper2(func):
start = time.time()
res = func()
print("exec time: %.2f" % (time.time()-start))
return res
print("log2 wrapper2")
return wrapper2
@log2() # 不带括号会报错,提示不需要参数,但是接收到了一个参数
def func2():
time.sleep(0.15)
print("call func2")
if __name__ == '__main__':
func1()
# 如果不使用装饰器,那么:
# func1 的调用方式等价于 func1 => log1(func1),这还是一个函数
# func2 的调用方式等价于 func2 => log2()(func2),这不是一个函数,而是一个结果了
如果不使用装饰器语法,而是普通的 Python 代码的方式是这样的(结果同上):
import time
def log1(func):
def wrapper1():
start = time.time()
res = func()
print("exec time: %.2f" % (time.time()-start))
return res
print("log1 wrapper1")
return wrapper1
def func1():
time.sleep(0.15)
print("call func1")
def log2():
def wrapper2(func):
start = time.time()
res = func()
print("exec time: %.2f" % (time.time()-start))
return res
print("log2 wrapper2")
return wrapper2
def func2():
time.sleep(0.15)
print("call func2")
if __name__ == '__main__':
func1 = log1(func1)
func1()
log2()(func2)
对于装饰器用法的代码,虽然没有调用 func2()
,但是它就会直接执行,而且调用 func2()
会报错。
@log2() # 不带括号会报错,提示不需要参数,但是接收到了一个参数
def func2():
time.sleep(0.15)
print("call func2")
不过这个错误反而提醒了我,装饰器的原理应该是:在程序运行时,会将被装饰的函数作为参数传递给装饰器,也就是 @ 后的整个部分。
因此对于 @log1
来说,这就相当于:func1 = log1(func1)
,所以之后执行 func1
就是被装饰后的函数了(可以观察到打印输出的语句)。函数是直接传到了装饰器内部,这样比较容易理解。
对于 @log2()
来说,这就相当于:log2()(func2)
,函数是传到了 log2()
返回的函数中了(这里是 wrapper2
),这样它的结果就不是一个函数了(非 callable),而是一个具体的值了(这里的结果为 None
)。如果我使用 @log2
,那么就是把函数传到 log2
中,但是这个函数是无参数的,如果给它传递参数就会报错了。参数是传递给它返回的函数中,所以装饰器需要加上括号调用,即 @log2()
。不过这里的写法犯了一个错误,因此导致了导致了它直接执行了,因为它返回的不是一个函数,而是它的执行结果了。所以,解决的方式就是在内部再嵌套一层函数,修改之后的代码如下:
def log2():
def wrapper2(func):
def wrapper3():
start = time.time()
res = func()
print("exec time: %.2f" % (time.time()-start))
return res
return wrapper3
print("log2 wrapper2")
return wrapper2
@log2()
def func2():
time.sleep(0.15)
print("call func2")
if __name__ == '__main__':
func2()
# 等价于 func2 = log2()(func2) 这里它还是一个函数(可调用),而不是一个返回值了()
我认为这里最大的问题就是这个装饰器的语法糖实在是太便利的,这样对于使用是非常方便的,但是凡事有好处就有坏处,它反而不利于我们对它的理解了。特别是,我之前有过 Java 的注解使用和学习经验。如果只是简单的使用确实不需要理解它是怎么工作的,但是对于想要深入理解的同学来说,还是需要去了解背后的运行机制。这里要把握的一点就是:在运行时,会将被装饰的函数作为参数传递给 @ 后面这整个部分,如果不带括号就是直接作为参数传输传入,然后返回一个新的函数。如果带括号,就是传递给它的返回值(内层函数,所以内层函数还要再嵌套才行,不然就是直接执行函数了)。所以即使被装饰的函数在内层,它的执行也是没有问题的,但你要明白这个过程!