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

时间:2018-04-19 04:04:44

标签: python python-3.x python-2.7 memory-management

说出像这样的列表理解:

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

Python用于评估右手表达式并将其分配给foo的确切步骤是什么?在2.x和3.x之间...是处理这两者之间的内部逻辑吗?

我认为从程序角度来看,Python首先执行foo[10:]然后开始遍历结果列表,剥离元素然后拆分它们然后将结果附加到新列表中,最后指向最后一个列表为foo

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

感谢您的任何见解。

2 个答案:

答案 0 :(得分:4)

在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

有关详细信息,请参阅参考文档中的Displays for lists, sets, and dictionaries

foo[10:]只调用foo.__getitem__(slice(None, 10, 10))。如果foolist,则可以通过创建包含从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文档位于List Displays

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

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

The tutorial有一个关于理解的很好的部分,但它解释了2.x的行为,即使在3.x中也是如此(因为它更容易理解,并且差异不大可能对新手和代码来说很重要 - 毕竟这是一个教程。

<子> 1。另外,请注意最外面的iterable作为参数传入的方式。这意味着您不会意外地在闭包中捕获嵌套变量。对于列表推导没有太大区别,但它对于生成器表达式很重要,其中迭代可能直到捕获的变量的值发生变化后才开始。

<子> 2。在2.3-2.6中,这种泄漏是您可以依赖的官方记录行为。在2.7中,它被弃用了,你不应该依赖它泄漏或不泄漏。但是在2.7的所有当前主要实现中(并且不会有任何新的实现),列表推导总是泄漏,尽管set和dict理解不会。

答案 1 :(得分:3)

是的,为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)`