闭包和装饰器
命名空间
变量在声明时所在的作用域,也叫作local namespace
(当前函数命名空间)
函数在声明时所在的作用域是可以层层向外层搜索的
不同的函数作用域中区分声明和赋值:
global name # 全局作用域
nonlocal name # 非全局的相比更外层的作用域
Python 中通过提供 namespace 来实现重名函数/方法、变量等信息的识别,其一共有三种 namespace,分别为:
- local namespace: 作用范围为当前函数或者类方法
- global namespace: 作用范围为当前模块
- build-in namespace: 作用范围为所有模块
当函数/方法、变量等信息发生重名时,Python 会按照local namespace
-> global namespace
-> build-in namespace
的顺序搜索用户所需元素,并且以第一个找到此元素的 namespace 为准。
同时,Python 中的内建函数locals()
和globals()
可以用来查看不同 namespace 中定义的元素。
闭包
闭包是函数作用域的体现
装饰器本质就是函数,功能是为其他函数添加附加功能
闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,闭包是由函数和与其相关的引用环境组合而成的实体。
闭包的原理,当内嵌函数引用了包含它的函数(enclosing function)中的 non-local 变量后,这些变量会被保存在 enclosing function 的
__closure__
属性中,成为 enclosing function 本身的一部分;也就是说,这些变量的生命周期会和 enclosing function 一样。在 Python 中创建一个闭包可以归结为以下三点:
- 闭包函数必须有内嵌函数
- 内嵌函数需要引用该嵌套函数上一级 namespace 中的变量
- 闭包函数必须返回内嵌函数
闭包示例
def foo():
step = 0
def add1():
nonlocal step
step += 1
print(step)
return add1
#foo函数执行会返回一个累加作用的函数add1
>>> foo()()
1
>>> foo()()
1
>>> foo()()
1
#函数执行结束作用域立即销毁 得利于python解释器的自动回收垃圾的机制
>>> a=foo()
>>> b=foo()
>>> a()
1
>>> [str(c.cell_contents) for c in a.__closure__][0]
1
>>> a()
2
>>> [str(c.cell_contents) for c in a.__closure__][0]
2
>>> a()
3
>>> [str(c.cell_contents) for c in a.__closure__][0]
3
>>> b()
1
>>> b()
2
>>>
#闭包让作用域不会被销毁
#__closure__属性拿到所引用的内部变量的内存地址(只做了解)
由于内存得不到自动释放,这里的 foo 执行会生成互不影响的累加器。
这与之后章节的生成器具有相同的功能。
开放封闭原则
不修改被修饰函数的源代码 不修改被修改函数的调用方式
装饰器
在 python 中,装饰器(Decorator
)就是一个返回函数的高阶函数,修改了目标函数的功能且符合开放封闭原则
装饰器的实现
利用:
- 高阶函数或实现了
__call__
的类 - 闭包或类的封装性
bar 声明的前一行@foo
def foo(func):
def wrapper(*args, **kwargs):
...
ret = func(*args, **kwargs)
...
return ret
return wrapper
@foo
def bar(): ...
相当于bar = foo(bar)
,这样,在不改变bar
调用方式的情况下增加了bar
的功能
但是 ,bar
的函数名却被修改为了'wrapper'
>>> bar.__name__
'wrapper'
使用装饰器functools.wraps
修改马甲,完整版如下
from functools import wraps
def foo(func):
@wraps(func)
def wrapper(*args, **kwargs):
...
ret = func(*args, **kwargs)
...
return ret
return wrapper
@foo
def bar(): ...
事实上要实现装饰器 @foo
只需要实现 foo(...)(...)
括号既可以是函数也可以是实例化一个类
更复杂的装饰器比如 @foo.bar(...)
也只是在这个基础上套娃。
偏函数
不同于数学意义,偏函数(Partial function),可以把函数分两次调用,每次传一部分参数的函数。
def foo(x, y):
return x + y
高阶函数functools.partial
可以帮助我们创建一个偏函数
>>> import functools
>>> bar = functools.partial(foo, y=-100)
>>> bar(50)
-50
>>> bar(x=100)
0
对于没有作用域的 for 语句块,利用偏函数构造闭包:
f = [lambda: x for x in range(3)]
f[0]() # 2
f[1]() # 2
f[2]() # 2
import functools
g = [functools.partial(lambda x: x, x) for x in range(3)]
g[0]() # 0
g[1]() # 1
g[2]() # 2
cmp
排序sorted
和list.sort
也是高阶函数,它们都返回列表。
arr = [2,5,3]
>>> list.sort(arr)
>>> arr
[2, 3, 5]
在 python2 中排序接受 cmp 参数。而在 python3 中取消了 cmp,因为 key 的惰性取值会提高性能。
key
在 python3 中只有关键字参数 key 和 reverse
arr = [(1,3),(2,2),(3,0)]
# 以成员[1]大小排序
>>> sorted(arr, key=lambda x:x[1])
[(3, 0), (2, 2), (1, 3)]
其中 reverse 接受布尔值表示是否倒序,key 接受一个函数,在下次比较时不重新计算 key。
cmp_to_key
cmp_to_key 可以兼容 python2 的 cmp 函数作为 key,本质是重载了比较运算符。
from functools import cmp_to_key
# 以成员求和后的大小排序
arr = [(1,3),(2,2),(3,0)]
>>> sorted(arr, key=cmp_to_key(lambda a,b:sum(a) - sum(b)))
[(3, 0), (1, 3), (2, 2)]
itemgetter
想用 sorted 函数对某一个非数值型的对象序列进行排序(根据元组中第三个值和第二个值来排序,如果第二个值相同,则按照第三个值进行排序),可以利用operator.itemgetter
函数来实现
from operator import itemgetter
a = [("john", "A", 15), ("jane", "B", 12), ("dave", "B", 10)]
sorted(a, key=itemgetter(1, 2))
# [('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]