Skip to content
每天都是好心情

Py装饰器

· 7 min

装饰器的名字很有意思,在某个函数需要使用装饰器的时候头上挂一个@xyz,这个函数就有了头上这个装饰器里预先实现的特殊功能。

想要明白装饰器的实现原理,首先要知道两个概念,函数是Python里的一等公民和装饰器是可以生成函数的函数。

01. 何为一等公民#

一等公民意味着函数可以像其他任何数据类型(如整数、字符串、列表等)一样被处理。 具体来说,函数可以被赋值给变量、作为参数传递给其他函数、作为函数的返回值,甚至可以存储在数据结构中。 这种特性使得 Python 的函数非常灵活和强大。

装饰器的实现就是函数可以作为参数传递给其他函数、作为函数的返回值这个的完整阐述。

02. 一切皆对象#

一切皆对象意味着 Python 中的每一个事物,包括变量、函数、类、模块等,都是对象。 函数当然也不例外,函数内部也有数据和方法,可以在运行时动态修改,改变函数的行为。

函数内部也有数据和方法,看起来可能比较奇怪,通常类实例化后的对象才有数据和方法,函数里为什么也会有数据和方法(函数)呢? 这个奥秘就在一切皆对象里。

Python里的普通变量,比如字符串,整数,字典,或元组,字面意义上代表某个内存里的数据,是一种数据对象。 class定义一个类,实例化后的东西,也叫对象,这个对象不仅仅是数据的容器,更重要的是它具有行为,可以理解成一种以行为为主的对象。 def定义的函数,表面看主要供调用者Call使用,传入一个东西,返回另一个东西,但在Python内部的表示形式并没那么简单。

首先看一个数据对象比如列表,在Python内部都有什么东东

默认可以点(·)出来的是Python内置到列表对象里常用的方法,用来对当前列表对象做一些操作。

继续在点后边输入下划线,tab提示出来了更多的东西,在Python里叫魔术方法和魔术属性,了解这些可以开发出一些更高级的功能,比如装饰器等等。

函数的通常用法是 func_name(arg1, arg2),但函数同样也有上边那些以下划线开头的魔术属性和方法。 换句话说,如果一个对象里边的这些属性和方法和某个函数里的这些内容相同,这个对象基本上等同于这个函数。 理解了这些,就能明白,函数的产生不仅限于def或lambda,还可以通过代码的形式动态生成,甚至类也一样,属于元编程更高级的话题了。 函数可以通过代码生成,同时函数可以作为参数传给另一个函数,那么如果把某个函数A传入一个特定函数D,然后在函数D里通过代码把函数A的某些属性取到或修改就成为可能了。

03. 实例#

装饰器最常用的例子就是打日志或计算函数运行耗时。 下边是个完整实例,装饰器log_decorator记录了函数调用时的参数值和函数执行开始到退出的耗时。

log_decorator函数接受一个函数func作为参数,定义了一个wrapper函数,然后把这个wrapper函数返回。 wrapper函数先是打印了func的调用记录,然后去实际调用传入的函数func,这一步保证了被装饰的函数的原功能正常运行。 然后在调用原函数前后计算执行耗时。

执行结果如下:

INFO:__main__:get_order called with args: ('KN001',), kwargs: {'status': 'CREATED'}
Entry get_order func KN001 CREATED
Exit get_order func KN001 CREATED
INFO:__main__:get_order finished in 0.41 seconds

从输出结果可以看出,调用被装饰的函数,其实就是在执行装饰器中定义的wrapper函数。 示例中装饰器做的事情比较简单,还可以进行异常捕获统一处理。 fastapi等类似框架更是大量使用这一技术,同样的功能用了装饰器可以让开发者写更少的代码。

装饰器只是一个语法糖,本质上是把def定义好的一个函数传给装饰器函数,生成一个新的函数。

20行的@log_decorator 可以在get_order定义后用下边的代码替代:

get_order = log_decorator(get_order)

如此而已。

04. Tip#

注意第10行的@functools.wraps(func),新函数wrapper在返回前又被装饰了一次。 如果去掉这一行,运行结果好像一样,但在某些场景下会有奇怪的表现。

如果没有用@functools.wraps(func)装饰一次wrapper,新生成的函数对象里内置的魔术属性和方法是装饰器中wrapper定义时的内容。 比如上图中,如果在代码其他地方需要用到这些属性,会发现被装饰后的函数丢失了自己的名字和文档字符串,会带来一些预料不到的bug。 @functools.wraps(func)的作用就是把原函数的马甲套到生成的新函数上,保证对象除了基本行为之外的一致性。