Python中使用pop()进行列表操作
简单来说,我需要根据索引从一个列表中删除多个项目。不过,我不能用pop方法,因为它会改变索引的位置(如果不使用一些复杂的补偿系统的话)。有没有办法可以同时删除多个项目呢?
我有一个算法,它会遍历一个列表,如果条件符合,就通过pop方法删除那个项目。问题是,这个过程是在一个循环中进行的。一旦使用了pop,列表就会少一个元素,所有的值都会向前移动一位。这样一来,循环就会超出范围。有没有办法可以同时删除多个项目,或者有没有其他的解决方案呢?
这是我遇到的问题的一个例子:
L = ['a', 'b', 'c', 'd']
for i in range(len(L)):
print L
if L[i] == 'a' or L[i] == 'c':
L.pop(i)
3 个回答
总结
- 可以使用列表推导式(或者生成器表达式)来从列表中删除多个项目
- 如果你的输入是一个很大的字节串,可以使用
str.translate()
来删除字符 - 一次删除一个项目
del L[i]
对于大列表来说速度很慢
如果项目是字节,就像你例子中的那样,你可以使用 str.translate()
:
def remove_bytes(bytestr, delbytes):
"""
>>> remove_bytes(b'abcd', b'ac') == b'bd'
True
"""
return bytestr.translate(None, delbytes)
一般来说,可以使用切片来删除多个项目:
def remove_inplace_without_order(L, delitems):
"""Remove all items from `L` that are in `delitems` (not preserving order).
>>> L = list(range(4)); remove_inplace_without_order(L, [0,2]); L
[3, 1]
"""
idel = len(L) # items idel.. to be removed
for i in reversed(range(len(L))):
if L[i] in delitems:
idel -= 1
L[i] = L[idel] # save `idel`-th item
del L[idel:] # remove items all at once
#NOTE: the function returns `None` (it means it modifies `L` inplace)
正如 @phooji 和 @senderle 已经提到的,列表推导式(或者生成器表达式)在你的情况下更好:
def remove_listcomp(L, delitems):
return [x for x in L if x not in delitems]
这是一个性能比较,针对 L=list("abcd"*10**5); delitems="ac"
:
| function | time, msec | ratio |
|------------------------------+------------+--------|
| list | 4.42 | 0.9 |
| remove_bytes | 4.88 | 1.0 |
| remove | 27.3 | 5.6 |
| remove_listcomp | 36.8 | 7.5 |
| remove_inplace_without_order | 71.2 | 14.6 |
| remove_inplace_senderle2 | 83.8 | 17.2 |
| remove_inplace_senderle | 15000 | 3073.8 |
#+TBLFM: $3=$2/@3$2;%.1f
其中
try:
from itertools import ifilterfalse as filterfalse
except ImportError:
from itertools import filterfalse # py3k
def remove(L, delitems):
return filterfalse(delitems.__contains__, L)
def remove_inplace_senderle(L, delitems):
for i in reversed(range(len(L))):
if L[i] in delitems:
del L[i]
def remove_inplace_senderle2(L, delitems):
write_i = 0
for read_i in range(len(L)):
L[write_i] = L[read_i]
if L[read_i] not in delitems:
write_i += 1
del L[write_i:]
remove_inplace_senderle()
速度慢是因为它使用了 O(N**2)
的算法。每次 del L[i]
可能会导致右边的所有项目都要向左移动来填补空缺。
上表中的时间列包括创建新输入列表所需的时间(第一行),因为某些算法会就地修改输入。
这是相同输入的时间,但在每次迭代时不创建新列表:
| function | time, msec | ratio |
|-----------------+------------+-------|
| remove_bytes | 0.391 | 1 |
| remove | 24.3 | 62 |
| remove_listcomp | 33.4 | 85 |
#+TBLFM: $3=$2/@2$2;%d
表格显示 itertools.ifilterfalse()
对于列表推导式并没有显著的提升。
一般来说,除非性能分析工具证明这段代码是瓶颈,并且对你的程序很重要,否则考虑这种任务的性能是没有必要的,甚至可能是有害的。不过,了解一些替代方法可能会对速度有显著的提升是有用的。
你想要一个列表推导式:
L = [c for c in L if c not in ['a', 'c']]
或者,如果你真的不想创建一个副本,可以反向操作:
for i in reversed(range(len(L))):
if L[i] in ['a', 'c']:
L.pop(i) # del L[i] is more efficient
感谢ncoghlan提供的reversed()
方法,以及phooji提供的del L[i]
建议。(我决定保留L.pop(i)
,因为这就是问题最初的表述方式。)
另外,正如J.S. Sebastian正确指出的,反向操作在空间上是高效的,但在时间上却不太高效;大多数情况下,使用列表推导式或生成器(L = (...)
而不是L = [...]
)是最好的选择。
编辑:
好吧,既然大家似乎想要一种比上面提到的反向方法更快的方式(我真想不通为什么... :) ),这里有一个保持顺序的原地过滤方法,它的速度应该只比列表推导式慢一个常数。(这就像我在C语言中过滤字符串时会做的事情。)
write_i = 0
for read_i in range(len(L)):
L[write_i] = L[read_i]
if L[read_i] not in ['a', 'c']:
write_i += 1
del L[write_i:]
print L
# output: ['b', 'd']
你的列表很大吗?如果是的话,可以使用 ifilter
,这个东西来自于 itertools
,它可以懒惰地过滤掉你不想要的元素(也就是说,不需要提前消耗资源)。
如果列表不大,那就直接用列表推导式吧:
newlist = [x for x in oldlist if x not in ['a', 'c'] ]
这样会创建一个新列表的副本。一般来说,这对效率影响不大,除非你特别在意内存的使用。
如果你想在语法上方便一点,又想保持效率(特别是对于大列表),可以用 (
)
来构造一个生成器,而不是用 [
]
来构造列表:
interestingelts = (x for x in oldlist if x not in ['a', 'c'])
这样做之后,你可以遍历 interestingelts
,但是不能通过索引来访问它:
for y in interestingelts: # ok
print y
print interestingelts[0] # not ok: generator allows sequential access only