Python 变量绑定与匿名函数

7 投票
2 回答
2508 浏览
提问于 2025-04-15 22:07

我一直在为自动化构建开发一个基本的测试框架。下面这段代码是用来测试两台机器之间用不同程序进行通信的简单测试。在我实际进行测试之前,我想先把所有的测试都定义好,所以下面这个测试在所有测试声明完成之前是不会运行的。这段代码只是一个测试的声明。

remoteTests = []
for client in clients:
    t = Test(
        name = 'Test ' + str(host) + ' => ' + str(client),
        cmds = [
            host.start(CMD1),
            client.start(CMD2),

            host.wait(5),

            host.stop(CMD1),
            client.stop(CMD2),
        ],
        passIf = lambda : client.returncode(CMD2) == 0
    )
remoteTests.append(t)

总之,在测试运行后,它会执行由'passIf'定义的函数。因为我想对多个客户端运行这个测试,所以我在循环中为每个客户端定义一个测试,这没什么大不了的。然而,在对第一个客户端运行测试后,'passIf'却是在客户端列表中的最后一个客户端上进行评估,而不是在lambda声明时的'client'。

我的问题是:Python在lambda中什么时候绑定变量引用?我原以为如果使用外部变量在lambda中是不合法的,解释器就会不知道我在说什么。结果,它却默默地绑定到了最后一个'client'的实例上。

另外,有没有办法强制按照我想要的方式来解决这个问题?

2 个回答

5

发生的事情是,你的 passIf 参数,也就是那个 lambda 表达式,引用了外部作用域中的 变量 client。它并不是指创建时 client 变量所指向的对象,而是指这个变量本身。如果你在循环结束后调用这些 passIf,那么它们都会指向循环中的最后一个值。(在闭包的术语中,Python 的闭包是 后期绑定,而不是 前期绑定。)

幸运的是,把后期绑定的闭包变成前期绑定的闭包其实很简单。你只需要给 lambda 表达式一个参数,并将你想要绑定的值作为默认值传入:

passIf = lambda client=client: client.returncode(CMD2) == 0

这确实意味着这个函数会多一个参数,如果不小心传入了一个参数,可能会搞乱事情——或者当你希望这个函数接受任意参数时。所以另一种方法是这样做:

# Before your loop:
def make_passIf(client):
    return lambda: client.returncode(CMD2) == 0

# In the loop
t = Test(
    ...
    passIf = make_passIf(client)
)
10

这个client变量是在外部定义的,所以当lambda被执行时,它总是会被设置为列表中的最后一个客户端。

为了得到想要的结果,你可以给lambda传一个带默认值的参数:

passIf = lambda client=client: client.returncode(CMD2) == 0

因为默认值是在定义lambda的时候就计算出来的,所以它的值会保持正确。

另一种方法是在一个函数内部创建lambda

def createLambda(client):
    return lambda: client.returncode(CMD2) == 0
#...
passIf = createLambda(client)

在这里,lambda引用的是createLambda函数中的client变量,这样它的值就是正确的。

撰写回答