Python può testare l'appartenenza a più valori in un elenco?


121

Voglio verificare se due o più valori hanno l'appartenenza a un elenco, ma ottengo un risultato imprevisto:

>>> 'a','b' in ['b', 'a', 'foo', 'bar']
('a', True)

Quindi, Python può testare l'appartenenza a più valori contemporaneamente in un elenco? Cosa significa questo risultato?

Risposte:


198

Questo fa quello che vuoi e funzionerà in quasi tutti i casi:

>>> all(x in ['b', 'a', 'foo', 'bar'] for x in ['a', 'b'])
True

L'espressione 'a','b' in ['b', 'a', 'foo', 'bar']non funziona come previsto perché Python la interpreta come una tupla:

>>> 'a', 'b'
('a', 'b')
>>> 'a', 5 + 2
('a', 7)
>>> 'a', 'x' in 'xerxes'
('a', True)

Altre opzioni

Ci sono altri modi per eseguire questo test, ma non funzioneranno per tanti tipi diversi di input. Come sottolinea Kabie , puoi risolvere questo problema usando i set ...

>>> set(['a', 'b']).issubset(set(['a', 'b', 'foo', 'bar']))
True
>>> {'a', 'b'} <= {'a', 'b', 'foo', 'bar'}
True

...qualche volta:

>>> {'a', ['b']} <= {'a', ['b'], 'foo', 'bar'}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

I set possono essere creati solo con elementi hashable. Ma l'espressione del generatore all(x in container for x in items)può gestire quasi tutti i tipi di contenitori. L'unico requisito è che containersia reiterabile (cioè non un generatore). itemspuò essere qualsiasi iterabile.

>>> container = [['b'], 'a', 'foo', 'bar']
>>> items = (i for i in ('a', ['b']))
>>> all(x in [['b'], 'a', 'foo', 'bar'] for x in items)
True

Test di velocità

In molti casi, il test del sottoinsieme sarà più veloce di all, ma la differenza non è scioccante, tranne quando la domanda è irrilevante perché i set non sono un'opzione. Convertire elenchi in set solo ai fini di un test come questo non sempre varrà la pena. E convertire i generatori in set a volte può essere incredibilmente dispendioso, rallentando i programmi di molti ordini di grandezza.

Ecco alcuni punti di riferimento per l'illustrazione. La differenza più grande si ha quando entrambi containere itemssono relativamente piccoli. In tal caso, l'approccio del sottoinsieme è di circa un ordine di grandezza più veloce:

>>> smallset = set(range(10))
>>> smallsubset = set(range(5))
>>> %timeit smallset >= smallsubset
110 ns ± 0.702 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
>>> %timeit all(x in smallset for x in smallsubset)
951 ns ± 11.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

Questa sembra una grande differenza. Ma finché containerè un set, allè ancora perfettamente utilizzabile su scale molto più grandi:

>>> bigset = set(range(100000))
>>> bigsubset = set(range(50000))
>>> %timeit bigset >= bigsubset
1.14 ms ± 13.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit all(x in bigset for x in bigsubset)
5.96 ms ± 37 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

L'uso del test dei sottoinsiemi è ancora più veloce, ma solo di circa 5 volte su questa scala. L'aumento di velocità è dovuto call'implementazione veloce di Python set, ma l'algoritmo fondamentale è lo stesso in entrambi i casi.

Se itemssei già archiviato in un elenco per altri motivi, dovrai convertirli in un set prima di utilizzare l'approccio del test del sottoinsieme. Quindi l'aumento di velocità scende a circa 2,5 volte:

>>> %timeit bigset >= set(bigsubseq)
2.1 ms ± 49.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

E se la tua containerè una sequenza e deve essere prima convertita, la velocità è ancora più piccola:

>>> %timeit set(bigseq) >= set(bigsubseq)
4.36 ms ± 31.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

L'unica volta che otteniamo risultati disastrosamente lenti è quando usciamo containercome sequenza:

>>> %timeit all(x in bigseq for x in bigsubseq)
184 ms ± 994 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

E, naturalmente, lo faremo solo se necessario. Se tutti gli elementi in bigseqsono hashable, allora lo faremo invece:

>>> %timeit bigset = set(bigseq); all(x in bigset for x in bigsubseq)
7.24 ms ± 78 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Questo è solo 1,66 volte più veloce dell'alternativa ( set(bigseq) >= set(bigsubseq), con un tempo superiore a 4,36).

Quindi il test dei sottoinsiemi è generalmente più veloce, ma non con un margine incredibile. D'altra parte, diamo un'occhiata a quando allè più veloce. E se itemsfosse lungo dieci milioni di valori ed è probabile che abbia valori che non sono presenti container?

>>> %timeit hugeiter = (x * 10 for bss in [bigsubseq] * 2000 for x in bss); set(bigset) >= set(hugeiter)
13.1 s ± 167 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
>>> %timeit hugeiter = (x * 10 for bss in [bigsubseq] * 2000 for x in bss); all(x in bigset for x in hugeiter)
2.33 ms ± 65.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

La conversione del generatore in un set risulta essere incredibilmente dispendiosa in questo caso. Il setcostruttore deve consumare l'intero generatore. Ma il comportamento di cortocircuito di allassicura che solo una piccola parte del generatore debba essere consumata, quindi è più veloce di un test di sottoinsieme di quattro ordini di grandezza .

Questo è un esempio estremo, è vero. Ma come mostra, non puoi presumere che un approccio o l'altro sarà più veloce in tutti i casi.

The Upshot

La maggior parte delle volte containervale la pena convertirsi in un set, almeno se tutti i suoi elementi sono modificabili. Questo perché inper gli insiemi è O (1), mentre inper le sequenze è O (n).

D'altra parte, probabilmente vale la pena usare il test dei sottoinsiemi solo a volte. Sicuramente fallo se i tuoi articoli di prova sono già memorizzati in un set. In caso contrario, allè solo leggermente più lento e non richiede spazio di archiviazione aggiuntivo. Può anche essere utilizzato con grandi generatori di oggetti e, in tal caso, a volte fornisce un'enorme accelerazione.


62

Un altro modo per farlo:

>>> set(['a','b']).issubset( ['b','a','foo','bar'] )
True

21
Fatto divertente: set(['a', 'b']) <= set(['b','a','foo','bar'])è un altro modo per scrivere la stessa cosa e sembra più "matematico".
Kirk Strauser

8
A partire da Python 2.7 puoi usare{'a', 'b'} <= {'b','a','foo','bar'}
Viktor Stískala

11

Sono abbastanza sicuro che inabbia una precedenza più alta di ,così la tua istruzione viene interpretata come 'a', ('b' in ['b' ...]), che quindi restituisce 'a', Truepoiché 'b'è nell'array.

Vedi la risposta precedente per sapere come fare quello che vuoi.


7

Se desideri controllare tutte le corrispondenze di input ,

>>> all(x in ['b', 'a', 'foo', 'bar'] for x in ['a', 'b'])

se vuoi controllare almeno una corrispondenza ,

>>> any(x in ['b', 'a', 'foo', 'bar'] for x in ['a', 'b'])

3

Il parser Python ha valutato quell'istruzione come una tupla, dove si trovava il primo valore 'a'e il secondo valore è l'espressione 'b' in ['b', 'a', 'foo', 'bar'](che restituisce True).

Puoi scrivere una semplice funzione fai quello che vuoi, però:

def all_in(candidates, sequence):
    for element in candidates:
        if element not in sequence:
            return False
    return True

E chiamalo come:

>>> all_in(('a', 'b'), ['b', 'a', 'foo', 'bar'])
True

2
[x for x in ['a','b'] if x in ['b', 'a', 'foo', 'bar']]

Il motivo per cui penso che sia migliore della risposta scelta è che non hai davvero bisogno di chiamare la funzione 'all ()'. L'elenco vuoto restituisce False nelle istruzioni IF, l'elenco non vuoto restituisce True.

if [x for x in ['a','b'] if x in ['b', 'a', 'foo', 'bar']]:
    ...Do something...

Esempio:

>>> [x for x in ['a','b'] if x in ['b', 'a', 'foo', 'bar']]
['a', 'b']
>>> [x for x in ['G','F'] if x in ['b', 'a', 'foo', 'bar']]
[]

1

Direi che possiamo anche tralasciare quelle parentesi quadre.

array = ['b', 'a', 'foo', 'bar']
all([i in array for i in 'a', 'b'])

0

Entrambe le risposte presentate qui non gestiranno elementi ripetuti. Ad esempio, se stai verificando se [1,2,2] è una sottolista di [1,2,3,4], entrambi restituiranno True. Potrebbe essere quello che intendi fare, ma volevo solo chiarire. Se vuoi restituire false per [1,2,2] in [1,2,3,4], dovresti ordinare entrambi gli elenchi e controllare ogni elemento con un indice mobile su ogni elenco. Solo un ciclo for leggermente più complicato.


1
'tutti e due'? Ci sono più di due risposte. Volevi dire che tutte le risposte soffrono di questo problema, o solo due delle risposte (e se sì, quali)?
Wipqozn

-1

come puoi essere pitonico senza lambda! .. da non prendere sul serio .. ma funziona anche in questo modo:

orig_array = [ ..... ]
test_array = [ ... ]

filter(lambda x:x in test_array, orig_array) == test_array

tralascia la parte finale se vuoi verificare se uno qualsiasi dei valori è nell'array:

filter(lambda x:x in test_array, orig_array)

1
Solo un avvertimento che questo non funzionerà come previsto in Python 3 dove filterc'è un generatore. Dovresti racchiuderlo listse desideri effettivamente ottenere un risultato che potresti testare con ==o in un contesto booleano (per vedere se è vuoto). È preferibile utilizzare una comprensione dell'elenco o un'espressione generatore in anyo all.
Blckknght

-1

Ecco come l'ho fatto:

A = ['a','b','c']
B = ['c']
logic = [(x in B) for x in A]
if True in logic:
    do something
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.