首页 / 知识

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

2023-04-15 04:14:00

How to retrieve an element from a set without removing it?

假设如下:

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

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

又快又脏:

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

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


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

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

要么...

1
e = next(iter(s))

但通常,集合不支持索引或切片。


最少的代码是:

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

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


TL;博士

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

你这样做

欢迎使用另一组Python 3.x时序,从wr。优秀的Python 2.x特定响应中推断出来。与AChampion同样有用的Python 3.x特定响应不同,下面的时间安排也是上面提出的时间异常解决方案 - 包括:

  • list(s)[0],John的新颖的基于序列的解决方案。
  • random.sample(s, 1),dF。基于RNG的折衷解决方案。

伟大的喜悦代码片段

打开,收听,计时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from timeit import Timer

stats = [
   "for i in range(1000):
\tfor x in s:
\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
s=set(range(100))"
)
    try:
        print("Time for %s:\t %f"%(stat, t.timeit(number=1000)))
    except:
        t.print_exc()

快速废弃的永恒时计

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

1
2
3
4
5
6
7
8
$ ./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真的需要糟糕的蛋糕。对于随机数上帝来说太多了。

我只是希望无定形他们会为我们提供一个set.get_first()方法。如果你正在读这篇文章,他们会说:"请。做点什么吧。"


要提供不同方法背后的一些时序数据,请考虑以下代码。
get()是我对Python的setobject.c的自定义添加,只是一个pop()而不删除元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from timeit import *

stats = ["for i in xrange(1000): iter(s).next()  ",
        "for i in xrange(1000):
\tfor x in s:
\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()

输出是:

1
2
3
4
5
6
7
$ ./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()解决方案更快)。


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

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

文档似乎没有提到random.sample的性能。从一个非常快速的经验测试中获得一个巨大的列表和一个庞大的集合,它似乎是一个列表的常量时间,但不是集合。此外,对集合的迭代不是随机的;订单未定义但可预测:

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

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

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

我想知道函数将如何针对不同的集合执行,所以我做了一个基准测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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()

enter image description here

该图清楚地表明某些方法(RandomSampleSetUnpackingListIndex)取决于集合的大小,在一般情况下应该避免(至少如果性能可能很重要)。 正如其他答案所示,最快的方法是ForLoop

然而,只要使用其中一个恒定时间方法,性能差异就可以忽略不计。

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

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

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


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

1
2
3
4
5
def anyitem(iterable):
    try:
        return iter(iterable).next()
    except StopIteration:
        return None

看似最紧凑(6个符号)虽然获取设定元素的速度很慢(PEP 3132可以实现):

1
e,*_=s

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

1
[*s][0]

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


关注@wr。发布,我得到类似的结果(对于Python3.5)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from timeit import *

stats = ["for i in range(1000): next(iter(s))",
        "for i in range(1000):
\tfor x in s:
\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()

输出:

1
2
3
4
5
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)的情况非常糟糕:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from timeit import *

stats = ["while s:
\ta = next(iter(s))
\ts.remove(a)"
,
        "while s:
\tfor x in s: break
\ts.remove(x)"
,
        "while s:
\tx=s.pop()
\ts.add(x)
\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()

结果是:

1
2
3
4
5
6
7
8
9
10
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

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


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

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

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

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

这种选择的副作用是您的代码将向后兼容旧版的set版本的Python。这可能不是最佳答案,但它是另一种选择。

编辑:你甚至可以做这样的事情来隐藏你使用dict而不是数组或集合的事实:

1
2
3
4
5
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()


删除集合元素检索

最新内容

相关内容

猜你喜欢