为什么列表推导式会在内部创建一个函数?

Why list comprehensions create a function internally?

提问人:Amir reza Riahi 提问时间:11/16/2023 最后编辑:Abdul Niyas P MAmir reza Riahi 更新时间:11/19/2023 访问量:1864

问:

这是 python 3.10 中列表推导式的反汇编:

Python 3.10.12 (main, Jun 11 2023, 05:26:28) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import dis
>>> 
>>> dis.dis("[True for _ in ()]")
  1           0 LOAD_CONST               0 (<code object <listcomp> at 0x7fea68e0dc60, file "<dis>", line 1>)
              2 LOAD_CONST               1 ('<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_CONST               2 (())
              8 GET_ITER
             10 CALL_FUNCTION            1
             12 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x7fea68e0dc60, file "<dis>", line 1>:
  1           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                 4 (to 14)
              6 STORE_FAST               1 (_)
              8 LOAD_CONST               0 (True)
             10 LIST_APPEND              2
             12 JUMP_ABSOLUTE            2 (to 4)
        >>   14 RETURN_VALUE

据我了解,它创建了一个代码对象,该对象执行实际迭代并返回结果列表,并立即调用它。 我无法理解需要创建一个单独的函数来执行这项工作。这是一种优化技巧吗?listcomp

python-3.x cpython python-internals python-3.12

评论

8赞 user19077881 11/16/2023
推导式对循环控制变量有自己的作用域,就好像是一个函数一样。有关此背景的解释和讨论,请参阅链接
7赞 ShadowRanger 11/17/2023
非常密切相关的问题(从 Py 2 开始,当时 listcomps 没有这样做,但 genexprs、setcomps 和 dictcomps 做到了):为什么 Python 2 中的生成器表达式和 dict/set 推导式使用嵌套函数,而不是列表推导式?

答:

55赞 Abdul Niyas P M 11/16/2023 #1

创建函数的主要逻辑是隔离推导式的迭代变量peps.python.org

通过创建函数:

理解迭代变量保持隔离,不会覆盖 外部作用域中同名的变量,也不可见 领悟后

但它在运行时效率低下。正因为如此, 实现了一个名为 comprehension inlining (PEP 709) 的优化 peps.python.org 这将不再创建单独的代码对象peps.python.org

字典、列表和集合推导式现在是内联的,而不是 为每次执行 理解。这将理解的执行速度提高多达 两次。有关详细信息,请参阅 PEP 709

以下是使用 反汇编的相同代码的输出:

>>> import dis
>>> 
>>> dis.dis("[True for _ in ()]")
  0           0 RESUME                   0

  1           2 LOAD_CONST               0 (())
              4 GET_ITER
              6 LOAD_FAST_AND_CLEAR      0 (_)
              8 SWAP                     2
             10 BUILD_LIST               0
             12 SWAP                     2
        >>   14 FOR_ITER                 4 (to 26)
             18 STORE_FAST               0 (_)
             20 LOAD_CONST               1 (True)
             22 LIST_APPEND              2
             24 JUMP_BACKWARD            6 (to 14)
        >>   26 END_FOR
             28 SWAP                     2
             30 STORE_FAST               0 (_)
             32 RETURN_VALUE
        >>   34 SWAP                     2
             36 POP_TOP
             38 SWAP                     2
             40 STORE_FAST               0 (_)
             42 RERAISE                  0
ExceptionTable:
  10 to 26 -> 34 [2]

如您所见,不再有操作码。相反, 使用LOAD_FAST_AND_CLEARdocs.python.org(at offset ) 和 (at offset ) 操作码来为迭代变量提供隔离。MAKE_FUNCTION6STORE_FAST30

引自 PEP 709 的规范部分peps.python.org

迭代变量的隔离是通过组合实现的 在偏移处的新操作码,它保存了任何 在运行推导器之前,堆栈的外部值,以及 ,在运行后恢复(如果有)的外部值 理解。xLOAD_FAST_AND_CLEAR6x30STORE_FASTx

以下是基准测试结果peps.python.org(使用 MacOS M2 测量):

$ python3.10 -m pyperf timeit -s 'l = [1]' '[x for x in l]'
Mean +- std dev: 108 ns +- 3 ns
$ python3.12 -m pyperf timeit -s 'l = [1]' '[x for x in l]'
Mean +- std dev: 60.9 ns +- 0.3 ns

评论

0赞 fyrepenguin 11/18/2023
这是否意味着他们将在 PEP 之后将变量泄漏到外部范围?还是在不以这种方式交互的情况下内联它们?
2赞 Abdul Niyas P M 11/18/2023
@fyrepenguin 不,它不会泄漏变量。请参阅规范部分,了解 PEP 709 如何为迭代变量提供隔离。另请注意,此 PEP 引入了一些可见的行为更改,此处列出了这些更改
1赞 fyrepenguin 11/18/2023
啊,谢谢!我看到是负责此的新操作码。整洁LOAD_FAST_AND_CLEAR
1赞 Abdul Niyas P M 11/18/2023
@fyrepenguin 是的,我更新了我的答案,使其更清晰。