列表推导的要点是生成一个结果值列表,每个源值一个(或者每个匹配源值一个,如果您有if 子句)。
换句话说,它与map (或map 和filter 调用链,如果您有多个子句)相同,除了您可以将每个新值描述为表达式旧值,而不必将其包装在函数中。
您不能将语句(如mysum = mysum + i)放入理解中,只能放入表达式。而且,即使你能想出一个具有你想要的副作用的表达式,这仍然是对理解的令人困惑的误用。如果您不想要结果值列表,请不要使用列表推导式。
如果您只是想在循环中执行计算,请编写显式 for 循环。
如果你真的需要它是一行,你总是可以这样做:
for i in [i for i in range(2, 10) if i%2==0 or i%5==0]: mysum += i
用理解构建要循环的事物列表;在for 循环中进行副作用-y 计算。
(当然,这是假设您已经在 mysum 中获得了一些价值来添加,例如,使用 mysum = 0。)
而且,一般来说,只要您想要一个仅用于循环一次的推导式,您想要的推导式就是生成器表达式,而不是列表推导式。所以,把这些方括号变成括号,你会得到:
for i in (i for i in range(2, 10) if i%2==0 or i%5==0): mysum += i
不过,无论哪种方式,两行代码都更具可读性和 Python 风格:
for i in (i for i in range(2, 10) if i%2==0 or i%5==0):
mysum += i
……甚至三个:
not2or5 = (i for i in range(2, 10) if i%2==0 or i%5==0)
for i in not2or5:
mysum += i
如果您使用的语言使 reduce/fold 比循环更直观,那么 Python 有一个 reduce 函数。然而,仅仅使用它来消除 for 循环并将块语句变成单行语句通常不被认为是 Pythonic。
更一般地说,在 Python 中,试图将内容塞进一行通常会降低可读性,而不是更多,这通常意味着您最终会在脑海中输入更多字符和处理更多标记,这不仅仅是取消节省线路的任何收益。
当然,在这种特定情况下,您真正想要做的只是总结一个列表的值。这正是sum 所做的。这很容易理解。所以:
mysum += sum(i for i in range(2, 10) if i%2==0 or i%5==0)
(同样,这是假设您已经在 mysum 中有一些要添加的内容。如果没有,只需将 += 更改为 =。后面的所有示例都是如此,所以我就不解释了。)
话虽如此,我可能会将其写为显式嵌套块:
for i in range(2, 10):
if i%2==0 or i%5==0:
mysum += i
…或者作为一系列迭代器转换(在这种情况下实际上只是一个转换):
not2or5 = (i for i in range(2, 10) if i%2==0 or i%5==0)
mysum += sum(not2to5)
以这种方式拆分内容确实没有任何成本(只要您使用生成器表达式而不是列表推导式),而且它通常会使您的代码意图更加明显。
对生成器表达式的一些进一步解释:
生成器表达式就像一个列表推导式,除了它构建一个迭代器而不是一个列表。迭代器类似于某些函数式语言中的“惰性列表”,只是您只能使用一次。 (通常,这不是问题。在上面的所有示例中,我们唯一想做的就是将它传递给sum 函数或在for 循环中使用它,然后我们再也不会引用它。)当您对其进行迭代时,每个值都是按需构建的,然后在您进入下一个之前被释放。
这意味着空间复杂度是恒定的,而不是线性的。您一次只能在内存中获得一个值,而对于列表,您显然已经获得了所有值。这通常是一个巨大的胜利。
但是,时间复杂度没有改变。列表推导会预先完成所有工作,因此它是构建的线性时间,然后可以免费使用。生成器表达式在您对其进行迭代时完成工作,因此可以免费构建,然后线性使用。无论哪种方式,同一时间。 (由于缓存/内存局部性、流水线等原因,生成器表达式实际上可以明显更快,更不用说避免所有内存移动和分配成本。另一方面,对于琐碎的情况,它会更慢,至少在CPython,因为它必须通过完整的迭代器协议而不是列表的快速特殊情况。)
(我在这里假设每个步骤的工作是恒定的——显然[sum(range(i)) for i in range(n)] 在 n 中是二次的,而不是线性的......)