¿Puede Python probar la pertenencia de varios valores en una lista?

121

Quiero probar si dos o más valores tienen membresía en una lista, pero obtengo un resultado inesperado:

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

Entonces, ¿Python puede probar la pertenencia de varios valores a la vez en una lista? ¿Qué significa ese resultado?

Noe Nieto
fuente

Respuestas:

197

Esto hace lo que quiere y funcionará en casi todos los casos:

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

La expresión 'a','b' in ['b', 'a', 'foo', 'bar']no funciona como se esperaba porque Python la interpreta como una tupla:

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

Otras opciones

Hay otras formas de ejecutar esta prueba, pero no funcionarán para tantos tipos diferentes de entradas. Como señala Kabie , puede resolver este problema usando conjuntos ...

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

...algunas veces:

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

Los conjuntos solo se pueden crear con elementos hash. Pero la expresión generadora all(x in container for x in items)puede manejar casi cualquier tipo de contenedor. El único requisito es que containersea ​​repetible (es decir, que no sea un generador). itemspuede ser iterable en absoluto.

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

Pruebas de velocidad

En muchos casos, la prueba de subconjunto será más rápida que all , pero la diferencia no es sorprendente, excepto cuando la pregunta es irrelevante porque los conjuntos no son una opción. Convertir listas en conjuntos solo con el propósito de una prueba como esta no siempre valdrá la pena. Y convertir generadores en conjuntos a veces puede ser un desperdicio increíble, lo que ralentiza los programas en muchos órdenes de magnitud.

Aquí hay algunos puntos de referencia para ilustrar. La mayor diferencia se produce cuando ambos containery itemsson relativamente pequeños. En ese caso, el enfoque de subconjunto es aproximadamente un orden de magnitud más rápido:

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

Esto parece una gran diferencia. Pero mientras containersea ​​un conjunto, alltodavía se puede utilizar perfectamente a escalas mucho mayores:

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

El uso de pruebas de subconjuntos es aún más rápido, pero solo alrededor de 5 veces en esta escala. El aumento de velocidad se debe a la cimplementación rápida de Python de set, pero el algoritmo fundamental es el mismo en ambos casos.

Si itemsya está almacenado en una lista por otros motivos, tendrá que convertirlos en un conjunto antes de utilizar el enfoque de prueba de subconjuntos. Luego, la aceleración se reduce a aproximadamente 2.5x:

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

Y si su containeres una secuencia, y debe convertirse primero, entonces la aceleración es aún menor:

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

La única vez que obtenemos resultados desastrosamente lentos es cuando salimos containercomo una secuencia:

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

Y, por supuesto, solo lo haremos si es necesario. Si todos los elementos bigseqson hash, haremos esto en su lugar:

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

Eso es solo 1,66 veces más rápido que la alternativa (set(bigseq) >= set(bigsubseq) cronometrado arriba en 4,36).

Por lo tanto, las pruebas de subconjuntos son generalmente más rápidas, pero no por un margen increíble. Por otro lado, veamos cuándo alles más rápido. ¿Qué pasa si itemstiene diez millones de valores y es probable que tenga valores que no están incluidos 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)

En este caso, convertir el generador en un conjunto resulta un desperdicio increíble. El setconstructor tiene que consumir todo el generador. Pero el comportamiento de cortocircuito de allasegura que solo se necesite consumir una pequeña porción del generador, por lo que es más rápido que una prueba de subconjunto en cuatro órdenes de magnitud .

Este es un ejemplo extremo, sin duda. Pero como muestra, no se puede asumir que un enfoque u otro será más rápido en todos los casos.

El resultado

La mayoría de las veces, la conversión containera un conjunto vale la pena, al menos si todos sus elementos son hash. Eso es porque inpara conjuntos es O (1), mientras que inpara secuencias es O (n).

Por otro lado, el uso de pruebas de subconjuntos probablemente solo valga la pena a veces. Definitivamente hágalo si sus elementos de prueba ya están almacenados en un conjunto. De lo contrario, alles solo un poco más lento y no requiere almacenamiento adicional. También se puede utilizar con grandes generadores de elementos y, en ocasiones, proporciona una aceleración masiva en ese caso.

remitente
fuente
62

Otra forma de hacerlo:

>>> set(['a','b']).issubset( ['b','a','foo','bar'] )
True
Kabie
fuente
21
Dato set(['a', 'b']) <= set(['b','a','foo','bar'])curioso : es otra forma de deletrear lo mismo y parece más "matemático".
Kirk Strauser
8
A partir de Python 2.7 puede usar{'a', 'b'} <= {'b','a','foo','bar'}
Viktor Stískala
11

Estoy bastante seguro de inque tiene una precedencia más alta que, ,por lo que su declaración se interpreta como 'a', ('b' in ['b' ...]), que luego se evalúa como 'a', Truedesde'b' está en la matriz.

Consulte la respuesta anterior para saber cómo hacer lo que quiere.

Foon
fuente
7

Si desea verificar todas sus coincidencias de entrada ,

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

si desea comprobar al menos una coincidencia ,

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

El analizador de Python evaluó esa declaración como una tupla, donde estaba el primer valor 'a'y el segundo valor es la expresión 'b' in ['b', 'a', 'foo', 'bar'](que se evalúa como True).

Sin embargo, puede escribir una función simple para hacer lo que quiera:

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

Y llámalo como:

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

La razón por la que creo que esto es mejor que la respuesta elegida es que realmente no es necesario llamar a la función 'all ()'. La lista vacía se evalúa como Falsa en declaraciones IF, la lista no vacía se evalúa como Verdadero.

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

Ejemplo:

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

Yo diría que incluso podemos omitir esos corchetes.

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

Ambas respuestas presentadas aquí no manejarán elementos repetidos. Por ejemplo, si está probando si [1,2,2] es una sublista de [1,2,3,4], ambos devolverán True. Puede que sea eso lo que pretendes hacer, pero solo quería aclararlo. Si desea devolver falso para [1,2,2] en [1,2,3,4], deberá ordenar ambas listas y marcar cada elemento con un índice móvil en cada lista. Solo un bucle for ligeramente más complicado.

usuario1419042
fuente
1
'ambos'? Hay más de dos respuestas. ¿Quiso decir que todas las respuestas sufren este problema, o solo dos de las respuestas (y si es así, cuáles)?
Wipqozn
-1

¡Cómo puedes ser pitónico sin lambdas! .. no para ser tomado en serio .. pero esta forma también funciona:

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

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

omita la parte final si desea probar si alguno de los valores está en la matriz:

filter(lambda x:x in test_array, orig_array)
Conejera
fuente
1
Solo un aviso de que esto no funcionará como se esperaba en Python 3, donde filterhay un generador. Debería ajustarlo listsi realmente desea obtener un resultado con el que pueda probar ==o en un contexto booleano (para ver si está vacío). Es preferible utilizar una lista de comprensión o una expresión generadora en anyo all.
Blckknght
-1

Así es como lo hice:

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