Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

c += map(lambda n: n *2, c) kills the Python 3 shell. Why?

Python 3.6.4 (v3.6.4:d48ecebad5, Dec 18 2017, 21:07:28)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> c = [1, 2]
>>> c += map(lambda n: n *2, range(1, 3))
>>> c
[1, 2, 2, 4]
>>> c = [1, 2]
>>> c += map(lambda n: n *2, c)
Killed!

However the same works with Python 2 where map results in a list unlike a generator in Python 3. Then why does c += map(lambda n: n *2, range(1, 3)) work?

like image 658
Nitin Avatar asked Mar 05 '23 07:03

Nitin


1 Answers

c += foo does the equivalent of c = c.__iadd__(foo). And if c is a list, the list.__iadd__ method accepts any iterable, including a generator expression:

>>> x = []
>>> x += (i for i in range(3))
>>> x
[0, 1, 2]

this even though list.__add__ does not support generator expressions:

>>> x + (i for i in range(3))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "generator") to list

The code crashes in Python 3 because the map generator iterates over the same list that is being extended, but the current position is always two elements behind the tail. The list.__iadd__ will always add new elements to the end one by one as they are extracted from the iterator; as this happens, the index used by the list iterator held by the map generator will be incremented... but a new element was added to the end of the list so there are always 2 elements after the current iteration position, and eventually Python runs out of memory or the computer grinds to halt due to swapping.

I.e. the behaviour is not unlike that of

for elem in c:
    c.append(elem * 2)

(kudos to Wim)

The Python 2 version works exactly because map will create a list before calling c.__iadd__; a new list of 2 elements will be passed to c.__iadd__. Likewise the c += map(lambda n: n * 2, range(1, 3)) would work because in Python 2 range(1, 3) would create a new list, and in Python 3 it would create a range sequence object, and both of them are distinct

If you replace the map with a generator expression, you can trigger pathological behaviour in Python 2 too:

Python 2.7.15rc1 (default, Nov 12 2018, 14:31:15) 
[GCC 7.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> x = [1, 2]
>>> x += (n * 2 for n in x)
like image 172