【问题标题】:How to retrieve an element from a set without removing it?如何从集合中检索元素而不删除它?
【发布时间】:2010-09-08 18:11:55
【问题描述】:

假设如下:

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

如何在不执行 s.pop() 的情况下从 s 中获取值(任何值)?我想将项目留在集合中,直到我确定我可以将其移除 - 我只能在异步调用另一个主机之后才能确定。

又快又脏:

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

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

【问题讨论】:

  • 有人知道为什么python还没有实现这个功能吗?
  • 用例是什么?赛特没有这个能力是有原因的。您应该遍历它并进行与集合相关的操作,例如 union 等,而不是从中获取元素。例如 next(iter({3,2,1})) 总是返回 1 所以如果你认为这会返回随机元素 - 它不会。所以也许你只是使用了错误的数据结构?用例是什么?
  • 相关:stackoverflow.com/questions/20625579/…(我知道,这不是同一个问题,但那里有有价值的替代方案和见解。)
  • @hlin117 因为 set 是 unordered collection。由于没有预期的顺序,因此在给定位置检索元素是没有意义的 - 它应该是随机的。
  • b = (a-set()).pop()

标签: python set


【解决方案1】:

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

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

或者……

e = next(iter(s))

但一般来说,集合不支持索引或切片。

【讨论】:

  • 这回答了我的问题。唉,我想我仍然会使用 pop(),因为迭代似乎对元素进行了排序。我希望它们以随机顺序...
  • 我不认为 iter() 正在对元素进行排序 - 当我创建一个 set 和 pop() 直到它为空时,我得到一致的(在我的示例中排序)排序,它是与迭代器相同 - pop() 不承诺随机顺序,只是任意顺序,如“我什么都不承诺”。
  • +1 iter(s).next() 不恶心但很棒。从任何可迭代对象中获取任意元素是完全通用的。如果你想小心如果集合是空的,你的选择。
  • next(iter(s)) 也可以,我倾向于认为它读起来更好。此外,您可以使用哨兵来处理 s 为空时的情况。例如。下一个(迭代器(s),设置())。
  • next(iter(your_list or []), None) 处理无集和空集
【解决方案2】:

另一种选择是使用包含您不关心的值的字典。例如,


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]

这种选择的副作用是您的代码将向后兼容旧的、pre-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()

【讨论】:

  • 这不会像你希望的那样工作。在 python 2 中,keys() 是一个 O(n) 操作,因此您不再是恒定时间,但至少 keys[0] 将返回您期望的值。在 python 3 中,keys() 是一个 O(1) 操作,所以耶!然而,它不再返回一个列表对象,它返回一个不能被索引的类似集合的对象,所以 keys[0] 会抛出 TypeError。 stackoverflow.com/questions/39219065/…
【解决方案3】:

既然你想要一个随机元素,这也可以:

>>> 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

【讨论】:

  • 如果你只想要一个元素,random.choice 更明智。
  • list(s).pop() 如果您不在乎要采用哪个元素,则可以。
  • @Gregg:你不能使用choice(),因为 Python will try to index your set 不起作用。
  • 虽然很聪明,但这实际上是按数量级建议的最慢的解决方案。是的,它就是慢。即使将集合转换为列表只是为了提取该列表的第一个元素也更快。对于我们中间的非信徒(...hi!),请参阅这些fabulous timings
【解决方案4】:

最少的代码是:

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

显然,这会创建一个包含集合中每个成员的新列表,因此如果您的集合非常大,则不是很好。

【讨论】:

  • @augurar:因为它以相对简单的方式完成了工作。有时这就是快速脚本中的全部内容。
  • @augurar 我认为人们对这个答案进行了投票,因为set 主要不是用于索引和切片;并且该用户只是将编码器转换为使用合适的数据类型进行此类工作,即list
  • @Vicrobot 是的,但它通过复制整个集合并将 O(1) 操作转换为 O(n) 操作来实现。这是一个糟糕的解决方案,任何人都不应该使用。
  • 另外,如果你的目标是“最少代码”(这很愚蠢),那么 min(s) 使用的字符更少,但同样糟糕且效率低下。
  • +1 代表代码高尔夫获胜者,我有一个“可怕且低效”的实际反例:min(s)next(iter(s)) 略快于 1 组,我来到这个答案专门针对从大小为 1 的集合中提取唯一元素的特殊情况。
【解决方案5】:

我使用我编写的实用程序函数。它的名字有点误导,因为它暗示它可能是一个随机项目或类似的东西。

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

【讨论】:

  • 你也可以用 next(iter(iterable), None) 来节省墨水:)
【解决方案6】:

要提供不同方法背后的一些时序图,请考虑以下代码。 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() 的设计感觉非常糟糕。
  • 好吧,那一行每次迭代都会创建一个新的 iter 对象。
  • @Ryan:不是也为for x in s 隐式创建了一个迭代器对象吗? "An iterator is created for the result of the expression_list."
  • @musiphil 这是真的;最初我错过了 0.14 的“突破”,这真的是违反直觉的。我想在有时间的时候深入研究一下。
  • 我知道这已经过时了,但是当将 s.remove() 添加到组合中时,iter 示例 foriter 都会变得非常糟糕。
【解决方案7】:

关注@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

【讨论】:

    【解决方案8】:

    tl;博士

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

    你这样做

    欢迎来到另一组 Python 3.x 时序,根据wr. 的出色Python 2.x-specific response 推断。与AChampion 的同样有用的Python 3.x-specific response 不同,以下时间 上面建议的时间异常值解决方案 - 包括:

    欢乐的代码片段

    开机、收听、计时:

    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()
    

    快速过时的永恒计时

    看哪!按从快到慢的 sn-ps 排序:

    $ ./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 时代(手动迭代速度至少快四倍)相比,差距已经缩小,但让我的 PEP 20 狂热者失望的是,最冗长的解决方案是最好的。至少将一个集合转换为一个列表只是为了提取集合的第一个元素,这和预期的一样可怕。 感谢 Guido,愿他的光芒继续指引我们。

    令人惊讶的是,基于 RNG 的解决方案非常糟糕。 列表转换很糟糕,但 random 真的 吃到了糟糕透顶的蛋糕。 Random Number God 就这么多。

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

    【讨论】:

    • 我认为抱怨next(iter(s))CPython 中的for x in s: break 慢两倍有点奇怪。我的意思是CPython。它会比 C 或 Haskell 做同样的事情慢大约 50-100 倍(或类似的东西)(在大多数情况下,尤其是在迭代中,没有尾调用消除,也没有任何优化。)。失去一些微秒并没有真正的区别。你不觉得吗?还有 PyPy
    【解决方案9】:

    看似最紧凑(6个符号)但非常慢获取集合元素的方式(由PEP 3132实现):

    e,*_=s
    

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

    [*s][0]
    

    这两个选项在我的机器上比 for-loop 方法慢大约 1000 倍。

    【讨论】:

    • for 循环方法(或更准确地说是迭代器方法)的时间复杂度为 O(1),而这些方法的时间复杂度为 O(N)。不过,它们简洁。 :)
    【解决方案10】:

    我想知道这些函数在不同集合中的表现如何,所以我做了一个基准测试:

    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
    

    我还将它包含在上面的基准测试中。它可以与其他两种“快速”解决方案竞争,但两者差别不大。

    【讨论】:

    • 这是一个很好的答案。感谢您花时间将其变为经验。
    • 图表给予答案更多关注
    • 我有一个小问题,为什么你在 ForLoop 中使用 break 而不是直接使用 return e?该函数应该在 return 执行的那一刻“中断”。
    • @Andreas 这是一个很好且有效的观点。谢谢你提出来。但是对于“为什么”:我想将运行时与其他答案进行比较,所以我只是从那些答案中复制了方法。在这种情况下,答案是break(参考stackoverflow.com/a/59841)...不是一个好的答案,但我只是不想过多地更改他们的代码。
    • @DanielJerrehian 在这种情况下,您可以提供默认值 first(set(), default=None) 例如:)
    【解决方案11】:

    s.copy().pop() 怎么样?我没有计时,但它应该可以工作而且很简单。然而,它最适合小集合,因为它复制了整个集合。

    【讨论】:

      【解决方案12】:

      我通常为小型集合做的是创建一种像这样的解析器/转换器方法

      def convertSetToList(setName):
      return list(setName)
      

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

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

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

      【讨论】:

      • 为什么不直接使用list 而不是创建一个转换器方法?
      【解决方案13】:

      您可以解压缩值以访问元素:

      s = set([1, 2, 3])
      
      v1, v2, v3 = s
      
      print(v1,v2,v3)
      #1 2 3
      

      【讨论】:

      • 我想你可以解压到v1, _*。如果没有通配符,您需要完全匹配元素的数量。但正如上一个答案stackoverflow.com/a/45803038/15416 所述,这很慢
      【解决方案14】:

      Python 3 中的另一种方式:

      next(iter(s))
      

      s.__iter__().__next__()
      

      【讨论】:

      • next(iter(s)) 会做同样的事情,但会更短,更 Pythonic。
      【解决方案15】:

      如果你只想要第一个元素,试试这个: b = (a-set()).pop()

      【讨论】:

      • Set 是一个无序的集合,所以没有“第一个元素”这样的东西:)
      猜你喜欢
      • 2011-02-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多