手记

根据面试题谈谈 Python 闭包

题目:

1.什么是闭包?闭包的定义?

2.手写一个简单的闭包

3.变量作用域规则与 nonlocal 关键字?

4.闭包的应用


答案要点:

1.首先,我们要了解变量作用域,上代码:

# 示例一
def test1(a):
    print(a)
    print(b)  # 当函数执行到这一步时会报错。
              # NameError: global name 'b' is not defined
test1(1) 

# 示例二
b = 6
def test2(a):
    print(a)
    print(b) 
test2(1)  # 1 6

# 示例三
b = 6
def test3(a):
    print(a)
    print(b)  # 当函数执行到这一步仍然会报错
              # UnboundLocalError: local variable 'b' referenced before assignment
    b = 9     # 比示例二多了一行赋值
test3(1)

学过其他语言,比如 Java ,对示例三的结果会比较惊讶,在 Java 中类似的情况,不会报错,会引用外部的全局变量,而如果在内部重新赋值后,再次使用则会用局部变量的值。而在 Python 中情况则不一样,它在编译函数时,发现对 b 有赋值的操作,它判定 b 是一个局部变量,所以在打印 b 时,它会去查询局部变量b,发现并没有赋值,所以会抛出异常。

引用《流畅的Python》中对此的解释:

这不是缺陷,而是设计选择:Python 不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。这比 JavaScript 的行为要好多了,JavaScript 也不要求声明变量,但是如果忘记把变量声明为局部变量(使用var),可能会在不知情的情况下获取全局部变量。

上段话第一次看可能会有点不明白,其实简单来说,Python 就是这样设计的,它认为在函数体中,如果对变量有赋值操作,则证明这个变量是一个局部变量,并且它只会从局部变量中去读取数据。这样设计可以避免我们在不知道的情况下,获取到全局变量的值,从而导致一些错误数据的出现。

至于解决方法,就是使用 global 关键字,来说明我们使用的是 全局变量 。示例如下:

b = 6
def test4(a):
    print(a)
    global b  # 1
    print(b)  # 6
    b = 9 
    print(b)  # 9    
test4(1)

2.闭包的定义:简单来说,闭包的概念就是当我们在函数内定义一个函数时,这个内部函数使用了外部函数的临时变量,且外部函数的返回值是内部函数的引用时,我们称之为闭包。有点绕,上代码:

# 一个简单的实现计算平均值的代码

def get_avg():
    scores = []  # 外部临时变量

    def inner_count_avg(val):  # 内部函数,用于计算平均值
        scores.append(val)  # 使用外部函数的临时变量
        return sum(scores) / len(scores)  # 返回计算出的平均值

    return inner_count_avg  # 外部函数返回内部函数引用

avg = get_avg()
print(avg(10))  # 10
print(avg(11))  # 10.5
...

3.nonlocal 关键字。上面的代码,有一个小缺陷,有很多重复的计算,当我们传入一个新的值想要得到新的平均值时,其他前一次的总和是可以通过外部临时变量存储的。于是我们很自然的想到下面的代码:

# 一个简单的实现计算平均值的代码改进版一

def get_avg():
    scores = 0  # 将外部临时变量由 list 改为一个 整型数值
    count = 0   # 同时新增一个变量,记录个数

    def inner_count_avg(val):  # 内部函数,用于计算平均值
        scores += val  # 使用外部函数的临时变量
        count += 1
        return scores / count  # 返回计算出的平均值

    return inner_count_avg  # 外部函数返回内部函数引用

avg = get_avg()
print(avg(10))  # 报错
...

这里报错的原因,请看第 1 点:变量的作用规则。因为 scores += val ,其实就是 scores = scores + val,有了赋值操作,则认为 scores 是局部变量了。而我们也没办法使用 global 关键字,因为此时 scores 和 count 是定义在 get_ave 函数内的,它们俩也是一个局部变量。而为什么我们使用 list 时,没有出现这个问题呢?也是很好理解的,因为我们使用的是 list.append() 方法,它没有赋值操作。你可以简单认为,可变对象(即我们可以通过调用自身一些方法去做增删改操作且变量地址值不变)不存在此问题,而不可变对象则会有。

在 Python 3 中引入了一个关键词 nonlocal 解决了这一个问题:

# 一个简单的实现计算平均值的代码改进版二

def get_avg():
    scores = 0  # 将外部临时变量由 list 改为一个 整型数值
    count = 0   # 同时新增一个变量,记录个数

    def inner_count_avg(val):  # 内部函数,用于计算平均值
        nonlocal count, scores
        scores += val  # 使用外部函数的临时变量
        count += 1
        return scores / count  # 返回计算出的平均值

    return inner_count_avg  # 外部函数返回内部函数引用

avg = get_avg()
print(avg(10))  # 报错

你也许会说,那在 Python 2 的环境下应该怎么解决呢?恩,其实也是有办法的,思路就是将不可变对象变为可变对象即可。代码如下:

# -*- coding:utf-8 -*-
class Score:
    pass


def get_avg():
    s = Score()  # 使用类对象
    s.scores = 0.0  # 注意 Python 2 中整数除法是舍弃小数的,所以要定义为浮点数
    s.count = 0

    def inner_get_avg(val):
        s.count += 1
        s.scores += val
        return s.scores / s.count

    return inner_get_avg


avg = get_avg()
print(avg(10))  # 10.0
print(avg(11))  # 10.5

4.闭包的应用:首先是装饰器,装饰器就是通过修改被装饰函数,来达到增加新功能的作用。当我们在内部函数去修改被装饰函数时,大部分情况都会使用到闭包。简单示例:

def decorator(func):  #  外部函数的局部变量 func
    def wrapper(*args, **kwargs):  # 接受被包装函数传入过来的参数
        return func(*args, **kwargs)  # 使用外部函数的局部变量 func
    return wrapper

@decorator
def basic_func(name):
    print 'my name is', name

# 等价于
decorator_func(func)

另外一个应用由之前求平均值的示例也可以看出来,可以在重复计算时提高效率。其次还有一个比较重要的应用场景,就是利用“惰性求值”这一特性,这一点在 Django 的 QuerySet 里有体现。当我们利用 ORM 去做 SQL 查询时,很多时候会根据不同的判断条件,去加 filter,加 filter 的时候并没有真正做查询,在最终获取结果的时候才真正执行了查询。这一点有兴趣的可以去看下源码。

6人推荐
随时随地看视频
慕课网APP