19

I would like to know if there is a built in function in python for the equivalent Haskell scanl, as reduce is the equivalent of foldl.

Something that does this:

Prelude> scanl (+) 0 [1 ..10]
[0,1,3,6,10,15,21,28,36,45,55]

The question is not about how to implement it, I already have 2 implementations, shown below (however, if you have a more elegant one please feel free to show it here).

First implementation:

 # Inefficient, uses reduce multiple times
 def scanl(f, base, l):
   ls = [l[0:i] for i in range(1, len(l) + 1)]
   return [base] + [reduce(f, x, base) for x in ls]

  print scanl(operator.add, 0, range(1, 11))

Gives:

[0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55]

Second implementation:

 # Efficient, using an accumulator
 def scanl2(f, base, l):
   res = [base]
   acc = base
   for x in l:
     acc = f(acc, x)
     res += [acc]
   return res

 print scanl2(operator.add, 0, range(1, 11))

Gives:

[0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55]

Thank you :)

elaRosca
  • 3,083
  • 4
  • 21
  • 22

4 Answers4

20

You can use this, if its more elegant:

def scanl(f, base, l):
    for x in l:
        base = f(base, x)
        yield base

Use it like:

import operator
list(scanl(operator.add, 0, range(1,11)))

Python 3.x has itertools.accumulate(iterable, func= operator.add). It is implemented as below. The implementation might give you ideas:

def accumulate(iterable, func=operator.add):
    'Return running totals'
    # accumulate([1,2,3,4,5]) --> 1 3 6 10 15
    # accumulate([1,2,3,4,5], operator.mul) --> 1 2 6 24 120
    it = iter(iterable)
    total = next(it)
    yield total
    for element in it:
        total = func(total, element)
        yield total
Matvey Aksenov
  • 3,802
  • 3
  • 23
  • 45
manojlds
  • 290,304
  • 63
  • 469
  • 417
  • 6
    I don't know python, but shouldn't you have a `yield` statement before the `for` loop? `scanl` should return a list one item longer than the input list. – rampion Jan 20 '13 at 14:31
  • @rampion You are right, Haskell's `scanl` includes the initial accumulator. A `yield base` is missing. – nh2 Oct 26 '13 at 13:10
  • @rampion all you need to do is change `for x in l` to `for x in [base] + l` – Cyoce Mar 08 '16 at 22:42
6

Starting Python 3.8, and the introduction of assignment expressions (PEP 572) (:= operator), which gives the possibility to name the result of an expression, we can use a list comprehension to replicate a scan left operation:

acc = 0
scanned = [acc := acc + x for x in [1, 2, 3, 4, 5]]
# scanned = [1, 3, 6, 10, 15]

Or in a generic way, given a list, a reducing function and an initialized accumulator:

items = [1, 2, 3, 4, 5]
f = lambda acc, x: acc + x
accumulator = 0

we can scan items from the left and reduce them with f:

scanned = [accumulator := f(accumulator, x) for x in items]
# scanned = [1, 3, 6, 10, 15]
Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
0

I had a similar need. This version uses the python list comprehension

def scanl(data):
    '''
    returns list of successive reduced values from the list (see haskell foldl)
    '''
    return [0] + [sum(data[:(k+1)]) for (k,v) in enumerate(data)]


>>> scanl(range(1,11))

gives:

[0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55]
kellerdk
  • 17
  • 2
  • 1
    I would pass sum as a function argument to the scanl function. – elaRosca Jul 11 '14 at 09:48
  • 2
    This is not exactly `scanl`, because (1) using the operator for `[:(k+1)]` is O(n) every time, so you have O(n^2) complexity here, which isn't the case for a real `scanl` (2) assumption is that `sum` is used. – sunapi386 Feb 22 '17 at 20:08
0

As usual, the Python ecosystem is also overflowing with solutions:

Toolz has an accumulate capable of taking a user-supplied function as an argument. I tested it with lambda expressions.

https://github.com/pytoolz/toolz/blob/master/toolz/itertoolz.py

https://pypi.python.org/pypi/toolz

as does more_itertools

http://more-itertools.readthedocs.io/en/stable/api.html

I did not test the version from more-itertools, but it also can take a user-supplied function.

Reb.Cabin
  • 5,426
  • 3
  • 35
  • 64