0

I have a question about how Python(3) internally loops when computing multiple maps. Here's a nonsense example:

from random import randint

A = [randint(0,20) for _ in range(100)]
map1 = map(lambda a: a+1, A)
map2 = map(lambda a: a-1, map1)
B = list(map2)

Because map() produces a lazy expression, nothing is actually computed until list(map2) is called, correct?

When it does finally do this computation, which of these methods is it more akin to?

Loop method 1:

A = [randint(0,20) for _ in range(100)]
temp1 = []
for a in A:
    temp1.append(a+1)

B = []
for t in temp1:
    B.append(t-1)

Loop method 2:

A = [randint(0,20) for _ in range(100)]
B = []
for a in A:
    temp = a+1
    B.append(temp-1)

Or does it compute in some entirely different manner?

Cœur
  • 37,241
  • 25
  • 195
  • 267
natemcintosh
  • 730
  • 6
  • 16
  • 1
    Prerequisite: https://stackoverflow.com/questions/16301253/what-exactly-is-pythons-iterator-protocol – VPfB Jul 11 '19 at 19:33

2 Answers2

1

In general, the map() function produces a generator, which in turn doesn't produce any output or calculate anything until it's explicitly asked to. Converting a generator to a list is essentially akin to asking it for the next element until there is no next element.

We can do some experiments on the command line in order to find out more:

>>> B = [i for i in range(5)]
>>> map2 = map(lambda b:2*b, B)
>>> B[2] = 50
>>> list(map2)
[0, 2, 100, 6, 8]

We can see that, even though we modify B after creating the generator, our change is still reflected in the generator's output. Thus, it seems that map holds onto a reference to the original iterable from which it was created, and calculates one value at a time only when it's asked to.


In your example, that means the process goes something like this:

A = [2, 4, 6, 8, 10]
b = list(map2)
    b[0] --> next(map2) = (lambda a: a-1)(next(map1))
             --> next(map1) = (lambda a: a+1)(next(A)) 
                 --> next(A) = A[0] = 2
             --> next(map1) = 2+1 = 3
         --> next(map2) = 3-1 = 2
    ...

In human terms, the next value of map2 is calculated by asking for the next value of map1. That, in turn, is calculated from A that you originally set.

Green Cloak Guy
  • 23,793
  • 4
  • 33
  • 53
0

This can be investigated by using map on functions with side-effects. Generally speaking, you shouldn't do this for real code, but it's fine for investigating the behaviour.

def f1(x):
    print('f1 called on', x)
    return x

def f2(x):
    print('f2 called on', x)
    return x

nums = [1, 2, 3]
map1 = map(f1, nums)
map2 = map(f2, map1)
for x in map2:
    print('printing', x)

Output:

f1 called on 1
f2 called on 1
printing 1
f1 called on 2
f2 called on 2
printing 2
f1 called on 3
f2 called on 3
printing 3

So, each function is called at the latest time it could possibly be called; f1(2) isn't called until the loop is finished with the number 1. Nothing needs to be done with the number 2 until the loop needs the second value from the map.

kaya3
  • 47,440
  • 4
  • 68
  • 97