Come recuperare un elemento da un set senza rimuoverlo?


427

Supponiamo che:

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

Come posso ottenere un valore (qualsiasi valore) ssenza farlo s.pop()? Voglio lasciare l'elemento nel set fino a quando non sono sicuro di poterlo rimuovere - qualcosa di cui posso essere sicuro solo dopo una chiamata asincrona a un altro host.

Veloce e sporco:

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

Ma conosci un modo migliore? Idealmente a tempo costante.


8
Qualcuno sa perché Python non ha già implementato questa funzione?
hlin117,

Qual è il caso d'uso? Set non ha questa capacità per un motivo. Dovresti iterare attraverso di esso e fare operazioni relative al set come unionecc. Non prendere elementi da esso. Ad esempio, next(iter({3,2,1}))ritorna sempre, 1quindi se pensavi che questo avrebbe restituito un elemento casuale, non lo sarebbe. Quindi forse stai solo usando la struttura dei dati sbagliata? Qual è il caso d'uso?
user1685095,

1
Correlati: stackoverflow.com/questions/20625579/… (Lo so, non è la stessa domanda, ma ci sono alternative e spunti utili lì.)
John Y

@ hlin117 Perché set è una raccolta non ordinata . Poiché non è previsto alcun ordine, non ha senso recuperare un elemento in una determinata posizione: si prevede che sia casuale.
Jeyekomon,

Risposte:


547

Due opzioni che non richiedono la copia dell'intero set:

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

O...

e = next(iter(s))

Ma in generale, i set non supportano l'indicizzazione o il slicing.


4
Questo risponde alla mia domanda. Purtroppo, suppongo che userò ancora pop (), poiché l'iterazione sembra ordinare gli elementi. Li preferirei in ordine casuale ...
Daren Thomas,

9
Non penso che iter () stia ordinando gli elementi - quando creo un set e pop () fino a quando non è vuoto, ottengo un ordinamento coerente (ordinato, nel mio esempio), ed è lo stesso dell'iteratore - pop ( ) non promette un ordine casuale, solo arbitrario, come in "Non prometto nulla".
Blair Conrad,

2
+1 iter(s).next()non è disgustoso ma eccezionale. Completamente generale per prendere un elemento arbitrario da qualsiasi oggetto iterabile. La tua scelta se vuoi stare attento se la raccolta è vuota però.
u0b34a0f6ae,

8
next (iter (s)) è anche OK e tendo a pensare che legga meglio. Inoltre, è possibile utilizzare una sentinella per gestire il caso quando s è vuoto. Ad esempio next (iter (s), set ()).
ja

5
next(iter(your_list or []), None)gestire nessuno dei set e set vuoti
MrE

111

Il codice minimo sarebbe:

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

Ovviamente questo creerebbe un nuovo elenco che contiene ogni membro del set, quindi non eccezionale se il tuo set è molto grande.


97
next(iter(s))supera solo list(s)[0]di tre caratteri ed è altrimenti drammaticamente superiore sia nella complessità del tempo che dello spazio. Quindi, mentre l'affermazione del "minimo codice" è banalmente vera, è anche banalmente vero che questo è l'approccio peggiore possibile. Anche rimuovere manualmente e quindi aggiungere nuovamente l'elemento rimosso al set originale è superiore a "costruire un contenitore completamente nuovo solo per estrarre il primo elemento", che è palesemente pazzo. Ciò che mi preoccupa di più è che 38 Stackoverflowers lo hanno effettivamente votato. So solo che vedrò questo nel codice di produzione.
Cecil Curry,

19
@augurar: perché esegue il lavoro in modo relativamente semplice. E a volte è tutto ciò che conta in una sceneggiatura veloce.
tonysdg,

4
@Vicrobot Sì, ma lo fa copiando l'intera collezione e trasformando un'operazione O (1) in un'operazione O (n). Questa è una soluzione terribile che nessuno dovrebbe mai usare.
agosto

9
Inoltre, se stai solo mirando al "minimo codice" (che è stupido), min(s)usa ancora meno caratteri pur essendo terribile e inefficiente come questo.
agosto

5
+1 per il vincitore del codice golf, che ho un controesempio pratico per essere "terribile e inefficiente": min(s)è leggermente più veloce rispetto next(iter(s))ai set di dimensioni 1, e sono arrivato a questa risposta in particolare cercando di estrarre l'unico elemento dai set di taglia 1
Lehiester

52

Mi chiedevo come funzionassero le funzioni per diversi set, quindi ho fatto un benchmark:

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

inserisci qui la descrizione dell'immagine

Questo grafico mostra chiaramente che alcuni approcci ( RandomSample, SetUnpackinge ListIndex) dipende dalle dimensioni del set e dovrebbe essere evitata nel caso generale (almeno se la performance potrebbe essere importante). Come già mostrato dalle altre risposte, il modo più veloce è ForLoop.

Tuttavia, finché viene utilizzato uno degli approcci a tempo costante, la differenza di prestazioni sarà trascurabile.


iteration_utilities(Dichiarazione di non responsabilità: sono l'autore) contiene una funzione di praticità per questo caso d'uso first::

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

L'ho incluso anche nel benchmark sopra. Può competere con le altre due soluzioni "veloci", ma la differenza non è molto importante in entrambi i casi.


43

tl; dr

for first_item in muh_set: breakrimane l'approccio ottimale in Python 3.x. Ti maledico, Guido.

fai questo

Benvenuti in un altro set di timing Python 3.x, estrapolato da wr. 's eccellente risposta 2.x-specifica Python . A differenza della altrettanto utile risposta specifica di Python 3.x di AChampion , i tempi di seguito indicati sono anche soluzioni temporanee suggerite sopra, tra cui:

Snippet di codice per una grande gioia

Accendi, sintonizza, cronometra:

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

Tempi senza tempo rapidamente obsoleti

Ecco! Ordinato per frammenti dal più veloce al più lento:

$ ./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

Faceplants per tutta la famiglia

Non sorprende che l'iterazione manuale rimanga almeno due volte più veloce della soluzione più veloce successiva. Sebbene il divario sia diminuito dai giorni Bad Old Python 2.x (in cui l'iterazione manuale era almeno quattro volte più veloce), delude lo zelo di PEP 20 in me che la soluzione più prolissa è la migliore. Almeno convertire un set in un elenco solo per estrarre il primo elemento del set è orribile come previsto. Grazie a Guido, possa la sua luce continuare a guidarci.

Sorprendentemente, la soluzione basata su RNG è assolutamente orribile. La conversione della lista è cattiva, ma prende random davvero la torta con salsa terribile. Questo per quanto riguarda il Dio numero casuale .

Vorrei solo che gli amorfi avrebbero già elaborato un set.get_first()metodo per noi. Se stai leggendo questo, loro: "Per favore. Fai qualcosa."


2
Penso che lamentarsi del fatto che next(iter(s)) sia due volte più lento che for x in s: breakin CPythonsia strano. Voglio dire che lo è CPython. Sarà circa 50-100 volte (o qualcosa del genere) più lento di C o Haskell che fanno la stessa cosa (per la maggior parte del tempo, specialmente per quanto riguarda l'iterazione, nessuna eliminazione della coda e nessuna ottimizzazione). Perdere alcuni microsecondi non fa davvero la differenza. Non pensi? E c'è anche PyPy
user1685095,

39

Per fornire alcune cifre temporali alla base dei diversi approcci, considerare il seguente codice. Get () è la mia aggiunta personalizzata al setobject.c di Python, essendo solo un pop () senza rimuovere l'elemento.

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

L'output è:

$ ./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

Ciò significa che la soluzione for / break è la più veloce (a volte più veloce della soluzione get () personalizzata).


Qualcuno ha idea del perché iter (s) .next () è molto più lento delle altre possibilità, anche più lento di s.add (s.pop ())? Per me sembra un pessimo design di iter () e next () se i tempi sembrano così.
peschü

Bene per quello che linea crea un nuovo oggetto iter ogni iterazione.
Ryan,

3
@Ryan: Non è stato creato implicitamente anche un oggetto iteratore for x in s? "Viene creato un iteratore per il risultato di expression_list."
musiphil,

2
@musiphil Questo è vero; originariamente mi mancava il "break" a 0.14, che è davvero contro-intuitivo. Voglio fare un tuffo profondo in questo quando ho tempo.
Ryan,

1
So che questo è vecchio, ma quando si aggiungono s.remove()nel mix iterentrambi gli esempi fore si iterva catastroficamente male.
AChampion,

28

Poiché desideri un elemento casuale, funzionerà anche:

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

La documentazione non sembra menzionare le prestazioni di random.sample. Da un test empirico veramente veloce con una lista enorme e una serie enorme, sembra essere il tempo costante per una lista ma non per la serie. Inoltre, l'iterazione su un set non è casuale; l'ordine è indefinito ma prevedibile:

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

Se la casualità è importante e hai bisogno di un mucchio di elementi in tempo costante (set di grandi dimensioni), utilizzerei prima random.samplee convertirò in un elenco:

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

14
Se vuoi solo un elemento, random.choice è più sensato.
Gregg Lind,

list (s) .pop () farà se non ti interessa quale elemento prendere.
Evgeny,

8
@Gregg: non puoi usarlo choice(), perché Python proverà ad indicizzare il tuo set e questo non funziona.
Kevin,

3
Sebbene intelligente, questa è in realtà la soluzione più lenta mai suggerita da un ordine di grandezza. Sì, è così lento. Anche convertire il set in un elenco solo per estrarre il primo elemento di tale elenco è più veloce. Per i non credenti tra noi ( ... ciao! ), Vedi questi tempi favolosi .
Cecil Curry,

9

Apparentemente il modo più compatto (6 simboli) anche se molto lento per ottenere un elemento impostato (reso possibile da PEP 3132 ):

e,*_=s

Con Python 3.5+ puoi anche usare questa espressione di 7 simboli (grazie a PEP 448 ):

[*s][0]

Entrambe le opzioni sono circa 1000 volte più lente sulla mia macchina rispetto al metodo for-loop.


1
Il metodo for loop (o più precisamente il metodo iteratore) ha una complessità temporale O (1), mentre questi metodi sono O (N). Sono concisi però. :)
ForeverWintr,

6

Uso una funzione di utilità che ho scritto. Il suo nome è in qualche modo fuorviante perché implica che potrebbe essere un oggetto casuale o qualcosa del genere.

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

2
Puoi anche andare con next (iter (iterable), None) per risparmiare inchiostro :)
1 ''

3

A seguito di @wr. post, ottengo risultati simili (per 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()

Produzione:

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

Tuttavia, quando si cambia il set sottostante (es. Call to remove()) le cose vanno male per gli esempi iterabili ( for, iter):

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

Risultati in:

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

Quello che faccio di solito per le piccole raccolte è creare una specie di metodo parser / converter come questo

def convertSetToList(setName):
return list(setName)

Quindi posso utilizzare il nuovo elenco e accedere in base al numero indice

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

Come elenco avrai tutti gli altri metodi con cui potresti aver bisogno di lavorare


perché non usare semplicemente listinvece di creare un metodo di conversione?
Daren Thomas,

-1

Che ne dici s.copy().pop()? Non l'ho cronometrato, ma dovrebbe funzionare ed è semplice. Funziona meglio per piccoli set, poiché copia l'intero set.


-6

Un'altra opzione è quella di utilizzare un dizionario con valori che non ti interessano. Per esempio,


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

Puoi considerare le chiavi come un set, tranne per il fatto che sono solo un array:


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

Un effetto collaterale di questa scelta è che il codice sarà retrocompatibile con le setversioni precedenti di Python. Forse non è la risposta migliore ma è un'altra opzione.

Modifica: puoi persino fare qualcosa del genere per nascondere il fatto che hai usato un dict anziché un array o un set:


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
Questo non funziona come speri. In python 2 keys () è un'operazione O (n), quindi non sei più un tempo costante, ma almeno i tasti [0] restituiranno il valore che ti aspetti. In python 3 keys () è un'operazione O (1), quindi yay! Tuttavia, non restituisce più un oggetto elenco, restituisce un oggetto simile a un set che non può essere indicizzato, quindi i tasti [0] generano TypeError. stackoverflow.com/questions/39219065/...
sage88
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.