Ir para o conteúdo principal

Lógica em nível de bits: operadores bitwise e máscaras em Python

Autor
Francisco Bustamante
Um químico trabalhando com Ciência de Dados e Programação em Python.
Tabela de conteúdos
Do Zero ao Float - Este artigo faz parte de uma série de artigos.
Parte 4: Esse Artigo

Nos artigos anteriores, exploramos a aritmética dos computadores. Aprendemos como eles somam, subtraem usando Complemento de Dois e como lidam com os limites físicos de memória (Overflow). No artigo anterior, usamos brevemente o operador & para revelar a representação interna dos números negativos em Python. Se você ficou curioso sobre o que aquele & realmente fez — e o que mais é possível fazer com ele —, chegou a hora de descobrir.

Nesses casos aritméticos, os bits eram tratados em conjunto para representar um valor numérico (peso posicional). Mas, às vezes, não queremos calcular o valor de um número. Queremos manipular a estrutura dele. Queremos acender uma “chave” específica, desligar um sensor, criptografar uma mensagem ou verificar permissões de um usuário.

É hora de deixar a aritmética de um pouco de lado e entrar no mundo da Lógica Bitwise (Bit a Bit). Aqui, o processador não enxerga o número “5” ou “10”, ele enxerga uma sequência de interruptores individuais que podem ser ligados (1) ou desligados (0).

A Conexão com a Lógica Proposicional
#

Se você acompanha o blog, já deve ter lido nosso artigo sobre Lógica para Programadores. Lá, vimos como criar tabelas-verdade para decidir se uma proposição complexa é Verdadeira (V) ou Falsa (F).

A lógica bitwise é a aplicação direta desse conceito, mas em escala industrial:

  • Verdadeiro (V) vira 1.
  • Falso (F) vira 0.

Em vez de comparar apenas duas proposições (“A porta está aberta” E “A luz está acesa”), o operador bitwise compara 8, 16, 32 ou 64 pares de proposições de uma só vez, em paralelo.

O Grande Alerta: and não é &
#

Antes de vermos os operadores, precisamos resolver uma confusão clássica em Python, linguagem mais adotada aqui no blog. Diferente de linguagens como C ou Java, Python é muito verbal, usando palavras como and, or e not. Porém, ele também possui os símbolos &, | e ~.

Eles não são a mesma coisa.

No artigo sobre Curto-Circuito em Python, explicamos que os operadores lógicos (and, or) são “preguiçosos”. Eles param de avaliar assim que descobrem a resposta.

Já os operadores Bitwise (&, |) são “trabalhadores incansáveis”. Eles avaliam todos os bits, sem exceção, e geram um novo número como resultado.

Tabela de Diferenças
#

Característica Operadores Lógicos Operadores Bitwise
Alvo Expressões Booleanas (True/False) ou objetos Números Inteiros (Bits individuais)
Avaliação Curto-Circuito (Lazy evaluation) Completa (Eager evaluation)
Resultado Retorna um dos operandos (ex: True) Retorna um novo número inteiro
Exemplo if idade > 18 and tem_cnh: masked = flags & 0xFF
Cuidado no Python

Jamais use & no lugar de and dentro de um if, a menos que você saiba exatamente o que está fazendo.

  • 5 and 6 retorna 6 (pois 5 é verdadeiro, ele segue e retorna o último).
  • 5 & 6 retorna 4 (pois faz a interseção dos bits de 101 e 110).

São comportamentos completamente diferentes que podem gerar bugs silenciosos.

Agora que separamos a lógica de fluxo da lógica de bits, vamos conhecer as quatro ferramentas fundamentais: AND, OR, XOR e NOT.

A Matemática dos Bits: AND e OR
#

Para entender bitwise, precisamos parar de olhar para o número decimal (ex: 12) e começar a olhar para a sua representação binária. As operações acontecem verticalmente, alinhando os bits correspondentes de cada número.

AND (&) - A Interseção
#

O operador E Bit a Bit (Bitwise AND) segue uma regra estrita: o bit de resultado será 1 se, e somente se, os bits de ambos os operandos forem 1.

Pense no AND como um Filtro ou uma Interseção.

  • 1 & 1 = 1
  • 1 & 0 = 0
  • 0 & 1 = 0
  • 0 & 0 = 0

Observe que o Zero é dominante: qualquer coisa combinada com zero vira zero. Essa propriedade torna o AND a ferramenta perfeita para desligar bits ou verificar valores (técnica conhecida como Masking).

Exemplo Prático: 12 & 10

Vamos operar 12 & 10.

  • 12 em binário é 1100.
  • 10 em binário é 1010.

Alinhamos os bits e aplicamos a lógica coluna por coluna:

$$ \begin{array}{r c c c c l} & 1 & 1 & 0 & 0 & (12) \\ \& & 1 & 0 & 1 & 0 & (10) \\ \hline & 1 & 0 & 0 & 0 & (8) \end{array} $$

Análise:

  • Bit 3 (esquerda): 1 & 1 resulta em 1.
  • Bit 2: 1 & 0 resulta em 0.
  • Bit 1: 0 & 1 resulta em 0.
  • Bit 0: 0 & 0 resulta em 0.

Resultado: 1000 (que é 8 em decimal).

No Python:

>>> 12 & 10
8
>>> bin(12 & 10)
'0b1000'

OR (|) - A União
#

O operador OU Bit a Bit (Bitwise OR) é mais permissivo: o bit de resultado será 1 se pelo menos um dos bits dos operandos for 1.

Pense no OR como uma Soma Lógica ou União.

  • 1 | 1 = 1
  • 1 | 0 = 1
  • 0 | 1 = 1
  • 0 | 0 = 0

Aqui, o Um é dominante: qualquer coisa combinada com 1 vira 1. Essa propriedade torna o OR a ferramenta ideal para ligar bits (forçar um valor a 1) sem alterar os outros.

Exemplo Prático: 12 | 10

Vamos operar 12 | 10.

  • 12 em binário é 1100.
  • 10 em binário é 1010.

Alinhamos os bits e aplicamos a lógica coluna por coluna:

$$ \begin{array}{r c c c c l} & 1 & 1 & 0 & 0 & (12) \\ \| & 1 & 0 & 1 & 0 & (10) \\ \hline & 1 & 1 & 1 & 0 & (14) \end{array} $$

Análise:

  • Onde havia pelo menos um bit 1 nas entradas, a saída foi 1.
  • Apenas a última coluna (0 | 0) resultou em zero.

Resultado: 1110 (que é 14 em decimal).

No Python:

>>> 12 | 10
14
>>> bin(12 | 10)
'0b1110'

Resumo da Lógica
#

Para não esquecer:

  • AND (&): Usado para Cortar/Limpar. Se você quer garantir que certos bits sejam zero, use AND com 0.
  • OR (|): Usado para Adicionar/Ligar. Se você quer garantir que certos bits sejam um, use OR com 1.

Na próxima seção, veremos outros operadores: o XOR (o localizador de diferenças) e o NOT (a inversão total).

Os Operadores XOR e NOT
#

Enquanto AND e OR são intuitivos (parecem interseção e união de conjuntos), os próximos dois operadores possuem comportamentos únicos que são a base da criptografia e da aritmética de inteiros.

XOR (^) - O Detetive de Diferenças
#

O OU Exclusivo (Exclusive OR, ou XOR) é um operador comum em criptografia e algoritmos de hash. A regra é: o bit de resultado será 1 se os bits dos operandos forem diferentes.

  • 1 ^ 1 = 0 (Iguais \(\to\) 0)
  • 0 ^ 0 = 0 (Iguais \(\to\) 0)
  • 1 ^ 0 = 1 (Diferentes \(\to\) 1)
  • 0 ^ 1 = 1 (Diferentes \(\to\) 1)

Pense no XOR como um Alternador (Toggle).

  • Se você faz XOR com 0, o bit original se mantém (1^0=1, 0^0=0).
  • Se você faz XOR com 1, o bit original se inverte (1^1=0, 0^1=1).

Sua utilidade em criptografia de textos vem do fato de que ele é “balanceado”, ou seja, há uma igual quantidade de 0s e 1s no resultado quando os bits de entrada são aleatórios. Isto dificulta a análise estatística de padrões.

Exemplo Prático: 12 ^ 10

Vamos operar 12 ^ 10.

$$ \begin{array}{r c c c c l} & 1 & 1 & 0 & 0 & (12) \\ \text{\textasciicircum} & 1 & 0 & 1 & 0 & (10) \\ \hline & 0 & 1 & 1 & 0 & (6) \end{array} $$

Análise:

  • Bit 3: 1 e 1 são iguais \(\to\) 0.
  • Bit 2: 1 e 0 são diferentes \(\to\) 1.
  • Bit 1: 0 e 1 são diferentes \(\to\) 1.
  • Bit 0: 0 e 0 são iguais \(\to\) 0.

Resultado: 0110 (que é 6 em decimal).

Curiosidade Criptográfica: O XOR é reversível. Se você calcular (A ^ B) ^ B, o resultado volta a ser A.

>>> chave = 123
>>> msg = 456
>>> cripto = msg ^ chave
>>> print(cripto)
435
>>> print(cripto ^ chave)  # Desfazendo a operação
456

NOT (~) - A Inversão Total
#

O operador NÃO Bit a Bit (Bitwise NOT) é o único que opera sobre um único número (operador unário). Ele simplesmente inverte todos os bits: o que é 0 vira 1, e o que é 1 vira 0.

Visualmente, parece simples. Mas se você testar no Python, o resultado pode te confundir:

>>> ~0
-1
>>> ~10
-11

Por que o resultado é negativo?

Aqui entra o conhecimento do nosso artigo anterior sobre Complemento de Dois.

  1. O Python trata inteiros como se tivessem “infinitos bits” à esquerda. O número 10 positivo é ...00001010.
  2. Quando aplicamos o NOT (~), invertemos todos os bits, inclusive os infinitos zeros à esquerda. O número vira ...11110101.
  3. Uma sequência infinita de 1s à esquerda indica um número negativo.

Matematicamente, em sistemas de Complemento de Dois, o operador ~x é equivalente ao Complemento de 1. A relação com o valor decimal segue a fórmula:

Entendendo a Fórmula (~10)

Queremos calcular ~10.

  1. Pela fórmula matemática: -(10) - 1 = -11.
  2. Pela visão de bits (em 8 bits para simplificar):
  • 10 é 0000 1010.

  • Invertendo tudo (~): 1111 0101.

  • O que é 1111 0101? Como o primeiro bit é 1, é negativo.

  • Para descobrir o valor, aplicamos a regra inversa (Inverte e soma 1):

  • Inverte: 0000 1010 (10)

  • Soma 1: 0000 1011 (11)

  • Logo, o valor original era -11.

No próximo tópico, veremos como mover esses bits de um lado para o outro com os operadores de Shift (Deslocamento).

Shift: A Aritmética do Deslocamento
#

Imagine que você tem o número decimal 23. Se você adicionar um zero à direita (230), você efetivamente multiplicou o número por 10 (a base do sistema). Se remover o último dígito (o 3), você dividiu por 10 (divisão inteira, restando 2).

No sistema binário, a lógica é idêntica, mas a base é 2. Mover os bits para a esquerda ou direita é a forma mais eficiente computacionalmente de multiplicar ou dividir números.

Left Shift (<<) - O Multiplicador
#

O operador Deslocamento à Esquerda (x << n) move todos os bits de x para a esquerda por n posições.

  • Os bits que “caem” pela esquerda são descartados (em tipos de tamanho fixo) ou o número cresce (em Python).
  • Zeros são inseridos nas posições vagas à direita.

Regra Matemática: Deslocar n bits para a esquerda equivale a multiplicar o número por \(2^n\).

$$ x \ll n \iff x \times 2^n $$
Exemplo: 3 « 2

O número 3 em binário (8 bits) é 0000 0011. Vamos deslocar 2 casas para a esquerda.

  1. Empurre tudo para a esquerda.
  2. Preencha os buracos da direita com 0. $$ \begin{array}{r c l} & 0000\,0011 & (3) \\ \downarrow & \text{Shift Left 2} & \\ & 0000\,11\mathbf{00} & (12) \end{array} $$ Verificação: \(3 \times 2^2 = 3 \times 4 = 12\).

Right Shift (>>) - O Divisor
#

O operador Deslocamento à Direita (x >> n) move os bits para a direita.

  • Os bits da direita “caem” no abismo e são perdidos.
  • O que entra pela esquerda? Aqui está o grande detalhe técnico.

Em linguagens de baixo nível, existem dois tipos de shift à direita:

  1. Logical Shift: Preenche sempre com 0. Usado para números sem sinal (Unsigned).
  2. Arithmetic Shift: Preenche com o valor do Bit de Sinal (o bit mais à esquerda). Se o número era negativo (começava com 1), ele preenche com 1s para manter o número negativo.

Como o Python faz? Como Python lida com números com sinal e precisão arbitrária, o operador >> se comporta como um Arithmetic Shift. Ele preserva o sinal matemático.

Regra Matemática: Deslocar n bits para a direita equivale a uma Divisão Inteira (Floor Division) por \(2^n\).

$$ x \gg n \iff \lfloor x / 2^n \rfloor $$
Exemplo Positivo vs. Negativo

Caso 1: Positivo (12 » 2) Binário: 0000 1100 Desloca direita, entra 0: 0000 0011 Resultado: 3. (Matemática: \(12 / 4 = 3\)).

Caso 2: Negativo (-12 » 2) Binário (Complemento de 2): ...1111 0100 Desloca direita. Como o bit de sinal é 1, entram 1s pela esquerda. Resultado: ...1111 1101

O resultado é -3. (Matemática: \(-12 / 4 = -3\)).

Isso garante que a divisão por potências de 2 funcione corretamente tanto para números positivos quanto negativos.

Na próxima seção, vamos juntar tudo o que aprendemos (AND, OR, NOT, SHIFT) para criar ferramentas práticas chamadas Máscaras de Bits (Bitmasks).

Máscaras de Bits (Bitmasks)
#

Agora que conhecemos os operadores, podemos combiná-los para realizar alterações precisas em números. A técnica de usar um número auxiliar para “selecionar” ou “modificar” bits específicos de outro número é chamada de Mascaramento (Masking).

Para os exemplos abaixo, imagine que queremos manipular o 3º bit (da direita para esquerda, índice 2). Para isso, criamos uma máscara onde apenas esse bit é 1:

MASK = 1 << 2  # Resultado: 0b00000100 (Decimal 4)

Consultar um Bit (Check)
#

Queremos saber: “O 3º bit está ligado?”. Usamos o AND (&). Como o AND com 0 zera tudo, se o resultado for diferente de zero, significa que o nosso bit alvo estava ligado.

flags = 0b10110111
is_on = (flags & MASK) > 0 
# O resultado será True se o 3º bit estiver ligado (que é o caso), False caso contrário.

Ligar um Bit (Set)
#

Queremos forçar o 3º bit a ser 1, sem mexer nos outros. Usamos o OR (|). Como o OR com 0 preserva o valor original, e o OR com 1 força virar 1, nossa máscara protege o resto e acende o alvo.

flags = flags | MASK 
# Agora o 3º bit é 1 com certeza. Os outros não mudaram.

Desligar um Bit (Clear)
#

Queremos forçar o 3º bit a ser 0. Esta é a operação mais complexa logicamente. Precisamos de um AND (para zerar), mas precisamos de uma máscara que tenha 0 no alvo e 1 em todo o resto (para preservar os vizinhos). Como geramos isso? Invertendo nossa máscara original com NOT (~).

A Lógica do Clear (Limpar Bit)
  1. Máscara Original (MASK): ...0000 0100
  2. Máscara Invertida (~MASK): ...1111 1011
  3. Operação (flags & ~MASK): ...10110011

O bit alvo foi zerado, os outros foram preservados pelos 1s da máscara.

flags = flags & (~MASK)

Alternar um Bit (Toggle)
#

Queremos inverter o estado atual: se estava ligado, desliga; se estava desligado, liga. Usamos o XOR (^), pois XOR com 1 sempre inverte o bit.

flags = flags ^ MASK

Exemplo Real: Sistema de Permissões
#

Vamos ver onde isso é usado na prática. Sistemas operacionais (como Linux) usam bits para controlar permissões de arquivos. Vamos simular isso em Python.

Imagine que temos 3 permissões possíveis, representadas por bits:

  • Leitura (READ): bit 2 (valor 4)
  • Escrita (WRITE): bit 1 (valor 2)
  • Execução (EXEC): bit 0 (valor 1)
# Definindo as constantes (MÁSCARAS)
READ  = 0b100  # 4
WRITE = 0b010  # 2
EXEC  = 0b001  # 1

# Cenário 1: Criando um usuário com permissão de Leitura e Execução
# Usamos OR (|) para combinar as permissões
permissao_usuario = READ | EXEC  
print(bin(permissao_usuario))  # Saída: 0b101 (Decimal 5)

# Cenário 2: O usuário pode escrever?
# Usamos AND (&) para verificar
if (permissao_usuario & WRITE):
    print("Acesso de escrita liberado")
else:
    print("Acesso de escrita negado")  # Vai cair aqui

# Cenário 3: Adicionando permissão de escrita dinamicamente
permissao_usuario = permissao_usuario | WRITE
print(bin(permissao_usuario))
# Agora o valor é 0b111 (7) - Permissão total (rwx)

# Cenário 4: Revogando permissão de execução
# Usamos AND com NOT
permissao_usuario = permissao_usuario & (~EXEC)
print(bin(permissao_usuario))
# Agora o valor é 0b110 (6) - Apenas Leitura e Escrita

Perceba como conseguimos armazenar 3 informações booleanas complexas em uma única variável inteira minúscula (ocupando apenas 3 bits de memória). É por isso que operações bitwise são vitais para performance e economia de recursos.

Conclusão
#

Entrar no mundo Bitwise é como olhar para a Matrix: você para de ver apenas os números superficiais e começa a enxergar a estrutura binária que sustenta tudo.

Embora Python seja uma linguagem de alto nível, entender &, |, ^ e << te dá superpoderes para lidar com protocolos de rede, processamento de imagens, criptografia e otimização de algoritmos.

E caso não use Python, esses conceitos são universais e se aplicam a praticamente todas as linguagens de programação.

Agora você sabe que:

  1. Bitwise não é Lógico: & não é and.
  2. Máscaras são a chave: Use AND para ler/limpar e OR para escrever.
  3. Bits são versáteis: Um único inteiro pode guardar dezenas de “interruptores” independentes.

Pronto para praticar? Tente criar um código que criptografa uma string usando XOR e depois descriptografa usando a mesma chave!

Com isso, encerramos o capítulo dos números inteiros nesta série. Ao longo dos últimos artigos, vimos como representar, converter, operar e manipular inteiros em diferentes bases — inclusive na estrutura de bits bruta. Agora é hora de dar o próximo salto: como o computador lida com números fracionários? Por que 0.1 + 0.2 não é exatamente 0.3 em Python? A resposta começa com algo que você já domina — conversão de bases —, mas aplicada agora à parte depois da vírgula.

Até a próxima!

Do Zero ao Float - Este artigo faz parte de uma série de artigos.
Parte 4: Esse Artigo

Relacionados

Aritmética de Computadores: Soma e Subtração em Binário, Octal e Hexadecimal

Você não precisa reaprender matemática para calcular em binário. Descubra como a lógica da soma e subtração decimal se aplica a qualquer base. O artigo detalha os algoritmos formais de ‘vai-um’ e ‘compensação’, e ensina como implementá-los do zero em Python.