0%

看了本文也可能搞不太懂的 Python 装饰器的使用

问题背景:之前学过 python 的装饰器的语法,但因当时接触的代码很少涉及装饰器的使用,所以很少使用以至于忘干净了。而今天接触的问题的确需要使用装饰器,果然还是任务驱动大法好。

问题背景:在 GUI 的开发中,有很多的 QLineEdit 对象,并在其中输入信息。假设某个输入框位置输入的是学号,学号作为学生数据表的主键插入,而字符串多一个空格会影响数据表内的存储,如 123123{ } 是两个不一样的东西,即在数据库内存储两份,但实际是一个东西。{ } 表示有个空格占位符。

而在输入时普通用户可能会一不小心多加了一个空格,从开发良好软件的角度出发:后台应该把这些空格得去掉。而有很多的 QLineEdit() 对象时,又不想一个个的找到对象的位置,写很多次去除空格的函数(并不是很好的编码习惯),这个时候可能就需要装饰器了。

在含有 QLineEdit() 的函数中,调用装饰器给函数装饰一下,在不影响原来代码结构和语句的基础上,增加去除空格的装饰工作。省时省力,不破坏原有结构,值得推荐。今天:写个函数 return 回来不好么,写装饰器改全局变量还不够费劲(不太想装饰器里面 global )。

那么举个好点的例子,在超大规模图融合时,融合分五个子函数完成,每处理一次就需要记录一次图的边割率、平衡率等日至信息。子函数之间传递的是图的信息,如果在不破坏子函数代码结构的情况下,可以使用装饰器来记录边割率、平衡率等日至信息。

装饰器 DecoratorsPython 的一个重要部分,本质上是一个 Python 函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象

它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

Python的函数

函数作为参数

函数可以作为参数执行:Python 中的函数可以像普通变量一样当做参数传递给另外一个函数。比如把 foo 函数作为参数传递给 bar 函数,bar 函数体内执行这个参数:

1
2
3
4
5
6
7
def foo():
print("foo")

def bar(func):
func()

bar(foo)

输出:

1
foo

函数中定义与调用函数

Python 中我们可以在一个函数中定义另一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def hi(name="yasoob"):
print("now you are inside the hi() function")

def greet():
return "now you are in the greet() function"

def welcome():
return "now you are in the welcome() function"

# 执行
print(welcome())
print(greet())

print("now you are back in the hi() function")

hi()

输出:

1
2
3
4
now you are inside the hi() function
now you are in the welcome() function
now you are in the greet() function
now you are back in the hi() function

从函数中返回函数

注意与上述代码的差别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def hi(name="yasoob"):

def greet():
return "now you are in the greet() function"

def welcome():
return "now you are in the welcome() function"

if name == "yasoob":
b = greet
return b
else:
return welcome

a = hi()
print(a)
# 把 a 返回的函数对象给执行掉
print(a())

输出:

1
2
<function hi.<locals>.greet at 0x7fb8b02579e0>
now you are in the greet() function

再次看看这个代码。在 if/else 语句中我们返回 greetwelcome,而不是 greet()welcome()。为什么这样?这是因为当你把一对小括号放在后面,这个函数就会执行;然而如果你不放括号在它后面,那它可以被到处传递,并且可以赋值给别的变量而不去执行它。

第一个装饰器和@语法糖

虽然可以通过函数传递的方式来完成函数执行前的修饰功能,如下。

1
2
3
4
5
6
7
8
def use_logging(func):
logging.warn("%s is running" % func.__name__)
func()

def foo():
print('i am foo')

use_logging(foo)

这样做逻辑上是没问题的,功能是实现了,但是我们调用的时候不再是调用真正的业务逻辑 foo 函数,而是换成了 use_logging 函数,这就破坏了原有的代码结构, 现在我们不得不每次都要把原来的那个 foo 函数作为参数传递给 use_logging 函数,那么有没有更好的方式的呢?当然有,答案就是装饰器。

如果函数 bar()bar2() 等函数都有类似的需求,怎么做?再写一个 loggingbar 函数里?这样就造成大量雷同的代码,为了减少重复写代码,我们可以这样做,重新定义一个新的函数:专门处理日志,日志处理完之后再执行真正的业务代码。

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
def use_logging(func):
def wrapper():
print("%s is running" % func.__name__)
return func()

return wrapper


# ============================
def foo():
print("i am foo")
return 1


foo = use_logging(foo)
foo()
# ============================
# 两个函数等价
# ============================
@use_logging
def foo():
print("i am foo")
return 1


foo()
# ============================

如上所示,有了 @,我们就可以省去 foo = use_logging(foo) 这一句了,直接调用 foo() 即可得到想要的结果。foo() 函数不需要做任何修改,只需在定义的地方加上装饰器,调用的时候还是和以前一样,如果我们有其他的类似函数,我们可以继续调用装饰器来修饰函数,而不用重复修改函数或者增加新的封装。这样,我们就提高了程序的可重复利用性,并增加了程序的可读性。

装饰器在 Python 使用如此方便都要归因于 Python 的函数能像普通的对象一样能作为参数传递给其他函数,可以被赋值给其他变量,可以作为返回值,可以被定义在另外一个函数内。

函数元信息问题

之前网上的文章都说在使用装饰器时,修饰函数的元信息会代替调用函数的元信息,但现在2019(快2020)年了,貌似没了这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
def logit(func):
def with_logging(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return with_logging

@logit
def addition_func(x):
return x + x

result = addition_func(4)
print(result)

输出:

1
2
addition_func was called
8

之前(别人博客写的)的输出:

1
2
with_logging was called
8

仅供参考:(我并没遇到此类问题):当然安全起见,可以使用下面的方法。使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring__name__、参数列表,我们有functools.wrapswraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器里面的 func 函数中,这使得装饰器里面的 func 函数也有和原函数 foo 一样的元信息了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from functools import wraps
def decorator_name(f):
# @wraps接受一个函数来进行装饰 这可以让我们在装饰器里面访问在装饰之前的函数的属性
@wraps(f)
# 接受func传入的参数 对应
def decorated(*args, **kwargs):
# 装饰工作
print('test---->')
# 执行原本的函数
return f(*args, **kwargs)
# 返回装饰器
return decorated

@decorator_name
def func():
return("Function is running")

print(func())

输出:

1
2
test---->
Function is running

带参数的装饰器

若不同方法对装饰器的需求有细微的差别,需要额外传入参数来完成差别的区分。如 @use_logging(level="warn") 等价于 @decorator,因此里面多了一组函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def use_logging(level):
# 解析装饰器参数
def decorator(func):
# 解析函数参数
def wrapper(*args, **kwargs):
if level == "warn":
print("%s is warning" % func.__name__)
elif level == "info":
print("%s is logging" % func.__name__)
return func(*args, **kwargs)

return wrapper

return decorator


@use_logging(level="warn")
def tmp(name="foo"):
print("i am %s" % name)


tmp()

上面的 use_logging 是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。。当我们使用 @use_logging(level="warn") 调用的时候,python 能够发现这一层的封装,并把参数传递到装饰器的环境中。

1
2
foo is running
i am foo

装饰器类

没错,装饰器不仅可以是函数,还可以是类,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器主要依靠类的__call__方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法。

假设有时你只想打日志到一个文件。而有时你想把引起你注意的问题发送到一个 email,同时也保留日志,留个记录。这是一个使用继承的场景,但目前为止我们只看到过用来构建装饰器的函数。幸运的是,类也可以用来构建装饰器。那我们现在以一个类而不是一个函数的方式,来重新构建 logit

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
from functools import wraps

class logit(object):
def __init__(self, logfile='out.log'):
self.logfile = logfile

def __call__(self, func):
@wraps(func)
def wrapped_function(*args, **kwargs):
log_string = func.__name__ + " was called"
print(log_string)
# 打开logfile并写入
with open(self.logfile, 'a') as opened_file:
# 现在将日志打到指定的文件
opened_file.write(log_string + '\n')
# 现在,发送一个通知
self.notify()
return func(*args, **kwargs)
return wrapped_function

def notify(self):
# logit只打日志,不做别的
print('-----test-----')

@logit()
def myfunc1():
print('fun1 running')

myfunc1()

输出:

1
2
3
myfunc1 was called
-----test-----
fun1 running

继承

给 logit 创建子类,来添加 email 的功能(虽然 email 这个话题不会在这里展开)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class email_logit(logit):
'''
一个logit的实现版本,可以在函数调用时发送email给管理员
'''
def __init__(self, email='admin@myproject.com', *args, **kwargs):
self.email = email
super(email_logit, self).__init__(*args, **kwargs)
self.notify2()

def notify2(self):
# 发送一封email到self.email
# 这里就不做实现了
print('--test1--')

@email_logit()
def myfunc2():
# 先执行基类的初始化,在初始化子类
# 而后调用子类的 call 函数
# 也就是,先发邮件,在引起注意
print('my func2 running')

myfunc2()

输出:

1
2
3
4
--test1--
myfunc2 was called
-----test-----
my func2 running

装饰器顺序

一个函数还可以同时定义多个装饰器,比如:

1
2
3
4
5
@a
@b
@c
def f ():
pass

它的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器,它等效于f = a(b(c(f)))

练习

去除字符串空格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from functools import wraps

text = '201614420112 '

def remove_blank(func):
@wraps(func)
def wrapper():
global text
text = text.strip()
print("%s is running" % func.__name__)
return func()
return wrapper

@remove_blank
def foo():
print(len(text))

foo()

但是并不推荐这种global的写法,容易引起变量混乱。不如以下方式简洁明了:

1
2
3
4
5
6
7
8
9
text = '201614420112 '

def foo(t):
text = t.strip()
return text

text = foo(text)

print(len(text))

记录函数执行时间

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
import time
def run_time(func):
def wrapper():
since = time.time()
func()
end = time.time()
print('use time: ' + str(end - since))
return wrapper

@run_time
def test1():
i = 0
while(i < 1000):
i += 3
i -= 1

@run_time
def test2():
i = 2
while(i < 1000):
i = i * 3
i = i - 2

test1()

test2()

输出:

1
2
use time: 8.249282836914062e-05
use time: 4.0531158447265625e-06

本文参考

更多的像是转载
https://www.runoob.com/w3cnote/python-func-decorators.html

感谢上学期间打赏我的朋友们。赛博乞讨:我,秦始皇,打钱。

欢迎订阅我的文章