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

Ami*_*ahi 47 python cpython list-comprehension python-internals dis

中列表理解的反汇编:

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
Run Code Online (Sandbox Code Playgroud)

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

Abd*_*P M 66

创建函数的主要逻辑是隔离 compressive\xe2\x80\x99s 迭代变量peps.python.org

\n

通过创建一个函数:

\n
\n

理解迭代变量保持隔离,不会\xe2\x80\x99覆盖\n外部作用域中的同名变量,并且在理解后\n它们也不可见

\n
\n

然而,这在运行时效率很低。由于这个原因,理解内联(PEP 709)peps.python.org的优化,它将不再创建单独的代码对象peps.python.org

\n
\n

字典、列表和集合推导式现在是内联的,而不是为推导式的每次执行创建一个新的一次性函数对象。可以将理解的执行速度提高多达\n两倍。有关更多详细信息,请参阅PEP 709 。

\n
\n

反汇编的相同代码的输出:

\n
>>> import dis\n>>> \n>>> dis.dis("[True for _ in ()]")\n  0           0 RESUME                   0\n\n  1           2 LOAD_CONST               0 (())\n              4 GET_ITER\n              6 LOAD_FAST_AND_CLEAR      0 (_)\n              8 SWAP                     2\n             10 BUILD_LIST               0\n             12 SWAP                     2\n        >>   14 FOR_ITER                 4 (to 26)\n             18 STORE_FAST               0 (_)\n             20 LOAD_CONST               1 (True)\n             22 LIST_APPEND              2\n             24 JUMP_BACKWARD            6 (to 14)\n        >>   26 END_FOR\n             28 SWAP                     2\n             30 STORE_FAST               0 (_)\n             32 RETURN_VALUE\n        >>   34 SWAP                     2\n             36 POP_TOP\n             38 SWAP                     2\n             40 STORE_FAST               0 (_)\n             42 RERAISE                  0\nExceptionTable:\n  10 to 26 -> 34 [2]
Run Code Online (Sandbox Code Playgroud)\n

正如您所看到的,不再有MAKE_FUNCTION操作码,也不再有单独的代码对象。相反,使用docs.python.org(在 offset 处)和(在 offset 处)操作码来为迭代变量提供隔离。LOAD_FAST_AND_CLEAR6STORE_FAST30

\n

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

\n
\n

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

\n
\n

除此之外,在 ,不再有一个单独的框架用于回溯中的理解

\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n
中的回溯中的回溯
>>> [1 / 0 for i in range(10)]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in <listcomp>
ZeroDivisionError: division by zero
Run Code Online (Sandbox Code Playgroud)
>>> [1 / 0 for i in range(10)]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
Run Code Online (Sandbox Code Playgroud)
\n
\n

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

\n
$ python3.10 -m pyperf timeit -s \'l = [1]\' \'[x for x in l]\'\nMean +- std dev: 108 ns +- 3 ns\n$ python3.12 -m pyperf timeit -s \'l = [1]\' \'[x for x in l]\'\nMean +- std dev: 60.9 ns +- 0.3 ns\n
Run Code Online (Sandbox Code Playgroud)\n

  • @fyrepenguin 不,它不会泄漏变量。请参阅[“规范”部分,了解 PEP 709 如何为迭代变量提供隔离](https://peps.python.org/pep-0709/#specification)。另请注意,此 PEP 引入了一些可见的行为更改,[此处]列出了这些更改(https://docs.python.org/3/whatsnew/3.12.html#pep-709-compressive-inlined) (3认同)
  • 啊,谢谢!我看到“LOAD_FAST_AND_CLEAR”是负责此操作的新操作码。整洁的 (3认同)