Python peut-il tester l'appartenance de plusieurs valeurs dans une liste?

122

Je veux tester si deux valeurs ou plus ont l'appartenance à une liste, mais j'obtiens un résultat inattendu:

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

Alors, Python peut-il tester l'appartenance de plusieurs valeurs à la fois dans une liste? Que signifie ce résultat?

Noe Nieto
la source

Réponses:

198

Cela fait ce que vous voulez et fonctionnera dans presque tous les cas:

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

L'expression 'a','b' in ['b', 'a', 'foo', 'bar']ne fonctionne pas comme prévu car Python l'interprète comme un tuple:

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

Autres options

Il existe d'autres moyens d'exécuter ce test, mais ils ne fonctionneront pas pour autant de types d'entrées différents. Comme le souligne Kabie , vous pouvez résoudre ce problème en utilisant des ensembles ...

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

...quelquefois:

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

Les ensembles ne peuvent être créés qu'avec des éléments hachables. Mais l'expression du générateur all(x in container for x in items)peut gérer presque tous les types de conteneurs. La seule exigence est qu'il containersoit réitérable (c'est-à-dire pas un générateur). itemspeut être tout itérable du tout.

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

Tests de vitesse

Dans de nombreux cas, le test du sous-ensemble sera plus rapide que all, mais la différence n'est pas choquante - sauf lorsque la question n'est pas pertinente car les ensembles ne sont pas une option. La conversion de listes en ensembles uniquement dans le but d'un test comme celui-ci n'en vaudra pas toujours la peine. Et la conversion de générateurs en ensembles peut parfois être un gaspillage incroyable, ralentissant les programmes de plusieurs ordres de grandeur.

Voici quelques repères à titre d'illustration. La plus grande différence vient quand les deux containeret itemssont relativement faibles. Dans ce cas, l'approche du sous-ensemble est d'environ un ordre de grandeur plus rapide:

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

Cela ressemble à une grande différence. Mais tant qu'il containers'agit d'un ensemble, il allest toujours parfaitement utilisable à des échelles beaucoup plus grandes:

>>> 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'utilisation des tests de sous-ensembles est encore plus rapide, mais seulement d'environ 5x à cette échelle. L'augmentation de la vitesse est due à l' cimplémentation rapide de Python set, mais l'algorithme fondamental est le même dans les deux cas.

Si vous itemsêtes déjà stocké dans une liste pour d'autres raisons, vous devrez les convertir en un ensemble avant d'utiliser l'approche de test de sous-ensemble. Ensuite, l'accélération tombe à environ 2,5x:

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

Et si votre containerest une séquence, et doit d'abord être convertie, alors l'accélération est encore plus petite:

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

Le seul moment où nous obtenons des résultats désastreusement lents, c'est lorsque nous partons containeren séquence:

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

Et bien sûr, nous ne le ferons que si nous le devons. Si tous les éléments de bigseqsont hachables, nous le ferons à la place:

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

C'est juste 1,66 fois plus rapide que l'alternative ( set(bigseq) >= set(bigsubseq), chronométré ci-dessus à 4,36).

Les tests de sous-ensembles sont donc généralement plus rapides, mais pas avec une marge incroyable. D'un autre côté, regardons quand allest plus rapide. Que faire si itemsest long de dix millions de valeurs et est susceptible d'avoir des valeurs qui ne sont pas incluses 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 conversion du générateur en un ensemble s'avère être un gaspillage incroyable dans ce cas. Le setconstructeur doit consommer tout le générateur. Mais le comportement de court-circuit de allgarantit que seule une petite partie du générateur doit être consommée, c'est donc plus rapide qu'un test de sous-ensemble de quatre ordres de grandeur .

C'est un exemple extrême, certes. Mais comme cela le montre, vous ne pouvez pas supposer qu'une approche ou une autre sera plus rapide dans tous les cas.

The Upshot

La plupart du temps, la conversion containeren un ensemble en vaut la peine, du moins si tous ses éléments sont hachables. C'est parce que inpour les ensembles est O (1), tandis que inpour les séquences est O (n).

D'un autre côté, l'utilisation de tests de sous-ensembles n'en vaut probablement la peine que parfois. Faites-le certainement si vos éléments de test sont déjà stockés dans un ensemble. Sinon, ce alln'est qu'un peu plus lent et ne nécessite aucun stockage supplémentaire. Il peut également être utilisé avec de gros générateurs d'objets, et fournit parfois une accélération massive dans ce cas.

expéditeur
la source
62

Une autre façon de le faire:

>>> set(['a','b']).issubset( ['b','a','foo','bar'] )
True
Kabie
la source
21
Fait amusant: set(['a', 'b']) <= set(['b','a','foo','bar'])est une autre façon d'épeler la même chose, et semble "mathier".
Kirk Strauser
8
À partir de Python 2.7, vous pouvez utiliser{'a', 'b'} <= {'b','a','foo','bar'}
Viktor Stískala
11

Je suis à peu près sûr inqu'elle a une priorité plus élevée que ,si votre déclaration est interprétée comme 'a', ('b' in ['b' ...]), qui s'évalue ensuite 'a', Truecomme 'b'étant dans le tableau.

Voir la réponse précédente pour savoir comment faire ce que vous voulez.

Foon
la source
7

Si vous souhaitez vérifier toutes vos correspondances d'entrée ,

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

si vous souhaitez vérifier au moins une correspondance ,

>>> any(x in ['b', 'a', 'foo', 'bar'] for x in ['a', 'b'])
Mohideen bin Mohammed
la source
3

L'analyseur Python a évalué cette instruction comme un tuple, où la première valeur était 'a', et la deuxième valeur est l'expression 'b' in ['b', 'a', 'foo', 'bar'](qui est évaluée à True).

Vous pouvez cependant écrire une fonction simple pour faire ce que vous voulez:

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

Et appelez-le comme:

>>> all_in(('a', 'b'), ['b', 'a', 'foo', 'bar'])
True
dcrosta
la source
2
[x for x in ['a','b'] if x in ['b', 'a', 'foo', 'bar']]

La raison pour laquelle je pense que c'est mieux que la réponse choisie est que vous n'avez vraiment pas besoin d'appeler la fonction 'all ()'. La liste vide a la valeur False dans les instructions IF, la liste non vide a la valeur True.

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

Exemple:

>>> [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']]
[]
dmchdev
la source
1

Je dirais que nous pouvons même laisser ces crochets de côté.

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

Les deux réponses présentées ici ne traiteront pas les éléments répétés. Par exemple, si vous testez si [1,2,2] est une sous-liste de [1,2,3,4], les deux renverront True. C'est peut-être ce que vous voulez faire, mais je voulais juste clarifier. Si vous voulez retourner false pour [1,2,2] dans [1,2,3,4], vous devez trier les deux listes et vérifier chaque élément avec un index mobile sur chaque liste. Juste une boucle for un peu plus compliquée.

user1419042
la source
1
'tous les deux'? Il y a plus de deux réponses. Vouliez-vous dire que toutes les réponses souffrent de ce problème, ou seulement deux des réponses (et si oui, lesquelles)?
Wipqozn
-1

comment pouvez-vous être pythonique sans lambdas! .. à ne pas prendre au sérieux .. mais cette méthode fonctionne aussi:

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

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

laissez de côté la partie finale si vous voulez tester si l' une des valeurs est dans le tableau:

filter(lambda x:x in test_array, orig_array)
Clapier
la source
1
Juste un avertissement que cela ne fonctionnera pas comme prévu dans Python 3 où se filtertrouve un générateur. Vous auriez besoin de l'envelopper listsi vous vouliez réellement obtenir un résultat que vous pourriez tester avec ==ou dans un contexte booléen (pour voir s'il est vide). Utiliser une compréhension de liste ou une expression de générateur dans anyou allest préférable.
Blckknght
-1

Voici comment je l'ai fait:

A = ['a','b','c']
B = ['c']
logic = [(x in B) for x in A]
if True in logic:
    do something
John
la source