Python 装饰器

函数名的使用

函数名是一个变量,但它是⼀个特殊的变量,与括号配合可以执行函数。

1
2
3
4
5
def func():
print('呵呵')

# 查看函数名的内存地址
print(func) # <function func at 0x000001F47C6E3E18>

一、函数名可以作为变量来使用

1
2
3
4
5
def func():
print('呵呵')

a = func # 把函数当成变量赋值给另外一个变量
a() # 通过变量a调用函数

二、作为容器的元素

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
def func1():
print('func1')

def func2():
print('func2')

def func3():
print('func3')

def func4():
print('func4')

lst = [func1, func2, func3, func4]
for i in lst:
i()

######################################################
lst2 = [func1(), func2(), func3(), func4()]
print("lst2的结果:", lst2)
# 若没有给函数设置返回值,函数默认返回None
# 执行结果
"""
func1
func2
func3
func4
lst2的结果: [None, None, None, None]
"""

三、作为参数使用

1
2
3
4
5
6
7
8
9
def func1():
print('func1')

def func2(arg):
print('start')
arg() # 执行传递进来的arg
print('end')

func2(func1) # 把func1当成参数传递给func2

四、作为返回值使用

1
2
3
4
5
6
7
8
9
def func1():
print('这里是func1')

def func2():
print('这里是func2')
return func2 # 把func2当成返回值返回

ret = func1() # 调用func1,把返回值赋值给ret,即ret = func2
ret() # 调用ret,即func2()

闭包

一、闭包的概念

一个内层函数中,引用了外层函数(非全局)的变量,这个内层函数就可以成为闭包。

1
2
3
4
5
6
7
8
def func():
name = "cdc"
def inner(): # 内层函数inner使用到了外层函数func的变量name,此时inner就是一个闭包
print(name)
return inner

ret = func()
ret()

二、闭包的作用

2.1 保护变量,防止变量被修改

将变量定义在全局是十分不安全的,在函数内部可以通过 global 关键字随意修改全局变量的值

1
2
3
4
5
6
7
8
9
10
11
12
13
name = "cdc"

def func1():
global name
name = "tr" # 全局变量被重新赋值

def func2():
if name == "cdc":
print("OK")
else:
print("NO")

# 执行结果为 NO

​ 闭包是将变量定义在外层函数内部,即便想要修改变量值,也只能使用 nonlocal 关键字在当前作用域内进行修改

1
2
3
4
5
6
7
def func1():
name = "cdc"
def inner():
nonlocal name
name = "tr" # 修改的是func1作用域内的变量,且只能在func1内部进行修改,其余函数操作无法修改name的值
return inner

2.2 让变量在内存中常驻

​ 对于一般的函数而言,在声明函数时只是将函数名放入命名空间,当调用函数时,才为函数内部的变量或者嵌套函数开辟新的内存,当函数调用结束后,函数内部的变量就会释放内存;当下一次 调用函数时,再重新为函数内部变量开辟新的内存。因此,重复调用复杂的函数时,会消耗一定的时间。

​ 对于 闭包函数而言,内层函数需要调用外层函数的变量,一旦外层函数的变量被释放,内层函数将无法执行,因此为了能让内层函数正常运行,python解释器会让外层的变量常驻在内存中。

1
2
3
4
5
6
7
8
def func1():
name = "cdc"
def inner():
print(name)
return inner

ret = func1()
ret()

上述示例中,当 func1 函数执行结束后,理论上 name 变量将被释放,但是一旦 name 被释放,内层的 inner 将无法正常执行,因此 python 解释器会将 name 变量常驻内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 闭包在爬虫中的应用
from urllib.request import urlopen

def craw_web():
content = urlopen("http://www.xiaohua100.cn/index.html").read()
def inner():
return content
return inner

fn = craw_web() # 这个时候就开始加载校花100的内容

content = fn() # 第一次获取内容
print(content)

content2 = fn() # 重新获取内容
print(content2)

# 第一次获取内容所需时间较长,因为需要发送请求和接收响应返回的信息
# 往后再获取内容时速度就会很快,因为第一次获取的内容会常驻在内存中,后面获取内容不需要重新请求,只需要去内存中获取,因此速度很快
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"""
小练习——编写代码实现func函数,使其实现以下效果:
foo = func(8)
print(foo(8)) # 输出64
print(foo(-1)) # 输出-8
"""

def func(a):
def inner(b):
return a * b
return inner

foo = func(8)
print(foo(-1))
print(foo(8))

可以使用__closure__来检测一个函数是否闭包,若有返回值,则代表该函数是闭包,若返回None,则该函数不是闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def func1():
name = "cdc"
def inner1():
print(name)
print(inner1.__closure__) # (<cell at 0x0000022663B68468: str object at 0x00000226657AB0A0>,),inner1 已经闭包
return inner1

ret1 = func1()
ret1()


def func2():
name = "cdc"
def inner2():
print("哈哈")
print(inner2.__closure__) # None inner2 未闭包
return inner2

ret2 = func2()
ret2()

装饰器

一、软件设计的原则

​ 开放封闭原则(开闭原则):开放封闭原则是指对扩展代码的功能是开放的,但是对修改源代码是封闭的。这样的软件设计思路可以保证我们更好的开发和维护我们的代码。

二、装饰器原理

​ 本质上就是一个函数,可用于在不改变函数的调用方式以及源码的基础上,为函数增加新的功能。

我们先来写一个例子,模拟一下约妹子出来吃饭:

1
2
3
4
def yue():
print("给妹子发消息,约妹子出来吃饭")

yue()

妹子的联系途径多种多样,我们可以通过微信来联系妹子:

1
2
3
4
5
def yue():
print("打开微信")
print("给妹子发消息,约妹子出来吃饭")

yue()

显然,我们已经违背了开闭原则,对原来的函数源码进行了修改。况且社交工具多种多样,想要通过不同的途径来联系妹子,每一次都需要修改源码,十分的麻烦,因此我们可以再写一个函数:

1
2
3
4
5
6
7
8
def yue():
print("给妹子发消息,约妹子出来吃饭")

def with_wechat():
print("打开微信")
yue()

with_wchat()

虽然我们避免了修改源码,但是当换一种社交工具时,又得再写新的函数。总结一句话就是如何在不改变函数的结构和调用方式的基础上,动态的给函数添加功能?可以用闭包的方法尝试一下:

1
2
3
4
5
6
7
8
9
10
11
def yue():
print("给妹子发消息,约妹子出来吃饭")

def with_tools(func):
def inner():
print(f"打开微信")
func()
return inner

ret1 = with_tools(yue)
ret1()

闭包虽然为改变原函数的结构,但还是改变了原来的调用方式,我们再稍作改动

1
2
3
4
5
6
7
8
9
10
11
def yue():
print("给妹子发消息,约妹子出来吃饭")

def with_tools(func):
def inner():
print(f"打开微信")
func()
return inner

yue = with_tools(yue)
yue()

将 with_tools 的赋值给一个变量 yue,再调用 yue 就恢复到了我们最开始的调用方式了,一切问题完美解决,其实这就是装饰器的一个雏形。看一下它的执行过程吧:

  • 首先访问with_tools(“微信”, yue)
  • 把yue函数赋值给了with_tools函数的形参func,记住后续执行func的话实际上是执行了最开始传入的yue函数。
  • with_tools函数执行过程就是一句话,返回了inner函数。这个时候把inner函数赋值给了yue这个变量
  • 执行yue的时候,相当于执行了inner函数,先打印打开微信再执行func,也就是我们最开始传入的yue函数

Python 中针对于上面的功能提供了一个快捷的写法,俗称装饰器语法糖。使用装饰器语法糖的写法,实现同样功能的代码如下:

1
2
3
4
5
6
7
8
9
10
11
def with_tools(func):
def inner():
print(f"打开微信")
func()
return inner

@with_tools
def yue():
print("给妹子发消息,约妹子出来吃饭")

yue()

三、装饰器的使用

3.1 装饰有返回值的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def func(fn):             # 接收的参数是一个函数名
def inner(): # 定义一个内层函数
print("我是cdc") # 新功能
ret = fn() # 在内层函数中拿到被装饰函数的结果
print("再见") # 新功能
return ret # 返回被装饰函数的执行结果
return inner

# 定义一个有返回值的函数
@func
def say_hello():
return "你好"

res = say_hello() # 调用被装饰函数并拿到结果
print(res)

3.2 装饰带参数的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def foo(func):  # 接收的参数是一个函数名
def inner(x, y): # 这里需要定义和被装饰函数相同的参数
print("这里是新功能...") # 新功能
func(x, y) # 被装饰函数名和参数都有了,就能执行被装饰函数了
return inner

# 定义一个需要两个参数的函数
@foo
def f1(x, y):
print("{}+{}={}".format(x, y, x+y))


# 调用被装饰函数
f1(100, 200)

###################################################
# 由于不知道参数实参究竟是怎么传的,因此可以把形参设置成可变长参数,这样就可以接收所有情况的参数了
def foo(func): # 接收的参数是一个函数名
def inner(*args, **kwargs): # 这里需要定义和被装饰函数相同的参数
print("这里是新功能...") # 新功能
func(*args, **kwargs) # 被装饰函数名和参数都有了,就能执行被装饰函数了
return inner

3.3 装饰器自身带参数

被装饰的函数可以带参数,装饰器同样也可以带参数。如果想让装饰器也带上参数,就必须在原来的闭包函数最外层再套一层函数,该函数就用于接收装饰器带来的参数,供内层函数使用。

1
2
3
4
5
6
7
8
# 带参数的装饰器需要定义一个三层的嵌套函数
def d(name): # d是新添加的最外层函数,为我们原来的装饰器传递参数,name就是我们要传递的参数
def f1(func): # f1是我们原来的装饰器函数,func是被装饰的函数
def f2(*arg, **kwargs): # f2是内部函数,*args和**kwargs是被装饰函数的参数
print(name) # 使用装饰器函数的参数
func(*arg, **kwargs) # 调用被装饰的函数
return f2
return f1
1
2
3
4
5
6
7
8
9
10
11
12
13
def with_tools(tool):
def inner1(func):
def inner2(*args, **kwargs):
print(f"打开{tool}")
func(*args, **kwargs)
return inner2
return inner1

@with_tools("微信")
def yue():
print("给妹子发消息,约妹子出来吃饭")

yue()

3.4 最完整的装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
def func(a = None):
def inner1(fn):
def inner2(*args, **kwargs):
print("新功能")
ret = fn(*args, **kwargs)
print("新功能")
return ret
return inner2
return inner1

@func()
def foo():
pass

3.5 装饰器的修复技术

被装饰的函数最终都会失去本来的__doc__等信息, Python给我们提供了一个修复被装饰函数的工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def with_tools(func):
@wraps(func)
def inner():
print(f"打开微信")
func()
return inner

@with_tools
def yue():
print("给妹子发消息,约妹子出来吃饭")

yue()
print(yue.__doc__)
print(yue.__name__)

Python 装饰器
https://clark-cdc.github.io/2019/04/10/0007-装饰器/
作者
clark
发布于
2019年4月10日
许可协议