使用列表推导式改进Python代码需要帮助

3

我在家写了一些小型的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风格。我在第一行是否做了很多不必要的原始列表复制?我感觉有更好的方法来解决这个问题,但我不知道是什么。

5个回答

9
试试这个:
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)

这显然是获取我的答案的最佳方式,但我将不会学到任何关于列表推导的知识。无论如何+1。 :-) - Michael Kristofik
2
学习技术X时,应该注意在不需要使用技术X的情况下不要使用--参见亚历克斯的意大利谚语。请注意,答案中仅使用第一个和最后一个距离表明差异算术平均数的预测能力可能不是很强。这里有一个更好的例子可以尝试你的技巧:计算指数移动平均值(最近的结果比早期的结果具有更大的权重)--它不能被优化为一行代码。 - John Machin

8
正如其他答案所指出的,除非您的oil_changes列表非常长,否则您不需要担心。然而,作为一名“基于流”的计算机爱好者,我认为值得一提的是,无论N有多大(即len(next_oil)),itertools都提供了计算您的next_oil值所需的所有工具,并且可以在O(1)空间(当然也是O(N)时间!)内完成。

izip本身是不充分的,因为它只能稍微减少乘法常数,但会使空间需求保持为O(N)。将这些需求降至O(1)的关键思路是将iziptee配对 - 并避免使用列表推导式,后者在空间上仍将是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仍然具有最后通过在izip放弃之前推进b而提取的值(就像thelen仍然具有最后一个计数值一样)。在最终表达式中,我仍然将该值命名为last_one,而不是直接使用j,因为我认为这样更清晰、更易读。
所以这就是它——我希望它是有益的!虽然对于你提出的这个特定问题的解决方案来说,它几乎肯定是过度设计的。我们意大利人有一句古老的谚语——“学艺术,把它放在一边!”——我认为它在这里非常适用:学习高级和复杂的方法来解决非常困难的问题是一件好事,以防万一你遇到了这些问题,但是对于所有这些,你需要在简单、直接的常见问题上去寻找简单的解决方案,而不是应用可能不需要的高级解决方案!

似乎总是存在一种权衡。根据 timeit 的测试结果,你的代码在这里的答案中是最慢的。 - Michael Kristofik
对于较短的列表,这可能是可行的;尝试在oil_changes中使用几百万个项目尝试各种方法...;-) - Alex Martelli
1
值得注意的是,构造tee+next+izip通常被抽象为pairwise(),详见itertools文档。另一方面,尽管解释器和Python社区都接受在循环外使用for变量,但我认为这样做相当丑陋。话虽如此,对于初学者来说,更具函数式(FP意义上)的解决方案可能更难理解,而且可能不被视为“Pythonic”(因为它将使用reduce/foldl等被讨厌的函数)。 - tokland

3

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))]

我不太确定,这并没有带来很大的改进。你的代码已经很不错了。


有趣的是,在这里的所有答案中,您对差异的另一种定义导致最快的运行时间(当然除了John Machin的答案)。 - Michael Kristofik
如果items的长度大于零,那么平均值能否只是sum(items)/len(items)呢? - Martlark

2

看起来还不错,真的。不是所有事情都简单(无论你如何构建它,一个简单的计算中有几个步骤)。有一些选项可以减少副本,比如使用itertools.islice和itertools.izip,但是(除了izip之外)代码中的额外步骤只会让它更加复杂。不是所有东西都需要成为列表推导式,但有时这是一个判断性的调用。哪种方式看起来更清晰?下一个阅读它的人最容易理解什么?三个月后当你回来修复那个bug时你会理解什么?


2
我是否在第一行做了很多不必要的原始列表复制?
从技术上讲,是的。但实际上并不是这样。除非你真的换了几百万次油,否则速度惩罚不太可能显著。你可以将zip改为izip,但这似乎不值得(在Python 3.0中,zip实际上就是izip)。
在此插入 Knuth的旧引用
(你也可以用oil_changes替换oil_changes[:-1],因为zip()会截断到最短输入序列的长度)

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