运行总计的列表理解

Posted

技术标签:

【中文标题】运行总计的列表理解【英文标题】:List comprehension for running total 【发布时间】:2011-03-26 20:37:21 【问题描述】:

我想从一个数字列表中得到一个运行总计。

出于演示目的,我从使用range的数字顺序列表开始

a = range(20)

runningTotal = []
for n in range(len(a)):
    new = runningTotal[n-1] + a[n] if n > 0 else a[n]
    runningTotal.append(new)

# This one is a syntax error
# runningTotal = [a[n] for n in range(len(a)) if n == 0 else runningTotal[n-1] + a[n]]

for i in zip(a, runningTotal):
    print "0:>31:>5".format(*i)

产量

  0    0
  1    1
  2    3
  3    6
  4   10
  5   15
  6   21
  7   28
  8   36
  9   45
 10   55
 11   66
 12   78
 13   91
 14  105
 15  120
 16  136
 17  153
 18  171
 19  190

如您所见,我在每次循环迭代中初始化了一个空列表[],然后是append()。有没有更优雅的方法,比如列表推导?

【问题讨论】:

【参考方案1】:

列表推导式没有很好的(干净、可移植的)方式来引用它正在构建的列表。一种好的和优雅的方法可能是在生成器中完成这项工作:

def running_sum(a):
  tot = 0
  for item in a:
    tot += item
    yield tot

当然,要将此作为列表,请使用list(running_sum(a))

【讨论】:

在 Python 3 上你应该使用 itertools.accumulate(a)【参考方案2】:

如果您可以使用numpy,它有一个名为cumsum 的内置函数可以执行此操作。

import numpy as np
tot = np.cumsum(a)  # returns a np.ndarray
tot = list(tot)     # if you prefer a list

【讨论】:

【参考方案3】:

我不确定“优雅”,但我认为以下内容更简单、更直观(以额外变量为代价):

a = range(20)

runningTotal = []

total = 0
for n in a:
  total += n
  runningTotal.append(total)

做同样事情的函数式方法是:

a = range(20)
runningTotal = reduce(lambda x, y: x+[x[-1]+y], a, [0])[1:]

...但可读性/可维护性要差得多,等等。

@Omnifarous 建议将其改进为:

a = range(20)
runningTotal = reduce(lambda l, v: (l.append(l[-1] + v) or l), a, [0])

...但我仍然觉得这不如我最初的建议那么容易理解。

请记住 Kernighan 的话:“首先,调试的难度是编写代码的两倍。因此,如果您尽可能巧妙地编写代码,那么根据定义,您还不够聪明,无法调试它。”

【讨论】:

+1 用于调试报价,强调 reduce 示例的不可读性:) 我会把reduce写成reduce(lambda l, v: (l.append(l[-1] + v) or l), a, [0]) @Satoru.Logic - 我认为通过故意使代码变得更加晦涩来消除reduce 是相当不诚实的。我还认为,reduce 是可怕的,你不应该在 Python 中进行函数式编程。 @Omnifarious 我也是。在我必须这样做之前,我从不在 Python 中使用 FP。 @Satoru.Logic - 好吧,当我认为它使问题的解决方案更清晰时,我会使用它。在这种情况下,我认为这是一个洗涤。【参考方案4】:

这可以在 Python 中用 2 行代码实现。

使用默认参数消除了在外部维护辅助变量的需要,然后我们只需对列表执行map

def accumulate(x, l=[0]): l[0] += x; return l[0];
map(accumulate, range(20))

【讨论】:

这个“利用”了一个 Python 的特性,这个特性曾经让我感到困惑。我喜欢它,但担心如果需要调试相关代码,它会成为一个令人讨厌的陷阱! 更像是 4 PEP-8 行 :) 一个官方的“accumulate”函数现在在 Python 3 中作为from itertools import accumulate 可用。此外,虽然聪明,satoru 的“累积”实现会在您尝试第二次运行时中断。 downvoted,因为正如@sffc 所说,这会在多次运行时给你一个不正确的结果【参考方案5】:

使用itertools.accumulate()。这是一个例子:

from itertools import accumulate

a = range(20)
runningTotals = list(accumulate(a))

for i in zip(a, runningTotals):
    print "0:>31:>5".format(*i)

这仅适用于 Python 3。在 Python 2 上,您可以使用 more-itertools 包中的反向端口。

【讨论】:

这是一个老问题,有很多老答案,但在 2015 年,这是最好的解决方案。【参考方案6】:

当我们对列表求和时,我们指定一个累加器 (memo),然后遍历列表,将二进制函数“x+y”应用于每个元素和累加器。从程序上看,这看起来像:

def mySum(list):
    memo = 0
    for e in list:
        memo = memo + e
    return memo

这是一种常见的模式,对除求和之外的其他事情很有用——我们可以将它推广到任何二进制函数,我们将把它作为参数提供,并让调用者指定一个初始值。这给了我们一个称为reducefoldlinject的函数[1]

def myReduce(function, list, initial):
    memo = initial
    for e in list:
        memo = function(memo, e)
    return memo

def mySum(list):
    return myReduce(lambda memo, e: memo + e, list, 0)

在 Python 2 中,reduce 是一个内置函数,但在 Python 3 中,它已移至 functools 模块:

from functools import reduce

我们可以用reduce 做各种很酷的事情,这取决于我们作为第一个参数提供的函数。如果我们将“sum”替换为“list concatenation”,将“zero”替换为“empty list”,我们会得到(浅)copy 函数:

def myCopy(list):
    return reduce(lambda memo, e: memo + [e], list, [])

myCopy(range(10))
> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

如果我们将transform 函数作为另一个参数添加到copy,并在连接之前应用它,我们会得到map

def myMap(transform, list):
    return reduce(lambda memo, e: memo + [transform(e)], list, [])

myMap(lambda x: x*2, range(10))
> [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

如果我们添加一个以e为参数并返回一个布尔值的predicate函数,并使用它来决定是否连接,我们得到filter

def myFilter(predicate, list):
    return reduce(lambda memo, e: memo + [e] if predicate(e) else memo, list, [])

myFilter(lambda x: x%2==0, range(10))
> [0, 2, 4, 6, 8]

mapfilter 是编写列表推导式的一种奇怪的方式——我们也可以说 [x*2 for x in range(10)][x for x in range(10) if x%2==0]reduce 没有对应的列表解析语法,因为 reduce 根本不需要返回列表(正如我们在前面的 sum 中看到的那样,Python 也恰好作为内置函数提供)。

事实证明,对于计算运行总和,reduce 的列表构建能力正是我们想要的,并且可能是解决这个问题的最优雅的方法,尽管它享有盛誉(与 lambda 一样)某种非蟒蛇式的陈词滥调。 reduce 在运行时留下其旧值副本的版本称为 reductionsscanl[1],它看起来像这样:

def reductions(function, list, initial):
    return reduce(lambda memo, e: memo + [function(memo[-1], e)], list, [initial])

如此装备,我们现在可以定义:

def running_sum(list):
    first, rest = list[0], list[1:]
    return reductions(lambda memo, e: memo + e, rest, first)

running_sum(range(10))
> [0, 1, 3, 6, 10, 15, 21, 28, 36, 45]

虽然在概念上很优雅,但这种精确的方法在 Python 的实践中表现不佳。因为 Python 的 list.append() 会在原地改变一个列表但不返回它,所以我们不能在 lambda 中有效地使用它,而必须使用 + 运算符。这构造了一个全新的列表,它所花费的时间与迄今为止累积列表的长度成正比(即 O(n) 操作)。因为当我们这样做时,我们已经在 reduce 的 O(n) for 循环中,所以整体时间复杂度复合为 O(n2)。

在像 Ruby[2] 这样的语言中,array.push e 返回变异的array,等效的运行时间为 O(n):

class Array
  def reductions(initial, &proc)
    self.reduce [initial] do |memo, e|
      memo.push proc.call(memo.last, e)
    end
  end
end

def running_sum(enumerable)
  first, rest = enumerable.first, enumerable.drop(1)
  rest.reductions(first, &:+)
end

running_sum (0...10)
> [0, 1, 3, 6, 10, 15, 21, 28, 36, 45]

javascript 中相同[2],其array.push(e) 返回e(不是array),但其匿名函数允许我们包含多个语句,我们可以使用这些语句分别指定返回值:

function reductions(array, callback, initial) 
    return array.reduce(function(memo, e) 
        memo.push(callback(memo[memo.length - 1], e));
        return memo;
    , [initial]);


function runningSum(array) 
    var first = array[0], rest = array.slice(1);
    return reductions(rest, function(memo, e) 
        return x + y;
    , first);


function range(start, end) 
    return(Array.apply(null, Array(end-start)).map(function(e, i) 
        return start + i;
    


runningSum(range(0, 10));
> [0, 1, 3, 6, 10, 15, 21, 28, 36, 45]

那么,我们如何解决这个问题,同时保留 reductions 函数的概念简单性,我们只需将 lambda x, y: x + y 传递给它以创建运行求和函数?让我们在程序上重写reductions。我们可以修复accidentally quadratic 问题,并在解决此问题时预先分配结果列表以避免堆抖动[3]

def reductions(function, list, initial):
    result = [None] * len(list)
    result[0] = initial
    for i in range(len(list)):
        result[i] = function(result[i-1], list[i])
    return result

def running_sum(list):
    first, rest = list[0], list[1:]
    return reductions(lambda memo, e: memo + e, rest, first)

running_sum(range(0,10))
> [0, 1, 3, 6, 10, 15, 21, 28, 36, 45]

这对我来说是最佳点:O(n) 性能,优化的过程代码隐藏在一个有意义的名称下,下次您需要编写一个将中间值累积到一个列表。

    名称 reduce/reductions 来自 LISP 传统,foldl/scanl 来自 ML 传统,inject 来自 Smalltalk 传统。 Python 的List 和Ruby 的Array 都是称为“动态数组”(或C++ 中的std::vector)的自动调整大小数据结构的实现。 JavaScript 的 Array 有点巴洛克风格,但如果您不分配越界索引或改变 Array.length,则行为相同。 在 Python 运行时中构成列表后备存储的动态数组将在每次列表长度超过 2 的幂时自行调整大小。调整列表大小意味着在两倍于旧列表大小的堆上分配一个新列表,将旧列表的内容复制到新列表中,并将旧列表的内存返回给系统。这是一个 O(n) 操作,但由于随着列表变得越来越大,它发生的频率越来越低,附加到列表的时间复杂度在平均情况下为 O(1)。但是,旧列表留下的“洞”有时可能难以回收,具体取决于它在堆中的位置。即使使用垃圾收集和强大的内存分配器,预先分配已知大小的数组也可以为底层系统节省一些工作。在没有操作系统优势的嵌入式环境中,这种微观管理变得非常重要。

【讨论】:

你刚刚复活了一个 5 年前的线程,但是,谢谢!我学到了很多:尤其是知道这是一种常见的模式,并且有最佳实践。 小错误:您需要在几个地方将reductions 中的索引加1;或者您可以重写归约以自动获取列表的第一项作为初始值(与内置的reduce 相同)。或者,您可以只创建一个附加到并返回列表的函数,并将原始 O(N^2) 中的 .append 替换为该函数。 另外,您认为itertools.accumulate 与您的reductions 本质上是相同的,还是两者之间存在一些有意义的差异(除了返回迭代器与列表)? @max - 是的,除了返回类型和评估风格之外,它们是等价的(我的 reductions 实现是严格的;itertools.accumulate 是懒惰的)。【参考方案7】:

我想做同样的事情来生成可以使用 bisect_left 的累积频率 - 这就是我生成列表的方式;

[ sum( a[:x] ) for x in range( 1, len(a)+1 ) ]

【讨论】:

我希望你的列表不是很长......那是 O(len(a)^2) 就在那里。 略短的版本(并使用 xrange):[ sum(a[:x+1]) for x in xrange(len(a)) ]【参考方案8】:

Python 3.8 开始,并引入assignment expressions (PEP 572)(:= 运算符),我们可以在列表解析中使用和递增变量:

# items = range(7)
total = 0
[(x, total := total + x) for x in items]
# [(0, 0), (1, 1), (2, 3), (3, 6), (4, 10), (5, 15), (6, 21)]

这个:

将变量 total 初始化为 0,表示运行总和 对于每个项目,这两个: 通过赋值表达式total增加当前循环项(total := total + x) 同时返回 total 的新值作为生成的映射元组的一部分

【讨论】:

【参考方案9】:

这是一个线性时间解决方案:

list(reduce(lambda (c,s), a: (chain(c,[s+a]), s+a), l,(iter([]),0))[0])

例子:

l = range(10)
list(reduce(lambda (c,s), a: (chain(c,[s+a]), s+a), l,(iter([]),0))[0])
>>> [0, 1, 3, 6, 10, 15, 21, 28, 36, 45]

简而言之,reduce 遍历列表累加总和并构造列表。最后的x[0] 返回列表,x[1] 将是运行总值。

【讨论】:

【参考方案10】:

另一个单线,在线性时间和空间中。

def runningSum(a):
    return reduce(lambda l, x: l.append(l[-1]+x) or l if l else [x], a, None)

我在这里强调线性空间,因为我在其他建议的答案中看到的大多数单行 - 基于模式 list + [sum] 或使用 chain 迭代器 - 生成 O(n)与此相比,列表或生成器对垃圾收集器的压力太大,以至于它们的性能非常差。

【讨论】:

这很优雅!我有点卡在 'or l' 部分,直到我意识到它是 ...; return(l) 的缩写【参考方案11】:

我会为此使用协程:

def runningTotal():
    accum = 0
    yield None
    while True:
        accum += yield accum

tot = runningTotal()
next(tot)
running_total = [tot.send(i) for i in xrange(N)]

【讨论】:

alex 的回答要简洁得多,但我会留下这个作为为什么不使用协程的例子 这个答案确实具有允许解释器分配适当大小的列表以预先保存结果的优点。我怀疑口译员通常还没有那么聪明。【参考方案12】:

您正在寻找两件事:折叠(减少)和一个有趣的函数,它保存另一个函数的结果列表,我称之为运行。我制作了带有和不带有初始参数的版本;无论哪种方式,这些都需要使用初始 [] 来减少。

def last_or_default(list, default):
    if len(list) > 0:
        return list[-1]
    return default

def initial_or_apply(list, f, y):
    if list == []:
        return [y]
    return list + [f(list[-1], y)]

def running_initial(f, initial):
    return (lambda x, y: x + [f(last_or_default(x,initial), y)])

def running(f):
    return (lambda x, y: initial_or_apply(x, f, y))

totaler = lambda x, y: x + y
running_totaler = running(totaler)
running_running_totaler = running_initial(running_totaler, [])

data = range(0,20)
running_total = reduce(running_totaler, data, [])
running_running_total = reduce(running_running_totaler, data, [])

for i in zip(data, running_total, running_running_total):
    print "0:>31:>42:>83".format(*i)

由于 + 运算符,这些将在非常大的列表上花费很长时间。在函数式语言中,如果正确完成,这个列表构造将是 O(n)。

这是输出的前几行:

0   0                      [0]
1   1                   [0, 1]
2   3                [0, 1, 3]
3   6             [0, 1, 3, 6]
4  10         [0, 1, 3, 6, 10]
5  15     [0, 1, 3, 6, 10, 15]
6  21 [0, 1, 3, 6, 10, 15, 21]

【讨论】:

【参考方案13】:

这是低效的,因为它每次都从头开始,但可能是:

a = range(20)
runtot=[sum(a[:i+1]) for i,item in enumerate(a)]
for line in zip(a,runtot):
    print line

【讨论】:

以上是关于运行总计的列表理解的主要内容,如果未能解决你的问题,请参考以下文章

对于下一个循环来计算运行总计将不起作用

SQL窗口函数和运行总计

运行总计 - 找到运行总计为 X 的日期

使用两列运行总计

计算运行总计/运行余额

使用 BigQuery 中的条件计算运行总计