Lógica Booleana en Python

La Lógica Booleana, también conocida como Álgebra de Boole, es una estructura algebraica que esquematiza las operaciones lógicas Y, O, NO, SI (AND, OR, NOT, IF), así como el conjunto de operaciones unión, intersección y complemento. Esta disciplina toma su nombre en honor a George Boole, destacado matemático inglés que fue el primero en definirla como parte del sistema lógico a mediados del siglo XIX. En la actualidad, el Álgebra de Boole se aplica de forma generalizada en el ámbito de la electrónica, la informática y las matemáticas en general. Es el álgebra de dos valores: 0 y 1, que también se traducen en falso y verdadero y constituyen los llamados valores de verdad.

Los lenguajes de programación han hecho uso de los fundamentos de la Lógica Booleana desde sus propios inicios, con el fin de implementar estructuras tan comunes y necesarias como las bifurcaciones (if...else) y los ciclos o loops (while).

Se dice que una variable tiene valor booleano cuando contiene un 0 lógico o un 1 lógico. Esto, en la mayoría de los lenguajes de programación, se traduce en false (falso) o true (verdadero) respectivamente y constituye el “valor de verdad” de la variable.

En Python, los valores booleanos quedan definidos por dos constantes: False y True, que son las dos instancias válidas de la clase bool y que se emplean para representar el valor de verdad de los objetos del lenguaje.

Valores de Verdad de los Objetos en Python

Cualquier objeto de Python puede ser interrogado para determinar su valor de verdad o valor booleano cuando los empleamos en sentencias condicionales como los if y los while o en operaciones booleanas, donde intervienen los operadores and, or y not.

De manera general un objeto siempre tendrán un valor de verdad True (Verdadero) a no ser que implemente un método __bool__() que retorne False (Falso) o un método __len__() que retorne cero. Un ejemplo clásico de este último comportamiento, son las secuencias y las colecciones del lenguaje.

>>> l = []

>>> bool(l)

False

>>> l.append('Python')

>>> l

['Python']

>>> l.__len__()

1

>>> bool(l)

True

Objetos Integrados (built-in) Considerados Falsos (Fase)

Los objetos integrados (built-in) que por defecto tienen asignado un valor de verdad falso (False) son:

  • Constantes definidas como falsas: None y False

  • El valor cero de cualquier tipo numérico, es decir: 0 (para los int), 0.0 (para los float), 0j (para los complex), Decimal(0), Fraction(0, 1)

  • Secuencias y colecciones vacías: '', str(), (), tuple(), [], list(), {}, dict(), set(), range(0)

El resto de los objetos no recogidos en estos tres grupos son considerados verdaderos (True), es decir, el propio objeto True, los valores numéricos distintos de cero, las secuencias y colecciones no vacías.

El valor de verdad de los objetos puede ser fácilmente determinado empleando la función integrada bool() que retorna un valor booleano, True o False según sea el valor de verdad del objeto que le pasemos como argumento.

>>> bool(None)

False

>>> bool(False)

False

>>> bool('')

False

>>> bool(str())

False

>>> bool([])

False

>>> bool({})

False

>>> bool(0.0)

False

La clase bool que se define como class bool([x]):, es subclase de int y tiene la particularidad de que no puede ser subclaseada. Sus únicas instancias válidas son False y True, quienes en realidad tienen valores de 0 y 1 respectivamente.

>>> int(False)

0

>>> int(True)

1

>>> True + True

2

>>> isinstance(True, bool)

True

>>> isinstance(True, int)

True

Es por esto que algunos dicen que Python no tiene un tipo Booleano realmente y de alguna manera es cierto, pero, el hecho de que la clase bool sea una representación personalizada de los valores 0 y 1 influye tremendamente en la legibilidad del código que escribimos. Por ejemplo, ahora podemos escribir ciclos infinitos de la forma while True: en lugar de la variante menos legible while 1:

Los Operadores Booleanos: and, or, not

Los operadores booleanos de Python se corresponden con las palabras inglesas and, or y not, lo cual viene a reforzar una de las ideas que fundamentan la filosofía del lenguaje: la legibilidad del código.

Un resumen de estos operares, su significado y su precedencia se muestra en la tabla siguiente:

Operación

Resultado

Observaciones

x or y

Si x es falsa, entonces y, si no x

El operador or es un operador de corto circuito, es decir, y solo se evalúa si x es falsa

x and y

Si x es falsa, entonces x, si no y

El operador and también es un operador de corto circuito, pero en este caso, y solo se evalúa si x es verdadera

not x

Si x es falsa, entonces True, si no False

El operador not tiene la menor prioridad o precedencia entre los operadores booleanos, de este modo not a == b es interpretado como not (a == b) y a == not b es un error de sintaxis

Es muy importante hacer notar que no es lo mismo el resultado de una operación donde intervengan los operadores booleanos, que el valor de verdad de la operación, veamos:

>>> x = 0 # x es False

>>> y = 10 # y es True

>>> x or y # El resultado es y

10

>>> x and y # El resultado es x

0

>>> not x # El resultado es True

True

>>> not y # El resultado es False

False

>>> bool(x or y) # El valor de verdad es True

True

>>> bool(x and y) # El valor de verdad es False

False

Como un Tip que se deriva de este comportamiento, podemos utilizar el operador or para evitar el conocido problema de los tipos de datos mutables como parámetros opcionales o por defecto en las funciones, por ejemplo:

>>> def do_something(lst=None):

... lst = lst or []

... # Do something with lst

De esta forma, si lst es None, le asignamos a lst una lista vacía sin necesidad de implementar el tradicional bloque condicional de la forma:

>>> def do_something(lst=None):

if lst is None:

... lst = []

... # Do something with lst

Operadores de Comparación

Existen ocho operaciones de comparación en Python. Todas ellas tienen la misma prioridad o precedencia y se resumen en la tabla siguiente:

Operación

Significado

<

Estrictamente menor que

<=

Menor o igual que

>

Estrictamente mayor que

>=

Mayor o igual que

==

Igual que

!=

Distinto de

is

Identidad de objeto

is not

Identidad de objeto negada

Las operaciones de comparación pueden ser encadenadas arbitrariamente, por ejemplo: x< y <= z es
equivalente a x < y and y <= z, excepto que y es evaluada solo una vez.

¿Cómo Funciona la Comparación en Python?

Las operaciones de comparación siguen las reglas siguientes:

  • Objetos de tipos diferentes nunca resultan iguales, excepto en los tipos numéricos

>>> d = []

>>> d = {}

>>> l = []

>>> d == l

False

>>> f = 1.0

>>> i = 1

>>> f == i

True

  • Los operadores <, <=, >, >= levantan una excepción TypeError cuando comparan números complejos con otros tipos numéricos

>>> c = 0j

>>> c

0j

>>> f = 0.0

>>> c == f

True

>>> c > f

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

TypeError: '>' not supported between instances of 'complex' and 'float'

  • Los objetos de tipo función resultan desiguales

>>> map == filter

False

  • Objetos no idénticos de una misma clase, resultan desiguales; a menos que la clase implemente un método __eq__()

  • El comportamiento de los operadores is e is not no puede ser modificado. Estos retornan True solo cuando se comparan objetos con la misma identidad o que ocupan la misma posición de memoria

Los Tipos Integrados (built-in)

  • Los números se comparan por magnitud relativa, después de ser convertidos al tipo de mayor jerarquía si es necesario, por ejemplo, en una comparación entre int y float, el int debe ser convertido a float para poder compararlos

  • Las cadenas de caracteres se comparan lexicográficamente (según el set de caracteres y el valor de retorno de la función ord()), y carácter por carácter hasta el final o hasta la primera diferencia

  • Las listas y las tuplas se comparan recursivamente elemento por elemento, de izquierda a derecha y hasta el final o hasta la primera diferencia

  • Los conjuntos son iguales si, y solo si, contienen los mismos elementos

  • Los diccionarios resultan iguales si sus pares clave-valor ordenados, son iguales.

La Diferencia entre == e is

Existen dos operadores de igualdad en Python, pero con comportamientos distintos:

  • El operador == comprueba la equivalencia de los valores asignados a los operandos

  • El operador is comprueba la identidad de los objetos, es decir, comprueba si los dos operandos son realmente el mismo objeto, es decir, si habitan en la misma dirección de memoria.

>>> L1 = [1, ('a', 3)]

>>> L2 = [1, ('a', 3)]

>>> L1 == L2 # El contenido de las listas es el mismo

True

>>> L1 is L2 # Las dos listas habitan en posiciones de memoria distintas

False

Ahora, veamos otra particularidad del lenguaje:

>>> n1 = 234

>>> n2 = 234

>>> n1 == n2

True

>>> n1 is n2

True

>>> # == e is devuelven True

>>> n3 = 580

>>> n4 = 580

>>> n3 == n4

True

>>> n3 is n4

False

>>> # == devuelve True e is, False

Este comportamiento, que podría parecer un error del lenguaje, es lo que se denomina interning, y no es más que una estrategia para optimizar el empleo de la memoria. El intervalo de los números donde funciona el interning es de -5 a 256, pero, ¿de donde sale este intervalo? Al parecer los desarrolladores (Core Developers) del lenguaje han definido este intervalo atendiendo a la frecuencia de empleo de estos números, por tanto, no hay garantía de que este intervalo se mantendrá fijo en el tiempo. El fragmento de código que sigue, nos permite determinar cuál es el intervalo vigente.

File: interning.py

interning = []

for number1, number2 in zip(range(-100, 1000), range(-100, 1000)):

    if number1 is number2:

        interning.append(number1)

if interning:

    print('Interning interval is: {0} to {1}'.format(interning[0],

          interning[-1]))

De forma general, la recomendación es: si vamos a comparar con objetos singleton, como es el caso de None, emplearemos is, por otro lado, si vamos a comparar otro tipo de objetos (Ej. números enteros), es recomendable emplear == que a pesar de ser un poco más lento que is en el intervalo de números antes mencionado, es mucho más legible y por demás, evita que se rompa nuestro código si en algún momento del desarrollo del lenguaje, este intervalo cambia.

Igualdades Entre Secuencias y Colecciones

Una de las operaciones más empleadas en la programación es, sin lugar a dudas, la comparación de igualdad. Este tipo de comparación en Python, siempre inspecciona todas las partes de un objeto complejo hasta llegar a un resultado. De hecho, cuando existen objetos anidados, Python automáticamente inspecciona cada uno de ellos de derecha a izquierda y con tanta profundidad como sea necesario. La primera diferencia que se encuentre a lo largo del camino, determina el resultado de la comparación. Este tipo de comparación se denomina comparación recursiva, pues a los objetos anidados se le aplica la misma comparación que se le solicita al objeto de nivel superior.

Veamos cómo Python maneja estas operaciones en el casos de las secuencias y las colecciones.

>>> l1 = [1,2,3]

>>> l2 = [3,2,1]

>>> l1 == l2

False

>>> l3 = [1,2,3]

>>> l1 == l3

True

>>> t1 = (1,2,3)

>>> t2 = (3,2,1)

>>> t1 == t2

False

>>> t3 = (1,2,3)

>>> t1 == t3

True

>>> t4 = (1,2,3,4)

>>> t1 == t4

False

>>> d1 = {'a': 1, 'b': 2}

>>> d2 = {'b': 2, 'a': 1}

>>> d1 == d2

True

>>> s1 = set((1,2,3))

>>> s2 = set((3,2,1))

>>> s1 == s2

True

>>> s3 = set((1, 2, 3, 4))

>>> s1 == s3

False

Las Sentencias if...

Como hemos visto, la regla general que emplea Python para establecer el valor de verdad de un objeto es:

  • Objeto vacío (o cero en los números) es falso, de otro modo es verdadero

Con esto en mente, es fácil darse cuenta por qué las sentencias if se escriben if x: (que asumiendo que x sea una cadena de caracteres, es equivalente a if x != ‘’:). Esta práctica es parte de las recomendaciones que debemos observar para que nuestro código sea pythonico y en Python, es mucho más eficiente y rápido el código escrito de esta forma.

Y bien, esto es todo por ahora, si este artículo te resultó interesante y/o útil, compártelo para que otros también puedan acceder a él. Déjanos tus comentarios y podremos mejorar nuestros contenidos.

Gracias de antemano,

lpozo