附加到简短的 python 列表的惯用语法是啥?

Posted

技术标签:

【中文标题】附加到简短的 python 列表的惯用语法是啥?【英文标题】:What's the idiomatic syntax for prepending to a short python list?附加到简短的 python 列表的惯用语法是什么? 【发布时间】:2012-01-22 04:49:39 【问题描述】:

list.append() 是添加到列表末尾的明显选择。这是缺少的list.prepend() 的reasonable explanation。假设我的列表很短并且性能问题可以忽略不计,是

list.insert(0, x)

list[0:0] = [x]

惯用语?

【问题讨论】:

就计算时间而言,new_list = [x] + your_list 的效率是否低于your_list.insert(x) 【参考方案1】:

s.insert(0, x) 形式是最常见的。

不过,当您看到它时,可能是时候考虑使用collections.deque 而不是列表了。在双端队列之前运行在恒定时间内。预先添加到列表以线性时间运行。

【讨论】:

“无论何时你看到它,可能是时候考虑使用 collections.deque 而不是列表了。”这是为什么呢? @MattM。如果您在列表的前面插入,python 必须将所有其他项目向前移动一个空格,列表不能“在前面腾出空间”。 collections.deque(双端队列)支持“在前面腾出空间”,在这种情况下要快得多。 @fejfo,我认为评论应该是答案的一部分。 就计算时间而言,new_list = [x] + your_list 的效率是否低于your_list.insert(x) @CharlieParker 是的,创建新列表的效率会降低,因为必须更新所有对象引用计数。否则,复制工作量是相似的。【参考方案2】:

如果你能走函数式的方式,下面就很清楚了

new_list = [x] + your_list

当然,您还没有将x 插入your_list,而是创建了一个带有x 的新列表。

【讨论】:

如您所见,这并没有添加到列表中。它正在创建一个新列表。因此它根本不能满足这个问题。 虽然它不能满足问题,但它会四舍五入,这就是本网站的目的。感谢您的评论,您是对的,但是当人们搜索此内容时,看到此内容会很有帮助。 另外,如果您想将列表添加到列表中,那么使用 insert 将无法按预期工作。但是这个方法可以! your_list = [x] + your_list 有什么问题?那不会创建一个新列表吧? @lightbox142 它将创建一个新列表并将其分配给your_list【参考方案3】:

如果有人像我一样发现这个问题,这里是我对提议方法的性能测试:

Python 2.7.8

In [1]: %timeit ([1]*1000000).insert(0, 0)
100 loops, best of 3: 4.62 ms per loop

In [2]: %timeit ([1]*1000000)[0:0] = [0]
100 loops, best of 3: 4.55 ms per loop

In [3]: %timeit [0] + [1]*1000000
100 loops, best of 3: 8.04 ms per loop

如您所见,insert 和切片分配的速度几乎是显式添加的两倍,并且结果非常接近。正如Raymond Hettinger 所指出的,insert 是更常见的选项,我个人更喜欢这种方式来添加到列表中。

【讨论】:

@Dakkaron 我认为你错了。相当多的来源引用了 list.insert 的线性复杂性,例如this nice table,并且通过提问者链接到的合理解释来暗示。我怀疑 CPython 在前两种情况下会重新分配列表中内存中的每个元素,因此所有这三种情况都可能具有线性复杂性。不过,我实际上并没有查看代码或自己测试过,如果这些来源有误,请见谅。 Collections.deque.appendleft 确实具有您所说的线性复杂性。 @Dakkaron 不正确,所有这些都具有同等的复杂性。尽管.insert[0:0] = [0] 就地工作,但它们仍然需要重新分配整个缓冲区。 这些基准很糟糕。初始列表应在单独的设置步骤中创建,而不是计时本身的一部分。最后一个创建了一个 1000001 长的新列表,因此与其他两个变异的就地版本相比是苹果和橙子。 你修复了你的测试吗?这不像wim所说的那样可靠。【参考方案4】:

添加到简短的 Python 列表的惯用语法是什么?

您通常不希望在 Python 中重复地预先添加到列表中。

如果列表很短,而你做的不多……那么好吧。

list.insert

list.insert可以这样使用。

list.insert(0, x)

但这效率低下,因为在 Python 中,list 是一个指针数组,Python 现在必须获取列表中的每个指针并将其向下移动一个,以将指向您的对象的指针插入到第一个槽中,所以这实际上只对相当短的列表有效,正如你所问的那样。

这是一个 sn-p from the CPython source 实现的地方 - 正如你所看到的,我们从数组的末尾开始,每次插入都将所有内容向下移动:

for (i = n; --i >= where; )
    items[i+1] = items[i];

如果您想要一个能够高效地添加元素的容器/列表,那么您需要一个链表。 Python 有一个双向链表,可以快速插入开头和结尾——称为deque

deque.appendleft

collections.deque 有许多列表的方法。 list.sort 是一个例外,这使得 deque 绝对不能完全用 Liskov 替代 list

>>> set(dir(list)) - set(dir(deque))
'sort'

deque 也有一个appendleft 方法(以及popleft)。 deque 是一个双端队列和一个双向链表 - 无论长度如何,它总是需要相同的时间来准备一些东西。在大 O 表示法中,O(1) 与 O(n) 时间的列表。用法如下:

>>> import collections
>>> d = collections.deque('1234')
>>> d
deque(['1', '2', '3', '4'])
>>> d.appendleft('0')
>>> d
deque(['0', '1', '2', '3', '4'])

deque.extendleft

同样相关的是双端队列的 extendleft 方法,它迭代地预先添加:

>>> from collections import deque
>>> d2 = deque('def')
>>> d2.extendleft('cba')
>>> d2
deque(['a', 'b', 'c', 'd', 'e', 'f'])

请注意,每个元素将一次添加一个,从而有效地颠倒它们的顺序。

listdeque 的性能

首先我们设置一些迭代前置:

import timeit
from collections import deque


def list_insert_0(prepends: int):
    l = []
    for i in range(prepends):
        l.insert(0, i)

def list_slice_insert(prepends):
    l = []
    for i in range(prepends):
        l[:0] = [i]      # semantically same as list.insert(0, i)

def list_add(prepends):
    l = []
    for i in range(prepends):
        l = [i] + l      # caveat: new list each time

def deque_appendleft(prepends):
    d = deque()
    for i in range(prepends):
        d.appendleft(i)  # semantically same as list.insert(0, i)

def deque_extendleft(prepends):
    d = deque()
    d.extendleft(range(prepends)) # semantically same as deque_appendleft above

还有一个用于分析的函数,以便我们可以公平地比较各种用法中的所有操作:

def compare_prepends(n, runs_per_trial):
    results = 
    for function in (
        list_insert_0, list_slice_insert,
        list_add, deque_appendleft, deque_extendleft,
        ):
        shortest_time = min(timeit.repeat(
            lambda: function(n), number=runs_per_trial))
        results[function.__name__] = shortest_time
    ranked_methods = sorted(results.items(), key=lambda kv: kv[1])
    for name, duration in ranked_methods:
        print(f'name took duration seconds')

和性能(调整每次试验的运行次数以补偿更多前置的较长运行时间 - repeat 默认执行三个试验):

compare_prepends(20, 1_000_000)
compare_prepends(100, 100_000)
compare_prepends(500, 100_000)
compare_prepends(2500, 10_000)
>>> compare_prepends(20, 1_000_000)
deque_extendleft took 0.6490256823599339 seconds
deque_appendleft took 1.4702797569334507 seconds
list_insert_0 took 1.9417422469705343 seconds
list_add took 2.7092894352972507 seconds
list_slice_insert took 3.1809083241969347 seconds
>>> compare_prepends(100, 100_000)
deque_extendleft took 0.1177942156791687 seconds
deque_appendleft took 0.5385235995054245 seconds
list_insert_0 took 0.9471780974417925 seconds
list_slice_insert took 1.4850486349314451 seconds
list_add took 2.1660344172269106 seconds
>>> compare_prepends(500, 100_000)
deque_extendleft took 0.7309095915406942 seconds
deque_appendleft took 2.895373275503516 seconds
list_slice_insert took 8.782583676278591 seconds
list_insert_0 took 8.931685039773583 seconds
list_add took 30.113558700308204 seconds
>>> compare_prepends(2500, 10_000)
deque_extendleft took 0.4839253816753626 seconds
deque_appendleft took 1.5615574326366186 seconds
list_slice_insert took 6.712615916505456 seconds
list_insert_0 took 13.894083382561803 seconds
list_add took 72.1727528590709 seconds

双端队列要快得多。随着列表变长,双端队列的性能会更好。如果您可以使用双端队列的extendleft,您可能会以这种方式获得最佳性能。

如果您必须使用列表,请记住,对于小列表,list.insert 的工作速度更快,但对于较大的列表,使用切片表示法插入会更快。

不要添加到列表中

列表是用来附加的,而不是附加到的。如果您遇到这种情况会损害代码的性能,请切换到双端队列,或者,如果您可以反转语义并实现相同的目标,请反转您的列表并改为追加。

一般来说,避免在内置 Python list 对象之前添加。

【讨论】:

就计算时间而言,new_list = [x] + your_list 的效率是否低于your_list.insert(x) 是的。它们在语义上是不同的——第一个创建了两个新列表并用x 丢弃了较短的列表,第二个改变了原始列表。在计算方面,我希望语义相似的部分具有相似的性能 - 并且对于新列表的空间分配,第一个性能会受到更大的影响。我通常能够通过仅附加到列表来避免可变性问题。如果我需要一个在列表开头发生变异的通用算法(例如,来自 Haskell),我可能会反转它以从结尾开始工作。【参考方案5】:

在我看来,在 Python 中,将一个元素或列表添加到另一个列表中最优雅和惯用的方法是使用扩展运算符 *(也称为解包运算符),

# Initial list
l = [4, 5, 6]

# Modification
l = [1, 2, 3, *l]

其中修改后的结果列表为[1, 2, 3, 4, 5, 6]

我也喜欢用 + 运算符简单地组合两个列表,如图所示,

# Prepends [1, 2, 3] to l
l = [1, 2, 3] + l

# Prepends element 42 to l
l = [42] + l

我不喜欢另一种常用方法l.insert(0, value),因为它需要一个幻数。此外,insert() 只允许添加单个元素,但上述方法在添加单个元素或多个元素时具有相同的语法。

【讨论】:

就计算时间而言,new_list = [x] + your_list 的效率是否低于your_list.insert(x) 怎么样? :Smile: 如果没有我的辩护律师在场,我唯一要说的是“过早的优化是万恶之源”。正如我的回答的第一段所述,我指的是连接两个列表的惯用方式。【参考方案6】:

让我们看看 4 种方法

    使用 insert()
>>> 
>>> l = list(range(5))
>>> l
[0, 1, 2, 3, 4]
>>> l.insert(0, 5)
>>> l
[5, 0, 1, 2, 3, 4]
>>> 
    使用 [] 和 +
>>> 
>>> l = list(range(5))
>>> l
[0, 1, 2, 3, 4]
>>> l = [5] + l
>>> l
[5, 0, 1, 2, 3, 4]
>>> 
    使用切片
>>> 
>>> l = list(range(5))
>>> l
[0, 1, 2, 3, 4]
>>> l[:0] = [5]
>>> l
[5, 0, 1, 2, 3, 4]
>>> 
    使用 collections.deque.appendleft()
>>> 
>>> from collections import deque
>>> 
>>> l = list(range(5))
>>> l
[0, 1, 2, 3, 4]
>>> l = deque(l)
>>> l.appendleft(5)
>>> l = list(l)
>>> l
[5, 0, 1, 2, 3, 4]
>>> 

【讨论】:

您的回答完美地总结了所有选项,但没有回答所提出的问题。请提供一个连贯的答案。 作为旧问题的新答案,您应该提供新的见解或信息。此答案不回答原始问题,只是重复其他答案中已有的信息。 就计算时间而言,new_list = [x] + your_list 的效率是否低于your_list.insert(x)

以上是关于附加到简短的 python 列表的惯用语法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

检查 Python 变量类型的最佳(惯用)方法是啥? [复制]

与 Django 的“重组”模板标签等效的惯用 Python 是啥?

将 Vue 对象附加到元素的最推荐语法是啥?

在 JavaScript 中处理非法参数的惯用方法是啥?

一次循环遍历 Javascript 数组多个元素的惯用方法是啥?

在 OTP/Erlang 中将 ID 映射到进程的惯用方式是啥?