预备知识

一切皆为对象

在python中一切皆为对象,函数也不例外。

函数闭包

在一些编程语言中,在函数中可以嵌套(定义)另一个函数,如果内部的函数引用了外部函数的变量,则可能产生闭包。闭包可以用来在一个函数与一组“私有”变量之间创建关联,在给定多次调用的过程中,这些私有变量能够保持其持久性。———— 维基百科

上面的定义太过于官方,通俗的将就是当内部的函数被当做对象返回的时候,夹带了外部函数的变量,就形成了一个闭包,闭包将捕捉内部函数执行所需的整个环境。
我们可以将闭包理解为一种特殊的函数,这种函数由两个函数的嵌套组成,且称之为外部函数(外层函数)和内部函数(内层函数),外部函数的返回值是内部函数的引用,此时就形成了闭包。
比如下面的伪代码对闭包格式的描述:

1
2
3
4
def 外层函数(参数):
def 内层函数():
print('内层函数执行', 参数)
return 内层函数

根据以上的伪代码我们可以知道闭包函数的必要条件有:

  • 闭包函数必须返回一个函数对象(函数对象的引用)
  • 闭包函数返回的那个函数必须引用外部变量(一般不能是全局变量),而返回的内层函数不一定需要return值
    几个典型闭包函数实例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # NO.1
    def line_conf_1(a, b):
    def line(x):
    return a * x + b
    return line

    # NO.2
    def line_conf_2():
    a, b = 1, 2
    def line(x):
    print(a * x + b)
    return line

    # NO.3
    def line_conf_3(a, b):
    def line_c(c):
    def line(x):
    return a * (x**2) + b * x + c
    return line
    return line_c

一脸懵逼?没关系,我们先看一下python中的函数作用域,python中的函数作用域由def关键字来进行界定,函数内的代码访问变量的方式是从其所在的层级由内向外的,比如NO.1里面的代码。嵌套函数使用变量a,b但是line函数自身是没有这两个变量的,所以line函数会逐级向外查找,往上走一层就会找到外层函数line_conf_传递的a,b参数。要是一直往外查找,知道全局作用域都没有查到所需要的参数变量的话,就会引发一个异常。

我们来实际调用一下NO.1这个闭包函数来进行分析一下:

1
2
3
4
5
6
# 定义两条直线
line_a = line_conf(2, 1)
line_b = line_conf(3, 2)
# 进行打印
print(line_a(1))
print(line_b(1))

结合上面的实例,不难发现定义的两条直线line_a、line_b实际上是line_conf返回的内部函数line的引用,所以line_a相当于 2x + 1 ,line_b相当于 3x + 2,随后line_a和line_b传入的参数1实际上就是内部函数的参数x。

为什么叫做闭包

先上代码:

1
2
3
4
5
6
7
8
9
def line_conf(a)
b = 1
def line(x):
return a * x + b
return line

line_c = line_conf(2)
b = 20
print(line_c(1))

想象一下print最终打印的值是3还是22?,答案是3,what?,如你所见,line_c对象作为line_conf返回的闭包对象,最终结果仍然引用是line_conf函数内的b的值,并没有使用全局定义的b的值20,这是因为闭包作为对象被返回的时候,它的引用变量就已经确定了(已经保存到closure属性当中了),不会被修改。闭包在被返回时,它的所有变量就已经固定,形成了一个封闭的对象,这个对象包含了其引用的所有外部、内部变量和表达式。当然,闭包的参数例外。说到这里大家可能会问,那么变量b到底有没有被修改呢?答案是当然被修改了啦…

闭包可以保存运行环境

思考下面的代码会输出什么:

1
2
3
4
5
6
7
_list = []
for i in range(3):
def func(a):
return i + a
_list.append(func)
for f in _list:
print(f(1))

结果是1、2、3吗?结果是3、3、3,因为在python中,循环体内定义的函数是无法保存循环执行过程中不断变化的外部变量的,即普通函数无法保存运行环境。
在循环体中使用闭包来进行保存环境变量:

1
2
3
4
5
6
7
8
9
10
_list = []
for i in range(3):
def func(i):
def f_closure(a):
return i + a
return f_closure
_list.append(func(i))

for f in _list:
print(f(1))

好了说了这么多关于闭包的知识,那么闭包有什么用呢?当然是为了给下面的装饰器的实现做铺垫啦。

装饰器

装饰器本质其实就是一个函数, 可以让其它函数不改动源代码的情况下增加其他新功能(amazing …),python中使用@标志符实现对函数的装饰。
装饰器的功能
装饰器的功能是可以在其他函数不需要做任何代码变动的前提下,增加而外的功能,装饰器的返回值也是一个函数对象。
装饰器的使用场景
比如:插入日志,性能测试,事务处理,缓存,权限校验等场景,装饰器是解决此类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能无关的雷同代码并继续使用。

装饰器的执行原理

1. 单个装饰器的