python装饰器详解

本文介绍python中装饰器使用及理解

背景

python中装饰器的原理,几种常见的写法,以及相关的应用

相关概念

由于装饰器的实现,涉及如下(1,2)两个基础概念,故我们先作了解

1.高阶函数

首先我们要记得一个概念,py中万物皆对象,不论是 变量/函数/class 都是一个对象。基于这一点,函数的参数列表可以支持函数对象参数的传入(即:将一个函数作为参数传递给 另外一个函数)

我们将有如下特点的函数,称作高阶函数:

1
2
3
可以接受函数作为参数

可以将函数作为返回值返回

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def hello():
print "hello tingyun"

#case: 接受函数作为参数 + 将函数值作为返回
def wrapper(func):
return func

f = wrapper(hello)
f() #这里的f函数,即是一个高级函数

#也可以使用如下写法
@wrapper
def hello():
print "hello tingyun"
f = hello

2.闭包

这里涉及到闭包概念的理解:一个持有外部(作用域之外)环境变量的函数就是闭包
闭包的作用:在不改变源代码、函数的调用方式的前提下给函数添加新的功能

实际上,闭包的作用就是装饰器的功能,我们的装饰器就是用于在不影响原始函数功能的基础之上,为函数添加一些新的功能

举例

1
2
3
4
5
6
7
def func(lst):
def in_func():
return Len(lst)
return in_func

f = func([1,2,3])
print(f())

这里我们通过在函数func内部定义一个新函数in_func,获取到了外层函数的参数lst ,并且返回了其长度

闭包的原理–作用域查找的优先级

一般在函数中,我们查找某个变量的优先级是按照如下顺序

1
2
3
4
5
6
7
L:local 函数内部作用域

E:enclosing 函数内部与内嵌函数之间(函数定义的变量可以被内嵌函数使用,py的装饰器主要是基于此处实现)

G:global 全局作用域

B:build-in 内置作用域(解析器自动导入的成员)

使用闭包会出现这样的效果:即你内部函数在调用完后(立刻)销毁,但是其使用到了外层的参数的变量引用

也就是说,我们通过闭包,记住了某个函数的 现场环境(变量+函数), 但是不会影响到原函数的正常功能(注:若想修改原函数中的可变变量,由于操作的是引用,是会影响原函数的参数

装饰器

有了以上两个知识点作为前言,我们可以说说python中的装饰器的实现了

简单总结:首先py接收 函数作为参数传递,这样一来,我们可以将 某个函数A传递给(装饰器)函数B,通过B的参数传递(B(A)),在B中我们可以再定义一个内置函数去获取到参数函数A的相关信息。
这样一来即完成了装饰器的封装,最后返回方法A,也在装饰器B中完成我们想要的操作

作用

当我们定义了一个函数A,想在其运行的时候,动态添加功能。但又不想改动函数内部的实现代码,这个时候,可以使用 一个新的函数B,将现有函数A传递给这个新的函数B,最后定义完了需要的操作,再返回这个A。

这样一来,我们既不需要改动A的代码,也能通过B函数,加上我们想对A函数 执行的一些操作

装饰器的几种写法

知道了装饰器的实现原理 + 作用 之后,我们用几段代码实现一下

函数装饰器

不带参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

def B(func):
def in_func(x):
#在这里能调用到外层func的函数和变量
print('call ' + func.__name__ + '()' + ' x: %d'%x)
return func(x)
return in_func

@B
def A(x):
return x*2

print(A(5))

#输出
>>>call A() x: 5
10
带参数

这里的带参数,指的是 装饰器函数的参数,为了实现更多样化的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def log(level):
def log_inner(func):
def wrapper(*args, **kw):
print("[%s]:"%level + "call " + func.__name__ + "()")
return func(*args, **kw)
return wrapper
return log_inner

@log("INFO")
def A(x):
return x*2

print(A(5))

#输出
>>>[INFO]:call A()
10

类class装饰器

cls装饰器是一种写法,实际上实现的功能也是跟函数装饰器一样(使用了魔法函数__call__使得cls对象完成了函数的功能

且这里我们就不区分是否有参数,因为类装饰器的参数是初始化变量,根据需要再添加即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#一个类装饰器
class logger(object):
def __init__(self, level='INFO'):
#此处的变量类似于函数的参数,不同的是使用构造函数赋值
self.level = level


#call()的本质是将一个类变成一个函数
def __call__(self, func):
def wrapper(*args, **kwargs):
print("[{level}]: the function {func}() is running..."\
.format(level=self.level, func=func.__name__))

return func(*args, **kwargs)

return wrapper #返回函数

@logger(level='INFO')
def A(x):
return x*2

print(A(5))

#输出
[INFO]: the function A() is running...
10
call魔法方法

此处作简单解释,想再详细了解的话可自行google

在class中的call方法相当于将 构造函数 转换成了一个方法,有如下效果

1
2
3
4
5
6
7
8
9
class Cls(objet):
def __init__(self):
pass

def __call__(self):
#code
pass

Cls(1,2)等同于调用Cls.__call__(1,2)

面试中常遇到的一些问题

写一个监控函数的执行时间的装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import time

#不带参数的函数装饰器
def B(func):
def in_func(x):
t1 = int(time.time()*1000)
func(x)
t2 = int(time.time()*1000)
print("时间执行:%sms"% ( t2-t1))
return in_func


@B
def A(x):
time.sleep(1)
return x*2

#A = B(A)
print(A(5))

#输出
时间执行:1003ms
None

监控执行时间,并且能设定超时退出

这里涉及到的知识点不只是装饰器了,而是包含了系统中的一些监控和中断的逻辑

通常有两种实现

1
2
3
4
5
try except 加上以下两种监控方式

1.信号量signal中断

2.Thread线程超时时间
实现1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import signal
import time
#设定超时时间,以及超时之后的回调函数
def set_timeout(num, callback):
def wrap(func):
def handle(signum, frame): # 收到信号 SIGALRM 后的回调函数,第一个参数是信号的数字,第二个参数是the interrupted stack frame.
raise RuntimeError
def to_do(*args, **kwargs):
try:
signal.signal(signal.SIGALRM, handle) # 设置信号和回调函数
signal.alarm(num) # 设置 num 秒的闹钟
print('start alarm signal.')
r = func(*args, **kwargs)
print('close alarm signal.')
signal.alarm(0) # 关闭闹钟
return r
except RuntimeError as e:
callback()
return to_do
return wrap
def after_timeout(): # 超时后的处理函数
print("Time out!")

@set_timeout(2, after_timeout) # 限时 2 秒超时
def connect(): # 要执行的函数
time.sleep(3) # 函数执行时间,写大于2的值,可测试超时
print('Finished without timeout.')

if __name__ == '__main__':
connect()
实现2

python中有一个模块叫 func_timeout
其实现就是基于Thread线程超时,感兴趣的可以安装这个包(pip install func_timeout),然后看看其源码的实现

也可以参考如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#!/usr/bin/env python
# -*- coding:utf-8 -*-

from threading import Thread
import time
class TimeoutException(Exception):
pass
ThreadStop = Thread._Thread__stop
def timelimited(timeout):
def decorator(function):
def decorator2(*args,**kwargs):
class TimeLimited(Thread):
def __init__(self,_error= None,):
Thread.__init__(self)
self._error = _error
def run(self):
try:
self.result = function(*args,**kwargs)
except Exception,e:
self._error = str(e)
def _stop(self):
if self.isAlive():
ThreadStop(self)
t = TimeLimited()
t.start()
t.join(timeout)
if isinstance(t._error,TimeoutException):
t._stop()
raise TimeoutException('timeout for %s' % (repr(function)))
if t.isAlive():
t._stop()
raise TimeoutException('timeout for %s' % (repr(function)))
if t._error is None:
return t.result
return decorator2
return decorator
@timelimited(2) # 设置运行超时时间2S
def fn_1(secs):
time.sleep(secs)
return 'Finished without timeout'
def do_something_after_timeout():
print('Time out!')
if __name__ == "__main__":
try:
print(fn_1(3)) # 设置函数执行3S
except TimeoutException as e:
print(str(e))
do_something_after_timeout()

以上代码均验证过能执行,可以丢到本地跑一下再尝试理解

工作中常用到的地方

1.loggin用于监控函数执行情况,打日志

2.在常见的web网站中,登录用户的权限检验,需要进入某个方法(网页,如:index)之前,加一个@login_required装饰器,检测cookie的信息,登录校验成功才会返回内容

end

总结

1.在了解 py的对象特性+闭包概念 之后,通过这两者结合,得到py下的装饰器实现;
2.基于写法的方式不同分成了:1.函数装饰器 2.类装饰器 ,且为了扩展装饰器的功能,可以添加装饰器参数;
3.最后面试中常遇到的一些问题,监控时长装饰器+超时处理装饰器的实现;

参考

https://juejin.cn/post/6913172123855323149
http://www.qb5200.com/article/377713.html