28

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

出于演示目的,我从一个顺序的数字列表开始,使用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:>3}{1:>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()在每次循环迭代中。有没有更优雅的方法,比如列表理解?

4

13 回答 13

30

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

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

当然,要将其作为列表获取,请使用list(running_sum(a)).

于 2010-08-08T02:28:57.647 回答
28

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

import numpy as np
tot = np.cumsum(a)  # returns a np.ndarray
tot = list(tot)     # if you prefer a list
于 2010-08-08T02:35:07.077 回答
12

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

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 的话:“调试的难度是编写代码的两倍。因此,如果您尽可能巧妙地编写代码,那么根据定义,您还不够聪明,无法调试它。”

于 2010-08-08T02:28:09.520 回答
10

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

使用默认参数消除了在外部维护 aux 变量的需要,然后我们只需map对列表进行操作。

def accumulate(x, l=[0]): l[0] += x; return l[0];
map(accumulate, range(20))
于 2010-08-08T02:50:18.163 回答
9

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

from itertools import accumulate

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

for i in zip(a, runningTotals):
    print "{0:>3}{1:>5}".format(*i)

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

于 2014-08-15T00:43:49.850 回答
8

当我们对列表求和时,我们指定一个累加器 ( 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根据我们作为第一个参数提供的函数,我们可以做各种很酷的事情。如果我们用“list concatenation”替换“sum”,用“empty list”替换“zero”,我们得到(浅)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]

如果我们添加一个以参数为参数并返回布尔值的predicate函数e,并使用它来决定是否连接,我们得到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]

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

事实证明,对于计算运行总和,列表构建能力reduce正是我们想要的,并且可能是解决这个问题的最优雅的方法,尽管它(与 一起lambda)被认为是一种非 Python 式的陈词滥调。该版本在reduce运行时会留下其旧值的副本,称为reductionsor scanl[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) 操作)。由于我们已经在执行此操作的 O(n)for循环中reduce,因此总体时间复杂度为 O(n 2 )。

在像 Ruby [2]这样array.push e返回 mutated的语言中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。我们可以解决意外的二次问题,并且在处理它的同时,预先分配结果列表以避免堆抖动[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) 性能,并且优化的过程代码隐藏在一个有意义的名称下,下次您需要编写一个将中间值累积到列表中的函数时可以重新使用它。

  1. 名称reduce/reductions来自 LISP 传统,foldl/scanl来自 ML 传统,以及inject来自 Smalltalk 传统。
  2. PythonList和 RubyArray都是自动调整大小的数据结构的实现,称为“动态数组”(或std::vector在 C++ 中)。JavaScriptArray有点巴洛克风格,但只要您不分配给越界索引或 mutate ,其行为就会相同Array.length
  3. 每当列表的长度超过 2 的幂时,在 Python 运行时中形成列表后备存储的动态数组将自行调整大小。调整列表大小意味着在两倍于旧列表大小的堆上分配一个新列表,将旧列表的内容复制到新列表中,并将旧列表的内存返回给系统。这是一个 O(n) 操作,但由于随着列表变得越来越大,它发生的频率越来越低,附加到列表的时间复杂度在平均情况下为 O(1)。但是,旧列表留下的“洞”有时可能难以回收,具体取决于它在堆中的位置。即使使用垃圾收集和强大的内存分配器,预先分配已知大小的数组也可以为底层系统节省一些工作。
于 2015-03-26T07:04:59.177 回答
4

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

[ sum( a[:x] ) for x in range( 1, len(a)+1 ) ]
于 2012-12-19T20:52:12.870 回答
3

开始Python 3.8,并引入赋值表达式(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作为生成的映射元组的一部分
于 2019-04-27T15:21:26.440 回答
2

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

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) 列表或生成器并强调垃圾收集器所以与此相比,他们的表现非常糟糕。

于 2016-11-21T10:40:04.037 回答
2

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

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]将是运行总值。

于 2016-03-16T17:49:49.263 回答
1

我会为此使用协程:

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)]
于 2010-08-08T02:35:26.420 回答
0

这是低效的,因为它从一开始就每次都这样做,但可能是:

a = range(20)
runtot=[sum(a[:i+1]) for i,item in enumerate(a)]
for line in zip(a,runtot):
    print line
于 2010-08-08T04:49:49.173 回答
0

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

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:>3}{1:>4}{2:>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]
于 2010-08-08T06:00:33.773 回答