Может ли Python проверить членство нескольких значений в списке?

122

Я хочу проверить, есть ли в списке два или более значений, но получаю неожиданный результат:

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

Итак, может ли Python проверить принадлежность нескольких значений сразу к списку? Что означает этот результат?

Ноэ Ньето
источник

Ответы:

198

Это делает то, что вы хотите, и будет работать почти во всех случаях:

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

Выражение 'a','b' in ['b', 'a', 'foo', 'bar']работает не так, как ожидалось, потому что Python интерпретирует его как кортеж:

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

Другие варианты

Есть и другие способы выполнить этот тест, но они не будут работать для такого количества различных типов входных данных. Как указывает Кэби , вы можете решить эту проблему, используя наборы ...

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

...иногда:

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

Наборы могут быть созданы только с хешируемыми элементами. Но выражение генератора all(x in container for x in items)может обрабатывать практически любой тип контейнера. Единственное требование - это containerповторение (т. Е. Не генератор). itemsможет быть любым итеративным.

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

Тесты на скорость

Во многих случаях тест подмножества будет быстрее all, но разница не шокирует - за исключением случаев, когда вопрос не имеет значения, потому что наборы не являются вариантом. Преобразование списков в наборы только для целей подобного теста не всегда стоит усилий. А преобразование генераторов в наборы иногда может быть невероятно расточительным, замедляя выполнение программ на много порядков.

Вот несколько тестов для иллюстрации. Самая большая разница возникает , когда оба containerи itemsотносительно невелики. В этом случае подход с использованием подмножеств примерно на порядок быстрее:

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

Похоже, это большая разница. Но пока containerэто набор, его allможно использовать в гораздо больших масштабах:

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

Тестирование подмножества все еще быстрее, но только примерно в 5 раз в этом масштабе. Повышение скорости связано с быстрой cреализацией Python set, но основной алгоритм одинаков в обоих случаях.

Если itemsони уже сохранены в списке по другим причинам, вам придется преобразовать их в набор, прежде чем использовать подход тестирования подмножества. Затем ускорение падает примерно до 2,5 раз:

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

А если ваша containerпоследовательность, и ее нужно сначала преобразовать, то ускорение еще меньше:

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

Единственный раз, когда мы получаем катастрофически медленные результаты, - это когда мы уходим containerв виде последовательности:

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

И, конечно, мы сделаем это только в случае необходимости. Если все элементы bigseqхешируются, мы сделаем это вместо этого:

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

Это всего в 1,66 раза быстрее, чем у альтернативы ( set(bigseq) >= set(bigsubseq)рассчитано выше на 4,36).

Таким образом, тестирование подмножества, как правило, происходит быстрее, но не намного. С другой стороны, давайте посмотрим, когда allбыстрее. Что, если itemsдлина составляет десять миллионов значений, и, скорее всего, у них есть значения, которых нет 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)

Преобразование генератора в комплект в этом случае оказывается невероятно расточительным. setКонструктор должен потреблять весь генератор. Но короткое замыкание allгарантирует, что потребляется лишь небольшая часть генератора, поэтому он быстрее, чем тест подмножества, на четыре порядка .

По общему признанию, это крайний пример. Но, как видно, нельзя предполагать, что тот или иной подход будет быстрее во всех случаях.

The Upshot

В большинстве случаев преобразование containerв набор того стоит, по крайней мере, если все его элементы хешируемы. Это потому, что inдля наборов - O (1), а inдля последовательностей - O (n).

С другой стороны, использование тестирования подмножества, вероятно, того стоит только иногда. Обязательно сделайте это, если ваши тестовые задания уже хранятся в наборе. В противном случае allон работает немного медленнее и не требует дополнительного хранилища. Его также можно использовать с большими генераторами предметов и в этом случае иногда обеспечивает значительное ускорение.

senderle
источник
62

Другой способ сделать это:

>>> set(['a','b']).issubset( ['b','a','foo','bar'] )
True
Kabie
источник
21
Интересный факт: set(['a', 'b']) <= set(['b','a','foo','bar'])это еще один способ записать одно и то же, и он выглядит «математичнее».
Кирк Штраузер
8
Начиная с Python 2.7, вы можете использовать{'a', 'b'} <= {'b','a','foo','bar'}
Виктор Стискала
11

Я почти уверен in, что у него более высокий приоритет, чем ,ваш оператор интерпретируется как 'a', ('b' in ['b' ...]), который затем оценивается как 'a', Trueпоскольку 'b'находится в массиве.

См. Предыдущий ответ, чтобы узнать, как делать то, что вы хотите.

Foon
источник
7

Если вы хотите проверить все свои входные совпадения ,

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

если вы хотите проверить хотя бы одно совпадение ,

>>> any(x in ['b', 'a', 'foo', 'bar'] for x in ['a', 'b'])
Мохидин бин Мохаммед
источник
3

Синтаксический анализатор Python оценил этот оператор как кортеж, где было первое значение 'a', а второе значение - это выражение 'b' in ['b', 'a', 'foo', 'bar'](которое оценивается как True).

Вы можете написать простую функцию, делающую то, что вы хотите:

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

И назовите это так:

>>> all_in(('a', 'b'), ['b', 'a', 'foo', 'bar'])
True
dcrosta
источник
2
[x for x in ['a','b'] if x in ['b', 'a', 'foo', 'bar']]

Причина, по которой я считаю, что это лучше, чем выбранный ответ, заключается в том, что вам действительно не нужно вызывать функцию all (). Пустой список оценивается как False в операторах IF, непустой список оценивается как True.

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

Пример:

>>> [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
источник
1

Я бы сказал, что мы можем даже опустить эти квадратные скобки.

array = ['b', 'a', 'foo', 'bar']
all([i in array for i in 'a', 'b'])
Szabadkai
источник
0

Оба представленных здесь ответа не будут обрабатывать повторяющиеся элементы. Например, если вы проверяете, является ли [1,2,2] подсписком [1,2,3,4], оба вернут True. Возможно, вы хотите это сделать, но я просто хотел уточнить. Если вы хотите вернуть false для [1,2,2] в [1,2,3,4], вам нужно будет отсортировать оба списка и проверить каждый элемент с движущимся индексом в каждом списке. Просто немного более сложный цикл for.

user1419042
источник
1
'обе'? Есть более двух ответов. Вы имели в виду, что от этой проблемы страдают все ответы или только два из них (и если да, то какие)?
Wipqozn
-1

как можно быть питонистом без лямбд! .. не воспринимать всерьез .. но этот способ тоже работает:

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

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

оставьте конечную часть, если хотите проверить, есть ли какие-либо значения в массиве:

filter(lambda x:x in test_array, orig_array)
закром
источник
1
Просто предупреждаем, что это не будет работать так, как задумано в Python 3, где filterесть генератор. Вам нужно будет обернуть его, listесли вы действительно хотите получить результат, который вы могли бы протестировать с помощью ==или в логическом контексте (чтобы увидеть, пуст ли он). Предпочтительно использование списка или выражения генератора в anyили all.
Blckknght
-1

Вот как я это сделал:

A = ['a','b','c']
B = ['c']
logic = [(x in B) for x in A]
if True in logic:
    do something
Джон
источник