如何从集合中检索元素而不删除它?


427

假设以下内容:

>>> s = set([1, 2, 3])

如何获得的值(任意值)出来s而不做s.pop()?我想将该项目保留在集合中,直到我确定可以删除它为止-只有在异步调用另一个主机后才能确定。

快速又肮脏:

>>> elem = s.pop()
>>> s.add(elem)

但是您知道更好的方法吗?理想的情况是恒定时间。


8
有人知道为什么python尚未实现此功能吗?
hlin117

用例是什么?Set由于某种原因没有此功能。您应该遍历它,并进行与set相关的操作,例如unionetc,而不是从中获取元素。例如,next(iter({3,2,1}))总是返回,1因此如果您认为这将返回随机元素-则不会。因此,也许您只是使用了错误的数据结构?用例是什么?
user1685095

1
相关信息:stackoverflow.com/questions/20625579/…(我知道,这不是一个相同的问题,但是那里有很多有价值的选择和见解。)
John Y

@ hlin117因为set是无序集合。由于预期没有顺序,因此在给定位置检索元素没有意义-预期是随机的。
Jeyekomon

Answers:


545

不需要复制整个集合的两个选项:

for e in s:
    break
# e is now an element from s

要么...

e = next(iter(s))

但是总的来说,集合不支持索引或切片。


4
这回答了我的问题。,我想我仍然会使用pop(),因为迭代似乎对元素进行了排序。我希望它们以随机顺序排列……
Daren Thomas

9
我不认为iter()对元素进行排序-当我创建一个set和pop()直到其为空时,我得到一致的(在我的示例中进行了排序)排序,并且它与迭代器相同-pop( )不保证随机顺序,而只是保证任意顺序,例如“我不承诺”。
布莱尔·康拉德

2
+1 iter(s).next()并不是总值,而是很好。完全通用,可以从任何可迭代对象中获取任意元素。您可以选择是否要小心,如果集合为空。
u0b34a0f6ae

8
next(iter(s))也可以,我倾向于认为它读起来更好。另外,当s为空时,您可以使用哨兵来处理案件。例如next(iter(s),set())。
2012年

5
next(iter(your_list or []), None)处理无集和空集
MrE '18

110

最少的代码是:

>>> s = set([1, 2, 3])
>>> list(s)[0]
1

显然,这将创建一个新列表,其中包含该集合的每个成员,因此如果您的集合很大,就不会很大。


95
next(iter(s))只有超过list(s)[0]三个字符,并以其他方式在时间和空间复杂度大大优越。因此,尽管“最少的代码”的主张是微不足道的,但也确实是最糟糕的方法。即使是手动删除然后再将删除的元素重新添加到原始集合中,也比“构造一个整个新容器只是为了提取第一个元素”优越,这显然是疯狂的。更令我担心的是,实际上有38株Stackoverflowers对此表示反对。我只知道我会在生产代码中看到这一点。
塞西尔·库里

19
@augurar:因为它以相对简单的方式完成工作。有时,这就是快速脚本中的全部问题。
tonysdg '17

4
@Vicrobot是的,但是它是通过复制整个集合并将O(1)操作转换为O(n)操作来实现的。这是一个可怕的解决方案,任何人都不要使用。
augurar

9
同样,如果您只是针对“最小的代码”(愚蠢的),那么min(s)使用更少的字符,同时又会像这样可怕而低效。
augurar

5
高尔夫代码获胜者+1,我有一个“糟糕而低效”的实际反例:min(s)next(iter(s))1号尺寸的套装稍快一些,我来到这个答案的目的是专门从特殊情况下从套装中提取唯一元素大小1
lehiester

49

我想知道这些功能如何在不同的集合上执行,所以我做了一个基准测试:

from random import sample

def ForLoop(s):
    for e in s:
        break
    return e

def IterNext(s):
    return next(iter(s))

def ListIndex(s):
    return list(s)[0]

def PopAdd(s):
    e = s.pop()
    s.add(e)
    return e

def RandomSample(s):
    return sample(s, 1)

def SetUnpacking(s):
    e, *_ = s
    return e

from simple_benchmark import benchmark

b = benchmark([ForLoop, IterNext, ListIndex, PopAdd, RandomSample, SetUnpacking],
              {2**i: set(range(2**i)) for i in range(1, 20)},
              argument_name='set size',
              function_aliases={first: 'First'})

b.plot()

在此处输入图片说明

该图清楚地表明,一些方法(RandomSampleSetUnpackingListIndex)依赖于集的大小,并应在一般情况下可避免(至少在性能可能是重要的)。正如其他答案已经显示的那样,最快的方法是ForLoop

但是,只要使用恒定时间方法之一,性能差异就可以忽略不计。


iteration_utilities(免责声明:我是作者)包含此用例的便捷功能first

>>> from iteration_utilities import first
>>> first({1,2,3,4})
1

我也将其包含在上述基准中。它可以与其他两个“快速”解决方案竞争,但是两者之间的差异并不大。


43

tl; dr

for first_item in muh_set: break仍然是Python 3.x中的最佳方法。诅咒你,圭多。

do这样做

欢迎使用从wr推断出来的另一组Python 3.x计时出色的特定于Python 2.x的响应。与AChampion同样有用的特定于Python 3.x的响应不同,下面的时间安排建议了上面提到的时间异常解决方案,包括:

欢乐代码段

开启,收听,定时:

from timeit import Timer

stats = [
    "for i in range(1000): \n\tfor x in s: \n\t\tbreak",
    "for i in range(1000): next(iter(s))",
    "for i in range(1000): s.add(s.pop())",
    "for i in range(1000): list(s)[0]",
    "for i in range(1000): random.sample(s, 1)",
]

for stat in stats:
    t = Timer(stat, setup="import random\ns=set(range(100))")
    try:
        print("Time for %s:\t %f"%(stat, t.timeit(number=1000)))
    except:
        t.print_exc()

快速过时的计时

看哪!按最快到最慢的片段排序:

$ ./test_get.py
Time for for i in range(1000): 
    for x in s: 
        break:   0.249871
Time for for i in range(1000): next(iter(s)):    0.526266
Time for for i in range(1000): s.add(s.pop()):   0.658832
Time for for i in range(1000): list(s)[0]:   4.117106
Time for for i in range(1000): random.sample(s, 1):  21.851104

全家人的面部植物

毫不奇怪,手动迭代的速度至少是下一个最快解决方案的两倍。尽管与Bad Old Python 2.x相比(以前的手动迭代速度至少快四倍),差距有所减小,但PPE 20狂热者对我而言最冗长的解决方案是最好的解决方案感到失望。至少将一个集转换为一个列表以仅提取该集的第一个元素是预期的那样可怕。感谢Guido,愿他的光芒继续指导我们。

令人惊讶的是,基于RNG的解决方案绝对可怕。列表转换不好,但random 确实会带来糟糕的结果。对于随机数上帝来说如此之多。

我只是希望那些无定形set.get_first()的人已经为我们准备了一种方法。如果您正在阅读本文,他们:“请。做点什么。”


2
我觉得抱怨是next(iter(s)) 两倍慢for x in s: breakCPython是那样的陌生。我的意思是CPython。它比C或Haskell做同一件事的速度要慢50到100倍(或类似的速度)(在大多数情况下,尤其是在迭代中,没有尾调用消除,也没有任何优化)。松散几微秒并没有真正的区别。你不觉得吗 还有PyPy
user1685095 '17

39

为了提供不同方法背后的一些时序图,请考虑以下代码。 get()是我自定义添加到Python的setobject.c中,只是一个pop()而没有删除该元素。

from timeit import *

stats = ["for i in xrange(1000): iter(s).next()   ",
         "for i in xrange(1000): \n\tfor x in s: \n\t\tbreak",
         "for i in xrange(1000): s.add(s.pop())   ",
         "for i in xrange(1000): s.get()          "]

for stat in stats:
    t = Timer(stat, setup="s=set(range(100))")
    try:
        print "Time for %s:\t %f"%(stat, t.timeit(number=1000))
    except:
        t.print_exc()

输出为:

$ ./test_get.py
Time for for i in xrange(1000): iter(s).next()   :       0.433080
Time for for i in xrange(1000):
        for x in s:
                break:   0.148695
Time for for i in xrange(1000): s.add(s.pop())   :       0.317418
Time for for i in xrange(1000): s.get()          :       0.146673

这意味着for / break解决方案是最快的(有时比自定义get()解决方案更快)。


有谁知道为什么iter(s).next()比其他可能性要慢得多,甚至比s.add(s.pop())还慢?对我来说,如果时序看起来像那样,iter()和next()的设计感觉非常糟糕。
peschü

好吧,该行在每次迭代中创建一个新的iter对象。
瑞安(Ryan)

3
@Ryan:也是不是隐式创建的迭代器对象for x in s吗?“为的结果创建了一个迭代器expression_list。”
musiphil,2015年

2
@musiphil是的;最初我错过了0.14的“突破”,这确实违反直觉。有空的时候,我想对此进行深入研究。
瑞安

1
我知道这是老了,但是当添加s.remove()到组合的iter例子都foriter走坏灾难性的。
AChampion '16

28

由于您需要一个随机元素,因此也可以使用:

>>> import random
>>> s = set([1,2,3])
>>> random.sample(s, 1)
[2]

该文档似乎并未提及的性能random.sample。从包含大量列表和大量集合的非常快速的实证检验来看,列表似乎是恒定时间,但集合却并非如此。同样,对集合的迭代也不是随机的。顺序不确定,但可以预测:

>>> list(set(range(10))) == range(10)
True 

如果随机性很重要,并且您需要在恒定时间内(大量集合)使用一堆元素,那么我会先使用random.sample并转换为列表:

>>> lst = list(s) # once, O(len(s))?
...
>>> e = random.sample(lst, 1)[0] # constant time

14
如果只需要一个元素,则random.choice更为明智。
Gregg Lind

如果您不在乎要使用哪个元素,则list.spop()将起作用。
叶夫根尼(Evgeny)2014年

8
@Gregg:您不能使用choice(),因为Python 会尝试为您的集合建立索引,这是行不通的。
凯文(Kevin)

3
虽然很聪明,但这实际上是迄今为止建议的最慢的解决方案,数量级高。是的,这缓慢的。即使将集合转换为列表以仅提取该列表的第一个元素也更快。对于我们中间的非信徒(... hi!),请看这些美妙的时机
Cecil Curry

9

似乎是最紧凑的(6个符号),但是获得集合元素的方法很慢(由PEP 3132实现):

e,*_=s

在Python 3.5+中,您还可以使用以下7符号表达式(感谢PEP 448):

[*s][0]

这两种选择在我的机器上都比for循环方法慢大约1000倍。


1
for循环方法(或更准确地说是迭代器方法)具有O(1)时间复杂度,而这些方法是O(N)。他们虽然简洁。:)
ForeverWintr

6

我使用编写的实用程序函数。它的名称有点误导,因为它暗示它可能是随机物品或类似物品。

def anyitem(iterable):
    try:
        return iter(iterable).next()
    except StopIteration:
        return None

2
您还可以去旁边(ITER(迭代器),无),以节省墨水:)
1“”

3

在@wr之后。帖子,我得到类似的结果(对于Python3.5)

from timeit import *

stats = ["for i in range(1000): next(iter(s))",
         "for i in range(1000): \n\tfor x in s: \n\t\tbreak",
         "for i in range(1000): s.add(s.pop())"]

for stat in stats:
    t = Timer(stat, setup="s=set(range(100000))")
    try:
        print("Time for %s:\t %f"%(stat, t.timeit(number=1000)))
    except:
        t.print_exc()

输出:

Time for for i in range(1000): next(iter(s)):    0.205888
Time for for i in range(1000): 
    for x in s: 
        break:                                   0.083397
Time for for i in range(1000): s.add(s.pop()):   0.226570

但是,在更改基础集(例如,调用remove())时,对于可迭代的示例(foriter)来说效果很差:

from timeit import *

stats = ["while s:\n\ta = next(iter(s))\n\ts.remove(a)",
         "while s:\n\tfor x in s: break\n\ts.remove(x)",
         "while s:\n\tx=s.pop()\n\ts.add(x)\n\ts.remove(x)"]

for stat in stats:
    t = Timer(stat, setup="s=set(range(100000))")
    try:
        print("Time for %s:\t %f"%(stat, t.timeit(number=1000)))
    except:
        t.print_exc()

结果是:

Time for while s:
    a = next(iter(s))
    s.remove(a):             2.938494
Time for while s:
    for x in s: break
    s.remove(x):             2.728367
Time for while s:
    x=s.pop()
    s.add(x)
    s.remove(x):             0.030272

1

我通常对小型馆藏所做的是创建这样的解析器/转换器方法

def convertSetToList(setName):
return list(setName)

然后我可以使用新列表并按索引号访问

userFields = convertSetToList(user)
name = request.json[userFields[0]]

作为列表,您将拥有可能需要使用的所有其他方法


为什么不使用list而不是创建转换器方法呢?
达伦·托马斯


-6

另一种选择是将字典与不需要的值一起使用。例如,


poor_man_set = {}
poor_man_set[1] = None
poor_man_set[2] = None
poor_man_set[3] = None
...

您可以将键视为一组,但它们只是一个数组:


keys = poor_man_set.keys()
print "Some key = %s" % keys[0]

这种选择的副作用是您的代码将与较旧set的Python 早期版本向后兼容。这也许不是最佳答案,但这是另一种选择。

编辑:您甚至可以执行以下操作来隐藏使用字典而不是数组或集合的事实:


poor_man_set = {}
poor_man_set[1] = None
poor_man_set[2] = None
poor_man_set[3] = None
poor_man_set = poor_man_set.keys()

3
这不能像您希望的那样工作。在python 2中,keys()是O(n)操作,因此您不再是恒定时间,但是至少keys [0]将返回您期望的值。在python 3中keys()是O(1)运算,所以好吧!但是,它不再返回列表对象,而是返回无法索引的类集合对象,因此keys [0]将引发TypeError。stackoverflow.com/questions/39219065/…–
sage88
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.