Kann Python die Zugehörigkeit mehrerer Werte in einer Liste testen?

121

Ich möchte testen, ob zwei oder mehr Werte Mitglied einer Liste sind, erhalte jedoch ein unerwartetes Ergebnis:

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

Kann Python die Mitgliedschaft mehrerer Werte gleichzeitig in einer Liste testen? Was bedeutet das Ergebnis?

Noe Nieto
quelle

Antworten:

197

Dies macht, was Sie wollen, und funktioniert in fast allen Fällen:

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

Der Ausdruck 'a','b' in ['b', 'a', 'foo', 'bar']funktioniert nicht wie erwartet, da Python ihn als Tupel interpretiert:

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

Andere Optionen

Es gibt andere Möglichkeiten, diesen Test auszuführen, aber sie funktionieren nicht für so viele verschiedene Arten von Eingaben. Wie Kabie betont , können Sie dieses Problem mit Sets lösen ...

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

...manchmal:

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

Sets können nur mit hashbaren Elementen erstellt werden. Der Generatorausdruck all(x in container for x in items)kann jedoch fast jeden Containertyp verarbeiten. Die einzige Voraussetzung ist, dass containersie erneut iterierbar ist (dh kein Generator). itemskann überhaupt iterierbar sein.

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

Geschwindigkeitstests

In vielen Fällen ist der Teilmengen-Test schneller als all , aber der Unterschied ist nicht schockierend - außer wenn die Frage irrelevant ist, weil Mengen keine Option sind. Das Konvertieren von Listen in Sets nur zum Zweck eines solchen Tests ist nicht immer die Mühe wert. Und das Umrüsten von Generatoren in Sets kann manchmal unglaublich verschwenderisch sein und Programme um viele Größenordnungen verlangsamen.

Hier einige Benchmarks zur Veranschaulichung. Der größte Unterschied kommt , wenn beide containerund itemsrelativ klein sind. In diesem Fall ist der Teilmengenansatz um eine Größenordnung schneller:

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

Das sieht nach einem großen Unterschied aus. Aber solange containeres sich um ein Set handelt, allist es in weitaus größeren Maßstäben immer noch perfekt verwendbar:

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

Das Verwenden von Teilmengen-Tests ist immer noch schneller, jedoch nur um das Fünffache in dieser Skala. Der Geschwindigkeitsschub ist auf die schnelle cImplementierung von Python zurückzuführen set, aber der grundlegende Algorithmus ist in beiden Fällen der gleiche.

Wenn Sie itemsaus anderen Gründen bereits in einer Liste gespeichert sind, müssen Sie diese in eine Menge konvertieren, bevor Sie den Testansatz für Teilmengen verwenden können. Dann sinkt die Beschleunigung auf etwa das 2,5-fache:

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

Und wenn Ihr containereine Sequenz ist und zuerst konvertiert werden muss, ist die Beschleunigung noch geringer:

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

Das einzige Mal, wenn wir katastrophal langsame Ergebnisse erzielen, ist, wenn wir containerals Sequenz gehen:

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

Und das machen wir natürlich nur, wenn wir müssen. Wenn alle Elemente in bigseqhashable sind, tun wir dies stattdessen:

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

Das ist nur 1,66x schneller als die Alternative ( set(bigseq) >= set(bigsubseq)oben auf 4,36 eingestellt).

Das Testen von Teilmengen ist also im Allgemeinen schneller, aber nicht mit einem unglaublichen Vorsprung. Schauen wir uns andererseits an, wann alles schneller geht. Was ist, wenn itemszehn Millionen Werte lang sind und wahrscheinlich Werte enthalten, die nicht vorhanden sind 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)

Das Umrüsten des Generators in ein Set erweist sich in diesem Fall als unglaublich verschwenderisch. Der setKonstruktor muss den gesamten Generator verbrauchen. Das Kurzschlussverhalten von allstellt jedoch sicher, dass nur ein kleiner Teil des Generators verbraucht werden muss, sodass es um vier Größenordnungen schneller als ein Teilmengen-Test ist .

Dies ist zugegebenermaßen ein extremes Beispiel. Aber wie es zeigt, können Sie nicht davon ausgehen, dass der eine oder andere Ansatz in allen Fällen schneller ist.

Das Ergebnis

Die Konvertierung containerin eine Menge lohnt sich meistens, zumindest wenn alle Elemente hashbar sind. Das liegt daran, dass infür Mengen O (1) ist, während infür Sequenzen O (n) ist.

Auf der anderen Seite lohnt es sich wahrscheinlich nur manchmal, Teilmengen-Tests durchzuführen. Tun Sie dies auf jeden Fall, wenn Ihre Testobjekte bereits in einem Set gespeichert sind. Ansonsten allist es nur wenig langsamer und benötigt keinen zusätzlichen Speicher. Es kann auch mit großen Generatoren von Gegenständen verwendet werden und bietet in diesem Fall manchmal eine massive Beschleunigung.

senderle
quelle
62

Ein anderer Weg, es zu tun:

>>> set(['a','b']).issubset( ['b','a','foo','bar'] )
True
Kabie
quelle
21
Unterhaltsame Tatsache: set(['a', 'b']) <= set(['b','a','foo','bar'])ist eine andere Möglichkeit, dasselbe zu buchstabieren, und sieht "mathematischer" aus.
Kirk Strauser
8
Ab Python 2.7 können Sie verwenden{'a', 'b'} <= {'b','a','foo','bar'}
Viktor Stískala
11

Ich bin mir ziemlich sicher in, ,dass Ihre Anweisung eine höhere Priorität hat, als dass Ihre Anweisung so interpretiert wird 'a', ('b' in ['b' ...]), dass sie ausgewertet wird, 'a', Trueda sie 'b'sich im Array befindet.

In der vorherigen Antwort erfahren Sie, wie Sie das tun, was Sie wollen.

Foon
quelle
7

Wenn Sie alle Ihre Eingabeübereinstimmungen überprüfen möchten ,

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

Wenn Sie mindestens eine Übereinstimmung überprüfen möchten ,

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

Der Python-Parser hat diese Anweisung als Tupel ausgewertet, wobei der erste Wert war 'a', und der zweite Wert ist der Ausdruck 'b' in ['b', 'a', 'foo', 'bar'](der ausgewertet wird True).

Sie können eine einfache Funktion schreiben und tun, was Sie wollen:

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

Und nenne es wie:

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

Der Grund, warum ich denke, dass dies besser ist als die gewählte Antwort, ist, dass Sie die Funktion 'all ()' wirklich nicht aufrufen müssen. Leere Liste wird in IF-Anweisungen mit False bewertet, nicht leere Liste mit True.

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

Beispiel:

>>> [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
quelle
1

Ich würde sagen, wir können diese eckigen Klammern sogar weglassen.

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

Beide hier vorgestellten Antworten behandeln keine wiederholten Elemente. Wenn Sie beispielsweise testen, ob [1,2,2] eine Unterliste von [1,2,3,4] ist, geben beide True zurück. Das ist vielleicht das, was Sie vorhaben, aber ich wollte es nur klarstellen. Wenn Sie in [1,2,3,4] für [1,2,2] false zurückgeben möchten, müssen Sie beide Listen sortieren und jedes Element mit einem sich bewegenden Index für jede Liste überprüfen. Nur eine etwas kompliziertere for-Schleife.

user1419042
quelle
1
'beide'? Es gibt mehr als zwei Antworten. Meinten Sie, dass alle Antworten unter diesem Problem leiden oder nur zwei der Antworten (und wenn ja, welche)?
Wipqozn
-1

Wie kann man ohne Lambdas pythonisch sein? .. nicht ernst zu nehmen .. aber so funktioniert es auch:

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

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

Lassen Sie den Endteil weg, wenn Sie testen möchten, ob sich einer der Werte im Array befindet:

filter(lambda x:x in test_array, orig_array)
Stall
quelle
1
Nur ein Hinweis, dass dies in Python 3, wo filtersich ein Generator befindet, nicht wie vorgesehen funktioniert . Sie müssten es einpacken, listwenn Sie tatsächlich ein Ergebnis erhalten möchten, das Sie mit ==oder in einem booleschen Kontext testen können (um zu sehen, ob es leer ist). Die Verwendung eines Listenverständnisses oder eines Generatorausdrucks in anyoder allist vorzuziehen.
Blckknght
-1

So habe ich es gemacht:

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