如何在Python列表推导中避免重复计算?

3
在下面的 Python 代码中:
keyboards = [3, 1]
drivers = [5, 2, 8]
upper_limit = 10
sums = [k + d for k in keyboards for d in drivers if (k + d) <= upper_limit]

我希望能够在列表推导式中存储k+d的结果,以便在列表推导式中引用它。在Python3中是否有可能实现?

我知道我们可以这样做:

sums = []
for k in keyboards:
    for d in drivers:
        s = k + d
        if s <= upper_limit:
            sums.append(s)

但我希望避免使用append操作时产生的副作用。


刚刚发现这个有重复。 - Karl Knechtel
2个回答

6
如果您使用的是Python 3.8或更高版本,则可以使用赋值运算符(也称为海象运算符)在列表推导式内创建新的本地名称并对其进行赋值。对于您的特定示例,您需要在<=比较的左操作数中执行此操作。
sums = [
    s
    for k in keyboards
    for d in drivers
    if (s := k + d) <= upper_limit
]

请注意,在这里必须使用括号将赋值括起来,因为太阳眼镜运算符是所有Python运算符中优先级最低的。如果没有括号,您将把k + d <= upper_limit的结果分配给s,因此是一个布尔值。
请注意,s将在周围的范围内可见,名称s的作用域与sums相同;在函数内部为局部,在模块级别运行列表推导式时为全局。另一方面,kd是列表推导循环的本地变量,并且在推导之外不可见。
演示:
>>> keyboards = [3, 1]
>>> drivers = [5, 2, 8]
>>> upper_limit = 10
>>> [s for k in keyboards for d in drivers if (s := k + d) <= upper_limit]
[8, 5, 6, 3, 9]
>>> s
9
>>> k
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'k' is not defined

在早期的Python版本中,只有在for循环的目标名称中才能使用列表推导式来将其赋值给一个新名称,因此如果您需要一个“本地”变量来引用一个计算,您需要找到添加额外循环的方法。所以在Python 3.7或更早版本中,您可以添加另一个循环来计算k + d的单个元组。
sums = [
    s
    for k in keyboards
    for d in drivers
    for s in (k + d,)
    if s <= upper_limit
]

(k + d,)是一个单元素元组,因此for s in (k + d,)对于每次迭代keyboardsdrivers都只执行一次,有效地将k + d分配给s

您还可以使用生成器表达式为两个嵌套的for循环生成k + d总和,然后迭代该表达式的结果:

sums = [
    s
    for s in (
        k + d
        for k in keyboards
        for d in drivers
    )
    if s <= upper_limit
]

在后一种情况下,您可以先将该表达式存储为单独的变量:
s_calc = (k + d for k in keyboards for d in drivers)
sums = [s for s in s_calc if s <= upper_limit]

使用这些选项,s始终局限于推导循环中,并且在sums作用域级别不可见。它不会“泄漏”出推导表达式。
后面没有需要翻译的内容了。
>>> [s for k in keyboards for d in drivers for s in (k + d,) if s <= upper_limit]
[8, 5, 6, 3, 9]
>>> [s for s in (k + d for k in keyboards for d in drivers) if s <= upper_limit]
[8, 5, 6, 3, 9]
>>> s_calc = (k + d for k in keyboards for d in drivers)
>>> [s for s in s_calc if s <= upper_limit]
[8, 5, 6, 3, 9]

1
对于给定的样例输入,单元素元组循环略微更快(100万次迭代中为1.11秒,而不是1.26秒)。我还没有测试较大的输入。 - Martijn Pieters
在CPython 3.9+中,“将k + d有效地赋给s”实际上意味着“将k + d赋给s”(不建立元组,也不进行迭代)。 - Kelly Bundy
@KellyBundy:是的,就生成的字节码而言,单次迭代的for循环字节码已经被优化掉了。有趣的是,海象运算符导致s被视为全局变量而不是局部变量。如果推导循环在函数作用域中,则它是一个非本地变量,这意味着它是函数作用域的一部分。 - Martijn Pieters

1

从3.8版本开始,您可以使用海象运算符来实现此功能:

sums = [s for k in keyboards for d in drivers if (s := k + d) <= upper_limit]

这个例子中性能优势似乎很小:
$ python -m timeit "[s for k in range(1000) for d in range(1000) if (s := k + d) <= 1000]"
5 loops, best of 5: 72.5 msec per loop
$ python -m timeit "[k + d for k in range(1000) for d in range(1000) if k + d <= 1000]"
5 loops, best of 5: 75.6 msec per loop

k + d 的计算可能需要更复杂的处理才能显示出显著的优势。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接