【问题标题】:Help needed improving Python code using List Comprehensions需要帮助使用 List Comprehensions 改进 Python 代码
【发布时间】:2009-07-09 02:51:36
【问题描述】:

我一直在家里编写一些小的 Python 程序来了解有关该语言的更多信息。我试图理解的最新功能是列表理解。我创建了一个小脚本,根据我过去换油的频率来估计我的汽车何时需要下一次换油。在下面的代码 sn-p 中,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 list-comprehension


    【解决方案1】:

    正如其他答案所指出的,除非您的 oil_changes 列表非常长,否则您不必担心。但是,作为“基于流”计算的粉丝,我认为有趣的是,itertools 提供了在 O(1) 空间(和 O(N) 时间)计算 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,以便我们可以跟踪我们通过循环的次数,即我们正在迭代的可迭代对象的长度(我们需要在最终表达式中加上 +1,因为enumerate 从 0 开始!-)。我们使用简单的+= 计算总和,这对于数字来说很好(sum 更好,但它不会跟踪长度!-)。

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

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

    【讨论】:

    • 似乎总是需要权衡取舍。根据 timeit,您的代码在此处的答案中是最慢的。
    • 对于简短的列表,它可能是;在 oil_changes 中尝试几百万个项目的各种方法,比如说...;-)
    • 值得注意的是,构造 tee+next+izip 通常被抽象为 pairwise(),详见 itertools 文档。另一方面,在循环外使用 for 变量(尽管解释器和 Python 社区都接受)恕我直言非常难看。也就是说,一个更实用(在 FP 意义上)的解决方案对于新手来说更难理解,并且可能不被视为“Pythonic”(因为它会使用如此讨厌的 reduce/foldl)。
    【解决方案2】:

    试试这个:

    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。 :-)
    • 学习技巧 X 应该包括在没有必要时不使用技巧 X——参见 Alex 的意大利谚语。请注意,答案中仅使用第一个和最后一个距离这一事实表明,差值的算术平均值的预测能力可能不是那么大。这里有一个更好的例子来尝试你的技术:计算指数移动平均线(最近的结果比早期的结果有更大的权重)——它不能被优化成单线。
    【解决方案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))]
    

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

    【讨论】:

    • 有趣的是,您对 diffs 的替代定义会导致此处的答案中运行时间最快(当然 John Machin 的答案除外)。
    • 如果 len(items)>0,平均可以是 sum(items)/len(items)?
    【解决方案4】:

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

    【讨论】:

      【解决方案5】:

      我是不是做了很多不必要的抄袭 在第一个原始列表中 行吗?

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

      在此处插入 old quote by Knuth

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

      【讨论】:

        猜你喜欢
        • 2020-02-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-08-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多