2

According to the list comprehension doc and this question: squares = [x**2 for x in range(10)] is equivalent to squares = list(map(lambda x: x**2, range(10)))

But does Python actually implement list comprehension via map and lambda? Or is the squares = list(map(lambda x: x**2, range(10))) in the document just an approximation instead of exactly equivalence?

  • If so, how to deal with iterating an enumerate object? For example, squares = [(idx, x**2) for idx, x in enumerate(range(10))]? I find it hard for me to imitate the previous example and re-write it using map and lambda.

  • If not(I tend to this option but not sure), is the list comprehension is implemented separately? Is there anything python sentence that is exactly equivalent to the list comprehension?


My research effort:

I tried squares = [print(locals()) for x in range(1)] vs squares = list(map(lambda x: print(locals()), range(1))), inspried by this question eval fails in list comprehension. The first one will give {'.0': <range_iterator object at 0x000002AB976C4430>, 'x': 0} while the latter gives {'x': 0}. Perhaps this indicates that they are not exactly equivalence.

I also found these question:

  1. Where are list comprehensions implemented in CPython source code?
  2. How does List Comprehension exactly work in Python?
  3. List comprehension vs map

They are using Python assembly to show the behavior of list comprehension. These answers perhaps indicate that the list comprehension is implemented separately.

So I tend to believe that the list comprehension is not implemented by map and lambda. However, I am not sure, and want to know "is there anything python sentence that is exactly equivalent to the list comprehension" as I mentioned above.


The origin of this question:

I am trying to answer a question. I want to explain strange behaviors related to the list comprehension without using python assembly. I used to think the list comprehension is implemented via lambda in my previous answer. However, I am doubting about this now.

hellohawaii
  • 3,074
  • 6
  • 21
  • 2
    List comprehensions are their own thing, `list(map(...))` is just an equivalent with the same _output_ (and is specifically discouraged by https://docs.python.org/3/whatsnew/3.0.html#views-and-iterators-instead-of-lists). It's similar to the pure-Python equivalents listed in [`itertools`](https://docs.python.org/3/library/itertools.html), they're just to show the behaviour, the real work happens in the underlying implementation (in C, for CPython - maybe in e.g. PyPy they're more closely aligned). – jonrsharpe Jul 15 '22 at 08:49
  • @jonrsharpe Actually it looks like the real work for listcomps happens during compilation (so yes, well, in C, but that's beside the point and PyPy does the same anyway) – the body of a listcomp is just turned into a function that technically accepts 1 argument (the iterable) and outputs a list. Which is pretty cool, imo :) – AKX Jul 15 '22 at 09:01
  • @jonrsharpe I prefer my previous version of this question. Would you mind me doing rollback? It takes me lot of time to format it. I really like to divide my post to several parts, I think it makes my post look clearer. And I also want "why I want to ask so" to be at the last of my post since it is not very closed to what I want to ask, but provide some additional information. – hellohawaii Jul 15 '22 at 09:13
  • Yes, I would. If nothing else, you were using quote formatting on things that didn't appear to be quotes. Starting with the context helps people understand the frame before getting into the details. And the headings (although this is more of a preference) just take up lots vertical height - if you feel you need to highlight "this is where my question is", you should probably try to be more concise instead. – jonrsharpe Jul 15 '22 at 09:47
  • Also you've accepted an answer with the same "Python assembly" you'd already dismissed from https://stackoverflow.com/a/6407222/3001761, so closing to send the next viewer to the fuller set of answers there. – jonrsharpe Jul 15 '22 at 09:49

1 Answers1

3

No, list comprehensions are not implemented by map and lambda under the hood, not in CPython and not in Pypy3 either.

CPython (3.9.13 here) compiles the list comprehension into a special code object that outputs a list and calls it as a function:

~ $ echo 'x = [a + 1 for a in [1, 2, 3, 4]]' | python3 -m dis
  1           0 LOAD_CONST               0 (<code object <listcomp> at 0x107446f50, file "<stdin>", line 1>)
              2 LOAD_CONST               1 ('<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_CONST               2 ((1, 2, 3, 4))
              8 GET_ITER
             10 CALL_FUNCTION            1
             12 STORE_NAME               0 (x)
             14 LOAD_CONST               3 (None)
             16 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x107446f50, file "<stdin>", line 1>:
  1           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                12 (to 18)
              6 STORE_FAST               1 (a)
              8 LOAD_FAST                1 (a)
             10 LOAD_CONST               0 (1)
             12 BINARY_ADD
             14 LIST_APPEND              2
             16 JUMP_ABSOLUTE            4
        >>   18 RETURN_VALUE

Whereas the equivalent list(map(lambda: ...)) thing is just function calls:

~ $ echo 'x = list(map(lambda a: a + 1, [1, 2, 3, 4]))' | python3 -m dis
  1           0 LOAD_NAME                0 (list)
              2 LOAD_NAME                1 (map)
              4 LOAD_CONST               0 (<code object <lambda> at 0x102701f50, file "<stdin>", line 1>)
              6 LOAD_CONST               1 ('<lambda>')
              8 MAKE_FUNCTION            0
             10 BUILD_LIST               0
             12 LOAD_CONST               2 ((1, 2, 3, 4))
             14 LIST_EXTEND              1
             16 CALL_FUNCTION            2
             18 CALL_FUNCTION            1
             20 STORE_NAME               2 (x)
             22 LOAD_CONST               3 (None)
             24 RETURN_VALUE

Disassembly of <code object <lambda> at 0x102701f50, file "<stdin>", line 1>:
  1           0 LOAD_FAST                0 (a)
              2 LOAD_CONST               1 (1)
              4 BINARY_ADD
              6 RETURN_VALUE
AKX
  • 152,115
  • 15
  • 115
  • 172
  • So the `list(map(lambda: ...))` build a `lambda` object and call it iteratively, while the list comprehension will build a `listcomp` function that handles iteration and call this listcomp` function once. Am I correct? – hellohawaii Jul 15 '22 at 09:06
  • 1
    Yep, that's correct – `map` will call the (lambda) function internally for each element, with all of the overhead involved in function calls, while the generated `listcomp` function is equivalent to `x = []; for a in y: x.append(...); return x` (though more efficiently still, as `x.append` doesn't take a lookup to do). – AKX Jul 15 '22 at 09:08