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

37
这是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的代码对象,该对象执行实际的迭代并返回结果列表,并立即调用它。 我无法理解为什么需要创建一个单独的函数来执行这个任务。这是一种优化技巧吗?

8
列表推导式有自己的作用域,就像函数一样。请参阅链接了解相关背景的解释和讨论。 - undefined
7
非常相关的问题(来自于 Python 2 中,当 listcomps 这样做时,但是 genexprs、setcomps 和 dictcomps 这样做的):为什么 Python 2 中的生成器表达式和字典/集合推导式使用嵌套函数而不是列表推导式? - undefined
1个回答

55
创建函数的主要逻辑是为了隔离推导式的迭代变量peps.python.org
通过创建函数:
推导式的迭代变量保持隔离,不会覆盖外部作用域中同名的变量,也不会在推导式之后可见。
但它在运行时效率低下。因此, 实现了一种优化,称为内联推导(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 <b>LOAD_FAST_AND_CLEAR</b>      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 <b>STORE_FAST</b>               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]

如您所见,不再存在MAKE_FUNCTION操作码。相反,使用LOAD_FAST_AND_CLEARdocs.python.org(在偏移量6处)和STORE_FAST(在偏移量30处)操作码来为迭代变量提供隔离。
引用自PEP 709的Specification sectionpeps.python.org
通过在偏移量为6处引入新的`LOAD_FAST_AND_CLEAR`操作码,将`x`迭代变量的隔离实现为在运行推导式之前将`x`的任何外部值保存在堆栈上,并在运行推导式后通过`30`个`STORE_FAST`操作码恢复`x`的外部值(如果有的话)。
这里是基准测试结果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

那是否意味着在那个PEP之后,它们会将变量泄漏到外部作用域中?还是会以不互动的方式将它们内联? - undefined
2
@fyrepenguin 不会泄漏变量。请查看Specification部分以了解PEP 709如何为迭代变量提供隔离。另外请注意,这个PEP引入了一些可见的行为变化,详细列在这里 - undefined
1
啊,谢谢!我看到LOAD_FAST_AND_CLEAR是负责这个的新操作码。真棒! - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接