Search code examples
pythonpython-3.xlazy-evaluation

Is Python's map function evaluation lazy?


I've been solving LeetCode's 1331. Rank Transform of an Array challenge and came up with this one-line solution:

class Solution:
    def arrayRankTransform(self):
        return map({e: i for i, e in enumerate(sorted(set(arr)), 1)}.get, arr)

It evaluates substitution dictionary and maps all element with it (something like str's translate method). But for this solution I have a question: does the dictionary evaluate only one time before looping over elements or does it re-evaluates for every element?

In other words, does map store the function before iterating or does it evaluate the function again for every element?

If you can answer, please provide reference for Python docs or source code


Solution

  • It creates and stores the dict's bound .get method once as each map object is constructed, not once-per-item in the iterable. It's a normal argument to a callable, map isn't a special syntactic construct, there's literally no way for Python to delay creating the dict and binding the .get method of it, any more than it could do so for a user-defined function.

    This is in fact one of the rare cases where map improves meaningfully on list comprehensions/generator expressions; map receiving the mapping function as an argument means an expensive-to-create callable is only created once; if you inlined it into a listcomp or genexpr, e.g.:

    class Solution:
        def arrayRankTransform(self, arr):
            return ({e: i for i, e in enumerate(sorted(set(arr)), 1)}.get(x) for x in arr)
    

    it would be created once per loop, adding quite a bit of overhead, and the only way to avoid it while still writing a genexpr would be to extract the construction to a cached value outside the genexpr, e.g.

    class Solution:
        def arrayRankTransform(self, arr):
            mapping = {e: i for i, e in enumerate(sorted(set(arr)), 1)}
            return (mapping.get(x) for x in arr)
    

    Technically, there are other tricks possible to do this as a one-liner with a genexpr that only constructs the mapping once, but they're ugly hacks; the two-liner is the way to go if you can't/won't use map and need to avoid per-item overhead.