python装饰器详解

装饰器(Decorators)是 Python 的一个重要部分。简单地说:他们是修改其他函数的功能的函数。他们有助于让我们的代码更简短,也更PythonicPython范儿)。

其主要作用就是在不改变原有函数代码的前提下,给该函数添加新的功能

什么是闭包

在函数嵌套的前提下,内部函数使用了外部函数的变量,并且外部函数返回了内部函数,我们把这个使用外部函数变量的内部函数称为闭包

闭包的构成条件

通过闭包的定义,我们可以得知闭包的形成条件:

  1. 在函数嵌套(函数里面再定义函数)的前提下
  2. 内部函数使用了外部函数的变量(还包括外部函数的参数)
  3. 外部函数返回了内部函数

闭包的作用

  • 闭包可以保存外部函数内的变量,不会随着外部函数调用完而销毁。

注意点:

  • 由于闭包引用了外部函数的变量,则外部函数的变量没有及时释放,消耗内存。

原始代码

原始要求就是一台电脑,功能是可以播放音乐,如果我们在不改变computer函数原有代码的情况下让电脑有其他功能该如何操作?

1
2
3
4
5
# 电脑原有功能
def computer():
print('我可以播放音乐')
# 运行电脑
computer()

使用其他函数调用

1
2
3
4
5
6
7
8
9
# 电脑原有功能
def computer():
print('我可以播放音乐')
# 电脑扩展功能
def extend():
print('我可以编写代码')
computer()
# 运行扩展
extend()

该方式确实没有改变原有函数代码,但是却改变了直接运行的函数,在一个项目中我们可能有很多地方要调用computer,如果这样改写势必要将之前所有调用computer的地方都要改成extend

进阶

我们是否可以定义一个叫computer的变量来接收extend,然后在执行computer,被调用的地方不是就不用修改了?我们尝试下

1
2
3
4
5
6
7
8
9
10
11
12
13
# 电脑原有功能
def computer():
print('我可以播放音乐')

# 扩展功能
def extend():
print('我可以编写代码')
# 运行电脑原有功能
computer()
# 将扩展功能赋值给computer
computer = extend
# 执行computer
computer()

执行代码我们看下结果:

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
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
我可以编写代码
Traceback (most recent call last):
File "E:/test.py", line 9, in <module>
computer()
File "E:/test.py", line 7, in extend
computer()
File "E:/test.py", line 7, in extend
computer()
File "E:/test.py", line 7, in extend
computer()
[Previous line repeated 993 more times]
File "E:/test.py", line 6, in extend
print('我可以编写代码')
RecursionError: maximum recursion depth exceeded while calling a Python object

很显然这样是有问题的,computer的值是extendextend内部又运行了一个computer,等于运行了extend,自己运行自己,死循环了。

使用闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 电脑原有功能
def computer():
print('我可以播放音乐')

# 扩展功能
def extend(func):
def method():
# 原有函数前扩展其功能
print('我可以编写代码')
# 执行原有函数
func()
return method
# 将扩展功能赋值给computer
computer = extend(computer)
# 执行computer
computer()

修改闭包内使用的外部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def func_out(num1):
def func_inner(num2):
# 修改外部变量为99
num1 = 99
res = num1 + num2
print("最后的结果是:", res)
# 打印num1初始值
print("num1初始:", num1)
# 运行内部函数
func_inner(8)
# 打印内部函数修改num1后的num1值
print("num1修改后:", num1)
return func_inner


func = func_out(66)
func(33)

以上代码运行结果来看,目的是达到了,但是自己观察发现我们在func_inner中修改的num1变量,没有修改成功。有人会讲了,那是因为你使用外部变量没有使用global,所以相当于在内部函数重新定义了一个局部变量num1,如你所愿我们来修改并运行下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def func_out(num1):
def func_inner(num2):
# 按大家要求我们引入全局变量
global num1
# 修改外部变量为99
num1 = 99
res = num1 + num2
print("最后的结果是:", res)
# 打印num1初始值
print("num1初始:", num1)
# 运行内部函数
func_inner(8)
# 打印内部函数修改num1后的num1值
print("num1修改后:", num1)
return func_inner


func = func_out(66)
func(33)

运行结果:

1
2
3
4
5
6
num1初始: 66
最后的结果是: 107
num1修改后: 66
最后的结果是: 132

Process finished with exit code 0

num1外部变量依然没被改掉,这是因为修改闭包内使用的外部函数变量使用 nonlocal 关键字来完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def func_out(num1):
def func_inner(num2):
# 使用nonlocal引入外部变量
nonlocal num1
# 修改外部变量为99
num1 = 99
res = num1 + num2
print("最后的结果是:", res)
# 打印num1初始值
print("num1初始:", num1)
# 运行内部函数
func_inner(8)
# 打印内部函数修改num1后的num1值
print("num1修改后:", num1)
return func_inner


func = func_out(66)
func(33)

运行结果:

1
2
3
4
num1初始: 66
最后的结果是: 107
num1修改后: 99
最后的结果是: 132

这时候才终于修改掉了外部变量。

带参数

如果原函数是需要传递参数该如何处理呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 电脑原有功能
def computer(name, something):
print(f'我叫{name}可以{something}')

# 扩展功能
def extend(func):
def method():
# 原有函数前扩展其功能
print('我可以编写代码')
# 执行原有函数
func()
return method
# 将扩展功能赋值给computer
computer = extend(computer)
# 执行computer
computer('银河1号', '播放音乐')

运行结果

method没有接受参数,但是你却给了我两个参数

1
2
3
4
Traceback (most recent call last):
File "E:/test.py", line 16, in <module>
computer('银河1号', '播放音乐')
TypeError: method() takes 0 positional arguments but 2 were given

改进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 电脑原有功能
def computer(name, something):
print(f'我叫{name}可以{something}')

# 扩展功能
def extend(func):
def method(*args, **kwargs):
# 原有函数前扩展其功能
print('我可以编写代码')
# 执行原有函数
func(*args, **kwargs)
return method
# 将扩展功能赋值给computer
computer = extend(computer)
# 执行computer
computer('银河1号', '播放音乐')

如此我们即完成了对原有函数功能的扩展,又使其能正确的使用参数

返回结果

上述代码如果我们要获取computer的返回值,该如何处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 电脑原有功能
def computer(name, something):
print(f'我叫{name}可以{something}')
return '我是computer原有代码的返回结果'
# 扩展功能
def extend(func):
def method(*args, **kwargs):
# 原有函数前扩展其功能
print('我可以编写代码')
# 执行原有函数
func(*args, **kwargs)
return method
# 将扩展功能赋值给computer
computer = extend(computer)
# 执行computer
res = computer('银河1号', '播放音乐')
print(res)

运行发现最后返回的是None,因为原有扩展功能函数中method中执行了原有的computer函数,但是却没有返回该函数的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 电脑原有功能
def computer(name, something):
print(f'我叫{name}可以{something}')
# 返回结果
return '我是computer原有代码的返回结果'
# 扩展功能
def extend(func):
def method(*args, **kwargs):
# 原有函数前扩展其功能
print('我可以编写代码')
# 执行原有函数并返回其运行后的结果
return func(*args, **kwargs)
return method
# 将扩展功能赋值给computer
computer = extend(computer)
# 执行computer
res = computer('银河1号', '播放音乐')
print(res)

使用装饰器语法糖

使用语法糖其实就是省略了上述代码中的15行,简写后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 扩展功能
def extend(func):
def method(*args, **kwargs):
# 原有函数前扩展其功能
print('我可以编写代码')
# 执行原有函数
return func(*args, **kwargs)
return method


# 电脑原有功能
@extend
def computer(name, something):
print(f'我叫{name}可以{something}')
return '我是computer原有代码的返回结果'

# 执行computer
res = computer('银河1号', '播放音乐')
print(res)

自定义装饰器规则

适用场景,上例中我们的computer有时让其拥有功能A,有时需要拥有功能B,甚至有时候我们只要其原有功能。简单来说就是想要看电影就给装一个电影播放器,想要写代码就给你装一个IDE,或者我什么都不想要了,就想要初始化的功能。

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
# 扩展功能
def wraper(flag):
# func为目标函数
def extend(func):
# *args与**kwargs为目标函数的参数
def method(*args, **kwargs):
# 判断wraper传递的参数,来决定扩展目标函数的哪种功能
if flag == 1:
# 原有函数前扩展其功能
print('我可以编写代码')
# 执行原有函数
return func(*args, **kwargs)
elif flag == 2:
# 原有函数前扩展其功能
print('我可以上网冲浪')
# 执行原有函数
return func(*args, **kwargs)
else:
return func(*args, **kwargs)
return method
return extend






# 电脑原有功能
# @wraper(3)等同于@extend,waraper(3)执行后返回了一个extend,所以实际上还是使用了extend来装饰目标函数
@wraper(3)
def computer(name, something):
print(f'我叫{name},我的原始功能可以{something}')
return '我是computer原有代码的返回结果'

# 执行computer
res = computer('银河1号', '播放音乐')
print(res)

多个装饰器

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
# 扩展功能1
def extend(func):
def method(*args, **kwargs):
# 原有函数前扩展其功能
print('我可以编写代码')
# 执行原有函数
return func(*args, **kwargs)
return method

# 扩展功能2
def extend2(func):
def method(*args, **kwargs):
# 原有函数前扩展其功能
print('我可以哈哈哈笑一整天的功能')
# 执行原有函数
return func(*args, **kwargs)
return method


# 电脑原有功能
@extend
@extend2
def computer(name, something):
print(f'我叫{name}可以{something}')
return '我是computer原有代码的返回结果'

# 执行computer
res = computer('银河1号', '播放音乐')
print(res)

执行结果:

1
2
3
4
我可以编写代码
我可以哈哈哈笑一整天的功能
我叫银河1号可以播放音乐
我是computer原有代码的返回结果

装饰器

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2020/11/25 13:39
# @Author : 托小尼
# @Email : 646547989@qq.com
# @URI : https://www.diandian100.cn
# @File : test.py
import time


def decorator(func):
print("我用来计算函数用时")
def inner():
start = time.time()
func()
print("用时:", time.time()-start)

return inner

@decorator
def run():
for item in range(100000):
print(item)

if __name__ == '__main__':
pass

运行结果:

1
我用来计算函数用时

注意:

装饰器的执行时间是加载模块时立即执行,这意味着只要加载了带装饰器的模块,即使不运行被装饰的函数,装饰器也运行了

装饰带有参数的函数

这里需求是,函数要输出错误编码和错误信息,装饰器负责将这些内容写入日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
def logging(func):
def inner(code, msg):
# 接收错误代码和错误信息
res = func(code, msg)
print("错误信息已写入日志:【%s】" % res)
return inner

@logging
def show(err, msg):
"""带参数的函数"""
return '错误代码:%s;错误提示:%s' % (err, msg)

show('DBERROR', '数据库出错了')

输出结果:

1
2
3
错误信息已写入日志:【错误代码:DBERROR;错误提示:数据库出错了】

Process finished with exit code 0

装饰带有不确定参数个数的函数

同样是上个问题,加入我们不知道要被装饰的函数的个数是几个,或者说,我们就想写个通用装饰器,传几个参数都可以,我们可以使用python基础里的不定长参数元组*args和**kwagrs

1
2
3
4
5
6
7
8
9
10
11
12
13
def logging(func):
def inner(*args, **kwargs):
# 接收错误代码和错误信息
res = func(*args, **kwargs)
print("错误信息已写入日志:【%s】" % res)
return inner

@logging
def show(err, msg):
"""带参数的函数"""
return '错误代码:%s;错误提示:%s' % (err, msg)

show('DBERROR', '数据库出错了')

输出结果跟上面的一样

所以我们总结下通用装饰器的语法可以如下方式书写:

1
2
3
4
5
6
7
8
9
10
# 通用装饰器
def func_out(fn):
def func_inner(*args, **kwargs):
print("--被装饰的函数执行前要做的操作--")
result = fn(*args, **kwargs)
print("--被装饰的函数执行后要做的操作--")
return result

return inner

使用多个装饰器

需求,我们想给某段文字添加固定的标签,如加入p段落,加上div容器。

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
def make_div(func):
"""添加div标签"""
def func_inner():
# 接收函数返回值
res = func()
# 格式化结果
return "<div>%s</div>" % res
return func_inner

def make_p(func):
"""添加p标签"""
def func_inner():
# 接收函数返回值
res = func()
# 格式化结果
return "<p>%s</p>" % res
return func_inner

@make_div
@make_p
def show():
"""带参数的函数"""
return '我就是简单一句话,怎么了?'

print(show())

运行结果:

1
2
3
<div><p>我就是简单一句话,怎么了?</p></div>

Process finished with exit code 0

总结:多个装饰器的装饰过程是: 离函数最近的装饰器先装饰,然后外面的装饰器再进行装饰,由内到外的装饰过程

类装饰器

这里需求是,函数要输出错误编码和错误信息,装饰器负责将这些内容写入日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Logging(object):
def __init__(self, func):
# 接收被装饰的函数,设为私有防止外部调用
self.__func = func

def __call__(self, *args, **kwargs):
print('装饰前的操作')
res = self.__func(*args, **kwargs)
print("错误信息已写入日志:【%s】" % res)


@Logging
def show(err, msg):
"""带参数的函数"""
return '错误代码:%s;错误提示:%s' % (err, msg)

show('DBERROR', '数据库出错了')

运行结果:

1
2
3
4
装饰前的操作
错误信息已写入日志:【错误代码:DBERROR;错误提示:数据库出错了】

Process finished with exit code 0

从结果看和我们刚才使用的函数装饰器能同样达到目的。

结论

  • 想要让类的实例对象能够像函数一样进行调用,需要在类里面使用call方法,把类的实例变成可调用对象(callable)
  • 类装饰器装饰函数功能在call方法里面进行添加

带有参数的装饰器

带有参数的装饰器就是使用装饰器装饰函数的时候可以传入指定参数,语法格式: @装饰器(参数,…)

我们先按照自己的思路写下带参数的装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
def logging(func, admin):
def inner(*args, **kwargs):
# 接收错误代码和错误信息
res = func(*args, **kwargs)
print("错误信息已写入日志:【%s】。操作人:%s" % (res, admin))
return inner

@logging('张三')
def show(err, msg):
"""带参数的函数"""
return '错误代码:%s;错误提示:%s' % (err, msg)

show('DBERROR', '数据库出错了')

结果:

1
2
3
4
5
6
Traceback (most recent call last):
File "/Users/tony/PycharmProjects/test_django/装饰器.py", line 244, in <module>
@logging('张三')
TypeError: logging() missing 1 required positional argument: 'admin'

Process finished with exit code 1

为什么错误?装饰器只能接收一个参数,并且还是函数类型。后面我们演示正确的写法。

函数装饰器

解决方案:在装饰器外面再包裹上一个函数,让最外面的函数接收参数,返回的是装饰器,因为@符号后面必须是装饰器实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def decorator(admin):
def logging(func):
def inner(*args, **kwargs):
# 接收错误代码和错误信息
res = func(*args, **kwargs)
print("错误信息已写入日志:【%s】。操作人:%s" % (res, admin))
return inner
return logging

@decorator('张三')
def show(err, msg):
"""带参数的函数"""
return '错误代码:%s;错误提示:%s' % (err, msg)

show('DBERROR', '数据库出错了')

运行结果:

1
2
3
错误信息已写入日志:【错误代码:DBERROR;错误提示:数据库出错了】。操作人:张三

Process finished with exit code 0

总结:使用带有参数的装饰器,其实是在装饰器外面又包裹了一个函数,使用该函数接收参数,返回是装饰器,因为 @ 符号需要配合装饰器实例使用

类装饰器

我们依照函数装饰器加参数的方式来写下类装饰器.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Logging(object):
def __init__(self, author):
self.author = author

def __call__(self, func):
def decorator(*args, **kwargs):
print('装饰前的操作')
res = func(*args, **kwargs)
print("错误信息已写入日志:【%s】,操作人:%s" % (res, self.author))
return decorator


@Logging('李四')
def show(err, msg):
"""带参数的函数"""
return '错误代码:%s;错误提示:%s' % (err, msg)

show('DBERROR', '数据库出错了')

运行结果:

1
2
3
4
装饰前的操作
错误信息已写入日志:【错误代码:DBERROR;错误提示:数据库出错了】,操作人:李四

Process finished with exit code 0

总结,从带参数的类装饰器看,本质上跟函数装饰器一样,都借助了一个外部函数,我们在类里使用的是__call__来作为外部函数使用了。