要素を削除せずにセットから取得する方法は?


427

以下を想定します。

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

実行sせずに値(任意の値)を取得するにはどうすればよいs.pop()ですか?アイテムを確実に削除できるまでセットに残しておきたいのですが、別のホストへの非同期呼び出しの後でしか確認できません。

素早く汚い:

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

しかし、もっと良い方法を知っていますか?理想的には一定の時間です。


8
Pythonにこの関数がまだ実装されていない理由を誰かが知っていますか?
hlin117 2015年

ユースケースは何ですか?セットには理由のためにこの機能がありません。あなたはそれを反復し、union要素から要素を取得しないなどのセット関連の操作を行うことになっています。たとえば、これはランダムな要素を返すと思った場合、next(iter({3,2,1}))常に返される1ので、そうではありません。では、間違ったデータ構造を使用しているだけでしょうか?ユースケースは何ですか?
user1685095

1
関連:stackoverflow.com/questions/20625579/…(同じ質問ではありませんが、価値のある代替案と洞察があります。)
John Y

@ hlin117セットは順序付けられていないコレクションであるため。順序が予期されていないため、特定の位置にある要素を取得しても意味がありません。ランダムであることが期待されます。
ジェイコモン

回答:


545

セット全体をコピーする必要がない2つのオプション:

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

または...

e = next(iter(s))

ただし、一般に、セットはインデックス作成やスライスをサポートしていません。


4
これは私の質問に答えます。悲しいかな、繰り返しは要素をソートしているように見えるので、私は今でもpop()を使用すると思います。私はそれらをランダムな順序で好みます...
ダレントーマス

9
iter()が要素をソートしているとは思いません-セットを作成してpop()が空になるまで、一貫した(この例ではソートされた)順序になり、イテレータと同じです-pop( )「私は何も約束しない」のように、ランダムな順序を約束せず、恣意的です。
ブレアコンラッド

2
+1 iter(s).next()はひどいものではなく、すばらしいものです。反復可能なオブジェクトから任意の要素を取得することは完全に一般的です。ただし、コレクションが空の場合は注意が必要です。
u0b34a0f6ae 2009年

8
next(iter(s))もOKで、読みやすいと思いがちです。また、sが空の場合に、番兵を使用してケースを処理できます。たとえば、next(iter(s)、set())です。
2012

5
next(iter(your_list or []), None)なしのセットと空のセットを処理するため
MrE 2018

111

最小コードは次のようになります:

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

明らかに、これはセットの各メンバーを含む新しいリストを作成するので、セットが非常に大きい場合はあまり良くありません。


96
next(iter(s))唯一超えlist(s)[0]3つの文字とそうでない場合は、時間と空間の複雑さの両方で劇的に優れています。したがって、「最小のコード」という主張はほんの一部ですが、これが考えられる最悪のアプローチであることもほんの一部です。手動で削除し、削除した要素を元のセットに再度追加することは、「最初の要素を抽出するためだけにまったく新しいコンテナを構築する」よりも優れています。もっと気になるのは、38個のStackoverflowersが実際にこれに賛成したことです。私はこれが製品コードで見られることを知っています。
セシルカレー

19
@augurar:比較的単純な方法で仕事を完了するため。そして、時にはそれが簡単なスクリプトで重要なことのすべてです。
tonysdg 2017

4
@Vicrobotそうですが、コレクション全体をコピーし、O(1)オペレーションをO(n)オペレーションに変換することでそうします。これは、誰も使用してはならない恐ろしいソリューションです。
オーギュラー

9
また、「ばかばかしい」「最小限のコード」を目指しているだけの場合は、これmin(s)よりもひどく非効率的でありながら、使用する文字数がさらに少なくなります。
8

5
コードゴルフの勝者の+1は、「ひどく非効率的」であるという実際的な反例があります。サイズ1のセットmin(s)よりもやや高速です。next(iter(s))特に、セットから唯一の要素を抽出する特別なケースに注目して、この答えに行きましたサイズ1の
lehiester

50

関数がさまざまなセットに対してどのように実行されるのか疑問に思ったので、ベンチマークを行いました。

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

ここに画像の説明を入力してください

このプロットは、いくつかのアプローチ(RandomSampleSetUnpackingおよびListIndex)がセットのサイズに依存し、一般的なケース(少なくともパフォーマンス重要な場合)で回避する必要があることをます。他の回答ですでに示されているように、最速の方法はForLoopです。

ただし、一定時間アプローチの1つが使用されている限り、パフォーマンスの違いは無視できます。


iteration_utilities (免責事項:私は作成者です)このユースケースに便利な関数が含まれています。 first::

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

上記のベンチマークにも含めました。他の2つの「高速」ソリューションと競合できますが、どちらの方法でも違いはありません。


43

tl; dr

for first_item in muh_set: breakPython 3.xの最適なアプローチのままです。あなたをのろい、グイド。

これをやる

wrから推定されたPython 3.xタイミングのさらに別のセットへようこそPython 2.x固有の優れた応答AChampionの同等に役立つPython 3.x固有の応答とは異なり、以下のタイミングは、上で提案された時間外れ値の解決策含みます。

Great Joyのコードスニペット

電源を入れ、調整し、時間を計ります:

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

家族全員のためのフェイスプラント

当然のことながら、手動の反復は、次に高速なソリューションの少なくとも2倍の速度維持します。Bad Old Python 2.xの日(手動での反復が少なくとも4倍の速さでした)からギャップは減少しましたが、最も冗長なソリューションが最善であるという点で、PEP 20熱狂的な期待を裏切っています。少なくとも、セットの最初の要素を抽出するためだけにセットをリストに変換することは、予想どおり恐ろしいことです。グイドに感謝します。彼の光が私たちを導き続けますように。

驚くべきことに、RNGベースのソリューションはひどいものです。リスト変換は悪いですが、random 本当にひどいソースケーキをとります。乱数神にそんなに。

私はアモルファスが彼らがset.get_first()すでに私たちのための方法をPEPすることを望みます。これを読んでいる場合、彼らは「お願いします。何かしてください。」


2
私はその文句を言うと思いnext(iter(s)) 遅くの2倍であるfor x in s: breakCPython奇妙なの一種であるが。つまりですCPython。これは、CまたはHaskellが同じことを行うよりも約50〜100倍(またはそのようなもの)遅くなります(ほとんどの場合、特に反復の場合、末尾呼び出しの削除や最適化はまったく行われません)。数マイクロ秒を緩めても、実際の違いはありません。思いませんか?PyPyもあります
user1685095

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オブジェクトを作成します。
ライアン

3
@ライアン:イテレータオブジェクトfor x in sも暗黙的に作成されていませんか?「イテレータはの結果に対して作成されますexpression_list。」
musiphil

2
@musiphilそれは本当です。もともと私は0.14にある「ブレイク」を逃しました、それは本当に直感に反しています。時間があれば、これについて詳しく説明したいと思います。
Ryan

1
私はこれが古いですけど、追加する際s.remove()に混入するiter例を両方foriter壊滅的に悪い行きます。
AChampion 2016年

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
1つの要素だけが必要な場合は、random.choiceの方が賢明です。
Gregg Lind

list(s).pop()は、どの要素を取るかを気にしない場合に実行します。
エフゲニー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
next(iter(iterable)、None)でインクを節約することもできます:)
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コンバーターメソッドを作成する代わりに、なぜ使用しないのですか?
ダレントーマス

-1

いかがs.copy().pop()ですか?計時はしていませんが、うまくいくはずで、簡単です。ただし、セット全体をコピーするため、小さなセットに最適です。


-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 と下位互換性を持つことです。それはおそらく最良の答えではありませんが、別のオプションです。

編集:次のようなことを行って、配列やセットの代わりにdictを使用したという事実を隠すこともできます。


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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.