'y' (booleano) frente a '&' (bit a bit): ¿Por qué la diferencia de comportamiento entre listas y matrices numerosas?
¿Qué explica la diferencia en el comportamiento de las operaciones booleanas y bit a bit en listas frente a matrices NumPy?
Estoy confundido acerca del uso apropiado de &
vs and
en Python, como se ilustra en los siguientes ejemplos.
mylist1 = [True, True, True, False, True]
mylist2 = [False, True, False, True, False]
>>> len(mylist1) == len(mylist2)
True
# ---- Example 1 ----
>>> mylist1 and mylist2
[False, True, False, True, False]
# I would have expected [False, True, False, False, False]
# ---- Example 2 ----
>>> mylist1 & mylist2
TypeError: unsupported operand type(s) for &: 'list' and 'list'
# Why not just like example 1?
>>> import numpy as np
# ---- Example 3 ----
>>> np.array(mylist1) and np.array(mylist2)
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
# Why not just like Example 4?
# ---- Example 4 ----
>>> np.array(mylist1) & np.array(mylist2)
array([False, True, False, False, False], dtype=bool)
# This is the output I was expecting!
Esta respuesta y esta respuesta me ayudaron a comprender que and
es una operación booleana pero &
es una operación bit a bit.
Leí sobre operaciones bit a bit para comprender mejor el concepto, pero me cuesta usar esa información para darle sentido a los 4 ejemplos anteriores.
El ejemplo 4 me llevó al resultado deseado, así que está bien, pero todavía estoy confundido acerca de cuándo/cómo/por qué debo usar and
vs &
. ¿Por qué las listas y los arreglos NumPy se comportan de manera diferente con estos operadores?
¿Alguien puede ayudarme a comprender la diferencia entre operaciones booleanas y bit a bit para explicar por qué manejan listas y matrices NumPy de manera diferente?
and
prueba si ambas expresiones son lógicas True
mientras &
(cuando se usa con valores True
/ False
) prueba si ambas son True
.
En Python, los objetos integrados vacíos normalmente se tratan de forma lógica, False
mientras que los objetos integrados no vacíos se tratan de forma lógica True
. Esto facilita el caso de uso común en el que desea hacer algo si una lista está vacía y otra cosa si no lo está. Tenga en cuenta que esto significa que la lista [False] es lógicamente True
:
>>> if [False]:
... print('True')
...
True
Entonces, en el Ejemplo 1, la primera lista no está vacía y, por lo tanto, lógicamente True
, el valor de verdad de and
es el mismo que el de la segunda lista. (En nuestro caso, la segunda lista no está vacía y, por lo tanto, es lógica True
, pero identificarla requeriría un paso de cálculo innecesario).
Por ejemplo 2, las listas no se pueden combinar de manera significativa en modo bit a bit porque pueden contener elementos arbitrarios diferentes. Las cosas que se pueden combinar bit a bit incluyen: verdaderos y falsos, números enteros.
Los objetos NumPy, por el contrario, admiten cálculos vectorizados. Es decir, le permiten realizar las mismas operaciones en varios datos.
El ejemplo 3 falla porque las matrices NumPy (de longitud > 1) no tienen valor de verdad, lo que evita la confusión de la lógica basada en vectores.
El ejemplo 4 es simplemente una and
operación de bits vectorizados.
Línea de fondo
Si no está trabajando con matrices y no está realizando manipulaciones matemáticas de números enteros, probablemente desee
and
.Si tiene vectores de valores de verdad que desea combinar, utilícelos
numpy
con&
.
Acerca delist
Primero un punto muy importante, del que se desprenderá todo (espero).
En Python normal, list
no es especial de ninguna manera (excepto por tener una linda sintaxis para la construcción, que es principalmente un accidente histórico). Una vez que se crea una lista [3,2,6]
, es para todos los efectos solo un objeto Python común, como un número 3
, un conjunto {3,7}
o una función lambda x: x+5
.
(Sí, admite el cambio de sus elementos, admite la iteración y muchas otras cosas, pero eso es exactamente lo que es un tipo: admite algunas operaciones, pero no admite otras. int admite elevar a una potencia, pero eso no hazlo muy especial: eso es exactamente lo que es un int. lambda admite llamadas, pero eso no lo hace muy especial: para eso está lambda, después de todo :).
Acerca deand
and
no es un operador (puedes llamarlo "operador", pero también puedes llamar "para" un operador :). Los operadores en Python son (implementados mediante) métodos llamados a objetos de algún tipo, generalmente escritos como parte de ese tipo. No hay manera de que un método realice una evaluación de algunos de sus operandos, pero and
puede (y debe) hacerlo.
La consecuencia de eso es que and
no se puede sobrecargar, al igual que for
no se puede sobrecargar. Es completamente general y se comunica a través de un protocolo específico. Lo que puedes hacer es personalizar tu parte del protocolo, pero eso no significa que puedas alterar el comportamiento por and
completo. El protocolo es:
Imagine que Python interpreta "a y b" (esto no sucede literalmente de esta manera, pero ayuda a comprender). Cuando llega a "y", mira el objeto que acaba de evaluar (a) y le pregunta: ¿es verdad? ( NO : ¿lo eres True
?) Si eres autor de una clase, puedes personalizar esta respuesta. Si a
responde "no", and
(se salta b por completo, no se evalúa en absoluto y) dice: a
es mi resultado ( NO : Falso es mi resultado).
Si a
no responde, and
le pregunta: ¿cuál es su longitud? (Nuevamente, puedes personalizar esto como autor de a
la clase). Si a
responde 0, and
hace lo mismo que arriba: lo considera falso ( NO Falso), omite by da a
como resultado.
Si a
responde algo distinto de 0 a la segunda pregunta ("cuál es tu longitud"), o no responde nada, o responde "sí" a la primera ("eres cierto"), and
evalúa b, y dice: b
es mi resultado. Tenga en cuenta que NO hace b
ninguna pregunta.
La otra forma de decir todo esto es que a and b
es casi lo mismo que b if a else a
, excepto que a se evalúa solo una vez.
Ahora siéntese durante unos minutos con lápiz y papel y convénzase de que cuando {a,b} es un subconjunto de {Verdadero,Falso}, funciona exactamente como esperaría de los operadores booleanos. Pero espero haberte convencido de que es mucho más general y, como verás, mucho más útil de esta manera.
Poniendo esos dos juntos
Ahora espero que entiendas tu ejemplo 1. and
No le importa si mylist1 es un número, una lista, una lambda o un objeto de una clase Argmhbl. Solo le importa la respuesta de mylist1 a las preguntas del protocolo. Y, por supuesto, mylist1 responde 5 a la pregunta sobre la longitud, por lo que devuelve mylist2. Y eso es. No tiene nada que ver con los elementos de mylist1 y mylist2: no entran en escena en ninguna parte.
Segundo ejemplo: &
enlist
Por otro lado, &
es un operador como cualquier otro, como +
por ejemplo. Se puede definir para un tipo definiendo un método especial en esa clase. int
lo define como "y" bit a bit, y bool lo define como "y" lógico, pero esa es solo una opción: por ejemplo, los conjuntos y algunos otros objetos como las vistas de claves dict lo definen como una intersección de conjuntos. list
simplemente no lo define, probablemente porque a Guido no se le ocurrió ninguna forma obvia de definirlo.
engordado
En el otro lado: -D, las matrices numerosas son especiales, o al menos intentan serlo. Por supuesto, numpy.array es solo una clase, no puede anularse and
de ninguna manera, por lo que hace lo siguiente mejor: cuando se le pregunta "¿es cierto?", numpy.array genera un ValueError, que efectivamente dice "por favor reformule la pregunta, mi visión de la verdad no encaja en su modelo". (Tenga en cuenta que el mensaje ValueError no habla de ello and
, porque numpy.array no sabe quién le hace la pregunta; solo habla de la verdad).
Para &
, es una historia completamente diferente. numpy.array puede definirlo como desee y lo define &
de forma coherente con otros operadores: puntualmente. Entonces finalmente obtienes lo que quieres.
HTH,
Los operadores booleanos de cortocircuito ( and
, or
) no se pueden anular porque no existe una forma satisfactoria de hacerlo sin introducir nuevas características del lenguaje o sacrificar el cortocircuito. Como puede que sepas o no, evalúan el primer operando por su valor de verdad y, dependiendo de ese valor, evalúan y devuelven el segundo argumento, o no evalúan el segundo argumento y devuelven el primero:
something_true and x -> x
something_false and x -> something_false
something_true or x -> something_true
something_false or x -> x
Tenga en cuenta que se devuelve el (resultado de la evaluación del) operando real, no el valor de verdad del mismo.
La única forma de personalizar su comportamiento es anularlo __nonzero__
(renombrarlo __bool__
en Python 3), de modo que pueda afectar qué operando se devuelve, pero no devolver algo diferente. Las listas (y otras colecciones) se definen como "veraces" cuando contienen algo y "falsas" cuando están vacías.
Las matrices NumPy rechazan esa noción: para los casos de uso a los que apuntan, dos nociones diferentes de verdad son comunes: (1) si algún elemento es verdadero y (2) si todos los elementos son verdaderos. Dado que estos dos son completamente (y silenciosamente) incompatibles, y ninguno es claramente más correcto o más común, NumPy se niega a adivinar y requiere que uses .any()
o explícitamente .all()
.
&
y |
(y not
, por cierto) se pueden anular por completo, ya que no provocan cortocircuitos. Pueden devolver cualquier cosa cuando se anulan, y NumPy hace un buen uso de eso para realizar operaciones de elementos, como lo hacen con prácticamente cualquier otra operación escalar. Las listas, por otro lado, no transmiten operaciones entre sus elementos. Así como mylist1 - mylist2
no significa nada y mylist1 + mylist2
significa algo completamente diferente, no existe ningún &
operador para las listas.