装饰器(选修)
装饰器
如果你有一批变量想统一按一个规则处理,并且需要缩减代码,你需要函数。
如果你有一批函数想统一按一个规则处理,并且需要缩减代码,你需要装饰器(Decorator)
理清下面2点:
函数
- 接受参数
- 做点事情
- 返回结果
装饰器
- 接受函数作为参数
- 做点事情
- 返回一个函数
用 @ 来使用装饰器
使用 @ 符号来将某个函数替换为装饰符之后的函数:
例如这个函数:
def dec(f):
print('I am decorating function', id(f))
return f
def foo(x):
print(x) # I am decorating function 45206384
foo = dec(foo)
可以替换为:
@dec
def foo(x):
print(x)
那么他有什么实际作用?故事的开始是这样的,你写好了2个函数:
def test1():
print('test1 ..')
def test2():
print('test2 ..')
test1()
test2()
当你准备把它放到服务器上,这个时候领导提醒你要输出日志,不然查错跑断腿。 输出要求是:在每次函数调用的前后加上时间。 于是你写成了下面这个样子
import time
def test1():
print('测试开始:现在时间是',time.time())
print('test1 ..')
print('测试结束:现在时间是',time.time())
def test2():
print('测试开始:现在时间是',time.time())
print('test2 ..')
print('测试结束:现在时间是',time.time())
test1()
test2()
领导说,他有3个问题:
- 首先代码1和代码2是一样的,也就是说把同样的代码写了2遍,这一点也不程序员!
- 而且,你修改了你的核心代码,使得它变得很长。后面要再删也很麻烦,万一手抖删错了就完了。
- 最后,在大项目合作中,可能test代码是A同事写的,输出日志代码是B同事写的,代码保密,每个程序员只能拿到部分片段,所以你根本不知道对方的代码,要提供一个通用的打印日志的方式。
思考下,可以怎么修改能既不修改源代码,又对代码结构影响最小呢?
我说,这样子,那我可以写成这样?
import time
def a_decorator(func):
print('测试开始:现在时间是',time.time())
func()
print('测试结束:现在时间是',time.time())
def test1():
print('test1 ..')
def test2():
print('test2 ..')
a_decorator(test1)
a_decorator(test2)
领导说:有进步,但是原本调用test1()的语法被你改成了a_decorator(test1),这要是再多几个功能不得把我绕晕了啊。
看来函数嵌套掌握的不熟啊,给你点提示,我带你透过现象看本质
- 变量的本质:就是变量指向的内存地址
- 函数名的本质:就是函数的内存地址
- 变量可以作为函数的参数,因此函数名可以用做函数的参数
- 变量可以作为函数的返回值,同理,函数名也可以作为函数的返回值
我说,那就写成这样?
import time
def a_decorator(func):
def wrap_the_func():
print('测试开始:现在时间是',time.time())
func()
print('测试结束:现在时间是',time.time())
return wrap_the_func
def test1():
print('test1 ..')
def test2():
print('test2 ..')
test1 = a_decorator(test1) #这里a_decorator(test1) 代指wrap_the_func(),把这个wrap_the_func()函数的地址赋值给test1,由于代码从上而下执行,从而替换掉原本test1的指向。
test2 = a_decorator(test2)
test1()
test1()
领导说:这倒数3、4行看着很碍眼,且会占据命名空间,你不会修饰符吗?我教你啊。
- 我们先定义一个函数(名字随便起,这里只是用a_decorator做示例)
- 然后简单的设置下这个函数运行逻辑,
- 最后在原有的函数的头上加@函数名就行啦
直接使用@函数修饰符是很方便的,你也看出来所谓【@函数修饰符】其实就是【函数】嵌入。
这里我再假设你的函数是带参数的。我也 用修饰符写一下吧。好好看,好好学。
核心代码(下方的test函数)无需知道我(下方的log函数)是怎么写的,我也无需知道核心代码是怎么写的,我们就能快速完成协作。
import time
#args 是 arguments 的缩写,表示位置参数;
#kwargs 是 keyword arguments 的缩写,表示关键字参数。
#这其实就是 Python 中可变参数的两种形式,
#并且 *args 必须放在 **kwargs 的前面,因为位置参数在关键字参数的前面。
def log(func):
def wrapper(*args,**kwargs):
print('测试开始:现在时间是',time.time())
ret = func(*args,**kwargs)
print('测试结束:现在时间是',time.time())
return ret
return wrapper
@log
def test1(s):
print('test1 ..', s)
return s
@log
def test2(s1, s2):
print('test2 ..', s1, s2)
return s1 + s2
test1(1)
test2(1,2)
于是你回想起之前Python也提供了一些自带函数,例如:print()、input()
那会不会也有一些自带的【@函数修饰符】呢?还真有,常见的包括:@property、@classmethod、@staticmethod还有typing里面各种用于测试的函数。
不过这些结构相对复杂,当你理解普通的@修饰符之后,这些自带的你只需要记得用法即可,原理都是一样的。
例子
定义两个装饰器函数,一个将原来的函数值加一,另一个乘二:
def plus_one(f):
def new_func(x):
return f(x) + 1
return new_func
def times_two(f):
def new_func(x):
return f(x) * 2
return new_func
定义函数,先乘二再加一:
@plus_one
@times_two
def foo(x):
return int(x)
b = foo(2)
b # 5
修饰器工厂
decorators factories 是返回修饰器的函数
它的作用在于产生一个可以接受参数的修饰器,
例如我们想将 函数 输出的内容写入一个文件去,可以这样做:
def super_loud(filename):
fp = open(filename, 'w')
def loud(f):
def new_func(*args, **kw):
fp.write(str(args))
fp.writelines('\n')
fp.write('calling with' + str(args) + str(kw))
# 确保内容被写入
fp.flush()
fp.close()
rtn = f(*args, **kw)
return rtn
return new_func
return loud
@super_loud('test.txt')
def foo(x):
print(x)
# 调用 foo 就会在文件中写入内容:
foo(100)
import os
os.remove('test.txt')
@classmethod 装饰器
在 Python 标准库中,有很多自带的装饰器,
例如 classmethod 将一个对象方法转换了类方法:
class Foo(object):
@classmethod
def bar(cls, x):
print('the input is', x)
def __init__(self):
pass
类方法可以通过 类名.方法 来调用:
Foo.bar(10)
@property 装饰器
有时候,我们希望像 Java 一样支持 getters 和 setters 的方法,
这时候就可以使用 property 装饰器:
class Foo(object):
def __init__(self, data):
self.data = data
@property
def x(self):
return self.data
此时可以使用 .x 这个属性查看数据(不需要加上括号):
foo = Foo(22)
print(foo.x)
这样做的好处在于,这个属性是只读的:
foo.x = 1 会报错
如果想让它变成可读写,可以加上一个装饰符 @x.setter:
class Foo(object):
def __init__(self, data):
self.data = data
@property
def x(self):
return self.data
@x.setter
def x(self, value):
self.data = value
foo = Foo(1000)
foo.x
foo.x = 2222
foo.x
应用:定时器
要求:写一个定时器功能,要求监控一个执行程序,超时则报警。
如何完成?
下方代码在mac下可用
import signal
import time
def set_timeout(num, callback):
def wrap(func):
def handle(signum, frame): # 收到信号 SIGALRM 后的回调函数,参数1是信号的数字,参数2是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("do something after timeout.")
raise RuntimeError
@set_timeout(2, after_timeout) # 限时 2 秒超时
def connect(): # 要执行的函数
time.sleep(2.4) # 函数执行时间,写大于2的值,可测试超时
return "完成"
class Demo:
@set_timeout(2, after_timeout)
def conn(self):
time.sleep(3)
return "ok"
试一下:
try:
a = connect()
print(a)
except Exception as e:
a = 'err'
print(a)
如果不超时:
b = Demo()
try:
c = b.conn()
print(c)
except RuntimeError as e:
print('run time err.')
class Demo:
@set_timeout(2, after_timeout)
def conn(self):
time.sleep(1)
return "ok"
b = Demo()
try:
c = b.conn()
print(c)
except RuntimeError as e:
print('run time err.')