带有 lambda 的 Python 列表理解 [重复]

Posted

技术标签:

【中文标题】带有 lambda 的 Python 列表理解 [重复]【英文标题】:Python list comprehension with lambdas [duplicate] 【发布时间】:2015-03-31 20:24:07 【问题描述】:

我正在运行 Python 3.4.2,但我对代码的行为感到困惑。我正在尝试创建一个具有递增程度的可调用多项式函数列表:

bases = [lambda x: x**i for i in range(3)]

但出于某种原因,它会这样做:

print([b(5) for b in bases])
# [25, 25, 25]

为什么bases 似乎是最后一个 lambda 表达式的列表,在列表推导中,重复了?

【问题讨论】:

您可能会觉得这很有帮助:Gotcha: Python, scoping, and closures unutbu:知道如何让它工作吗? 也许你想要的是bases = lambda x:[x**i for i in range(3)] @user3467349 这完全不同。然后你会做base_results = bases(5) @AdamSmith 我明白了......,那么据我所知,这是一个解释器错误......“延迟”。这应该 raise 无效(这可能是合理的)或使用适当的迭代器分配...... 【参考方案1】:

问题是classic "gotcha",是 lambda 函数中引用的i 是not looked up until the lambda function is called。那时i的值是它的最后一个值 绑定到for-loop 结束时,即2

如果您将i 绑定到lambda 函数定义中的默认值,则每个i 都将成为一个局部变量,并在lambda 被计算时评估其默认值并将其绑定到函数定义而不是调用。

因此,当调用 lambda 时,i 现在会在 本地范围 中查找,并使用其默认值:

In [177]: bases = [lambda x, i=i: x**i for i in range(3)]

In [178]: print([b(5) for b in bases])
[1, 5, 25]

供参考:

Python scopes and namespaces

【讨论】:

整洁 (+1)。如果答案解释了为什么原始代码不起作用以及为什么会起作用,那就太棒了。谢谢。 有什么好的理由让python像这样解释代码吗?这相当模糊...... @user3467349:好吧,目前,Python 使用dynamic name resolution。但是the docs state,“语言定义正在向静态名称解析发展”,所以将来可能会改变规则。 @user3467349 该原理称为closure,有些人认为这种行为很有用。 但是您可以使用 bases = [lambda x: x**($n-1) for i in range($n)] 复制该行为(其中 $n 是一个预设值) - 所以不清楚为什么这个特定的应用程序 需要 像它那样运行。 (如果有一种非混淆方式来做某事,那么在混淆方式上引发错误几乎总是更好)。【参考方案2】:

更“pythonic”的方法: 使用嵌套函数

def polyGen(degree):
    def degPolynom(n):
        return n**degree
    return degPolynom

polynoms = [polyGen(i) for i in range(5)]
[pol(5) for pol in polynoms]

输出:

>> [1, 5, 25, 125, 625]

【讨论】:

【参考方案3】:

作为替代解决方案,您可以使用偏函数:

>>> bases = [(lambda i: lambda x: x**i)(i) for i in range(3)]
>>> print([b(5) for b in bases])
[1, 5, 25]

与@unutbu 给出的经典解决方案相比,该构造的唯一优势在于,您不能通过使用错误数量的参数调用函数来引入偷偷摸摸的错误:

>>> print([b(5, 8) for b in bases])
#             ^^^
#             oups
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <listcomp>
TypeError: <lambda>() takes 1 positional argument but 2 were given

正如 Adam Smith 在下面的评论中所建议的那样,您可以使用 functools.partial 而不是使用“嵌套 lambda”,具有相同的好处:

>>> import functools
>>> bases = [functools.partial(lambda i,x: x**i,i) for i in range(3)]
>>> print([b(5) for b in bases])
[1, 5, 25]
>>> print([b(5, 8) for b in bases])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <listcomp>
TypeError: <lambda>() takes 2 positional arguments but 3 were given

【讨论】:

如果你要构建部分函数,​​你应该使用functools.partial! @Adam 是的。你说的对。我已经相应地编辑了我的答案。使用partial 时困扰我的一件事是错误消息报告了“错误”数量的所需位置参数。但对于更易于理解的语法来说,这是一个相当小的问题。【参考方案4】:

我认为问题的“为什么会发生”方面尚未得到回答。

在函数中命名非本地名称不被视为常量的原因是这些非本地名称将匹配全局名称的行为。也就是说,函数创建后全局名称的变化会在函数被调用时观察到。

例如。

# global context
n = 1
def f():
    return n
n = 2
assert f() == 2

# non-local context
def f():
    n = 1
    def g():
        return n
    n = 2
    assert g() == 2
    return g
assert f()() == 2

您可以看到,在全局和非本地上下文中,如果名称的值发生更改,那么该更改将反映在将来调用引用该名称的函数中。如果对全局变量和非局部变量进行不同的处理,那将是令人困惑的。因此,行为是一致的。如果您需要将名称的当前值设置为新函数的常量,那么惯用的方法是将函数的创建委托给另一个函数。该函数是在创建函数的范围内创建的(没有任何变化),因此名称的值不会改变。

例如。

def create_constant_getter(constant):
    def constant_getter():
        return constant
    return constant_getter

getters = [create_constant_getter(n) for n in range(5)]
constants = [f() for f in getters]
assert constants == [0, 1, 2, 3, 4]

最后,作为附录,函数可以修改非本地名称(如果名称被标记为这样),就像它们可以修改全局名称一样。例如。

def f():
    n = 0
    def increment():
        nonlocal n
        n += 1
        return n
    return increment
g = f()
assert g() + 1 == g()

【讨论】:

很好的解释,感谢您告诉我有关“非本地”的信息!

以上是关于带有 lambda 的 Python 列表理解 [重复]的主要内容,如果未能解决你的问题,请参考以下文章

如何创建 lambda 列表(在列表理解/for 循环中)?

如何创建 lambda 列表(在列表理解/for 循环中)?

奇怪的行为:列表理解中的 Lambda

Python的带有下划线的lambda作为参数?

python lambda引发带有多个参数的变量未定义错误

带有 lambda 函数和标量的字典理解