Python列表拼接效率

27

当需要:

  • list_b中的项目在list_a之前
  • 结果必须放置在list_a

最高效的连接两个列表list_alist_b的方法是什么?我有四种可能的选择:

# 1
list_a = list_b + list_a

# 2
for item in list_b:
    list_a.insert(0, item)

# 3
for item in self.list_a:
    list_b.append(item)
list_a = list_b

# 4
list_a[0:0] = list_b

谢谢!


如果您经常这样做或处理非常大的数据,那么有更好的数据结构可用(一个简单的方法:将所有列表反转存储)。 - Katriel
6个回答

37

这是一个关于BigYellowCactus的答案中使用的时间随列表长度变化的图表。纵轴表示在微秒内初始化两个列表并将一个插入到另一个前面所需的时间。横轴表示列表中的项目数。

Asymptotic behaviour of the possibilities

t1:


list_a = list_b + list_a

t2:

for item in list_b:
    list_a.insert(0, item)

t3:

for item in list_a:
    list_b.append(item)
list_a = list_b

t4

list_a[0:0] = list_b

我知道这个问题很久以前就被问过了,但我想知道:如果我只是将listb添加到lista中...那么a然后b...这种情况仍然适用吗?或者有没有更有效的方法将n个列表添加到一个列表中? - Thornhale
这张图表是错误的。它计算了创建list_a所需的时间,而这绝对支配了两种最快方法的运行时间,即list_a = list_b + list_alist_a[0:0] = list_b - user3064538

8

鉴于

list_a = list_b + list_a

如果一个对象适合你的目的,那么你实际上并不需要list_a对象本身来存储list_a中的所有数据 - 你只需要将它命名为list_a(即,你没有或者不关心其它变量浮动到指向同一个列表的情况)。
如果你也不关心它确切地是一个列表,而只关心它是否可迭代,那么你可以使用itertools.chain:
list_a = itertools.chain(list_b, list_a)

如果您关心一些列表事物,可以构建类似于chain的行为像列表一样的东西,例如:

class ListChain(list):
    def __init__(self, *lists):
        self._lists = lists

    def __iter__(self):
        return itertools.chain.from_iterable(self._lists)

    def __len__(self):
        return sum(len(l) for l in self._lists)

    def append(self, item):
        self._lists[-1].append(item)

    def extend(self, iterable):
        self._lists.append(list(iterable))

    def __getitem__(self, item):
       for l in self._lists:
           if item < len(l):
              return l[item]
           item -= len(l)
       else:
          raise IndexError

等等。要让这个在所有情况下都起作用,需要花费大量的努力(可能不值得)-例如,处理切片和负索引会让人想到。但对于非常简单的情况,这种方法可以避免大量复制列表内容。


深入的回答!但对我来说没有用。list_a是来自第三方模块,我不想去修改它。 - Thorfin

6

您可以将list_b分配给一个切片,该切片恰好为空,但位于list_a的开头:

list_a[0:0] = list_b

这是将列表插入到另一个列表中的最快方法,可以在任何位置进行插入。

如何使用此方法将元素添加到列表末尾。 - Piyush Divyanakar
使用 list.extend() 将元素添加到列表末尾。你也可以使用 list_a[len(list_a):len(list_a)] 来赋值到末尾,但这样写、读和执行起来都更加繁琐。 - Martijn Pieters

5

试试这个:

list_a[0:0] = list_b

5

enter image description here

itertools.chain 只是生成一个生成器,所以如果您可以使用生成器而不是列表,那么生成时间是恒定的,但在访问每个元素时需要付出代价。否则,list_a[0:0] = list_blist_a = list_b + list_a 快大约6倍。

我认为list_a = list_b + list_a 是最易读的选择,而且已经相当快了。

您提到的两种使用append()for循环方法速度非常慢,因此我没有包含它们。


在 1.6 GHz 双核 Intel Core i5 上,使用 16 GB 的 2133 MHz LPDDR3 RAM 运行 Python 3.7.5 [Clang 11.0.0 (clang-1100.0.33.8)],使用以下代码运行:

from timeit import timeit
import random
import matplotlib.pyplot as plt

num_data_points = 1000
step = 10
methods = [
    # ordered from slowest to fastest to make the key easier to read
    # """for item in list_a: list_b.append(item); list_a = list_b""",
    # """for item in list_b: list_a.insert(0, item)""",
    # "list_a = list(itertools.chain(list_b, list_a))",
    "list_a = list_b + list_a",
    "list_a[0:0] = list_b",
    "list_a = itertools.chain(list_b, list_a)",
]

x = list(range(0, num_data_points * step, step))
y = [[] for _ in methods]
for i in x:
    list_a = list(range(i))
    list_b = list(range(i))
    random.shuffle(list_a)
    random.shuffle(list_b)
    setup = f"list_a = {list_a}; list_b = {list_b}"
    for method_index, method in enumerate(methods):
        y[method_index].append(timeit(method, setup=setup, number=30))
    print(i, "out of", num_data_points * step)

ax = plt.axes()
for method_index, method in enumerate(methods):
    ax.plot(x, y[method_index], label=method)
ax.set(xlabel="number of elements in both lists", ylabel="time (s) (lower is better)")
ax.legend()
plt.show()

1
我发布了这个答案,因为被接受的答案中的时间不正确,列表的创建不应该是计时的一部分。 - user3064538

4
为什么不只使用 timeit
import timeit

create_data = """\
list_a = range(10)
list_b = range(10)
"""

t1 = timeit.Timer(stmt=create_data + """\
list_a = list_b + list_a
""")

t2 = timeit.Timer(create_data + """\
for item in list_b:
    list_a.insert(0, item)
""")

t3 = timeit.Timer(create_data + """\
for item in list_a:
    list_b.append(item)
list_a = list_b
""")

t4 = timeit.Timer(create_data + """\
list_a[0:0] = list_b
""")

for i, t in enumerate([t1,t2,t3,t4]):
    print i, "%.2f usec/pass" % (1000000 * t.timeit(number=100000)/100000)

结果:

0 0.73微秒/次
1 2.79微秒/次
2 1.66微秒/次
3 0.77微秒/次


5
你现在还在计时列表创建的时间。最好将其移至设置部分,而不是计时部分。但由于你每次都必须重新设置输入,所以这可能不会有太大的区别。 - Martijn Pieters

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