连接两个列表 - '+='和extend()的区别

351

我看到在Python中实际上有两种(可能更多)连接列表的方法:

一种方法是使用extend()方法:

a = [1, 2]
b = [2, 3]
b.extend(a)

另一种方法是使用加号 (+) 运算符:

b += a

现在我想知道:这两种选项中哪一种是“Pythonic”(符合 Python 语言特性的)的列表连接方式,它们之间是否有区别?(我已经查阅了官方 Python 教程,但没有找到任何关于这个话题的内容。)


1
也许在鸭子类型方面存在更多的差异,如果你的可能不是真正的列表但类似于列表支持.__iadd__()/.__add__()/.__radd__().extend()之间的区别会更加明显。 - Nick T
12个回答

297

唯一的字节码级别上的区别是,.extend 方法涉及一个函数调用,在Python中比INPLACE_ADD稍微昂贵一些。

这并不是你应该担心的事情,除非你需要执行数十亿次此操作。然而,瓶颈很可能出现在其他地方。


22
或许在鸭子类型和你的“可能并非真正的列表但类似于列表”的对象是否支持 .__iadd__()/.__add__()/.__radd__().extend() 这些方法方面,差异具有更多的含义。 - Nick T
16
这个答案未提及重要的作用域差异。 - wim
12
实际上,extends 比 INPLACE_ADD() 即列表拼接更快。 - Archit
3
对我来说,这个答案并没有真正帮助我决定应该使用哪一个作为一般原则。我认为一致性很重要,而且知道像是它不能与非本地变量一起使用,也无法链式调用(从其他答案中得知)提供了更实用、功能性的理由,即使有选择,也应该使用extend()而不是操作符。“数十亿次操作”的用例是一个有效的观点,但在我的职业生涯中,我没有遇到过这种情况超过几次。 - void.pointer
5
.extend is faster than +. There is nothing to do with extend having an extra function call. + is an operator and it also causes a function call. The reason .extend is faster is because it does much less work. + will (1) create a list, copy all elements (references) from that list, then it will get the second list and add the references. .extend will not create a new list nor copy references elements from that list. extend is equivalent to a[len(a):] = iterable. extend will operate over the list you are doing the operation and should be used instead of L = L + iterable - fsan

239

对于非局部变量(即对于函数不是本地的且也不是全局的变量),您不能使用 +=。

def main():
    l = [1, 2, 3]

    def foo():
        l.extend([4])

    def boo():
        l += [5]

    foo()
    print l
    boo()  # this will fail

main()

这是因为对于extend情况,编译器将使用LOAD_DEREF指令加载变量l,但对于+=情况,它将使用LOAD_FAST - 这样你就会得到*UnboundLocalError: local variable 'l' referenced before assignment*的错误。


6
我对你的解释“既不是函数内部变量,也不是全局变量”的理解有困难,你能给出这样一个变量的例子吗? - Stephane Rolland
12
我的例子中的变量'l'正是这种类型。它对于'foo'和'boo'函数来说不是本地的(在它们的作用域之外),但它也不是全局的(在'main'函数内定义,而不是在模块级别上定义)。 - monitorius
3
我可以确认,在Python 3.4.2版本中仍会出现此错误(您需要为print语句添加括号,但其他部分保持不变)。 - trichoplax is on Codidact now
8
没错。但在Python3中,你至少可以使用“nonlocal l”语句在boo中。 - monitorius
1
编译器 -> 解释器 - joel
1
一个从程序文本到Python字节码的编译器将创建这些指令,然后字节码解释器将运行它们 =) - monitorius

74
您可以链接函数调用,但不能直接使用+=调用函数:
class A:
    def __init__(self):
        self.listFoo = [1, 2]
        self.listBar = [3, 4]

    def get_list(self, which):
        if which == "Foo":
            return self.listFoo
        return self.listBar

a = A()
other_list = [5, 6]

a.get_list("Foo").extend(other_list)
a.get_list("Foo") += other_list  #SyntaxError: can't assign to function call

9

我认为在使用numpy时会有一些不同(我刚看到这个问题是关于连接两个列表,而不是numpy数组,但由于这可能是初学者的问题,比如我,希望这可以帮助寻找此帖子解决方案的人),例如:

import numpy as np
a = np.zeros((4,4,4))
b = []
b += a

会返回错误信息

值错误:无法将形状为(0,)的操作数广播到形状为(4,4,4)的操作数上

b.extend(a) 可以完美地工作


9

实际上,这三个选项之间存在差异:ADDINPLACE_ADDextend。前者总是较慢,而后两者大致相同。

有了这些信息,我宁愿使用extend,它比ADD更快,并且对你正在做的事情更加明确,比INPLACE_ADD更好。

尝试几次以下代码(适用于Python 3):

import time

def test():
    x = list(range(10000000))
    y = list(range(10000000))
    z = list(range(10000000))

    # INPLACE_ADD
    t0 = time.process_time()
    z += x
    t_inplace_add = time.process_time() - t0

    # ADD
    t0 = time.process_time()
    w = x + y
    t_add = time.process_time() - t0

    # Extend
    t0 = time.process_time()
    x.extend(y)
    t_extend = time.process_time() - t0

    print('ADD {} s'.format(t_add))
    print('INPLACE_ADD {} s'.format(t_inplace_add))
    print('extend {} s'.format(t_extend))
    print()

for i in range(10):
    test()

ADD 0.3540440000000018 s
INPLACE_ADD 0.10896000000000328 s
extend 0.08370399999999734 s

ADD 0.2024550000000005 s
INPLACE_ADD 0.0972940000000051 s
extend 0.09610200000000191 s

ADD 0.1680199999999985 s
INPLACE_ADD 0.08162199999999586 s
extend 0.0815160000000077 s

ADD 0.16708400000000267 s
INPLACE_ADD 0.0797719999999913 s
extend 0.0801490000000058 s

ADD 0.1681250000000034 s
INPLACE_ADD 0.08324399999999343 s
extend 0.08062700000000689 s

ADD 0.1707760000000036 s
INPLACE_ADD 0.08071900000000198 s
extend 0.09226200000000517 s

ADD 0.1668420000000026 s
INPLACE_ADD 0.08047300000001201 s
extend 0.0848089999999928 s

ADD 0.16659500000000094 s
INPLACE_ADD 0.08019399999999166 s
extend 0.07981599999999389 s

ADD 0.1710910000000041 s
INPLACE_ADD 0.0783479999999912 s
extend 0.07987599999999873 s

ADD 0.16435900000000458 s
INPLACE_ADD 0.08131200000001115 s
extend 0.0818660000000051 s

2
你不能将 ADDINPLACE_ADDextend() 进行比较。ADD 会生成一个新的列表,并将两个原始列表的元素复制到其中。当然,它会比 INPLACE_ADDextend() 的原地操作慢。 - wombatonfire
4
我知道。这个例子的重点是比较将所有元素放在一起的不同方式。当然,这需要更长的时间因为它执行了不同的操作,但如果你想保持原始对象不变,了解这种方法仍然是很好的。 - dalonsoa

6

ary += ext 会创建一个新的列表对象,并将列表 "ary" 和 "ext" 的数据复制到其中。

ary.extend(ext) 只是将 "ext" 列表的引用添加到 "ary" 列表的末尾,从而减少内存事务。

因此,.extend 的速度比 += 快数个数量级,并且不会使用任何额外的内存(除了被扩展的列表和要扩展的列表之外)。

╰─➤ time ./list_plus.py
./list_plus.py  36.03s user 6.39s system 99% cpu 42.558 total
╰─➤ time ./list_extend.py
./list_extend.py  0.03s user 0.01s system 92% cpu 0.040 total

第一个脚本使用了超过200MB的内存,而第二个脚本与一个“裸”的python3进程一样不会使用任何更多的内存。

话虽如此,在原地加法似乎做的事情与.extend相同。


你能否在这里添加/list_plus.py/list_extend.py的内容? - Niko Pasanen

6

列表上的 .extend() 方法可以与任何可迭代对象*一起使用,+= 可以与一些对象一起使用,但可能会变得有点奇怪。

import numpy as np

l = [2, 3, 4]
t = (5, 6, 7)
l += t
l
[2, 3, 4, 5, 6, 7]

l = [2, 3, 4]
t = np.array((5, 6, 7))
l += t
l
array([ 7,  9, 11])

l = [2, 3, 4]
t = np.array((5, 6, 7))
l.extend(t)
l
[2, 3, 4, 5, 6, 7]

Python 3.6
*我很确定.extend()适用于任何可迭代对象,但如果我错了,请评论说明

编辑:将“extend()”更改为“列表上的.extend()方法” 注意:David M. Helmuth下面的评论很好很清晰。


元组(Tuple)肯定是可迭代的,但它没有extend()方法。extend()方法与迭代无关。 - wombatonfire
".extend是列表类的一个方法。根据Python文档:list.extend(iterable),通过添加可迭代对象中的所有项来扩展列表。相当于a[len(a):] = iterable。我猜我自己回答了我的星号问题。" - grofte
哦,你的意思是可以将任何可迭代对象传递给extend()。我把它理解为“extend()适用于任何可迭代对象”:)我的错,但听起来有点含糊不清。 - wombatonfire
1
总的来说,这不是一个好的例子,至少在这个问题的背景下不是。当你使用+=运算符与不同类型的对象(与问题中的两个列表相反)时,你不能期望得到对象的连接。你也不能期望会返回一个list类型。看一下你的代码,你得到了一个numpy.ndarray而不是list - wombatonfire
1
@grofte 给出了正确的答案,不过需要做一些澄清,因此我建议如下:使用 extend() 方法将另一个可迭代对象中包含的值连接到列表时,无论该可迭代对象是列表、元组甚至 NumPy 数组,都会得到一致的行为。但是,如果使用 += 运算符将第二个可迭代对象连接到列表,则不会有这种一致的行为。(请参见下面的示例)- 这就是它们之间的区别。 - David M. Helmuth

5

来自Python CPython 3.5.2源代码

没有太大的区别。

static PyObject *
list_inplace_concat(PyListObject *self, PyObject *other)
{
    PyObject *result;

    result = listextend(self, other);
    if (result == NULL)
        return result;
    Py_DECREF(result);
    Py_INCREF(self);
    return (PyObject *)self;
}

3

1

只有 .extend() 可以在元组中使用列表

这将起作用。

t = ([],[])
t[0].extend([1,2])

虽然这不会

t = ([],[])
t[0] += [1,2]

原因是+=会生成一个新的对象。如果你看长版本的代码:
t[0] = t[0] + [1,2]

你可以看到这会改变元组中的对象,这是不可能的。使用.extend()修改了元组中的一个对象,这是被允许的。


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