需要帮助改进使用列表推导式的Python代码
我在家里写了一些小的Python程序,想更好地了解这个语言。最近我尝试理解的一个功能是列表推导式。我写了一个小脚本,用来估算我的车什么时候需要换机油,这个估算是根据我过去换机油的频率来做的。在下面的代码片段中,oil_changes
是我换机油时的里程数列表。
# Compute a list of the mileage differences between each oil change.
diffs = [j - i for i, j in zip(oil_changes[:-1], oil_changes[1:])]
# Use the average difference between oil changes to estimate the next change.
next_oil = oil_changes[-1] + sum(diffs) / len(diffs)
这段代码得出的结果是正确的(我手动算过来验证过),但我觉得它还不够“Pythonic”,也就是不够符合Python的风格。我在第一行是不是做了很多不必要的原始列表复制?我觉得应该有更好的方法来实现这个,但我不知道是什么。
5 个回答
itertools
这个包提供了一些额外的生成器风格的函数。比如,你可以用izip
来代替zip
,这样可以节省一些内存。
你也可以写一个average
函数,这样就能把diffs
变成一个生成器,而不是用列表推导式来做:
from itertools import izip
def average(items):
sum, count = 0, 0
for item in items:
sum += item
count += 1
return sum / count
diffs = (j - i for i, j in izip(oil_changes[:-1], oil_changes[1:])
next_oil = oil_changes[-1] + average(diffs)
另外,你也可以把diffs
的定义改成:
diffs = [oil_changes[i] - oil_changes[i-1] for i in xrange(1, len(oil_changes))]
我觉得,这其实并不是一个很大的改进。你的代码现在已经挺不错的了。
试试这个:
assert len(oil_changes) >= 2
sum_of_diffs = oil_changes[-1] - oil_changes[0]
number_of_diffs = len(oil_changes) - 1
average_diff = sum_of_diffs / float(number_of_diffs)
正如其他回答所提到的,除非你的 oil_changes
列表特别长,否则其实不用太担心。不过,作为一个喜欢“流式”计算的人,我觉得有必要提一下,itertools
提供了你所需的所有工具,可以在 O(1) 的空间内计算出你的 next_oil
值(当然,时间复杂度是 O(N)!)无论 N,也就是 len(next_oil)
有多大。
单独使用 izip
是不够的,因为它只是稍微减少了乘法常数,但空间需求仍然是 O(N)。要把空间需求降到 O(1),关键是把 izip
和 tee
结合起来——而且要避免使用列表推导式,因为那样的空间复杂度也是 O(N),我们应该用一个简单的老式循环来替代!接下来是:
it = iter(oil_changes)
a, b = itertools.tee(it)
b.next()
thesum = 0
for thelen, (i, j) in enumerate(itertools.izip(a, b)):
thesum += j - i
last_one = j
next_oil = last_one + thesum / (thelen + 1)
我们不是从列表中切片,而是对它使用迭代器,使用 tee
(生成两个可以独立前进的克隆),然后让其中一个克隆 b
前进一次。tee
占用的空间是 O(x),其中 x 是各个克隆之间前进的最大绝对差值;在这里,两个克隆的前进最多只相差 1,所以空间需求显然是 O(1)。
izip
逐个“拉链”这两个稍微错开的克隆迭代器,我们用 enumerate
来装饰它,这样我们就可以跟踪循环的次数,也就是我们正在迭代的可迭代对象的长度(在最终表达式中需要加 1,因为 enumerate
是从 0 开始的!)。我们用简单的 +=
来计算总和,这对于数字来说是可以的(sum
更好,但它无法跟踪长度!)。
在循环结束后,使用 last_one = a.next()
是很诱人的,但这样做不行,因为 a
实际上已经耗尽了——izip
是从左到右推进它的参数可迭代对象,所以在意识到 b
结束之前,它已经最后推进了 a
一次!这没关系,因为 Python 的循环变量并不局限于循环本身——在循环结束后,j
仍然保留着在 b
前进之前最后提取的值(就像 thelen
仍然保留着 enumerate
返回的最后计数值)。我仍然把这个值命名为 last_one
,而不是直接用 j
,因为我觉得这样更清晰易读。
所以就是这样——希望对你有帮助!虽然对于你这次提出的具体问题来说,这几乎是多余的。我们意大利人有句古老的谚语——“Impara l'Arte, e mettila da parte!”... “学会这门艺术,然后放在一边”——我觉得这在这里非常适用:学习高级和复杂的方法来解决非常困难的问题是件好事,以防你遇到它们,但在大多数情况下,面对简单、普通的问题时,还是应该追求简单和直接,而不是使用那些大多数情况下根本不需要的高级解决方案!-)