python

Python可变参数、闭包陷阱说明及解决方案

文章暂存

systemime
2021-05-25
6 min

python 函数中可变参数类型及闭包延迟绑定问题一直是初学者常见且面试常问的问题,那么为什么以及如何解决

# 1 可变默认参数

基于 Python 的灵活性,在函数定义参数时,允许我们定义可变参数类型,当然在标准代码规范中,这是一种不建议的行为,比如下面这段代码

def append_to(element, to=[]):
    to.append(element)
    return to

my_list = append_to(12)
print(my_list)

my_other_list = append_to(42)
print(my_other_list)

# 1.1 期望结果

上述代码不难看出,预期(假设预期)想将一个数字添加到列表中,然后返回,得到的应该是这样的结果

[12]
[42]

# 1.2 实际结果

实际上,在 Python 中,函数被定义时,一个新的列表就被创建一次,每次调用函数时,使用均是同一个列表(函数中内容是调用完毕被销毁的局部变量,但是这里的列表属于函数对象本身的内容,并不会被销毁

所以实际使用中得到了这样的结果:

[12]
[12, 42]

# 1.3 解决方案

这是一个相对简单的修改方案,只需要修改默认的参数即可(假设你的需求与我上述期望一致的情况下,因为存在需要可变参数类型的需求,请往下阅读 1.4 内容

def append_to(element, to=None):
    if to is None:
        to = []
    to.append(element)
    return to

# 1.4 需要可变参数的情况 (利用 "陷阱")

什么情况下陷阱不是陷阱?

有时您可以专门 “利用”(或者说特地使用)这种行为来维护函数调用间的状态。这通常用于 编写缓存函数。

感兴趣的同学可以自己实现一个缓存列表,用来存放一些计算结果或任务

# 2 闭包陷阱 —— 延迟绑定闭包

初学者经常遇到的另一个问题是 Python 在闭包 (或在周围全局作用域 surrounding global scope) 中 绑定变量的方式,将一个函数 (或匿名函数) 做对象传递时,忽视变量实际传递结果

# 2.1 代码示例

def create_multipliers():
    return [lambda x : i * x for i in range(5)]

# 2.2 期望结果

上述代码不难看出,利用列表推导式生成了一个匿名函数计算列表,传入参数后得到这样的结果

# x为传入的参数
[0 * x, 1 * x, 2 * x, 3 * x, 4 * x]


理想很美好,现实很骨感,你得到的是实际上只有

[4 * x, 4 * x, 4 * x, 4 * x, 4 * x]

比如:

for multiplier in create_multipliers():
    print(multiplier(2))


0
2
4
6
8


8
8
8
8
8

# Why?

为什么会这样?这也是一个常见的面试考点

**谨记:Python 的闭包是 迟绑定 **
这意味着闭包中用到的变量的值,是在内部函数被调用时查询得到的。

如何理解这句话?结合上面 2.1 的例子,可以看出,我们定义的列表中,lambda 函数在使用时才会去查询 i 的值,然而,在我们使用时,i 作为循环变量,在循环结束后,他的值很明显是 range(0, 4) 的最后一个值 4

所以,当你使用时,得到的结果都会 乘以 4


小明:那我选择不用 lambda,反正可读性不好还麻烦
老师:滚出去


开个玩笑,再看个例子

def create_multipliers():
    multipliers = []

    for i in range(5):
        def multiplier(x):
            return i * x
        multipliers.append(multiplier)

    return multipliers

这里把 lambda 写成实际的函数,还会发生这种情况吗?当然会

这个问题是由闭包引起,而不是 lambda,请回忆一下闭包的定义

闭包,又称闭包函数或者闭合函数,和嵌套函数类似,不同之处在于,闭包中外部函数返回的不是一个具体的值,而是一个函数。
在函数嵌套的前提下,内部函数使用了外部函数的变量,并且外部函数返回了内部函数,我们把这个使用外部函数变量的内部函数称为闭包。
一般情况下,返回的函数会赋值给一个变量,这个变量可以在后面被继续执行调用。
闭包的内部变量对于外界来讲是完全隐藏的

OK,回到上面的代码,multiplier 在闭包函数内部,在使用时,通过multipliers 列表拿到的还是一个等待调用的函数对象,其中的 i 仍然是循环结束后的最后一个值 —— 4

# 2.3 解决方案

# 2.3.1 立即赋值

最一般的解决方案可以说是有点取巧(hack)。由于 Python 拥有在前文提到的为函数默认参数 赋值的行为,可以创建一个立即绑定参数的闭包, 像下面这样:

def create_multipliers():
    return [lambda x, i=i : i * x for i in range(5)]

# 2.3.2 使用 functools.partial

from functools import partial
from operator import mul

def create_multipliers():
    return [partial(mul, i) for i in range(5)]

# 2.4 什么情况下陷阱不是陷阱

有时你就想要闭包有如此表现,迟绑定在很多情况下是不错的。不幸的是,循环创建 独特的函数是一种会使它们出差错的情况

上次编辑于: 5/25/2021, 6:54:57 AM