在​​ python​​的学习过程中,如果我们想进阶的话,装饰器这个概念无论如何都是绕不开的,初学者甚至还嫌弃上了,这玩意有什么用呢?看着概念又费解,还用的不畅快,不如不学,正所谓技多不压身,学了顶多掉头发不是嘛,没其他坏处O(∩_∩)O而且,这个是从初学者向高阶晋级的一道坎

装饰器概念

在​​ python​​中的装饰器是为对象提供一些额外的功能,这些对象可以是类、方法、也可以是函数等等。装饰器本身就是一个闭包的一种应用,而闭包就是在函数中再嵌套一个函数,并且可以引用外包函数的变量。装饰器就是用于扩展原来函数的一种函数,类似原函数的插件,按照现在的说法应该是原函数plus,这个插件函数的返回值比较特殊,也是一个函数,其他的也就在使用上,需要在相对应的函数头上加​​ @demo​​就行了

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。

理解

什么叫装饰,就是装点、提供一些额外的点缀,前面说的装饰器的相关概念呢,确实有点绕口啥的,我这里举个简单的例子,来辅助一下理解,比如说女朋友跟你出来约会,本来人家就天生丽质了,还要精心打扮一下,头上戴个小发夹,手上小手提,还要有个小手环,还要再问一下:“宝儿,今天我有什么不一样?” 兄弟们这个回答有啥好犹豫的:“没啥不一样啊,是更美了好像?” 这就是装饰器的本质,不改变该对象的基本特性,女票原本什么样还是怎么样,不以外在形象为转移,关键是变美了,这是新特性就是装饰器带来的,比如说女票的小发夹带着更俏皮了,小手环袋子就温婉了一点,不是嘛?

更进一步

在使用装饰器的过程中,我们可以更进一步的体会到面向对象编程的开放封闭原则的应用 开放封闭原则主要体现在两个方面:

  • 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
  • 对修改封闭,意味着类一旦设计完成,就可以独立其工作,而不要对类尽任何修改。

为什么要用装饰器

假设你写了几个函数,有一天老板心血来潮说,你把每个函数的运行时长(结束时间-开始时间)统计下,作为一个python101练习生的你可能会这样写

原始函数

def func_a():
print("hello")

def func_b():
print("naug")


if __name__ == '__main__':
func_a()
func_b()

计算运行时间

作为python101练习生的你,大概率会这样写

import time
def func_a():
start = time.time()
print("love")
time.sleep(0.8)
end = time.time()
print("运行时长:%.4f 秒" % (end-start))


def func_b():
start = time.time()
print("naug")
time.sleep(0.8)
end = time.time()
print("运行时长:%.4f 秒" % (end-start))


if __name__ == '__main__':
func_a()
func_b()

# 聊一聊悟空编辑器 # 装饰器探析_参数传递



上面的代码虽然满足了领导的要求,但是如果你写的函数很多的话,每个函数都这样去添加,会显得代码很臃肿,有很多重复代码。老板看了代码大概率只会摇头和沉默,唉,毕竟是练习生还能要求啥呢,再带带吧 然后边上的一个扫地僧看了下你的代码,给你指了条明路:装饰器

函数装饰器

装饰器可以写成函数式装饰器,也可以写成一个类装饰器,先从简单的函数装饰器开始学习。 再强调一下,python装饰器本质上就是一个函数,是闭包在python中的一种实现,它可以让其他函数在不需要做任何代码变动的前提下增加额外的功能,装饰器的返回值也是一个函数对象。 runtime函数就是一个装饰器了,它对原函数做了包装并返回了另外一个函数,额外添加了一些功能。在函数上方使用​​ @语法糖​​就可以调用这个装饰器了

import time 
def runtime(func):
def wrapper():
start = time.time()
f = func() # 原函数
end = time.time()
print("运行时长:%.4f 秒" % (end-start))
return f
return wrapper




@runtime
def func_a():
print("love")
time.sleep(0.5)


@runtime
def func_b():
print("naug")
time.sleep(0.8)


if __name__ == '__main__':
func_a()
func_b()

# 聊一聊悟空编辑器 # 装饰器探析_函数体_02


感受到了吗,当我们成功封装了这个装饰器,也就意味着我们构建完成一个函数运行通用计时器​


​ runtime​​,通过这个计时器,我们在需要测算运行性能的函数都可以通过​​ @runtime​​的形式来进行挂载使用,从而完成对相对应的函数的运行时间测算,一次开发,多次使用,降低重复造轮子的成本,总之一个字!绝

函数带参数

事实上这个​​ runtime​​并不强壮,如果函数里面带有参数,那就不管用了,并且函数的参数是不固定的,这时候就需要用到​​ *args​​,​​ **kwargs​​这个组合了

import time


def runtime(func):
def wrapper(*args, **kwargs):
start = time.time()
f = func(*args, **kwargs) # 原函数
end = time.time()
print("运行时长:%.4f 秒" % (end-start))
return f
return wrapper


@runtime
def func_a(girl):
print("love "+girl)
time.sleep(0.5)

@runtime
def func_b(b, c="xx"):
print("hi "+b+c)
time.sleep(0.8)


if __name__ == '__main__':
func_a("naug")
func_b("world ", c="new start")


# 聊一聊悟空编辑器 # 装饰器探析_语法糖_03


类装饰器

说到​​ __call__​​,那就需要再补充一个个概念,可调用对象(callable),我们平时自定义的函数、内置函数和类都属于可调用对象,凡是可以把一对括号()应用到某个对象身上都可称之为可调用对象,判断对象是否为可调用对象可以用函数​​ callable​​。如果在类中实现了​​ __call__​​方法,那么实例对象也将成为一个可调用对象 还是原来那个计时器的例子,我们将这个方法封装成一个对象,再来体会一下类装饰器的特点

import time


class runtime(object):
def __init__(self, func):
self.func = func

def __call__(self, *args, **kwargs):
start = time.time()
f = self.func(*args, **kwargs) # 原函数
end = time.time()
print("运行时长:%.4f 秒" % (end-start))
return f

@runtime
def func_a(girl):
print("love "+girl)
time.sleep(0.5)


@runtime
def func_b(b, c="xx"):
print("hi "+b+c)
time.sleep(0.8)


if __name__ == '__main__':
func_a("naug")
func_b("world ", c="new start")

# 聊一聊悟空编辑器 # 装饰器探析_封装_04


装饰器带参数

冬天到了,老板说运行的速度先不要太快,让客户先加钱,然后再以正常的速度显示,那么现在的需求是让每个函数的运行时间加50%,该如何实现呢? 这就到了装饰器的高级语法,装饰器也需要带上参数了

函数装饰器

import time
def runtime(slowly=1):
def wrapper(func):
def inner_wrapper(*args, **kwargs):
start = time.time()
f = func(*args, **kwargs) # 原函数
end = time.time()
t = end-start
time.sleep((slowly-1)*t) # 延迟效果
new_end = time.time()
print("运行时长:%.4f 秒" % (new_end-start))
return f
return inner_wrapper
return wrapper


@runtime(1.5)
def func_a(girl):
print("love "+girl)
time.sleep(0.5)


@runtime(1.5)
def func_b(b, c="xx"):
print("hi "+b+c)
time.sleep(0.8)


if __name__ == '__main__':
func_a("naug")
func_b("world ", c="new start")

# 聊一聊悟空编辑器 # 装饰器探析_封装_05

类装饰器

import time
class runtime(object):
def __init__(self, slowly=1):
self.slowly = slowly

def __call__(self, func):
def wrapper(*args, **kwargs):
start = time.time()
f = func(*args, **kwargs) # 原函数
end = time.time()
t = end-start
time.sleep((self.slowly-1)*t) # 延迟效果
new_end = time.time()
print("运行时长:%.4f 秒" % (new_end-start))
return f
return wrapper


@runtime(1.5)
def func_a(girl):
print("love "+girl)
time.sleep(0.5)

@runtime(1.5)
def func_b(b, c="xx"):
print("hi "+b+c)
time.sleep(0.8)


if __name__ == '__main__':
func_a("naug")
func_b("world ", c="new start")

# 聊一聊悟空编辑器 # 装饰器探析_参数传递_06


装饰器应用场景

上面的例子主要是一个很简单的应用场景,如果你用过​​ locust​​,设置权重会用到​​ @task(1)​​,如果你用过​​ pytest​​框架,使用​​ fixture​​功能的时候经常会用到​​ @pytest.fixture(scope="function")​​,也被大量使用于​​ Flask​​和​​ Django​​等web框架中,检查是否被授权去使用一个web应用的端点(endpoint)。如​​ @login_required​​也可以自己写个装饰器添加日志

装饰器执行机制分析

以前面的​​ runtime​​为例,我们来对装饰器的运行机制来进行解析,

def runtime(func):
def wrapper(*args, **kwargs):
start = time.time()
f = func(*args, **kwargs) # 原函数
end = time.time()
print("运行时长:%.4f 秒" % (end-start))
return f
return wrapper
  1. 程序开始运行,从上往下解释,读到​​ def runtime(func):​​的时候,发现这是个*一等公民*函数,于是把函数体加载到内存里,之后进行下一个函数的解析
  2. 读到​​ @runtime​​的时候,程序被​​ @​​这个语法糖吸引住了,知道这是个装饰器,按规矩要立即执行的,于是程序开始运行​​ @​​后面那个名字​​ runtime​​所定义的函数。
  3. 程序返回到​​ runtime​​函数,开始执行装饰器的语法规则。规则是:被装饰的函数的名字会被当作参数传递给装饰函数。装饰函数执行它自己内部的代码后,会将它的返回值赋值给被装饰的函数。原来的​​ func_a​​函数被当做参数传递给了​​ slowly​​,而​​ func_a​​这个函数名之后会指向​​ wrapper​​函数。


# 聊一聊悟空编辑器 # 装饰器探析_python_07

注意:

  • ​ @runtime​​和​​ @runtime()​​有区别,没有括号时,​​ runtime​​函数依然会被执行,这和传统的用括号才能调用函数不同,需要注意一下
  • ​ func_a​​这个函数名(而不是​​ func_a()​​这样被调用后)当做参数传递给装饰函数​​ runtime​​,也就是:​​ func = func_a​​,​​ @runtime​​等于​​ runtime(func_a)​​,实际上传递了​​ func_a​​的函数体,而不是执行​​ func_a​​后的返回值。
  • ​ runtime​​函数​​ return​​的是​​ wrapper​​这个函数名,而不是​​ wrapper()​​这样被调用后的返回值。
  1. 程序开始执行​​ runtime​​函数内部的内容,一开始它又碰到了一个函数​​ wrapper​​,​​ wrapper​​函数定义块被程序观察到后不会立刻执行,而是读入内存中(这是默认规则)。
  2. 再往下,碰到​​ return wrapper​​,返回值是个函数名,并且这个函数名会被赋值给​​ func_a​​这个被装饰的函数,也就是​​ func_a = wrapper​​。根据前面的知识,我们知道,此时​​ func_a​​函数被新的函数​​ wrapper​​覆盖了(实际上是​​ func_a​​这个函数名更改成指向​​ wrapper​​这个函数名指向的函数体内存地址,​​ func_a​​不再指向它原来的函数体的内存地址),再往后调用​​ func_a​​的时候将执行​​ wrapper​​函数内的代码,而不是先前的函数体。那么先前的函数体去哪了?还记得我们将​​ func_a​​当做参数传递给​​ runtime​​这个形参么?​​ func​​这个变量保存了老的函数在内存中的地址,通过它就可以执行老的函数体,你能在​​ wrapper​​函数里看到​​ f = func()​​这句代码,它就是这么干的!
  3. 当业务需要,依然通过​​ func_a()​​的方式调用​​ func_a​​函数时,执行的就不再是旧的​​ func_a​​函数的代码,而是​​ wrapper​​函数的代码。在本例中,它首先会记录开始时间戳,当然了根据业务需要,显然你可以换成任意的代码,然后,它会执行​​ func​​函数并将返回值赋值给变量​​ f​​,这个​​ func​​函数就是旧的​​ func_a​​函数,执行完成之后记录结束时间戳,再打印时间间隔,最后返回​​ f​​这个变量。我们在业务需要的代码上可以用​​ f = func_a()​​的方式接受​​ f​​的值。
  4. 以上流程走完后,你应该看出来了,这个过程中没有对原来函数(方法)和接口调用方式做任何修改,也没有对基础平台部原有的代码做内部修改,仅仅是添加了一个装饰函数,就实现了我们的需求,在函数调用开始时间计算,调用后写入日志。这就是装饰器的最大作用。

那么为什么我们要搞一个​​ runtime​​函数一个​​ wrapper​​函数这么复杂呢?一层函数不行吗?答:请注意,​


​ @runtime​​这句代码在程序执行到这里的时候就会自动执行​​ runtime​​函数内部的代码,如果不封装一下,在业务代码还未进行调用的时候,就执行了,这和初衷不符。当然,如果你对这个有需求也不是不行。请看下面的例子,它只有一层函数。

def runtime(func):
start = time.time()
f = func() # 原函数
end = time.time()
print("运行时长:%.4f 秒" % (end-start))
return f



@runtime
def func_a():
print("love naug")
time.sleep(0.5)


if __name__ == '__main__':
func_a()

# 聊一聊悟空编辑器 # 装饰器探析_python_08


看见了吗?我们只是定义好了装饰器,业务代码还没有调用​


​ runtime​​函数呢,程序就把工作全做了。这就是为什么要封装一层函数的原因。

更进一步

装饰器执行顺序是否有规定呢?到底是按照我们添加语法糖的顺序来加载还是倒序的输出呢?这个是应用了队列结构还是栈的结构呢?不妨来进行下一步的实验

import time

def runtime(func):
def wrapper(*args, **kwargs):
start = time.time()
f = func(*args, **kwargs) # 原函数
time.sleep(0.5)
end = time.time()
print("运行时长:forever")
return f
return wrapper


def dating(func):
def wrapper(*args, **kwargs):
f = func(*args, **kwargs) # 原函数
print("nice time!")
return f
return wrapper


@dating
@runtime
def func_a(girl):
print("love "+girl)



@runtime
@dating
def func_b(girl):
print("love "+girl)


if __name__ == '__main__':
func_a('naug')
func_b('naug')

# 聊一聊悟空编辑器 # 装饰器探析_函数体_09


通过这个例子我们知道了函数是可以被多个装饰器装饰的,就像女孩子会带上各种各种漂漂亮亮的装饰品一样,我们这里要关注的是加载语法糖的顺序对执行器的影响,从demo中我们可以看到,​


​ func_a​​和​​ func_b​​都是优先执行自己方法里面的逻辑,之后再执行装饰器的逻辑,越靠近​​ def​​的越先执行

参考资料
  1. [​​Decorators I: Introduction to Python Decorators​
  2. [​​Python Decorators II: Decorator Arguments​