Python 中复杂列表编译语句的操作顺序和内部创建的资源数量



对于这样的列表理解:

foo = [_.strip().split() for _ in foo[10:]]

Python 正在采取哪些确切步骤来评估右手表达式并将其分配给foo?在 2.x 和 3.x 之间 ...处理这样的事情的内部逻辑在两者之间是否有所不同?

我可以想象,从程序的角度来看,Python 首先执行foo[10:]然后开始迭代结果列表,剥离元素,然后拆分它们,然后将结果附加到一个新列表,最后指向最后一个列表作为foo

它是否在内部为每个操作分配一个新列表?(一个列表用于结果foo[10:],然后另一个列表用于strip的结果,等等?

感谢您的任何见解。

在 Python 3.x 中,你的列表推导被编译成这样的东西:

def _comp(it):
result = []
for _ in it:
result.append(_.strip().split())
return result
foo = _comp(iter(foo[10:]))

有一些细微的区别 - 编译器可以使用比result.append快一点的东西,因为result不可访问;_comp实际上被命名为不是有效标识符的东西,因此您不会意外调用它;等。但基本上就是这样。1

有关完整详细信息,请参阅参考文档中的列表、集和词典的显示。

foo[10:]只是叫foo.__getitem__(slice(None, 10, 10)).如果foo是一个list,则通过创建一个新列表来处理,其中包含从 10 到foo末尾的元素。但是,如果foo是一个 numpy 数组,它可能是对foo的相同内存的视图,如果它是你创建的一些疯狂类的实例,只是为了看看你如何搞砸事情,它可以是你想要的任何东西,比如字符串'abc'

同样,如果foo的元素是字符串(或bytes),则strip方法返回一个新字符串,其中包含复制的所有字符,但去除的空格除外,split方法返回复制字符串的新列表。


在 Python 2.x 中,它更像这样:

_result = []
_it = iter(foo[10:])
for _ in _it:
_result.append(_.strip().split())
foo = _result

虽然同样,它不完全是这样 -_result_it的名称不是有效的标识符,并且使用了append的优化特殊版本,等等。

2.x 文档位于列表显示中。

更改的主要原因是 2.x 设计意味着_泄漏到封闭范围中,2尽管它允许理解和生成器表达式共享大多数相同的代码是另一个好处。

其他列表和字符串操作在 2.x 和 3.x 之间是相同的。虽然在 3.x 中,许多函数确实更改为返回迭代器而不是复制的列表,但切片和拆分不在其中。


本教程有一个很好的关于推导的部分,但它解释了 2.x 的行为,即使在 3.x 中(因为它更容易理解,而且差异对新手的代码来说不太可能重要——毕竟这是一个教程)。


1. 另外,请注意最外层的可迭代对象作为参数传入的方式。这意味着您不会意外地最终捕获闭包中的嵌套变量。这对于列表推导没有太大区别,但对于生成器表达式很重要,在生成器表达式中,迭代可能要等到捕获变量的值更改后才会开始。

2. 在 2.3-2.6 中,此泄漏是您可以信赖的官方记录行为。在 2.7 中,它已被弃用,您不应该依赖它泄漏或不泄漏。但是在 2.7 的所有当前主要实现中(并且不会有任何新的实现),列表推导总是泄漏,尽管集合和字典推导不会。

是的,为foo[10:]创建一个新列表(假设foo是一个列表),并且为每个.split调用创建一个新列表(假设_是一个str)。

它相当于:

foo = []
for _ in foo[:10]: # list slices always create new lists
foo.append(_.strip().split())

除了foo直到最后才被分配到

请注意,按照惯例,不应使用_作为变量名称,除非您打算不使用它。

最后,Python 2 和 Python 3 之间的列表推导之间的一个主要区别是,Python 3 为列表推导中的表达式创建了一个封闭范围(本质上是一个函数范围)。Python 2 推导不会,变量将从结构中"泄漏"出来。

所以,在Python 3中:

>>> [x for x in range(4)]
[0, 1, 2, 3]
>>> x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined

但是,在 Python 2 中:

>>> [x for x in range(4)]
[0, 1, 2, 3]
>>> x
3

如果你真的想要,你可以使用dis来深入研究CPython的内部结构:

In [1]: import dis
In [2]: def f(): foo = [_.strip().split() for _ in foo[10:]]
...:
In [3]: dis.dis(f)
1           0 LOAD_CONST               1 (<code object <listcomp> at 0x105d83b70, file "<ipython-input-2-82d65e58298d>", line 1>)
3 LOAD_CONST               2 ('f.<locals>.<listcomp>')
6 MAKE_FUNCTION            0
9 LOAD_FAST                0 (foo)
12 LOAD_CONST               3 (10)
15 LOAD_CONST               0 (None)
18 BUILD_SLICE              2
21 BINARY_SUBSCR
22 GET_ITER
23 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
26 STORE_FAST               0 (foo)
29 LOAD_CONST               0 (None)
32 RETURN_VALUE

请注意,前三个操作本质上创建了一个函数,其中列表理解魔术发生。我们可以进一步反省:

In [8]: f.__code__.co_consts[1]
Out[8]: <code object <listcomp> at 0x105d83b70, file "<ipython-input-2-82d65e58298d>", line 1>
In [9]: dis.dis(f.__code__.co_consts[1])
1           0 BUILD_LIST               0
3 LOAD_FAST                0 (.0)
>>    6 FOR_ITER                24 (to 33)
9 STORE_FAST               1 (_)
12 LOAD_FAST                1 (_)
15 LOAD_ATTR                0 (strip)
18 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
21 LOAD_ATTR                1 (split)
24 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
27 LIST_APPEND              2
30 JUMP_ABSOLUTE            6
>>   33 RETURN_VALUE

这是为了理解而实际执行的字节码。请注意,该列表的名称为.0,您可以在此处看到:3 LOAD_FAST 0 (.0)'

最新更新