在Python中从序列中删除项目的优雅方法?

57

写Python代码时,我经常需要根据某些条件从列表或其他序列类型中删除项目。然而,目前尚未找到优雅且高效的解决方案,因为在循环迭代过程中删除列表项通常是不好的。例如,您不能这样做:

for name in names:
    if name[-5:] == 'Smith':
        names.remove(name)

我通常会做类似这样的事情:

toremove = []
for name in names:
    if name[-5:] == 'Smith':
        toremove.append(name)
for name in toremove:
    names.remove(name)
del toremove

这种方式效率低下,看起来相当丑陋,并且可能存在错误(如何处理多个“John Smith”条目?)。有没有更优雅的解决方案,或者至少更高效的方案呢?

使用字典怎么样?


你的代码是否已经移除了多个Smith,还是你已经编辑过它了? - Geoffrey
14个回答

56

两种简单的过滤方法如下:

  1. 使用 filter:

    names = filter(lambda name: name[-5:] != "Smith", names)

  2. 使用列表解析:

    names = [name for name in names if name[-5:] != "Smith"]

请注意,这两种情况都保留了谓词函数评估为True的值,因此您需要反转逻辑(即您说“保留没有姓氏为 Smith 的人”而不是“删除姓氏为 Smith 的人”)。

编辑 有趣的是... 当我发布我的回答时,两个人分别发布了我建议的两个答案。


12
not name.endswith("Smith")翻译为中文是:name不以"Smith"结尾看起来更好 :-)。 - Jochen Ritzel
5
没问题,如果您喜欢易读性或其他类似的东西。 - John
3
“[-5:]”选取名字的最后五个字符,因为我们想知道这个名字是否以“Smith”结尾。正如Jochen所建议的那样,“name[:-5]!='Smith'”这个表达式可以更易读地写成“not name.endswith('Smith')”。 - Edward Loper
@notbad.jpeg: 微观优化并不重要,但是使用name.endswith("Smith")与索引相比会导致性能下降。对此进行基准测试(我已经做过了!) - Gerrat
@Gerrat 感谢您的纠正。我认为 endswith() 会有更好的性能,因为它是更符合 Python 风格的比较方式,并且它可以利用优化,因为它不必制作字符串的子副本。 - notbad.jpeg
显示剩余2条评论

37
您可以在列表上进行反向迭代:
for name in reversed(names):
    if name[-5:] == 'Smith':
        names.remove(name)

这种方法的优点是它不会创建新列表(比如filter或列表推导式),而是使用迭代器,因此不需要复制整个列表(比如[:])。

需要注意的是,尽管在倒序迭代中删除元素是安全的,但插入元素却有些棘手。


这是一个非常创新且符合Python风格的解决方案。我喜欢它! - richo
如果列表中有与谓词匹配的重复项,这是否有效? - Jon-Eric
@Jon-Eric:是的,它有效。如果有重复项,则删除第一个,列表缩小,并且reversed()第二次产生相同的name。这是O(n**2)算法,不像被接受的答案使用O(n)算法。 - jfs

29

明显的答案是John和其他几个人提供的那个,即:

>>> names = [name for name in names if name[-5:] != "Smith"]       # <-- slower

但是这种方法的缺点是它会创建一个新的列表对象,而不是重新使用原始对象。我进行了一些分析和实验,最有效的方法是:

>>> names[:] = (name for name in names if name[-5:] != "Smith")    # <-- faster

将值赋给"names[:]"基本上意味着"用以下值替换names列表的内容"。它不同于仅仅对names进行赋值,因为它不会创建一个新的列表对象。赋值语句的右侧是一个生成器表达式(请注意使用的是圆括号而不是方括号)。这将导致Python遍历整个列表。

一些快速的分析表明,这种方法比列表推导式方法快约30%,比过滤器方法快约40%。

警告:虽然这种解决方案比显而易见的解决方案更快,但它更加晦涩,并且依赖于更高级的Python技术。如果您使用它,我建议附带一条注释。它可能只值得在您真正关心此特定操作的性能时使用(无论如何,这个操作非常快)。 (在我使用这个方法的情况下,我正在进行A*波束搜索,并使用它从搜索波束中删除搜索点。)


2
非常有趣的性能发现。您可以分享更多关于您的分析环境和评估方法的信息吗? - Drake Guan
我敢打赌,如果你使用not name.endswith('Smith')而不是在每次迭代中创建一个切片,你甚至可以使它更快。无论如何,这是一条有价值的信息,如果不是因为你的答案,我可能永远都找不到它,谢谢。 - notbad.jpeg
1
names[:] 的建议对于使用 os.walk 过滤要遍历的目录名非常有帮助。 - wowest

10

似乎对整数不起作用。temprevengelist = "0-12354-6876" temprevengelist = temprevengelist.split('-') list = [x for x in temprevengelist if x[-5:] != 6876] - Fahim Akhter
@FahimAkhter:这是因为你正在比较一个整数和一个字符串:在Python中,6876(整数)和"6876"(字符串)是两个不同的值,它们不相等。尝试用x[-5:] != "6876"int(x[-5:]) != 6876替换x[-5:] != 6876 - Edward Loper

4

有时候使用过滤器(filter)或列表推导式(list comprehension)无法解决问题。当其他对象持有对你正在修改的列表的引用,并且你需要就地修改该列表时,就会出现这种情况。

for name in names[:]:
    if name[-5:] == 'Smith':
        names.remove(name)

唯一的区别是在for循环中使用names[:]而不是names。这样代码会迭代列表的(浅)副本,移除操作也能按预期进行。由于列表复制是浅层复制,所以速度相当快。

3

使用筛选器会非常棒。简单的例子:

names = ['mike', 'dave', 'jim']
filter(lambda x: x != 'mike', names)
['dave', 'jim']

编辑: Corey 的列表推导也很棒。


2

关于使用字典的问题,你需要注意 Python 3.0 版本将包含 字典推导式

>>> {i : chr(65+i) for i in range(4)}

同时,您可以通过以下方式进行准字典理解:
>>> dict([(i, chr(65+i)) for i in range(4)])

或者更直接的答案是:
dict([(key, name) for key, name in some_dictionary.iteritems if name[-5:] != 'Smith'])

除非生成器表达式不是唯一的参数,否则您不需要在其周围放置()。而[]会使生成器表达式实现一个列表,这就是为什么dict([(k,v) for k,v in d.items()])dict(((k,v) for k,v in d.items()))慢得多的原因。 - Dan D.

2
names = filter(lambda x: x[-5:] != "Smith", names);

2

两种解决方案,过滤器列表推导式都需要构建一个新的列表。我不太了解Python内部情况,但我认为一种更传统(但不够优美)的方法可能更加高效:

names = ['Jones', 'Vai', 'Smith', 'Perez']

item = 0
while item <> len(names):
    name = names [item]
    if name=='Smith':
        names.remove(name)
    else:
        item += 1

print names

无论如何,对于短列表,我会坚持之前提出的两种解决方案中的任意一种。

我认为 names.remove(name) 可能是一个 O(n) 操作,这将使得该算法的时间复杂度为 O(n^2)。 - postfuturist
1
我个人会将 while 表达式写为 item < len(names),以防在循环内部搞砸逻辑。(即使看起来你没有犯错) - Miquella
使用del names[item]或names.pop(item)比使用names.remove(name)更有效率。这样做不太可能是O(n),尽管我不知道它的实际内部工作原理。 - rjmunro

2
如果需要就地过滤列表且列表很大,则基于list.remove()的算法可能不适用,因为它们的计算复杂度为O(n^2),可以使用以下非常Pythonic的函数:
def filter_inplace(func, original_list):
  """ Filters the original_list in-place.

  Removes elements from the original_list for which func() returns False.

  Algrithm's computational complexity is O(N), where N is the size
  of the original_list.
  """

  # Compact the list in-place.
  new_list_size = 0
  for item in original_list:
    if func(item):
      original_list[new_list_size] = item
      new_list_size += 1

  # Remove trailing items from the list.
  tail_size = len(original_list) - new_list_size
  while tail_size:
    original_list.pop()
    tail_size -= 1


a = [1, 2, 3, 4, 5, 6, 7]

# Remove even numbers from a in-place.
filter_inplace(lambda x: x & 1, a)

# Prints [1, 3, 5, 7]
print a

编辑:实际上,https://dev59.com/xHVD5IYBdhLWcg3wU56H#4639748 上的解决方案比我的更好。它更符合Python风格,并且速度更快。因此,这里是一个新的filter_inplace()实现:

def filter_inplace(func, original_list):
  """ Filters the original_list inplace.

  Removes elements from the original_list for which function returns False.

  Algrithm's computational complexity is O(N), where N is the size
  of the original_list.
  """
  original_list[:] = [item for item in original_list if func(item)]

删除列表末尾的元素:del original_list[new_list_size:] - jfs

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