Я хочу получить промежуточную сумму из списка чисел.

Для демонстрации я начну с последовательного списка чисел, используя 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() в каждой итерации цикла. Есть ли более элегантный способ для этого, как понимание списка?

25
Kit 8 Авг 2010 в 06:19

13 ответов

Лучший ответ

У понимания списка нет хорошего (чистого, переносимого) способа обратиться к самому списку, который он создает. Одним хорошим и элегантным подходом может быть работа в генераторе:

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

Чтобы получить его в виде списка, конечно, используйте list(running_sum(a)).

28
Alex Martelli 8 Авг 2010 в 02:28

Начало 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 как часть созданного сопоставленного кортежа
0
Xavier Guihot 27 Апр 2019 в 15:21

Это неэффективно, так как это происходит каждый раз с самого начала, но возможно:

a = range(20)
runtot=[sum(a[:i+1]) for i,item in enumerate(a)]
for line in zip(a,runtot):
    print line
0
Tony Veijalainen 8 Авг 2010 в 18:46

Вы ищете две вещи: сложение (уменьшение) и забавную функцию, которая хранит список результатов другой функции, которую я назвал запущенной. Я сделал версии как с начальным параметром, так и без него; в любом случае их нужно уменьшить с начальным [].

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]
0
Cirdec 8 Авг 2010 в 06:00

Я бы использовал сопрограмму для этого:

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)]
1
aaronasterling 8 Авг 2010 в 02:35

Еще одна строчка, в линейном времени и пространстве.

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) ) списки или генераторы и нагрузка сборщика мусора настолько, что они работают очень плохо, по сравнению с этим.

2
nickie 21 Ноя 2016 в 10:40

Вот линейное решение времени на один лайнер:

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]

Короче говоря, сокращение идет по списку, накапливая сумму и создавая список. Финал x[0] возвращает список, x[1] будет значением промежуточного итога.

2
topkara 16 Мар 2016 в 19:57

Я хотел сделать то же самое для генерации кумулятивных частот, которые я мог бы использовать над bisect_left - таким образом я сгенерировал список;

[ sum( a[:x] ) for x in range( 1, len(a)+1 ) ]
4
user1908017 19 Дек 2012 в 20:52

Используйте 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 пакет.

6
Boris 24 Янв 2019 в 08:37

Когда мы берем сумму списка, мы назначаем аккумулятор (memo) и затем просматриваем список, применяя двоичную функцию «x + y» к каждому элементу и аккумулятору. Процедурно это выглядит так:

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

Это общий шаблон, и он полезен для других вещей, помимо получения сумм - мы можем обобщить его для любой двоичной функции, которую мы предоставим в качестве параметра, а также позволить вызывающей стороне указать начальное значение. Это дает нам функцию, известную как reduce, foldl или inject [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 в зависимости от функции, которую мы предоставляем в качестве первого аргумента. Если мы заменим «сумму» на «конкатенацию списка», а «ноль» на «пустой список», мы получим функцию (неглубокая) 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)] или [x for x in range(10) if x%2==0]. Для reduce нет соответствующего синтаксиса понимания списка, потому что reduce вообще не требуется возвращать список (как мы видели ранее в sum), который Python также предлагает как встроенная функция).

Оказывается, что для вычисления промежуточной суммы возможности построения списка reduce - это именно то, что нам нужно, и, возможно, самый элегантный способ решения этой проблемы, несмотря на ее репутацию (наряду с lambda) как что-то из непитонного шибболет. Версия reduce, которая оставляет копии своих старых значений при запуске, называется reductions или 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() изменяет список на месте, но не возвращает его, мы не можем эффективно использовать его в лямбда-выражении, и вместо этого мы должны использовать оператор +. Это создает совершенно новый список, который занимает время, пропорциональное длине накопленного списка (то есть операция O (n)). Так как мы уже находимся внутри цикла O (n) for reduce, общая сложность времени составляет O (n 2 ).

В языке, подобном 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 процедурно. Мы можем исправить случайно квадратичную проблему, и пока мы занимаемся этим, предварительно выделим список результатов для избегайте кучи [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. Python List и Ruby Array являются реализациями автоматически изменяемой структуры данных, известной как «динамический массив» (или std::vector в C ++). JavaScript Array немного более барочный, но ведет себя идентично, если вы не назначаете индексы за пределами границ или не изменяете Array.length.
  3. Динамический массив, который формирует резервное хранилище списка во время выполнения Python, будет изменять свой размер каждый раз, когда длина списка пересекает степень двойки. Изменение размера списка означает выделение нового списка в куче, в два раза превышающего старый, копирование содержимого старого списка в новый и возврат памяти старого списка в систему. Это операция O (n), но поскольку она происходит все реже и реже, так как список становится все больше и больше, сложность времени добавления к списку в среднем составляет O (1). Тем не менее, «дыру», оставленную старым списком, иногда бывает трудно перерабатывать, в зависимости от его положения в куче. Даже при сборке мусора и надежном распределителе памяти предварительное выделение массива известного размера может сэкономить работающим системам некоторую работу. Во встроенной среде без использования ОС этот вид микроуправления становится очень важным.
7
April Arcus 12 Апр 2015 в 19:00

Я не уверен насчет «элегантного», но я думаю, что следующее гораздо проще и интуитивно понятнее (за счет дополнительной переменной):

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])

... но я все еще нахожу это менее понятным, чем мое первоначальное предложение.

Вспомните слова Кернигана: «Во-первых, отладка вдвое сложнее, чем писать код. Поэтому, если вы пишете код настолько умно, насколько это возможно, вы, по определению, недостаточно умны для его отладки».

8
pjz 11 Авг 2010 в 13:25

Это может быть реализовано в 2 строки в Python.

Использование параметра по умолчанию избавляет от необходимости поддерживать переменную aux снаружи, а затем мы просто делаем map в списке.

def accumulate(x, l=[0]): l[0] += x; return l[0];
map(accumulate, range(20))
10
satoru 8 Авг 2010 в 02:57

Если вы можете использовать numpy, у него есть встроенная функция cumsum, которая делает это.

import numpy
tot = numpy.cumsum(a)  # returns a numpy.ndarray
tot = list(tot)        # if you prefer a list
26
Acumenus 23 Янв 2012 в 05:03