从列表中删除字典

86

假设我有一个字典列表,比如:

[{'id': 1, 'name': 'paul'},
 {'id': 2, 'name': 'john'}]

我想从字典列表中删除id为2(或名称'john')的字典,以编程方式最高效的方法是什么(也就是说,我不知道列表中条目的索引,所以不能简单地使用pop函数)。

9个回答

151
thelist[:] = [d for d in thelist if d.get('id') != 2]

编辑: 由于在评论中对这段代码的性能提出了一些疑问(有些是基于误解Python的性能特征,有些是假设列表中仅有一个字典具有'id'键的值为2),因此我希望在这一点上提供保证。

在一台旧的Linux机器上测试了这段代码:

$ python -mtimeit -s"lod=[{'id':i, 'name':'nam%s'%i} for i in range(99)]; import random" "thelist=list(lod); random.shuffle(thelist); thelist[:] = [d for d in thelist if d.get('id') != 2]"
10000 loops, best of 3: 82.3 usec per loop

大约有57微秒用于random.shuffle(需要确保要移除的元素不总是位于同一位置; -),0.65微秒用于初始复制(谁担心Python列表的浅复制性能影响显然是在午餐时间出门了;-),需要避免在循环中更改原始列表(以便循环的每个部分都有东西可删除)。

当已知只有一个要删除的项时,可以更加迅速地定位和删除它:

$ python -mtimeit -s"lod=[{'id':i, 'name':'nam%s'%i} for i in range(99)]; import random" "thelist=list(lod); random.shuffle(thelist); where=(i for i,d in enumerate(thelist) if d.get('id')==2).next(); del thelist[where]"
10000 loops, best of 3: 72.8 usec per loop

(当然,如果您使用的是Python 2.6或更高版本,请使用内置的next而不是.next方法)-但是如果满足删除条件的字典数量不止一个,则此代码会出现问题。 将其泛化,我们有:

$ python -mtimeit -s"lod=[{'id':i, 'name':'nam%s'%i} for i in range(33)]*3; import random" "thelist=list(lod); where=[i for i,d in enumerate(thelist) if d.get('id')==2]; where.reverse()" "for i in where: del thelist[i]"
10000 loops, best of 3: 23.7 usec per loop

因为我们已经知道有三个等间距的字典要去除,所以可以删除洗牌操作。列表推导式不变,表现良好:

$ python -mtimeit -s"lod=[{'id':i, 'name':'nam%s'%i} for i in range(33)]*3; import random" "thelist=list(lod); thelist[:] = [d for d in thelist if d.get('id') != 2]"
10000 loops, best of 3: 23.8 usec per loop

即使仅删除3个值为99的元素,列表仍然完全平手。当列表更长、重复次数更多时,这种情况会更加明显。

$ python -mtimeit -s"lod=[{'id':i, 'name':'nam%s'%i} for i in range(33)]*133; import random" "thelist=list(lod); where=[i for i,d in enumerate(thelist) if d.get('id')==2]; where.reverse()" "for i in where: del thelist[i]"
1000 loops, best of 3: 1.11 msec per loop
$ python -mtimeit -s"lod=[{'id':i, 'name':'nam%s'%i} for i in range(33)]*133; import random" "thelist=list(lod); thelist[:] = [d for d in thelist if d.get('id') != 2]"
1000 loops, best of 3: 998 usec per loop

总之,使用制作和反转索引列表来删除元素的技巧,与使用简单明显的列表推导式相比,很明显是不值得的。在一些小的情况下可能会获得100微秒的性能提升,但在较大的情况下却会损失113毫秒。避免或批评简单、直接且完全满足性能要求的解决方案(例如对于“从列表中删除某些项目”这种通用类别问题,使用列表推导式)是Knuth和Hoare所说的“过早优化是编程之恶”的特别恶劣的示例!-)


1
这种方法不好的两个原因是:它复制了整个列表,并且即使包含id 2的字典是第一个元素,它也会遍历整个列表。 - Imagist
16
@imagist,它仍然是最快的——测量一下吧,求你了,请不要假设自己知道自己在说什么,尤其是当你显然不知道时;-),特别是当需要删除的项目是第一个时(这样可以避免移动每个其他项目)。原始问题中没有迹象表明列表中的每个字典必须始终具有与“id”对应的不同值。 - Alex Martelli
@Alex,没错 - 我喜欢列表推导式的速度之快。而且,由于从Python 3开始filter将返回一个迭代器,这应该成为标准做法。 - Meredith L. Patterson
4
theList[:] 相当于 theList[0:len(theList)]。在此上下文中,它意味着“就地更改 theList”。 - John Fouhy
6
theList[:] = ..theList = ..有什么区别? - u0b34a0f6ae
显示剩余5条评论

12
这里有一种使用列表推导式的方法(假设你的列表名为“foo”):
[x for x in foo if not (2 == x.get('id'))]

根据需要替换'john' == x.get('name')或其他适当内容。

filter也可以使用:

foo.filter(lambda x: x.get('id')!=2, foo)

如果您想要一个生成器,可以使用itertools:

itertools.ifilter(lambda x: x.get('id')!=2, foo)

然而,在Python 3中,filter将始终返回一个迭代器,因此像Alex建议的那样使用列表推导式确实是最佳选择。


在这里,.get比[]更好,因为如果列表中的某个字典没有键'id'的条目,它不会中断。 - Alex Martelli

10
# assume ls contains your list
for i in range(len(ls)):
    if ls[i]['id'] == 2:
        del ls[i]
        break

这种方法通常会比列表推导式的方法快,因为如果它在早期找到所需项,它就不会遍历整个列表。


如果字典中没有“id”,则会引发“KeyError”错误。这不是 OP 所要求的。 - SilentGhost
@Imagist +1 这正是我正在寻找的。提醒一下 @SilentGhost:如果你想针对另一个值进行定位,可以使用不同于 id 的键,例如:if ls[i]['name'] == 'john': 将匹配并删除该字典。 - twknab

8

我认为你已经有了一些很好的答案,这不是一个合适的答案,但是...你考虑过使用<id>:<name>字典而不是字典列表吗?


3
如果很困难,你可能做错了。如果你想通过属性删除东西,请使用一个以属性为键的字典,这样会更简单。 - S.Lott
3
只要你不关心项目顺序的保留,永远不想通过其他属性删除项目,对于该属性不允许有任何重复,等等等等——这些限制远远超出了提问者所表达的规范,使得这个建议变得不合理;-)。 - Alex Martelli
如果我必须把所有这些规格都当作理所当然,我会说“使用数据库”xD。 - fortran

2

假设您的Python版本为3.6或更高,并且您不需要已删除的项,这将更加经济...

如果列表中的字典是唯一的:

for i in range(len(dicts)):
    if dicts[i].get('id') == 2:
        del dicts[i]
        break

如果您想删除所有匹配的项:

for i in range(len(dicts)):
    if dicts[i].get('id') == 2:
        del dicts[i]

您可以这样做,以确保获取id键不会引发KeyError,而不管Python版本如何

如果dicts[i].get('id', None) == 2


3
删除所有匹配项的代码将无法正常工作。从列表中删除元素会导致索引变化,这将使此代码跳过一个项目。 - andres101

1
你可以尝试以下方法:
a = [{'id': 1, 'name': 'paul'},
     {'id': 2, 'name': 'john'}]

for e in range(len(a) - 1, -1, -1):
    if a[e]['id'] == 2:
        a.pop(e)

如果你不能从开头弹出 - 从结尾弹出,这不会破坏for循环。

你的意思是 "range(len(a) - 1, -1, -1)",而不是 "range(len(a) - 1, 0, -1)"。这不包括列表的第一个元素。我听说现在更倾向于使用 reversed() 函数。请看下面的代码。 - hughdbrown
这是我想表达的意思:
a = list(range(5)) a [0, 1, 2, 3, 4] range(len(a) - 1, -1, -1) [4, 3, 2, 1, 0] range(len(a) - 1, 0, -1) [4, 3, 2, 1]
等待评论混淆...
- hughdbrown

0

试试这个: 从列表中删除“joh”示例

for id,elements in enumerate(dictionary):
    if elements['name']=='john':
        del dictionary[id]

0

从有关通用拆包(Python 3.5及以上版本)的pep448的更新开始,当迭代一个字典列表时使用临时变量,比如说row,你可以将当前迭代的字典作为 **row 输入,合并新的键或使用布尔运算从字典列表中过滤掉一个或多个字典。

请记住 **row 将输出一个新的字典。

例如,您的起始字典列表:

data = [{'id': 1, 'name': 'paul'},{'id': 2, 'name': 'john'}]

如果我们想要过滤掉ID为2的数据:

data = [{**row} for row in data if row['id']!=2]

如果您想过滤掉约翰:
data = [{**row} for row in data if row['name']!='John']

虽然与问题不直接相关,但如果您想添加新的键:

data = [{**row, 'id_name':str(row['id'])+'_'+row['name']} for row in data]

这个解决方案也稍微比被接受的解决方案快一点。

1
如果行的id不等于2,则为if row['id']!=2 - Sharukh Rahman

0
你可以尝试以下方法:
def destructively_remove_if(predicate, list):
      for k in xrange(len(list)):
          if predicate(list[k]):
              del list[k]
              break
      return list

  list = [
      { 'id': 1, 'name': 'John' },
      { 'id': 2, 'name': 'Karl' },
      { 'id': 3, 'name': 'Desdemona' } 
  ]

  print "Before:", list
  destructively_remove_if(lambda p: p["id"] == 2, list)
  print "After:", list

除非你建立类似于数据索引的东西,否则我认为你无法比全表扫描更好地处理整个列表。如果你的数据按照你使用的键进行排序,你可能可以使用bisect模块来更快地找到你要查找的对象。


什么是xrange?@Dirk - Lutaaya Huzaifah Idris
xrange在Python 2中使用,而在Python 3中现在被称为range。正如所写的示例仍然是Python 2代码(请查看日期,观察将print用作语句而不是函数)。 - Dirk

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