2

Recently I've been learning python and I have been working with Sets and the map function. I noticed some behavior that I don't quite understand. Say I have the code:

1 A = set()
2 l = [1, 2, 3, 4, 5]
3 
4 map(A.add, l)

From my understanding of map, all line 4 should do is return an iterator over the function A.add() on each element of l, thus not actually modifying A (Which it doesn't as expected).

However, if I replace line 4 with: set(map(A.add, l)) or list(map(A.add, l)) or even tuple(map(A.add, l)), A is now modified to the set {1, 2, 3, 4, 5}. Why does simply casting the return value of map change what happens to A?

My guess as to why this is happening is that line 4 in the first example is simply creating the iterator whereas when I cast it the iterator is actually iterated through in order to perform the cast, thus actually making the A.add() function calls and populating A with the expected values.

1
  • 4
    Try to forget the word "cast" - it rarely applies in Python. set(), list(), and tuple() are functions that wholly materialize an iterable argument (whether the result of a map() or anything else) to construct a container object of the appropriate type. Commented Jun 28, 2020 at 4:03

4 Answers 4

5

Try looking at it this way:

A = set()
l = [1, 2, 3, 4, 5]
 
m = map(A.add, l)

print(A)
# empty set

next(m) # calls A.add(1)
print(A)
# {1}

next(m) # calls A.add(2)
print(A) 
# {1, 2}

Using map() creates an iterator that will lazily evaluate the function on each call to next(). The function A.add() will certainly effect A — that's what it's supposed to do — but it only gets called once each time next() is called. Until next() is called, nothing happens.

Passing m to something like list() will cause it to go through the whole list — it's the same as calling next() until you run out of values. So all the values will be passed one at a time to A.add(). The result is all the values get added to A.

Sign up to request clarification or add additional context in comments.

Comments

1

My understanding:

The map() function returns a Map object. This Map object is an iterator that will apply the operation on each element once iterated. When you call map(A.add, l), you're creating the iterator but not actually iterating.

Now, the set() function is actually a constructor. It can iterate through an iterable and add each item to the new set. However, in this instance, your map object isn't returning anything, it's modifying A.

>>> set(map(A.add, l))
{None}

We see that the result set has a single None, because that's all the map object returned (it actually returned 5 Nones which got set'd down to one). However, A has now been modified since the map has performed its operations.

Comments

0

Your understanding of map is incorrect. From the docs for map:

Return an iterator that applies function to every item of iterable, yielding the results. If additional iterable arguments are passed, function must take that many arguments and is applied to the items from all iterables in parallel. With multiple iterables, the iterator stops when the shortest iterable is exhausted. For cases where the function inputs are already arranged into argument tuples, see itertools.starmap().

The equivalent of map with just loops would be:

def map_with_loops(fn, iterable):
    for item in iterable:
        yield fn(item)

This means that your examples will evaluate to the following:

a = set()
l = [1, 2, 3]

# Doesn't do anything since add has not been consumed yet
map(a.add, l)

# adds all elements of l to a and evaluates to [None, None, None]
list(map(a.add, l))

# adds all elements of l to a and evaluates to {None}
set(map(a.add, l))

Comments

0

set add isn't a very good example for use of map:

In [20]: A = set()                                                                      
In [21]: alist = [1,2,3,4]                                                              
In [22]: A.add(1)           # modifies A, returns None                                                                       
In [23]: A.add(2)                                                                       
In [24]: A                                                                              
Out[24]: {1, 2}
In [25]: list(map(A.add, alist))                                                        
Out[25]: [None, None, None, None]

The correct way(s) to add a list to a set are:

In [26]: set(alist)          # populate a set from a list                                                           
Out[26]: {1, 2, 3, 4}

In [28]: A = set()                                                                      
In [29]: A.update(alist)                                                                
In [30]: A                                                                              
Out[30]: {1, 2, 3, 4}

In [31]: A = set()                                                                      
In [32]: A.update(alist)                                                                
In [33]: A                                                                              
Out[33]: {1, 2, 3, 4}

In [34]: A = set()                                                                      
In [35]: for i in alist: A.add(i)                                                       
In [36]: A                                                                              
Out[36]: {1, 2, 3, 4}

Use of map (or a list comprehension) just for the side effects, and not for the returned value, is discouraged (but not prohibited).

But using map with a function that returns a new value is fine:

In [37]: g = map(float, alist)                                                          
In [38]: g                                                                              
Out[38]: <map at 0x7f78bd07fc18>      # unevaluated map
In [39]: list(g)                      # evaluate/iterate                                                  
Out[39]: [1.0, 2.0, 3.0, 4.0]

tuple and set work as well.

In [40]: tuple(map(float, alist))                                                       
Out[40]: (1.0, 2.0, 3.0, 4.0)

In [41]: set(map(float, alist))                                                         
Out[41]: {1.0, 2.0, 3.0, 4.0}

If it helps you can think of these as set(list(map(float, alist))). Python might not actually populate the list, but there's no harm in thinking that way.

Personally I prefer the clarity of a list comprehension (or generator expression).

In [42]: [float(i) for i in alist]  
Out[42]: [1.0, 2.0, 3.0, 4.0]  
In [43]: {float(i) for i in alist}                                                      
Out[43]: {1.0, 2.0, 3.0, 4.0}
In [46]: tuple(float(i) for i in alist)                                                 
Out[46]: (1.0, 2.0, 3.0, 4.0)

Almost anything that takes an iterable (or sequence) as argument can work with a map, a generator expression, or list.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.