Бинарное представление данных: как числа, текст, изображения и звук хранятся в битах

Бинарное представление данных: как числа, текст, изображения и звук хранятся в битах Полезное

Бинарная (двоичная) система — это позиционная система счисления с основанием 2: любое число записывается двумя цифрами, 0 и 1. Бинарное представление данных — это запись любой информации (чисел, текста, изображений, звука) в виде последовательности таких нулей и единиц, то есть битов.

Основные преимущества двоичной системы для представления информации: два состояния проще и надежнее различать в электронике, чем десять; арифметика и логика сводятся к нескольким простым правилам; одним и тем же способом кодируются данные любого вида. Ниже — коротко о каждом преимуществе, а затем разбор того, как именно разные данные превращаются в биты. Код проверен на Python 3.14.

Четыре термина, которые часто смешивают

Термин Что это Пример
Двоичная система счисления Способ записи чисел двумя цифрами 77 = 1001101
Бит Один двоичный разряд, значение 0 или 1 1
Байт Группа из 8 битов, 256 разных значений 01001101
Двоичный код (формат) Договоренность, что означают биты UTF-8, IEEE 754, RGB

Главное различие: система счисления отвечает только за запись чисел. Чтобы записать в битах букву или цвет, нужна еще договоренность — формат, который сопоставляет объекту число, а числу байты. Цепочка всегда одна: объект -> число -> биты.

Преимущества двоичной системы для представления информации

  1. Надежное различение состояний. Элементу памяти или линии связи достаточно отличать «низкий уровень» от «высокого». Между ними остается большой запас, поэтому небольшая помеха не превращает 0 в 1. При десяти уровнях на том же диапазоне запас был бы в разы меньше.
  2. Простые элементы. Устройство с двумя устойчивыми состояниями сделать проще и дешевле, чем с десятью: на этом построены и процессоры, и память, и накопители.
  3. Простая арифметика. Таблица сложения одного разряда — четыре строки (0+0, 0+1, 1+0, 1+1), таблица умножения — тоже четыре. Схемы для таких операций компактны.
  4. Прямая связь с логикой. 0 и 1 естественно читаются как «ложь» и «истина», поэтому вычисления строятся из логических операций И, ИЛИ, НЕ.
  5. Универсальность. Число, буква, пиксель и отсчет звука хранятся одинаково — байтами. Одно и то же устройство хранит и передает любые данные.

Главный недостаток касается человека: двоичная запись длинная. Число 255 — это восемь единиц подряд. Поэтому люди читают биты в шестнадцатеричной записи, где одна цифра заменяет четыре бита: 11111111 = FF.

Двоичная система — инженерный выбор, а не единственно возможный: в 1958 году в МГУ построили троичную ЭВМ «Сетунь». Массовой двоичная техника стала именно из-за простоты и надежности элементов.

Один пример: шесть видов данных в битах

Скрипт ниже берет число, отрицательное число, дробь, две буквы, пиксель и отсчет звука и печатает их байты. Его можно скопировать и запустить целиком.

import struct


def bits(data: bytes) -> str:
    return " ".join(f"{byte:08b}" for byte in data)


# 1. Целое без знака, 1 байт
print("77       ->", bits((77).to_bytes(1, "big")))
# 2. Отрицательное целое, 1 байт, дополнительный код
print("-77      ->", bits((-77).to_bytes(1, "big", signed=True)))
# 3. Вещественное число, 8 байт, формат IEEE 754 binary64
print("0.1      ->", bits(struct.pack(">d", 0.1)))
# 4. Текст в кодировке UTF-8
print("A        ->", bits("A".encode("utf-8")))
print("Я        ->", bits("Я".encode("utf-8")))
# 5. Один пиксель: оранжевый цвет в RGB, по байту на канал
print("пиксель  ->", bits(bytes([255, 165, 0])))
# 6. Один отсчет звука: 16 бит со знаком
print("отсчет   ->", bits(struct.pack(">h", -12345)))

Вывод:

77       -> 01001101
-77      -> 10110011
0.1      -> 00111111 10111001 10011001 10011001 10011001 10011001 10011001 10011010
A        -> 01000001
Я        -> 11010000 10101111
пиксель  -> 11111111 10100101 00000000
отсчет   -> 11001111 11000111

В примере байты идут от старшего к младшему: это задают аргумент "big" и знак > в struct.pack. В памяти большинства современных процессоров порядок байтов многобайтного числа обратный, поэтому без явного указания результат зависит от платформы. Дальше — разбор каждой строки.

Целые числа

Каждый разряд двоичного числа — степень двойки. 01001101 = 64 + 8 + 4 + 1 = 77. В одном байте без знака помещаются значения от 0 до 255, в n битах — от 0 до 2^n — 1. Пошаговый перевод между системами разобран в статье о двоичной системе счисления.

Размер хранилища ограничен, и число в него может не поместиться.

print((300).to_bytes(1, "big"))
OverflowError: int too big to convert

Исправление — посчитать нужное число байтов по длине числа в битах:

value = 300
size = max(1, (value.bit_length() + 7) // 8)   # для неотрицательного числа; нулю тоже нужен байт
print(size, "байта:", " ".join(f"{b:08b}" for b in value.to_bytes(size, "big")))
2 байта: 00000001 00101100

Python сообщает об ошибке. В языках с целыми фиксированной ширины (C, Java, C#) поведение другое и зависит от языка и типа: значение может молча «завернуться» по модулю 2^n, поэтому диапазон типа проверяют заранее.

Отрицательные числа: дополнительный код

Знака «минус» в битах нет, поэтому отрицательные целые почти везде хранят в дополнительном коде. Чтобы получить -77 в 8 битах, нужно:

  1. Записать 77: 01001101.
  2. Инвертировать все биты: 10110010.
  3. Прибавить единицу: 10110011.

Результат совпадает с выводом скрипта. Старший бит у отрицательных чисел равен 1, а диапазон байта со знаком — от -128 до 127. Удобство этого кода в том, что сложение и вычитание выполняет одна и та же схема: 77 + (-77) дает 00000000 с отброшенным переносом.

Вещественные числа: IEEE 754

Дробные числа хранятся по стандарту IEEE 754. Тип float в Python — это 64-битный формат binary64: 1 бит знака, 11 битов порядка и 52 бита дробной части мантиссы. По сути это запись вида «мантисса, умноженная на степень двойки».

В выводе скрипта для 0.1 виден повторяющийся узор 1001 1001: дробь 0.1 в двоичной системе бесконечная периодическая, как 1/3 в десятичной. В 52 бита помещается только ее округление.

from decimal import Decimal

print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print(Decimal(0.1))
print(0.5 + 0.25 == 0.75)
0.30000000000000004
False
0.1000000000000000055511151231257827021181583404541015625
True

Третья строка — точное значение, которое реально хранится вместо 0.1. Последняя строка показывает границу: дроби, которые раскладываются в сумму степеней двойки (0.5, 0.25, 0.75), хранятся точно. Отсюда практика: вещественные числа сравнивают с допуском, а деньги считают в целых копейках или десятичным типом.

Текст

Текст проходит два шага. Сначала символу сопоставляется номер в наборе Unicode — кодовая точка. Затем кодировка превращает этот номер в байты.

  • A -> U+0041 (65) -> 01000001, один байт в UTF-8.
  • Я -> U+042F (1071) -> 11010000 10101111, два байта в UTF-8.

Число байтов на символ в UTF-8 переменное, от одного до четырех. Базовая латиница (ASCII) занимает один байт, кириллица — два, большинство эмодзи — четыре. Подробнее об устройстве кодировок — в статье о кодировках символов и UTF-8.

Изображения

Растровое изображение — это сетка пикселей. В распространенной модели RGB с глубиной 8 бит на канал цвет пикселя — три байта: доля красного, зеленого и синего от 0 до 255. Оранжевый из примера — это (255, 165, 0).

Три байта дают 2^24 = 16 777 216 цветов. Кадр 1920×1080 без сжатия занимает 1920 * 1080 * 3 = 6 220 800 байт, около 5,93 МиБ. Форматы вроде PNG и JPEG хранят изображение в сжатом виде, поэтому файлы заметно меньше: PNG сжимает без потерь, JPEG — с потерями, и пиксели после распаковки лишь близки к исходным; бывают и другие глубины цвета и четвертый канал прозрачности.

Звук

Звук — непрерывное колебание, и перед записью его превращают в числа. Устройство много раз в секунду измеряет уровень сигнала (дискретизация) и округляет каждое измерение до целого (квантование). Одно такое число называется отсчетом.

В формате аудио-CD это 44 100 отсчетов в секунду, 16 бит на отсчет, два канала. Секунда звука без сжатия — 44 100 * 2 * 2 = 176 400 байт, минута — около 10,09 МиБ. Отсчет -12345 из примера хранится как целое со знаком в дополнительном коде — так же, как -77, только в двух байтах.

Видео объединяет оба способа: последовательность кадров плюс звуковая дорожка. Без сжатия оно занимало бы слишком много места, поэтому кодеки хранят часть кадров целиком, а остальные — как отличия от соседних.

Биты не знают, что они значат

Одни и те же два байта можно прочитать по-разному.

raw = bytes([0b11010000, 0b10101111])

print(int.from_bytes(raw, "big"))               # целое без знака
print(int.from_bytes(raw, "big", signed=True))  # целое со знаком
print(raw.decode("utf-8"))                      # текст в UTF-8
print(raw.hex(" "))                             # те же биты в шестнадцатеричной записи
53423
-12113
Я
d0 af

Смысл байтам придает формат, а не сами биты. Поэтому у файлов есть расширения и заголовки, у переменных — типы, а текст, открытый не в той кодировке, превращается в «кракозябры».

Выводы

  • Бинарная система — позиционная система счисления с основанием 2; бинарное представление данных — запись любой информации битами по правилам конкретного формата.
  • Преимущества двоичной системы: надежное различение двух состояний, простые элементы, простая арифметика, прямая связь с логикой, единый способ хранения для любых данных. Недостаток — длинная запись, которую люди сокращают шестнадцатеричной.
  • Целые числа хранятся как суммы степеней двойки, отрицательные — в дополнительном коде, вещественные — по IEEE 754 с округлением.
  • Текст кодируется цепочкой «символ -> кодовая точка -> байты», изображение — числами яркости каналов каждого пикселя, звук — последовательностью отсчетов.
  • Одни и те же биты означают разное в разных форматах, поэтому данные всегда читают вместе с их типом или форматом.

Где применяется / связь с практикой

Понимание двоичного представления нужно в повседневных задачах программиста: выбрать тип данных под диапазон значений, объяснить «странный» результат 0.1 + 0.2, прочитать файл в правильной кодировке, разобрать бинарный формат или сетевой пакет, оценить объем данных без сжатия.

Освойте тему на практике

Типы данных, работа с числами, строками, байтами и файлами последовательно разбираются на курсе «Python-разработчик. Базовый уровень». Познакомиться с форматом занятий можно на открытых уроках Otus.

FAQ

Бинарный файл и текстовый файл — в чем разница, если все хранится в битах?
Оба состоят из байтов. Текстовым называют файл, байты которого целиком читаются как символы в некоторой кодировке; в бинарном байты означают числа, пиксели или структуры формата, и текстовый редактор покажет их бессмысленным набором знаков.

Сколько значений помещается в n битов?
Ровно 2^n разных комбинаций: 8 битов — 256, 16 битов — 65 536, 32 бита — 4 294 967 296. Как эти комбинации распределены между положительными и отрицательными числами, зависит от формата.

Килобайт — это 1000 или 1024 байта?
По стандарту приставка «кило» означает 1000, а для 1024 байт введена единица кибибайт (КиБ). На практике часть программ и систем по-прежнему называет килобайтом 1024 байта, поэтому объемы в разных источниках расходятся.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)