En el nivel del hardware, los transistores solo pueden estar encendidos o apagados: dos estados que tradicionalmente representamos con 1 y 0.
Estos son los binary digits, abreviados como bits.
Conocer los bits y el sistema binary es especialmente importante para los programadores de sistemas que trabajan en lenguajes de bajo nivel.
Sin embargo, durante la mayor parte de la historia de la informática, la prioridad de la programación ha sido encontrar formas cada vez más sofisticadas de abstraer esta realidad binaria.
En Python (y en muchos otros lenguajes de programación de alto nivel), trabajamos con int, float, string y otros tipos definidos, incluidos los formatos de audio y vídeo.
Dejamos que el funcionamiento interno de Python se encargue de traducirlo todo a bits (al final).
No obstante, usar los operadores a nivel de bits y las operaciones a nivel de bits puede tener a veces ventajas significativas en velocidad y eficiencia de memoria, incluso en un lenguaje de alto nivel como Python.
Como era de esperar, Python interactúa con el usuario usando números decimales, pero quien programa puede anular este comportamiento predeterminado.
De hecho, Python acepta sin problema un int en formato binary, hexadecimal u octal, y realiza sin quejarse operaciones matemáticas entre ellos.
Para más detalles, puedes consultar el concepto binary-octal-hexadecimal.
Los números binarios se introducen con el prefijo 0b, igual que 0x se puede usar para el hexadecimal (los números hex son una forma concisa de representar grupos de 4 bits), y oct se puede usar para los números octales.
Hay varias formas de convertir números enteros en cadenas binarias, que se diferencian en si incluyen el prefijo 0b y en si admiten relleno a la izquierda con ceros:
# Binary entry.
>>> 0b10111
23
# Converting an int display to binary string, with prefix.
>>> bin(23)
'0b10111'
>>> number = 23
# Binary without prefix, padded to 8 digits.
>>> format(number, '08b')
'00010111'
# Same format, but using an f-string.
>>> f"{number} in decimal is {number:08b} in binary and {number:x} in hex"
'23 in decimal is 00010111 in binary and 17 in hex'
Bitwise LogicEn el concepto bools, hablamos de los operadores lógicos and, or y not usados con valores Boolean (True y False).
Las mismas reglas lógicas se aplican cuando trabajamos con bits.
Sin embargo, los equivalentes a nivel de bits de los operadores lógicos & (and), | (or), ~ (not) y ^ (XOR) se aplican a cada bit de una representación binaria, tratando 1 como True («encendido») y 0 como False («apagado»).
Un ejemplo con el & a nivel de bits puede dejarlo más claro:
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x & y, '08b')
'00100010'
Solo las posiciones con un 1 en ambos números de entrada se ponen a 1 en la salida.
El & a nivel de bits se usa habitualmente para aislar bits individuales en un conjunto compacto de valores True/False, como los ajustes configurables por el usuario en una aplicación.
Esto permite que el valor de bits individuales controle la lógica del programa:
>>> number = 0b0110
>>> number & 0b0001 > 0
False
>>> number & 0b0010 > 0
True
En el | a nivel de bits (or), se pone un 1 en la salida si hay un 1 en cualquiera de las dos entradas:
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x | y, '08b')
'01101110'
Con el operador ^ para el o exclusivo (xor) a nivel de bits, se pone un 1 si aparece en cualquiera de las dos entradas pero no en ambas.
Quizá este símbolo te resulte familiar del concepto sets, donde se usa para la diferencia simétrica de set, que es lo mismo que el xor aplicado a conjuntos.
Si el xor ^ te parece extraño, ten en cuenta que es, con diferencia, la operación más común en criptografía.
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x ^ y, '08b')
'01001100'
Por último, está el operador ~ (el carácter virgulilla), que es un not a nivel de bits que toma una sola entrada e invierte todos los bits, ¡que quizá no sea el resultado que esperabas!
Cada 1 de la representación cambia a 0, y viceversa.
Consulta la sección siguiente para más detalles.
En la representación decimal, distinguimos los números positivos de los negativos usando un signo + o - a la izquierda de los dígitos.
Usar estos símbolos a nivel binario resultó poco eficiente para la computación digital y planteó el problema de que +0 no es lo mismo que -0.
En lugar de usar - y +, todos los ordenadores modernos usan una representación en two's complement para los números negativos, hasta el nivel del propio chip de silicio.
Esto significa que todos los bits se invierten y un número se interpreta como negativo si el bit más a la izquierda (también llamado «bit más significativo», o MSB) es un 1.
Los números positivos tienen un MSB de 0.
Esta representación tiene la ventaja de que solo hay una versión del cero, de modo que quien programa no tiene que gestionar -0 y +0.
Esta forma de representar los números negativos y positivos añade una complicación para Python: en el núcleo del lenguaje no hay conceptos de enteros finitos como int32 o int64.
En el Python «moderno», los int son de tamaño ilimitado (limitados solo por la capacidad del hardware), y un número negativo o invertido a nivel de bits tiene un número (teóricamente) infinito de 1 a la izquierda, igual que un número positivo tiene infinitos 0.
Esto hace que sea difícil dar un ejemplo útil del bitwise not:
>>> x = 0b01100110
>>> format(x, '08b')
'01100110'
# This is a negative binary (not twos-complement display).
>>> format(~x, '08b')
'-1100111'
# Decimal representation.
>>> x
102
# Using the Bitwise not, with an unintuitive result.
>>> ~x
-103
Esto no es el 0b10011001 que veríamos en lenguajes con enteros de tamaño fijo.
El operador ~ solo funciona como se espera con tipos de byte o entero sin signo, o con tipos de entero de tamaño fijo.
Estos tipos numéricos son compatibles con paquetes de terceros como NumPy, pandas y sympy, pero no con el núcleo de Python.
En la práctica, los programadores de Python usan con bastante frecuencia &, |, ^ y los operadores de desplazamiento que se describen más abajo solo con números positivos.
Las operaciones a nivel de bits con números negativos son mucho menos comunes.
Una técnica consiste en sumar 2**32 (or 1 << 32) a un valor negativo para convertir un int en no firmado, pero se vuelve difícil de gestionar.
Otra estrategia es trabajar con el módulo ctypes y usar tipos de entero al estilo C, pero resulta igual de engorroso.
Shift operatorsEl operador de desplazamiento a la izquierda x << y mueve todos los bits de x y posiciones a la izquierda, rellenando los nuevos huecos con ceros.
Ten en cuenta que es aritméticamente idéntico a multiplicar un número por (2**y).
El operador de desplazamiento a la derecha x >> y hace lo contrario.
Es aritméticamente idéntico a la división entera x // (2**y).
Ten en cuenta la sección anterior sobre los números negativos y sus trampas al desplazarlos en Python.
>>> x = 8
>>> format(x, '08b')
'00001000'
# A left bit shift.
>>> x << 2
32
>>> format(x << 2, '08b')
'00100000'
# A right bit shift.
>>> format(x >> 2, '08b')
'00000010'