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。我是否对第一行的原始列表做了很多不必要的复制?我觉得有更好的方法可以做到这一点,但我不知道它是什么。

4

5 回答 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)
于 2009-07-09T03:07:40.527 回答
9

oil_changes正如其他答案所指出的那样,除非您的清单非常长,否则您不必担心。但是,作为“基于流”计算的粉丝,我认为有趣的是指出它提供了在 O(1) 空间(当然还有 O(N) 时间!-)itertools中计算价值所需的所有工具next_oil无论 N 有多大,即len(next_oil), 得到。

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(制作两个可独立推进的克隆),然后推进一次克隆之一btee占用空间 O(x),其中 x 是各个克隆的进步之间的最大绝对差;在这里,两个克隆的进度最多只相差1,因此空间需求显然是O(1)。

izip对两个稍微倾斜的克隆迭代器进行一次一个“压缩”,然后我们将其修饰一下,enumerate以便我们可以跟踪我们通过循环的次数,即我们正在迭代的可迭代对象的长度on(我们需要在最终表达式中加上 +1,因为enumerate从 0 开始!-)。我们用一个简单的 来计算总和+=,这对数字来说很好(sum甚至更好,但它不会跟踪长度!-)。

在循环之后使用 很诱人last_one = a.next(),但这不起作用,因为a实际上已经用尽了——izip从左到右推进它的参数迭代,所以它a在意识到结束之前已经推进了最后一次b!-)。没关系,因为 Python 循环变量的范围不限于循环本身——在循环之后,仍然具有在放弃之前j通过推进最后提取的值(就像仍然具有返回的最后一个计数值一样)。我仍然在命名值而不是直接在最终表达式中使用,因为我认为它更清晰,更具可读性。bizipthelenenumeratelast_onej

就是这样——我希望它具有指导意义!-)——尽管对于你这次提出的具体问题的解决方案,它几乎肯定是矫枉过正的。我们意大利人有一句古老的谚语——“Impara l'Arte, e mettila da parte!”……“学习艺术,然后把它放在一边”——我认为这句话在这里很适用:学习是件好事解决非常困难的问题的高级和复杂的方法,以防万一你遇到它们,但是在更常见的简单、普通问题的情况下,你需要采取简单和直接的方式——不要应用最有可能获胜的高级解决方案不需要!-)

于 2009-07-09T05:15:57.113 回答
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))]

我不知道,这并不是一个巨大的进步。你的代码是相当不错的。

于 2009-07-09T03:01:17.633 回答
2

看起来不错,真的。并非一切都是简单的(无论您如何构建它,您在其他简单的计算中都有几个步骤)。有一些选项可以减少副本,例如使用 itertools.islice 和 itertools.izip,但是(除了 izip)代码中的额外步骤只会使其更加复杂。并非所有内容都需要列表理解,但有时它是一个判断电话。什么对你来说看起来更干净?下一个阅读它的人最能理解什么?当你在三个月内回来修复那个错误时,你会明白什么?

于 2009-07-09T02:56:11.637 回答
2

我是否对第一行的原始列表做了很多不必要的复制?

从技术上讲,是的。实际上,没有。除非您实际上已经更换了数百万次机油,否则速度损失不太可能是显着的。您可以更改zipizip,但这似乎不值得(在 python 3.0 中,zip实际上 izip)。

在此处插入Knuth 的旧语录

(你也可以oil_changes[:-1]用 just替换oil_changes,因为zip()无论如何都会截断到最短输入序列的长度)

于 2009-07-09T04:11:17.773 回答