Skip to content

闭包和装饰器

命名空间

变量在声明时所在的作用域,也叫作local namespace(当前函数命名空间)
函数在声明时所在的作用域是可以层层向外层搜索的
不同的函数作用域中区分声明和赋值:

python
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 中的变量
  • 闭包函数必须返回内嵌函数

闭包示例

shell
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

python
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'

shell
>>> bar.__name__
'wrapper'

使用装饰器functools.wraps修改马甲,完整版如下

shell
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),可以把函数分两次调用,每次传一部分参数的函数。

python
def foo(x, y):
    return x + y

高阶函数functools.partial可以帮助我们创建一个偏函数

shell
>>> import functools
>>> bar = functools.partial(foo, y=-100)
>>> bar(50)
-50
>>> bar(x=100)
0

对于没有作用域的 for 语句块,利用偏函数构造闭包:

python
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

排序sortedlist.sort也是高阶函数,它们都返回列表。

shell
arr = [2,5,3]
>>> list.sort(arr)
>>> arr
[2, 3, 5]

在 python2 中排序接受 cmp 参数。而在 python3 中取消了 cmp,因为 key 的惰性取值会提高性能。

key

在 python3 中只有关键字参数 key 和 reverse

shell
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,本质是重载了比较运算符。

shell
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函数来实现

python
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)]