6

This is somehow a follow-up to this question

So first, you'll notice that you cannot perform a sum on a list of strings to concatenate them, python tells you to use str.join instead, and that's good advice because no matter how you use + on strings, the performance is bad.

The "cannot use sum" restriction doesn't apply to list, and though, itertools.chain.from_iterable is the preferred way to perform such list flattening.

But sum(x,[]) when x is a list of lists is definitively bad.

But should it stay that way?

I compared 3 approaches

import time
import itertools

a = [list(range(1,1000)) for _ in range(1000)]

start=time.time()
sum(a,[])
print(time.time()-start)

start=time.time()
list(itertools.chain.from_iterable(a))
print(time.time()-start)


start=time.time()
z=[]
for s in a:
    z += s
print(time.time()-start)

results:

  • sum() on the list of lists: 10.46647310256958. Okay, we knew.
  • itertools.chain: 0.07705187797546387
  • custom accumulated sum using in-place addition: 0.057044029235839844 (can be faster than itertools.chain as you see)

So sum is way behind because it performs result = result + b instead of result += b

So now my question:

Why can't sum use this accumulative approach when available?

(That would be transparent for already existing applications and would make possible the use of the sum built-in to flatten lists efficiently)

Community
  • 1
  • 1
Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219

3 Answers3

6

We could try to make sum() smarter, but Alex Martelli and Guido van Rossum wanted to keep it focused on arithmetic summations.

FWIW, you should get reasonable performance with this simple code:

result = []
for seq in mylists:
    result += seq

For your other question, "why can't sum use this accumulative approach when available?", see this comment for builtin_sum() in Python/bltinmodule.c:

    /* It's tempting to use PyNumber_InPlaceAdd instead of
       PyNumber_Add here, to avoid quadratic running time
       when doing 'sum(list_of_lists, [])'.  However, this
       would produce a change in behaviour: a snippet like

         empty = []
         sum([[x] for x in range(10)], empty)

       would change the value of empty. */
Raymond Hettinger
  • 216,523
  • 63
  • 388
  • 485
  • Maybe it could be tweaked to check if `empty` is empty, and use another empty list in that case (or make a copy if not too big). That would be transparent and would not cost a lot of CPU (copied the comment made to the other answerer) – Jean-François Fabre Mar 04 '17 at 09:10
  • 2
    Language design is very nuanced. Tricky tweaks can lead to other unexpected behaviors. And if people got in the habit of using *sum()* for non-numeric work, they are more likely to fall prey to quadratic behavior with other types, ``sum(list_of_tuples, ())`` or ``sum(list_of_sets, set())``. And even if those were made performant, there is still some question of whether the word *sum* is the clearest way to express the business logic concepts of *concatenate* or *set.union* or "flatten". Guido's design instincts have an excellent track record. It is perilous to second-guess the BDFL ;-) – Raymond Hettinger Mar 04 '17 at 09:45
  • that's a good point. In that case, why can't we just prevent the usage of `sum` in those cases like it was smartfuly done with strings ? maybe in python 4? – Jean-François Fabre Mar 04 '17 at 09:50
  • 2
    It's always great to see an answer from a Python core dev on questions about Python internals and design decisions. – PM 2Ring Mar 04 '17 at 10:12
1
/* It's tempting to use PyNumber_InPlaceAdd instead of
           PyNumber_Add here, to avoid quadratic running time
           when doing 'sum(list_of_lists, [])'.  However, this
           would produce a change in behaviour: a snippet like
             empty = []
             sum([[x] for x in range(10)], empty)
           would change the value of empty. */
temp = PyNumber_Add(result, item);

Taken from Python's built-in source code https://github.com/python/cpython/blob/master/Python/bltinmodule.c#L2146 Line:2342

Abhishek J
  • 2,386
  • 2
  • 21
  • 22
1

FWIW, we can trick the interpreter into letting us use sum on strings by passing an appropriate custom class instance as the start arg to sum.

class Q(object):
    def __init__(self, data=''):
        self.data = str(data)

    def __str__(self):
        return self.data

    def __add__(self, other):
        return Q(self.data + str(other))

print(sum(['abc', 'def', 'ghi'], Q()))

output

abcdefghi

Of course, this is a rather silly thing to do. :)

PM 2Ring
  • 54,345
  • 6
  • 82
  • 182
  • silly BUT the same thing could be done with lists with in-place add coded in `__add__`, so it's a good idea. – Jean-François Fabre Mar 04 '17 at 10:11
  • @Jean-FrançoisFabre I guess so, but it's hard to argue with the points Raymond makes. OTOH, rather than tweaking `sum` to handle this operation efficiently, `list` could get a new method, eg `.merge` (giving it a `.join` method would lead to a world of confusion). – PM 2Ring Mar 04 '17 at 10:20
  • Good idea, and block `sum` (but that would break some programs, like the `xrange`=>`range` did. – Jean-François Fabre Mar 04 '17 at 10:40