帮助需要使用List Comprehensions改进Python代码

时间:2009-07-09 02:51:37

标签: python list-comprehension

我一直在家里写一些Python程序来学习更多有关该语言的知识。我试图理解的最新功能是List Comprehensions。我创建了一个小脚本,根据我过去换油的频率来估计我的汽车何时需要换油。在下面的代码段中,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 个答案:

答案 0 :(得分: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 :(得分:9)

正如其他答案所指出的那样,除非你的oil_changes列表非常长,否则你不必担心。但是,作为“基于流”的计算的粉丝,我认为有必要指出itertools提供了在O(1)空间(和O)中计算next_oil值所需的所有工具。 (N)当然是时间! - )无论多大的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)

我们不是从列表中取出切片,而是在其上取一个迭代器,开发它(制作两个可独立推进的克隆),然后推进一次克隆btee占用空间O(x),其中x是各种克隆的推进之间的最大绝对差异;在这里,两个克隆的进展最多只相差1,所以空间要求显然是O(1)。

izip对两个稍微歪斜的克隆迭代器进行一次一个“压缩”,我们在enumerate中修改它,这样我们就可以跟踪我们经历了多少次循环,即我们迭代的迭代的长度(我们在最终表达式中需要+1,因为enumerate从0开始! - )。我们使用简单的+=来计算总和,这对数字来说很好(sum甚至更好,但它不会跟踪长度! - 。)。

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

所以它是 - 我希望它是有益的! - ) - 虽然为了解决你这次提出的具体问题,但几乎可以肯定会有点矫枉过正。我们意大利人有一句古老的谚语 - “Impara l'Arte,e mettila da parte!”......“学习艺术,然后把它放在一边” - 我认为这在这里非常适用:学习是件好事先进而复杂的方法来解决非常困难的问题,以防万一你遇到它们,但是对于所有你需要在简单和普通问题的更常见的情况下寻求简单和直接的方法 - 不应用最有可能赢得的高级解决方案不需要! - )

答案 2 :(得分: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))]

我不知道,这不是一个巨大的进步。你的代码非常好。

答案 3 :(得分:2)

看起来很好,真的。并非一切都很简单(无论你如何构建它,你都可以通过简单的计算得到几个步骤)。有一些减少副本的选项,比如使用itertools.islice和itertools.izip,但是(除了izip之外)代码中的额外步骤会使其进一步复杂化。并非所有事情都需要成为列表理解,但有时候这是一种判断。什么看起来更干净?下一个阅读它的人最了解什么?当你三个月内回来修复那个bug时你会明白什么?

答案 4 :(得分:2)

  

我做了很多不必要的复制   第一个中的原始列表   线?

技术上,是的。实际上,没有。除非你几百万次改变你的油量,否则速度惩罚不太可能显着。您可以将zip更改为izip,但这似乎不值得(在python 3.0中,zip有效 izip)。

在此处插入old quote by Knuth

(您也可以只用oil_changes[:-1]替换oil_changes,因为zip()会截断到最短输入序列的长度。