Czy Python może przetestować przynależność do wielu wartości na liście?

121

Chcę sprawdzić, czy dwie lub więcej wartości ma członkostwo na liście, ale otrzymuję nieoczekiwany wynik:

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

Czy zatem Python może przetestować przynależność do wielu wartości jednocześnie na liście? Co to oznacza?

Noe Nieto
źródło

Odpowiedzi:

197

Robi to, co chcesz i zadziała w prawie wszystkich przypadkach:

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

Wyrażenie 'a','b' in ['b', 'a', 'foo', 'bar']nie działa zgodnie z oczekiwaniami, ponieważ Python interpretuje je jako krotkę:

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

Inne opcje

Istnieją inne sposoby wykonania tego testu, ale nie będą one działać dla wielu różnych rodzajów danych wejściowych. Jak wskazuje Kabie , możesz rozwiązać ten problem za pomocą zestawów ...

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

...czasami:

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

Zestawy można tworzyć tylko z elementami, które można mieszać. Ale wyrażenie generatora all(x in container for x in items)może obsłużyć prawie każdy typ kontenera. Jedynym wymaganiem jest możliwość containerwielokrotnej iteracji (tj. Nie jest to generator). itemsmoże być w ogóle dowolna iterowalna.

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

Testy prędkości

W wielu przypadkach test podzbioru będzie szybszy niż all, ale różnica nie jest szokująca - z wyjątkiem sytuacji, gdy pytanie jest nieistotne, ponieważ zestawy nie są opcją. Konwertowanie list na zestawy tylko na potrzeby takiego testu nie zawsze będzie warte zachodu. A przekształcanie generatorów w zestawy może czasami być niewiarygodnie marnotrawne, spowalniając programy o wiele rzędów wielkości.

Oto kilka wzorców dla ilustracji. Największą różnicą jest, gdy oba containeri itemssą stosunkowo małe. W takim przypadku podejście do podzbioru jest o rząd wielkości szybsze:

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

To wygląda na dużą różnicę. Ale tak długo, jak containerjest to zestaw, allnadal doskonale nadaje się do użytku w znacznie większych skalach:

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

Korzystanie z testów podzbioru jest nadal szybsze, ale tylko o około 5x w tej skali. Przyspieszenie prędkości wynika z szybkiej cimplementacji Pythona set, ale podstawowy algorytm jest taki sam w obu przypadkach.

Jeśli Twoje itemssą już przechowywane na liście z innych powodów, będziesz musiał przekonwertować je na zestaw przed użyciem metody testowania podzbioru. Następnie przyspieszenie spada do około 2,5x:

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

A jeśli twoja containerjest sekwencją i musi zostać najpierw przekonwertowana, to przyspieszenie jest jeszcze mniejsze:

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

Jedyny przypadek, w którym uzyskujemy katastrofalnie powolne wyniki, to sytuacja, gdy wychodzimy containerw sekwencji:

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

Oczywiście zrobimy to tylko wtedy, gdy będziemy musieli. Jeśli wszystkie elementy bigseqmożna haszować, zamiast tego zrobimy to:

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

To tylko 1,66x szybciej niż alternatywa ( set(bigseq) >= set(bigsubseq)powyżej 4,36).

Tak więc testowanie podzbiorów jest generalnie szybsze, ale nie z niewiarygodnym marginesem. Z drugiej strony spójrzmy, kiedy alljest szybszy. A jeśli itemsma długość dziesięciu milionów wartości i prawdopodobnie zawiera wartości, których nie ma 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)

Przekształcenie generatora w zestaw okazuje się w tym przypadku niebywale marnotrawne. setKonstruktor ma zużywają cały generatora. Ale zachowanie zwarciowe allzapewnia, że ​​tylko niewielka część generatora musi zostać zużyta, więc jest szybsza niż test podzbioru o cztery rzędy wielkości .

Trzeba przyznać, że to skrajny przykład. Ale jak widać, nie można zakładać, że jedno podejście będzie szybsze we wszystkich przypadkach.

Upshot

W większości przypadków konwersja containerdo zestawu jest tego warta, przynajmniej jeśli wszystkie jego elementy dają się haszować. Dzieje się tak, ponieważ indla zbiorów wynosi O (1), podczas gdy indla sekwencji wynosi O (n).

Z drugiej strony, testowanie podzbiorów jest prawdopodobnie tego warte tylko czasami. Zdecydowanie zrób to, jeśli twoje pozycje testowe są już przechowywane w zestawie. W przeciwnym razie alljest tylko trochę wolniejszy i nie wymaga dodatkowej pamięci. Może być również używany z dużymi generatorami przedmiotów, a czasami zapewnia ogromne przyspieszenie w tym przypadku.

nadawca
źródło
62

Inny sposób:

>>> set(['a','b']).issubset( ['b','a','foo','bar'] )
True
Kabie
źródło
21
Ciekawostka: set(['a', 'b']) <= set(['b','a','foo','bar'])to inny sposób przeliterowania tego samego i wygląda bardziej matematycznie.
Kirk Strauser
8
Od wersji Python 2.7 możesz używać{'a', 'b'} <= {'b','a','foo','bar'}
Viktor Stískala
11

Jestem prawie pewien, że inma wyższy priorytet niż ,tak, że twoja instrukcja jest interpretowana jako 'a', ('b' in ['b' ...]), która następnie obliczana jest na, 'a', Trueponieważ 'b'jest w tablicy.

Zobacz poprzednią odpowiedź, aby dowiedzieć się, jak robić to, co chcesz.

Foon
źródło
7

Jeśli chcesz sprawdzić wszystkie wprowadzone dopasowania ,

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

jeśli chcesz sprawdzić co najmniej jeden mecz ,

>>> any(x in ['b', 'a', 'foo', 'bar'] for x in ['a', 'b'])
Mohideen bin Mohammed
źródło
3

Analizator składni Pythona ocenił tę instrukcję jako krotkę, gdzie pierwsza wartość była 'a', a druga wartość to wyrażenie 'b' in ['b', 'a', 'foo', 'bar'](którego wynikiem jest True).

Możesz jednak napisać prostą funkcję i robić to, co chcesz:

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

I nazwij to tak:

>>> all_in(('a', 'b'), ['b', 'a', 'foo', 'bar'])
True
dcrosta
źródło
2
[x for x in ['a','b'] if x in ['b', 'a', 'foo', 'bar']]

Powodem, dla którego uważam, że jest to lepsze niż wybrana odpowiedź, jest to, że naprawdę nie musisz wywoływać funkcji „all ()”. Pusta lista ma wartość Fałsz w instrukcjach JEŻELI, a niepusta lista ma wartość Prawda.

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

Przykład:

>>> [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
źródło
1

Powiedziałbym, że możemy nawet pominąć te nawiasy kwadratowe.

array = ['b', 'a', 'foo', 'bar']
all([i in array for i in 'a', 'b'])
szabadkai
źródło
0

Obie przedstawione tutaj odpowiedzi nie będą dotyczyły powtarzających się elementów. Na przykład, jeśli sprawdzasz, czy [1,2,2] jest podlistą o wartości [1,2,3,4], obie zwrócą wartość True. Może to chcesz zrobić, ale chciałem tylko wyjaśnić. Jeśli chcesz zwrócić false dla [1,2,2] w [1,2,3,4], musisz posortować obie listy i sprawdzić każdą pozycję z ruchomym indeksem na każdej liście. Tylko trochę bardziej skomplikowana pętla for.

user1419042
źródło
1
'obie'? Jest więcej niż dwie odpowiedzi. Czy chodziło Ci o to, że wszystkie odpowiedzi dotyczą tego problemu, czy tylko dwie z nich (a jeśli tak, to które)?
Wipqozn
-1

jak możesz być pythonem bez lambd! .. nie traktować poważnie .. ale tak też działa:

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

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

pomiń część końcową, jeśli chcesz sprawdzić, czy którakolwiek z wartości znajduje się w tablicy:

filter(lambda x:x in test_array, orig_array)
Niecka
źródło
1
Tylko ostrzeżenie, że nie będzie to działać zgodnie z przeznaczeniem w Pythonie 3, gdzie filterjest generator. Musiałbyś to opakować, listjeśli chcesz faktycznie uzyskać wynik, który możesz przetestować ==w kontekście logicznym lub w kontekście logicznym (aby sprawdzić, czy jest pusty). Korzystanie z listy ze zrozumieniem lub wyrażenie wytwórca anylub alljest korzystne.
Blckknght
-1

Oto jak to zrobiłem:

A = ['a','b','c']
B = ['c']
logic = [(x in B) for x in A]
if True in logic:
    do something
Jan
źródło