【问题标题】:What is an attribute lookup to list.append?什么是 list.append 的属性查找?
【发布时间】:2021-12-31 17:44:03
【问题描述】:

在考虑创建列表的选项时,有列表推导式、list() 构造函数和 [] 文字语法。

我找到了一个article,它说列表推导被转换为在底层调用list.append() 的函数。

所以这给我的印象是 append() 在列表中的每次迭代中都会被调用,就像在类似的实现中说的那样,如果你愿意,你可以使用 for loop 来编写它。

但是,我现在找到了之前的 answer here on SO,上面写着:

列表理解版本利用了特殊的字节码 LIST_APPEND 直接为我们调用 PyList_Append。因此它避免了 list.append 的属性查找和 Python 中的函数调用 级别。

那么,当您编写如上面的正方形示例这样的列表推导式时,它是在每次迭代时调用append(),还是不调用?

相对于对 list.append() 的实际 c 级调用,对 list.append 的属性查找意味着什么?

【问题讨论】:

  • 目前尚不清楚这个问题除了与您链接到的问题的重复之外还有什么其他意义。您似乎在问该问题中接受的答案是否有效。
  • 不,完全没有质疑接受的答案的有效性!自从解决了我的困惑后,我已经改写了我的问题。
  • 当代码中出现类似“squares.append”的东西时,总是需要检查“squares”是否是一个列表,并检索属性“append”引用的函数并做一些其他的事情。在列表推导中,运行时知道它正在处理一个列表,并且它知道应该使用列表的“追加”方法而不用查找它。
  • 当您执行some_object.whatever 之类的属性查找 时,需要在运行时搜索该属性。请注意,您可以使用 whatever = some_object.whatever 完成相同的操作

标签: python


【解决方案1】:

我认为 append append() 方法不会在 python 内部被调用。

它在编译器内部被隐式调用:

In [17]: class list2(list):
    ...:     def append(self, *args, **kwargs):
    ...:         print("I have been called")
    ...:         super().append(*args, **kwargs)
    ...: 

In [18]: a = (i for i in range(10))

In [19]: l = list2(a)

In [20]: l
Out[20]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

In [21]: l.append(10)
I have been called

In [22]: l
Out[22]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

我检查了它的代码的反汇编:

In [28]: def f():
    ...:     return [i for i in range(10)]
    ...: 

In [29]: dis.dis(f)
  2           0 LOAD_CONST               1 (<code object <listcomp> at 0x7f06bf957d60, file "<ipython-input-28-461835cd83dc>", line 2>)
              2 LOAD_CONST               2 ('f.<locals>.<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_GLOBAL              0 (range)
              8 LOAD_CONST               3 (10)
             10 CALL_FUNCTION            1
             12 GET_ITER
             14 CALL_FUNCTION            1
             16 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x7f06bf957d60, file "<ipython-input-28-461835cd83dc>", line 2>:
  2           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                 4 (to 14)
              6 STORE_FAST               1 (i)
              8 LOAD_FAST                1 (i)
             10 LIST_APPEND              2
             12 JUMP_ABSOLUTE            2 (to 4)
        >>   14 RETURN_VALUE

如果您查看第二部分,则会发现正在返回的对象的反汇编。有一个FOR_ITERJUMP_ABSOLUTE 关闭。像(to 4) 这样的数字是指跳到哪里。它一直在迭代,同时在追加。

【讨论】:

  • 我也试过拆机。用list()LIST_APPEND替换为POP_TOP
【解决方案2】:

dis 模块可用于直接检查内部 python 字节码。下面是两个函数(稍作修改以更加自洽),以及解析后发送到 python 解释器的结果字节码:

def comprehension():
    squares = [num for num in range(1,11) if num % 2 == 0]
    return squares
    
def loop():
    squares = []
    for num in range(1,11):
        if num % 2 == 0:
            squares.append(num)
    return squares

然后拨打dis.dis(comprehension)

>>> dis(comprehension)
 10           0 LOAD_CONST               1 (<code object <listcomp> at 0x0000021EB480BBE0, file "C:\Users\aaron\Documents\scripts\python\untitled2.py", line 10>)
              2 LOAD_CONST               2 ('comprehension.<locals>.<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_GLOBAL              0 (range)
              8 LOAD_CONST               3 (1)
             10 LOAD_CONST               4 (11)
             12 CALL_FUNCTION            2
             14 GET_ITER
             16 CALL_FUNCTION            1
             18 STORE_FAST               0 (squares)

 11          20 LOAD_FAST                0 (squares)
             22 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x0000021EB480BBE0, file "C:\Users\aaron\Documents\scripts\python\untitled2.py", line 10>:
 10           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                20 (to 26)
              6 STORE_FAST               1 (num)
              8 LOAD_FAST                1 (num)
             10 LOAD_CONST               0 (2)
             12 BINARY_MODULO
             14 LOAD_CONST               1 (0)
             16 COMPARE_OP               2 (==)
             18 POP_JUMP_IF_FALSE        4
             20 LOAD_FAST                1 (num)
             22 LIST_APPEND              2
             24 JUMP_ABSOLUTE            4
        >>   26 RETURN_VALUE

解释这个输出需要一点练习,但一般流程是首先创建“列表理解函数”(MAKE_FUNCTION 跳到第二个“反汇编”块),它负责创建一个新列表 @987654326 @,遍历迭代器FOR_ITER,测试条件POP_JUMP_IF_FALSE,并且(当条件为真时)直接附加到列表LIST_APPEND。创建这个“理解函数”后,我们返回主函数创建迭代器,然后将迭代器传递给我们创建的函数。最后将结果存储到一个变量中并返回该变量。


loop 函数实际上是更少的字节码指令,但不一定将其解释为更快或更好……一些字节码指令比其他指令更复杂/更慢。只有实际的分析才能真正告诉您速度上的差异。

>>> dis(loop)
 14           0 BUILD_LIST               0
              2 STORE_FAST               0 (squares)

 15           4 LOAD_GLOBAL              0 (range)
              6 LOAD_CONST               1 (1)
              8 LOAD_CONST               2 (11)
             10 CALL_FUNCTION            2
             12 GET_ITER
        >>   14 FOR_ITER                26 (to 42)
             16 STORE_FAST               1 (num)

 16          18 LOAD_FAST                1 (num)
             20 LOAD_CONST               3 (2)
             22 BINARY_MODULO
             24 LOAD_CONST               4 (0)
             26 COMPARE_OP               2 (==)
             28 POP_JUMP_IF_FALSE       14

 17          30 LOAD_FAST                0 (squares)
             32 LOAD_METHOD              1 (append)
             34 LOAD_FAST                1 (num)
             36 CALL_METHOD              1
             38 POP_TOP
             40 JUMP_ABSOLUTE           14

 18     >>   42 LOAD_FAST                0 (squares)
             44 RETURN_VALUE

遵循这个函数的路径稍微简单一些,但关键区别在于python必须先从“squares”中查找“append”方法才能调用它,因为它本身并不知道"squares" 是一个列表,因此不能只调用LIST_APPEND

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-08-05
    • 2014-09-02
    • 2017-12-09
    • 2020-10-10
    • 2019-09-13
    • 2013-03-17
    • 2016-12-01
    • 2015-02-10
    相关资源
    最近更新 更多