Percursos
/
Python
Python
/
Programa
/
Operadores bit a bit
Op

Operadores bit a bit em Python

{one: "1 exercício", many: "%{count} exercícios", other: "%{count} exercícios"}

Sobre Operadores bit a bit

Lá ao nível do hardware, os transístores só podem estar ligados ou desligados: dois estados que representamos tradicionalmente com 1 e 0. São estes os binary digits, abreviados como bits. Ter consciência de bits e de binary é particularmente importante para programadores de sistemas que trabalham com linguagens de baixo nível.

No entanto, durante a maior parte da história da computação, a prioridade da programação tem sido encontrar formas cada vez mais sofisticadas de abstrair esta realidade binária. Em Python (e em muitas outras linguagens de programação de alto nível), trabalhamos com int, float, string e outros tipos definidos, incluindo até formatos de áudio e vídeo. Deixamos que os mecanismos internos do Python tratem de (eventualmente) traduzir tudo para bits.

Ainda assim, usar operadores bit a bit e operações bit a bit pode, por vezes, trazer vantagens significativas em velocidade e eficiência de memória, mesmo numa linguagem de alto nível como o Python.

Introduzir e mostrar números binários

Como seria de esperar, o Python comunica com o utilizador através de números decimais, mas o programador pode alterar esse comportamento predefinido. Na verdade, o Python aceita sem problemas um int em formato binary, hexadecimal ou octal, e executa alegremente operações matemáticas entre eles. Para mais detalhes, podes consultar o conceito binary-octal-hexadecimal.

Os números binários introduzem-se com o prefixo 0b, tal como 0x pode ser usado para hexadecimal (os números hexadecimais são uma forma concisa de representar grupos de 4 bits) e oct pode ser usado para números octais.

Há várias formas de converter inteiros em strings binárias, que variam consoante incluam ou não o prefixo 0b e consoante suportem ou não preenchimento à esquerda com zeros:

# 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 Logic

No conceito bools, falámos dos operadores lógicos and, or e not usados com valores Boolean (True e False). As mesmas regras de lógica aplicam-se quando trabalhamos com bits.

Contudo, os equivalentes bit a bit dos operadores lógicos & (and), | (or), ~ (not) e ^ (XOR) são aplicados a cada bit de uma representação binária, tratando 1 como True ("ligado") e 0 como False ("desligado"). Um exemplo com o & bit a bit pode tornar isto mais claro:

>>> x = 0b01100110
>>> y = 0b00101010

>>> format(x & y, '08b')
'00100010'

Só as posições com um 1 em ambos os números de entrada ficam a 1 na saída.

O & bit a bit é frequentemente usado para isolar bits individuais num conjunto compactado de valores True/False, como as configurações personalizáveis de uma aplicação. Isto permite que o valor de bits individuais controle a lógica do programa:

>>> number = 0b0110
>>> number & 0b0001 > 0
False

>>> number & 0b0010 > 0
True

Num | bit a bit (or), fica um 1 na saída se houver um 1 em qualquer uma das entradas:

>>> x = 0b01100110
>>> y = 0b00101010

>>> format(x | y, '08b')
'01101110'

Com o operador ^ para bit a bit exclusivo or (xor), fica um 1 se aparecer em qualquer uma das entradas, mas não em ambas. Este símbolo pode parecer-te familiar do conceito sets, onde é usado para a diferença simétrica de set, que é o mesmo que xor aplicado a conjuntos. Se o xor ^ te parecer estranho, tem em conta que é, de longe, a operação mais comum na criptografia.

>>> x = 0b01100110
>>> y = 0b00101010

>>> format(x ^ y, '08b')
'01001100'

Por fim, há o operador ~ (o caráter til), que é um not bit a bit que recebe uma única entrada e inverte todos os bits, o que pode não ser o resultado que estavas à espera! Cada 1 na representação passa a 0, e vice-versa. Consulta a secção abaixo para mais detalhes.

Números negativos e representação binária

Na representação decimal, distinguimos os números positivos dos negativos através de um sinal + ou - à esquerda dos algarismos. Usar estes símbolos ao nível binário revelou-se ineficiente para a computação digital e levantou o problema de +0 não ser o mesmo que -0.

Em vez de usar - e +, todos os computadores modernos usam uma representação em two's complement para os números negativos, até ao nível do chip de silício. Isto significa que todos os bits são invertidos e que um número é interpretado como negativo se o bit mais à esquerda (também chamado de "bit mais significativo", ou MSB) for um 1. Os números positivos têm um MSB de 0. Esta representação tem a vantagem de só ter uma versão do zero, de modo que o programador não tem de gerir -0 e +0.

Esta forma de representar números negativos e positivos traz uma complicação para o Python: não existem conceitos de inteiros de tamanho finito, como int32 ou int64, internamente na linguagem base. No Python 'moderno', os ints têm tamanho ilimitado (limitado apenas pela capacidade do hardware), e um número negativo ou com os bits invertidos tem um número (teoricamente) infinito de 1s à esquerda, tal como um número positivo tem um número ilimitado de 0s.

Isto torna difícil dar um exemplo útil de 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

Isto não é o 0b10011001 que veríamos em linguagens com inteiros de tamanho fixo.

O operador ~ só funciona como esperado com tipos de bytes ou inteiros unsigned, ou com tipos inteiros de tamanho fixo. Estes tipos numéricos são suportados em pacotes de terceiros como NumPy, pandas e sympy, mas não no Python base.

Na prática, os programadores de Python usam muitas vezes &, |, ^ e os operadores de deslocamento descritos abaixo apenas com números positivos. As operações bit a bit com números negativos são muito menos comuns. Uma técnica é somar 2**32 (or 1 << 32) a um valor negativo para tornar um int unsigned, mas isto torna-se difícil de gerir. Outra estratégia é trabalhar com o módulo ctypes e usar tipos inteiros ao estilo do C, mas isto é igualmente complicado.

Shift operators

O operador de deslocamento à esquerda x << y move todos os bits de x em y posições para a esquerda, preenchendo os novos espaços com zeros. Repara que isto é aritmeticamente idêntico a multiplicar um número por (2**y).

O operador de deslocamento à direita x >> y faz o contrário. Isto é aritmeticamente idêntico à divisão inteira x // (2**y).

Tem em conta a secção anterior sobre números negativos e as suas armadilhas ao deslocá-los em 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'
Editar via GitHub A ligação abre numa nova janela ou separador