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?

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.


62

Outra maneira de fazer isso:

>>> set(['a','b']).issubset( ['b','a','foo','bar'] )
True

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.


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

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

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']]
[]

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

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.


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)

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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.