Лекция 4. Машинное представление: числа, кодировки, байты¶
В этой лекции — как данные физически хранятся в памяти компьютера и как с этим работать в Python и Go. План:
- системы счисления (двоичная, восьмеричная, шестнадцатеричная);
- представление целых чисел (прямой и дополнительный код);
- представление вещественных чисел (IEEE 754);
- кодировки текста (ASCII, кодовые страницы, Unicode, UTF-8/16);
- байтовые строки (
bytes,bytearray,memoryviewв Python;[]byteв Go); - преобразование между текстом и байтами.
Системы счисления¶
У человека десять пальцев — отсюда десятичная система. Любое число можно записать как сумму цифр, умноженных на степени базы:
У компьютера два «пальца» — ток есть или нет. Поэтому база — двойка, цифры — 0 и 1.
Перевод двоичная → десятичная¶
Перевод десятичная → двоичная¶
Алгоритм: ищем максимальную степень 2, не превосходящую число; вычитаем; повторяем для остатка.
Для \(57_{10}\):
- \(2^5 = 32 \leq 57\), разряд
1, остаток \(25\); - \(2^4 = 16 \leq 25\), разряд
1, остаток \(9\); - \(2^3 = 8 \leq 9\), разряд
1, остаток \(1\); - \(2^2 = 4 > 1\), разряд
0; - \(2^1 = 2 > 1\), разряд
0; - \(2^0 = 1 \leq 1\), разряд
1.
\(57_{10} = 111001_2\).
Восьмеричная и шестнадцатеричная¶
В программировании часто используют:
- восьмеричную (база 8, цифры 0–7) — три бита упаковываются в одну цифру;
- шестнадцатеричную (база 16, цифры 0–9, A–F) — четыре бита упаковываются в одну цифру.
Hex особенно удобен — один байт = две цифры:
Литералы в коде¶
В Python и Go одинаковые префиксы:
0b10011 # двоичная — 19
0o23 # восьмеричная — 19
0x13 # шестнадцатеричная — 19
1_000_000 # подчёркивания для читаемости
Преобразования¶
В Python:
# Из строки в число
int("19") # 19
int("10011", 2) # 19 — из двоичной
int("FF", 16) # 255
# Из числа в строку
bin(19) # '0b10011'
oct(19) # '0o23'
hex(255) # '0xff'
# Через format / f-strings
f"{255:b}" # '11111111'
f"{255:08b}" # '00000000' с шириной 8 + лидирующие нули
f"{255:o}" # '377'
f"{255:x}" # 'ff'
f"{255:X}" # 'FF'
В Go — пакет strconv:
n, _ := strconv.ParseInt("FF", 16, 64) // 255
s := strconv.FormatInt(255, 16) // "ff"
// fmt.Printf
fmt.Printf("%b\n", 255) // 11111111
fmt.Printf("%08b\n", 255) // 11111111
fmt.Printf("%o\n", 255) // 377
fmt.Printf("%x\n", 255) // ff
Целые числа в памяти¶
Беззнаковые¶
Все биты — под число. В одном байте (8 бит) — числа от 0 до 255.
Знаковые: прямой код¶
Самый старший (левый) бит — знак: 0 = плюс, 1 = минус. В байте — числа от -127 до +127.
Проблема: два представления нуля (00000000 и 10000000), сложная арифметика — нужно отдельно обрабатывать знак.
Знаковые: дополнительный код¶
Современные компьютеры используют дополнительный код (two's complement). Алгоритм для отрицательного числа -m:
- записать
|m|в прямом коде; - инвертировать все биты (получится обратный код);
- прибавить 1.
Пример для -52 в 8 битах:
52 = 00110100 (прямой код модуля)
~ = 11001011 (обратный код)
+1 = 11001100 (дополнительный код = -52)
Преимущества:
- одно представление нуля;
- сложение/вычитание — обычное двоичное сложение, без проверок знака;
- диапазон в байте: -128..+127 (на одно отрицательное больше).
В Python тип int имеет неограниченную разрядность — переполнение невозможно. Но при битовых операциях нужно помнить про знак:
В Go типы фиксированной разрядности, переполнение возможно и не вызывает ошибки:
Чтобы безопасно проверять — math/bits, errors.As(err, &strconv.ErrRange) при парсинге.
Размеры целых в Go:
| Тип | Бит | Диапазон |
|---|---|---|
int8 / uint8 |
8 | -128..127 / 0..255 |
int16 / uint16 |
16 | -32768..32767 / 0..65535 |
int32 / uint32 |
32 | -2.1·10⁹..+2.1·10⁹ / 0..4.3·10⁹ |
int64 / uint64 |
64 | ±9.2·10¹⁸ / 0..1.8·10¹⁹ |
int / uint |
32 или 64 (зависит от платформы) | — |
Вещественные числа: IEEE 754¶
Современные компьютеры представляют дробные числа в формате с плавающей запятой по стандарту IEEE 754.
Идея — нормализованная экспоненциальная запись:
где m — мантисса, q — порядок (экспонента).
Структура float32 (single precision, 32 бита)¶
| Поле | Бит | Назначение |
|---|---|---|
| знак | 1 | 0 = положительное, 1 = отрицательное |
| смещённый порядок | 8 | q + 127 |
| мантисса | 23 | дробная часть нормализованной записи (целая всегда 1, не хранится) |
Пример: -25.625 в float32¶
- Перевод в двоичную: \(25 = 11001_2\), \(0.625 = 0.101_2\) → \(-25.625_{10} = -11001.101_2\).
- Нормализация: \(-11001.101_2 = -1.1001101_2 \cdot 2^4\).
- Смещённый порядок: \(127 + 4 = 131 = 10000011_2\).
- Мантисса (без ведущей 1):
1001101+ нули до 23 бит.
| знак | порядок | мантисса |
|---|---|---|
| 1 | 10000011 | 10011010000000000000000 |
В hex: 0xC1CD0000.
Точность¶
float32 хранит примерно 7 значащих десятичных цифр, float64 — около 15-17. Это и причина классической проблемы:
0.1 в двоичной системе — бесконечная периодическая дробь, которую обрезают. Накапливаются микроскопические ошибки.
Для денежных расчётов никогда не используйте float. Используйте:
- Python:
decimal.Decimal(точная десятичная арифметика),fractions.Fraction(точные рациональные числа). - Go:
math/big.Float,shopspring/decimal(сторонняя библиотека).
from decimal import Decimal
Decimal("0.1") + Decimal("0.1") + Decimal("0.1")
# Decimal('0.3') — точно
Специальные значения IEEE 754¶
+0.0и-0.0— два нуля (равны при сравнении);+Inf/-Inf— переполнение или деление ненулевого на ноль;NaN(Not a Number) — результат0/0,sqrt(-1), ...; не равно ни чему, включая себя:
import math
x = float("nan")
print(x == x) # False!
print(math.isnan(x)) # True — правильная проверка
print(1.0 / 0) # ZeroDivisionError в Python
print(float("inf")) # inf
import "math"
n := math.NaN()
fmt.Println(n == n) // false
fmt.Println(math.IsNaN(n)) // true
inf := math.Inf(1) // +Inf
Кодировки текста¶
Цифры понятны: пишем в двоичной системе. А что делать с буквами и знаками препинания?
ASCII (1963)¶
American Standard Code for Information Interchange — 7-битная кодировка, 128 символов:
- 0–31, 127 — управляющие (
\n,\t,BEL, ...); - 32 — пробел;
- 33–47, 58–64, 91–96, 123–126 — знаки пунктуации;
- 48–57 — цифры
0–9(важно: коды0x30–0x39, младшие 4 бита = значение цифры); - 65–90 —
A–Z; - 97–122 —
a–z.
ord("A") # 65
ord("a") # 97
ord("0") # 48
chr(65) # 'A'
chr(48) # '0'
# Преобразование цифрового символа в число
int("5") # 5
ord("5") - ord("0") # 5
Кодовые страницы (8-bit)¶
ASCII занимает 7 бит, восьмой свободен — можно использовать для 128 дополнительных символов. Из этого выросли национальные кодировки:
- CP866 — DOS-кириллица;
- Windows-1251 — Windows-кириллица (актуальна для legacy-Windows);
- KOI8-R — Unix-кириллица;
- ISO-8859-1 (Latin-1) — западноевропейские.
Проблема: один и тот же байт 0xE0 — в windows-1251 это «а», в KOI-8 — другой символ. Тексты не переносятся без указания кодировки. Файл, открытый «не той» кодировкой, выглядит как «крокозябры».
Unicode¶
В начале 1990-х придумали Unicode — кодировка со всеми символами всех современных и многих мёртвых письменностей плюс математические, музыкальные и эмодзи. Каждый символ имеет уникальный код-поинт U+XXXX:
U+0041—A(латинская);U+0430—а(кириллическая);U+221A—√;U+1F600— 😀.
Сейчас в Unicode более 140 000 символов.
Представления Unicode: UTF-8, UTF-16, UTF-32¶
Unicode — это отображение «символ → число». Чтобы записать его в файл/память, нужно представление (encoding form):
| Представление | Размер символа | Применение |
|---|---|---|
| UTF-8 | 1–4 байта | Web, nix, Python source, JSON, де-факто стандарт* |
| UTF-16 | 2 или 4 байта | Windows API, Java, JavaScript внутри движка |
| UTF-32 | всегда 4 байта | Редко — память расходуется неэффективно |
UTF-8 — главный¶
- ASCII-совместимость: первые 128 кодов кодируются одним байтом, идентично ASCII. Любой ASCII-файл уже валидный UTF-8.
- Самосинхронизация: старшие биты байта говорят, начало это символа или продолжение.
- Переменная длина:
a— 1 байт,я— 2 байта,中— 3 байта, эмодзи — 4 байта.
Из-за переменной длины число символов в строке не равно числу байт:
Big-endian и little-endian¶
UTF-16 и UTF-32 могут хранить старший байт «слева» (BE) или «справа» (LE). Чтобы файл можно было прочитать, в начале ставят BOM (Byte Order Mark): 0xFEFF для BE, 0xFFFE для LE.
UTF-8 в BOM не нуждается (один байт = одна единица), но Windows-приложения иногда добавляют EF BB BF в начало — это «UTF-8 BOM», часто создающий проблемы парсерам.
Текст и байты в Python¶
В Python 3 строгое разделение:
str— Unicode-строка, абстракция «последовательность символов»;bytes— последовательность байт, абстракция «сырые данные»;bytearray— изменяемыйbytes;memoryview— «окно» в чужие байты без копирования.
str ↔ bytes¶
s = "Привет"
b = s.encode("utf-8") # b'\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82'
text = b.decode("utf-8") # "Привет"
# С обработкой ошибок
s.encode("ascii") # UnicodeEncodeError
s.encode("ascii", errors="ignore") # b'' — потерянно
s.encode("ascii", errors="replace") # b'??????' — заменили
s.encode("ascii", errors="backslashreplace") # b'\\u041f...'
Главное правило: в коде программы — всегда str (Unicode), на границах (файлы, сеть) — bytes.
Литералы¶
# str
s1 = "обычная"
s2 = "с экранированием П" # символ Unicode по коду
s3 = r"raw\nстрока" # обратный слэш не экранируется
# bytes — префикс b
b1 = b"ascii bytes"
b2 = b"\xd0\x9f" # сырые байты
b3 = "Привет".encode("utf-8")
bytes: операции¶
b = b"\x48\x65\x6c\x6c\x6f" # b'Hello'
len(b) # 5
b[0] # 72 — целое (значение байта), не bytes!
b[0:3] # b'Hel' — срез возвращает bytes
b.hex() # '48656c6c6f'
bytes.fromhex("48656c6c6f") # b'Hello'
# Из списка целых
bytes([72, 101, 108, 108, 111]) # b'Hello'
bytearray: изменяемые байты¶
ba = bytearray(b"hello")
ba[0] = ord("H") # bytearray(b'Hello') — можно менять
ba.append(0x21) # bytearray(b'Hello!')
Удобен для построения буферов, разбора сетевых пакетов.
memoryview: «окно» без копирования¶
Срезы bytes создают копию — для гигабайтных данных это дорого. memoryview даёт срезы и индексацию без копирования.
import time
# Замедленная версия — копирование на каждом срезе
data = b"x" * 1_000_000
start = time.time()
b = data
while b:
b = b[1:] # каждый раз — новый bytes длиной N-1
print("bytes:", time.time() - start)
# ~5 секунд
# Быстрая версия — memoryview
data = b"x" * 1_000_000
start = time.time()
b = memoryview(data)
while b:
b = b[1:] # «окно» сдвигается, копия не делается
print("memoryview:", time.time() - start)
# ~0.05 секунд
memoryview поддерживает протокол buffer — работает с bytes, bytearray, array.array, numpy.ndarray. Полезен для интероп с C-библиотеками.
Текст и байты в Go¶
В Go строка — это неизменяемая последовательность байт, обычно в кодировке UTF-8 (но язык это не гарантирует).
string и []byte¶
s := "Привет"
fmt.Println(len(s)) // 12 — БАЙТ, не символов
fmt.Println(s[0]) // 208 — байт, не код символа
// Преобразования
b := []byte(s) // строка → байты (копия)
s2 := string(b) // байты → строка (копия)
// Hex
fmt.Printf("%x\n", b) // d09fd180d0b8d0b2d0b5d182
rune — Unicode code point¶
import "fmt"
s := "Привет"
// for range по строке — итерация по рунам
for i, r := range s {
fmt.Printf("%d: %c (%U)\n", i, r, r)
}
// 0: П (U+041F)
// 2: р (U+0440)
// 4: и (U+0438)
// ...
// Индекс i — позиция в БАЙТАХ, не в символах.
// Преобразование в []rune
runes := []rune(s)
fmt.Println(len(runes)) // 6 — количество символов
rune — это просто алиас int32.
Подсчёт «символов» правильно¶
Перекодирование¶
В стандартной библиотеке golang.org/x/text/encoding (расширенная):
import (
"golang.org/x/text/encoding/charmap"
)
decoder := charmap.Windows1251.NewDecoder()
utf8Bytes, _ := decoder.Bytes([]byte{0xCF, 0xF0, 0xE8, 0xE2, 0xE5, 0xF2})
// "Привет" в UTF-8
Сравнение Python ↔ Go: текст и байты¶
| Аспект | Python | Go |
|---|---|---|
| Тип строки | str — Unicode-абстракция |
string — байты (обычно UTF-8) |
| Индексация | по символам | по байтам |
| Длина | по символам | по байтам |
| Перебор | по символам | по рунам через for range |
| Сырые байты | bytes, bytearray |
[]byte |
| Преобразование | s.encode(enc) / b.decode(enc) |
[]byte(s) / string(b) (только UTF-8) |
| Неизменяемый | str, bytes |
string |
| Изменяемый | bytearray |
[]byte |
| Без копирования | memoryview |
срезы (но обычно тоже копируют для безопасности) |
| Расширенные кодировки | в стандартной библиотеке (codecs) |
golang.org/x/text/encoding |
Подводные камни¶
- «Длина строки» — почти всегда подразумевается «число символов», но
len()в Go возвращает число байт. На кириллице это другое. - Файлы без указания кодировки — открытый «как есть» файл может оказаться в windows-1251. Всегда указывайте
encoding="utf-8". - BOM в UTF-8 — Excel и некоторые Windows-программы добавляют
EF BB BFв начало файла. Pythonopen(... encoding="utf-8")это не обрабатывает — используйтеencoding="utf-8-sig". - Сравнение float через
==— почти всегда ошибка. Используйтеmath.isclose(a, b). - Деньги во float — никогда. Decimal.
Контрольные вопросы¶
- Чем дополнительный код отрицательного числа отличается от прямого, и зачем он нужен?
- Что произойдёт при
int8(127) + 1в Go? А в Python? - Почему
0.1 + 0.2 != 0.3, и какие есть способы избежать этой проблемы? - В чём разница между Unicode (как стандарт) и UTF-8 (как представление)?
- Сколько байт занимает буква «П» в UTF-8? А в UTF-16?
- Почему
len("Привет")в Python даёт 6, а в Go — 12? - Что такое
runeв Go и чем отличается отbyte? - Когда стоит применять
memoryviewв Python? - Чем
bytesотличается отbytearray? - Что такое BOM и почему он создаёт проблемы?