Python oferece um conjunto robusto de ferramentas para manipulação de dados binários em baixo nível. O tipo nativo bytearray permite trabalhar com sequências mutáveis de bytes, enquanto módulos como struct facilitam a conversão entre tipos de dados Python e suas representações binárias compactadas. Este artigo explora métodos para analisar bits individuais dentro de um bytearray, converter sequências de bytes em inteiros e lidar com a codificação de números de ponto flutuante.
Análise Bit a Bit em bytearray
A necessidade de examinar o valor de bits específicos dentro de uma sequência de bytes é comum em protocolos de comunicação ou parsing de formatos de arquivo. Um bytearray pode ser tratado como uma coleção de bits, e operações bit a bit podem ser usadas para extrair informações.
Considere um bytearray e a função para obter o valor booleano de um bit em uma posição específica:
dados_binarios = bytearray(b'\x5A\xC3\xF0\x0F') # Exemplo: 01011010 11000011 11110000 00001111
def obter_estado_bit(buffer: bytearray, indice_bit: int) -> bool:
"""
Retorna o valor booleano (True se 1, False se 0) de um bit
em uma posição específica dentro de um bytearray.
"""
if indice_bit < 0:
raise ValueError("O índice do bit não pode ser negativo.")
indice_byte = indice_bit // 8
offset_bit_no_byte = indice_bit % 8
if indice_byte >= len(buffer):
raise IndexError(f"Índice de bit {indice_bit} fora do alcance do bytearray (tamanho {len(buffer)*8} bits).")
byte_alvo = buffer[indice_byte]
# Desloca o bit desejado para a posição 0 e aplica uma máscara com 0x01 (binário 00000001)
# para isolar seu valor.
valor_bit = (byte_alvo >> offset_bit_no_byte) & 0x01
return bool(valor_bit)
print("Análise completa de bits no bytearray:")
for i in range(len(dados_binarios) * 8): # Itera por todos os bits
print(f"Bit {i}: {obter_estado_bit(dados_binarios, i)}")
Neste exemplo, indice_byte calcula qual byte contém o bit desejado, e offset_bit_no_byte detremina a posição do bit dentro desse byte (de 0 a 7). A operação >> desloca os bits para a direita, e & 0x01 isola o bit menos significativo.
Conversão de Bytes para Inteiros com int.from_bytes
A função estática int.from_bytes() permite converter uma sequência de bytes diretamente em um inteiro. Ela é crucial para desserializar dados binários em valores numéricos.
Os parâmetros principais são:
bytes: A sequência de bytes (bytesoubytearray) a ser convertida.byteorder: Especifica a ordem dos bytes ('big' para big-endian, 'little' para little-endian).signed: Um booleano que indica se o inteiro deve ser interpretado como assinado (True) ou não assinado (False).
O parâmetro signed é particularmente importante, pois determina como os valores são interpretados quando o bit mais significativo do número resultante é 1. Se signed=True, o número pode ser negativo; se signed=False, ele será sempre não negativo.
sequencia_bytes = b'\x00\x01\x80\x00' # Exemplo de 4 bytes
# Exemplo 1: Big-endian, não assinado
# 0x00018000 = 98304
valor_unsigned_be = int.from_bytes(sequencia_bytes, byteorder='big', signed=False)
print(f"Bytes: {sequencia_bytes.hex()}")
print(f"Int (Big Endian, Unsigned): {valor_unsigned_be}")
# Exemplo 2: Little-endian, não assinado
# 0x00800100 = 8388880
valor_unsigned_le = int.from_bytes(sequencia_bytes, byteorder='little', signed=False)
print(f"Int (Little Endian, Unsigned): {valor_unsigned_le}")
# Exemplo 3: Big-endian, assinado (usando apenas 2 bytes, b'\x80\x00')
bytes_assinados = sequencia_bytes[2:4] # b'\x80\x00'
# Em 2 bytes, 0x8000 representa -32768
valor_signed_be = int.from_bytes(bytes_assinados, byteorder='big', signed=True)
print(f"\nBytes: {bytes_assinados.hex()}")
print(f"Int (Big Endian, Signed): {valor_signed_be}")
# O mesmo b'\x80\x00' como não assinado
valor_unsigned_de_signed = int.from_bytes(bytes_assinados, byteorder='big', signed=False)
print(f"Int (Big Endian, Unsigned): {valor_unsigned_de_signed}") # 32768
# Exemplo 4: Um único byte negativo
byte_unico = b'\xFF'
# Com 1 byte, 0xFF é -1 se assinado, 255 se não assinado
int_signed_byte = int.from_bytes(byte_unico, byteorder='big', signed=True)
int_unsigned_byte = int.from_bytes(byte_unico, byteorder='big', signed=False)
print(f"\nBytes: {byte_unico.hex()}")
print(f"Int (Big Endian, Signed): {int_signed_byte}")
print(f"Int (Big Endian, Unsigned): {int_unsigned_byte}")
Ao contrário de linguagens como C ou C# que possuem tipos inteiros de tamanho fixo (uint, int, long), Python lida com inteiros de precisão arbitrária. Isso significa que int.from_bytes() pode converter sequências de bytes de qualquer comprimento em um inteiro Python, e o tamanho do tipo é inferido a partir do número de bytes fornecidos.
Codifiacção e Decodificação de Ponto Flutuante com struct
O módulo struct do Python é essencial para converter valores entre tipos nativos do Python (como inteiros, floats e strings) e suas representações binárias empacotadas, conforme definido por uma string de formato C. Ele é amplamente utilizado em I/O binário, comunicação de rede e parsing de arquivos binários.
A função struct.pack() é usada para converter valores Python em uma sequência de bytes, enquanto struct.unpack() faz o caminho inverso.
import struct
# Valor de ponto flutuante para demonstração
temperatura = 25.75
# Codificando como float de precisão simples (4 bytes, formato 'f')
# Assume a ordem de bytes nativa do sistema por padrão
bytes_float_simples = struct.pack('f', temperatura)
print(f"Valor {temperatura} (float 'f'): {bytes_float_simples.hex()} (Hex)")
# Decodificando de volta para float de precisão simples
valor_decodificado_f = struct.unpack('f', bytes_float_simples)[0]
print(f"Decodificado (float 'f'): {valor_decodificado_f}")
# Codificando como float de precisão dupla (8 bytes, formato 'd')
bytes_float_duplo = struct.pack('d', temperatura)
print(f"\nValor {temperatura} (double 'd'): {bytes_float_duplo.hex()} (Hex)")
# Decodificando de volta para float de precisão dupla
valor_decodificado_d = struct.unpack('d', bytes_float_duplo)[0]
print(f"Decodificado (double 'd'): {valor_decodificado_d}")
# Exemplo com ordem de bytes explícita (Big Endian)
# >f para float big-endian, >d para double big-endian
bytes_float_be = struct.pack('>f', temperatura)
bytes_double_be = struct.pack('>d', temperatura)
print(f"\nValor {temperatura} (float '>f', Big Endian): {bytes_float_be.hex()} (Hex)")
print(f"Valor {temperatura} (double '>d', Big Endian): {bytes_double_be.hex()} (Hex)")
As strings de formato ('f' para float de precisão simples, 'd' para float de precisão dupla) são essenciais para o struct. Prefixos como '>' podem ser usados para especificar a ordem de bytes (big-endian), garantindo a portabilidade da representação binária entre diferentes arquiteturas de sistema.