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 binary es especialmente importante para quienes programan sistemas en lenguajes de bajo nivel.
Sin embargo, durante la mayor parte de la historia de la computación, 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, incluso con formatos de audio y video.
Dejamos que los mecanismos internos de Python se encarguen de traducir todo a bits (tarde o temprano).
Aun así, usar los operadores a nivel de bits y las operaciones a nivel de bits a veces puede tener ventajas importantes 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 mediante números decimales, pero quien programa puede cambiar ese comportamiento predeterminado.
De hecho, Python acepta sin problemas un int en formato binary, hexadecimal u octal, y realiza operaciones matemáticas entre ellos sin quejarse.
Para más detalles, puedes repasar el concepto binary-octal-hexadecimal.
Los números binarios se escriben con el prefijo 0b, del mismo modo que 0x puede usarse para hexadecimal (los números hex son una forma concisa de representar grupos de 4 bits), y oct puede usarse para números octales.
Hay varias maneras de convertir enteros a cadenas binarias, que varían 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 que se usan 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 aclararlo:
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x & y, '08b')
'00100010'
Solo las posiciones que tienen un 1 en ambos números de entrada se ponen en 1 en la salida.
El & a nivel de bits se usa comúnmente para aislar bits individuales en un conjunto compacto de valores True/False, como la configuración que el usuario ajusta 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
Con un | a nivel de bits (or), se pone un 1 en la salida si hay un 1 en cualquiera de las 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 entradas, pero no en ambas.
Este símbolo puede resultarte familiar del concepto sets, donde se usa para la diferencia simétrica de set, que es lo mismo que xor aplicado a conjuntos.
Si el xor ^ te parece extraño, ten en cuenta que es, con mucho, 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, toma una sola entrada e invierte todos los bits, ¡y puede que el resultado no sea el que esperabas!
Cada 1 de la representación pasa a 0, y viceversa.
Consulta la sección de abajo 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 +, todas las computadoras modernas usan una representación de two's complement para los números negativos, hasta el nivel del chip de silicio.
Esto significa que todos los bits se invierten y un número se interpreta como negativo si el bit situado 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 contar con una sola versión del cero, de modo que quien programa no tiene que lidiar con -0 y +0.
Esta forma de representar números negativos y positivos añade una complicación para Python: no existen conceptos de enteros de tamaño fijo como int32 o int64 dentro del lenguaje base.
En el Python «moderno», los int tienen un tamaño ilimitado (limitado solo por la capacidad del hardware), y un número negativo o invertido bit a bit tiene, en teoría, una cantidad infinita de 1 a la izquierda, igual que un número positivo tiene una cantidad ilimitada de 0.
Esto dificulta dar un ejemplo útil del bitwise not a nivel de bits:
>>> 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
Este no es el 0b10011001 que veríamos en lenguajes con enteros de tamaño fijo.
El operador ~ solo funciona como se espera con tipos byte o entero sin signo, o con tipos enteros de tamaño fijo.
Estos tipos numéricos son compatibles con paquetes de terceros como NumPy, pandas y sympy, pero no en el Python base.
En la práctica, quienes programan en Python suelen usar &, |, ^ y los operadores de desplazamiento que se describen 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 uno sin signo, pero resulta difícil de manejar.
Otra estrategia es trabajar con el módulo ctypes y usar tipos enteros al estilo de C, pero es igual de engorroso.
Shift operatorsEl operador de desplazamiento a la izquierda x << y mueve todos los bits de x y posiciones hacia la izquierda, rellenando los nuevos huecos con ceros.
Ten en cuenta que esto 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.
Esto es aritméticamente idéntico a la división entera x // (2**y).
Ten presente 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'