O Python pode testar a associação de vários valores em uma lista?

121

Quero testar se dois ou mais valores têm associação em uma lista, mas estou obtendo um resultado inesperado:

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

Portanto, o Python pode testar a associação de vários valores de uma vez em uma lista? O que significa esse resultado?

Noe Nieto
fonte

Respostas:

197

Isso faz o que você deseja e funcionará em quase todos os casos:

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

A expressão 'a','b' in ['b', 'a', 'foo', 'bar']não funciona conforme o esperado porque Python a interpreta como uma tupla:

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

Outras opções

Existem outras maneiras de executar este teste, mas elas não funcionarão para tantos tipos diferentes de entradas. Como Kabie aponta, você pode resolver este problema usando conjuntos ...

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

...as vezes:

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

Os conjuntos só podem ser criados com elementos hashable. Mas a expressão do gerador all(x in container for x in items)pode lidar com quase qualquer tipo de contêiner. O único requisito é que containerseja reiterável (ou seja, não seja um gerador). itemspode ser qualquer iterável.

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

Testes de Velocidade

Em muitos casos, o teste de subconjunto será mais rápido do que all , mas a diferença não é chocante - exceto quando a questão é irrelevante porque os conjuntos não são uma opção. Converter listas em conjuntos apenas para fins de um teste como esse nem sempre valerá a pena. E converter geradores em conjuntos pode às vezes ser um desperdício incrível, reduzindo a velocidade dos programas em muitas ordens de magnitude.

Aqui estão alguns benchmarks para ilustração. A maior diferença surge quando containere itemssão relativamente pequenos. Nesse caso, a abordagem de subconjunto é cerca de uma ordem de magnitude mais rápida:

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

Isso parece uma grande diferença. Mas, enquanto containerfor um conjunto, allainda é perfeitamente utilizável em escalas muito maiores:

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

Usar o teste de subconjunto ainda é mais rápido, mas apenas cerca de 5x nesta escala. O aumento de velocidade é devido à cimplementação de apoio rápido do Python deset , mas o algoritmo fundamental é o mesmo em ambos os casos.

Se itemsjá estiverem armazenados em uma lista por outros motivos, você terá que convertê-los em um conjunto antes de usar a abordagem de teste de subconjunto. Então, a aceleração cai para cerca de 2,5x:

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

E se você containerfor uma sequência e precisar ser convertido primeiro, a aceleração será ainda menor:

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

A única vez em que obtemos resultados desastrosamente lentos é quando partimos containerem uma sequência:

>>> %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, claro, só faremos isso se for necessário. Se todos os itens bigseqforem hashable, então faremos isso em vez disso:

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

Isso é apenas 1,66x mais rápido do que a alternativa (set(bigseq) >= set(bigsubseq) , cronometrado acima em 4,36).

Portanto, o teste de subconjunto é geralmente mais rápido, mas não por uma margem incrível. Por outro lado, vamos ver quando allé mais rápido. E se itemstiver dez milhões de valores e for provável que tenha valores que não estão 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)

Converter o gerador em um conjunto acaba sendo um desperdício incrível neste caso. O setconstrutor deve consumir todo o gerador. Mas o comportamento de curto-circuito de allgarante que apenas uma pequena parte do gerador precise ser consumida, então é mais rápido do que um teste de subconjunto em quatro ordens de magnitude .

Este é um exemplo extremo, admito. Mas, como mostra, você não pode presumir que uma abordagem ou outra será mais rápida em todos os casos.

The Upshot

Na maioria das vezes, containervale a pena converter para um conjunto, pelo menos se todos os seus elementos forem hashable. Isso porque inpara conjuntos é O (1), enquanto inpara sequências é O (n).

Por outro lado, usar o teste de subconjunto provavelmente só vale a pena algumas vezes. Definitivamente faça isso se seus itens de teste já estiverem armazenados em um conjunto. Caso contrário, allé apenas um pouco mais lento e não requer nenhum armazenamento adicional. Ele também pode ser usado com grandes geradores de itens e, às vezes, fornece uma grande aceleração nesse caso.

remetente
fonte
62

Outra maneira de fazer isso:

>>> set(['a','b']).issubset( ['b','a','foo','bar'] )
True
Kabie
fonte
21
Curiosidade: set(['a', 'b']) <= set(['b','a','foo','bar'])é outra maneira de escrever a mesma coisa, e parece mais "matemática".
Kirk Strauser
8
Você pode usar o Python 2.7{'a', 'b'} <= {'b','a','foo','bar'}
Viktor Stískala
11

Tenho certeza de que intem precedência mais alta do que ,a sua declaração está sendo interpretada como 'a', ('b' in ['b' ...]), que então avalia como, 'a', Truevisto que 'b'está na matriz.

Veja a resposta anterior para saber como fazer o que deseja.

Foon
fonte
7

Se você quiser verificar todas as correspondências de entrada ,

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

se você quiser verificar pelo menos uma correspondência ,

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

O analisador Python avaliou essa instrução como uma tupla, onde o primeiro valor era 'a', e o segundo valor é a expressão 'b' in ['b', 'a', 'foo', 'bar'](que avalia comoTrue ).

Você pode escrever uma função simples, faça o que quiser:

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

E chamá-lo assim:

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

Acho que isso é melhor do que a resposta escolhida porque você realmente não precisa chamar a função 'all ()'. A lista vazia é avaliada como False em instruções IF, a lista não vazia é avaliada como True.

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

Exemplo:

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

Eu diria que podemos até mesmo deixar esses colchetes de fora.

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

Ambas as respostas apresentadas aqui não lidarão com elementos repetidos. Por exemplo, se você estiver testando se [1,2,2] é uma sublista de [1,2,3,4], ambos retornarão True. Pode ser isso que você pretende fazer, mas eu só queria esclarecer. Se você quiser retornar falso para [1,2,2] em [1,2,3,4], você precisará classificar as duas listas e verificar cada item com um índice móvel em cada lista. Apenas um loop for ligeiramente mais complicado.

user1419042
fonte
1
'ambos'? Existem mais de duas respostas. Você quis dizer que todas as respostas sofrem com esse problema, ou apenas duas das respostas (e se sim, quais)?
Wipqozn
-1

como você pode ser pitônico sem lambdas! .. não deve ser levado a sério .. mas desta forma também funciona:

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

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

deixe de fora a parte final se quiser testar se algum dos valores está na matriz:

filter(lambda x:x in test_array, orig_array)
Hutch
fonte
1
Apenas um aviso de que isso não funcionará como pretendido no Python 3, onde filterhá um gerador. Você precisaria envolvê-lo listse quisesse realmente obter um resultado que pudesse testar com ==ou em um contexto booleano (para ver se está vazio). Usar uma compreensão de lista ou uma expressão geradora em anyou allé preferível.
Blckknght
-1

Veja como eu fiz:

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