Mad Map: Python Road

I've been using Python for several years and I used to think there was nothing in the language that can surprise me. It was so until recently. The story I'm about to tell is about how even so simple and well-known thing like map() function can surprise you after years of using, and what led me to believe that it's poorly designed.

So, what's wrong with map()? We use it everyday and everywhere, and it seems OK, and none of us have met any problems. Yes, indeed, map() works just fine if you use it how it's intended to be used. For instance, if you use map() to multiply each element of some sequence by 2 -

x = map(lambda a: 2 * a, [1, 2])

Just keep going, everything's ok. But let me show another usage example -

x = map(None, ['a', 'b', 'c'], [1, 2])

Can you tell now what is x, ha? I always thought that map() function receives precisely two arguments:

Can you imagine how surprised I was when it turned out that I was wrong? Python has always been about readability, and even if you don't know the language you can pretty much guess most of its parts intuitively. So I started thinking what could possible be returned in this case? What None may mean in this context? I recalled that if None is passed as a function argument to filter() then only truthy elements are returned. Hm, maybe map() behaves similar to filter() and returns a list of booleans? Let's check.

>>> map(None, [-1, 0, 1])
[-1, 0, 1]

No, it's not :( So I went to the documentation and learned that if a function argument is None, the identity function is assumed. In other words it's equal to the following call -

>>> map(lambda a: a, [-1, 0, 1])
[-1, 0, 1]

Wait a minute. None means identity?! Does it make sense? Perhaps if we're talking about default behaviour, but it makes no sense from a user point of view. You know, it looks like "pass None if you want to get identity function behaviour". But let's go on.

I continued guessing about how map() should deal with two iterables? It was obvious to me that map() should chain them: when first is over, the second will be used.

Let's take a loot at the original snippet one more time.

x = map(None, ['a', 'b', 'c'], [1, 2])

What is x? Taking into account that was written above I was expecting x to be -

['a', 'b', 'c', 1, 2]

Boom! I missed again, because x was -

[('a', 1), ('b', 2), ('c', None)]

And I stunned again because that means these two iterables were zipped, not chained. Why zipped? I never asked for this! Is this really Python? I always liked the language because of good design and good intentions. And when I didn't know something it always fitted my expectations, but this was something really weird.

I agree that someone may expect iterables to be zipped, but I'm so much disappointed that that one may stuck upon expression that could not be correctly guessed. I wish map() to be dead simple: receive one function and one iterable, and leave zipping or chaining up to programmers. Moreover, I wish a function argument to be always a function, no way to fallback to default behaviour. If someone wants identity behaviour let's pass it explicitly, no way to do it through None.

The most frustrating thing is that it hasn't been removed in Python 3, but changed! First, you can't use None as identity function anymore (alleluia). If you try to do that you'll fall with TypeError exception:

TypeError: 'NoneType' object is not callable

Second, iterables are zipped by zip(), not zip_longest(). If the first change seems rational, the second one is mad. It doesn't provide any benefits, but may cause a lot of pain for those who porting some software to Python. Why? You see, if two iterables are equal in size the behavior is still the same -

language / expression map(lambda x: x, ['a', 'b'], [1, 2])
Python 2 [('a', 1), ('b', 2)]
Python 3 [('a', 1), ('b', 2)]

if not, it's different -

language / expression map(lambda x: x, ['a', 'b', 'c'], [1, 2])
Python 2 [('a', 1), ('b', 2), ('c', None)]
Python 3 [('a', 1), ('b', 2)]

I'm really really sad about such a mad map() and I wish I never knew about that. I don't know why it was designed so, but I hope none of pythonistas will use it and one day it will be removed from the language.