Перейти к основному содержимому

Модель данных Python

📺 Слайды к лекции

Материалы в разработке

Эта страница находится в процессе подготовки и не является финальной версией. Содержание будет дорабатываться и обновляться в течение текущего семестра.

Организационные моменты

Формат курса, контактная информация и программа — на странице курса.

Типы данных и динамическая типизация

Python использует динамическую типизацию — тип переменной определяется во время выполнения, а не при компиляции. Проверить тип можно функцией type(x), а привести к другому типу — с помощью встроенных функций (int(), str(), float() и т.д.).

dynamic_typing.py
x = 1
print(x, type(x)) # 1 <class 'int'>
x = x * 1.5
print(x, type(x)) # 1.5 <class 'float'>
x = str(x)
print(x, type(x)) # 1.5 <class 'str'>
print(type(type(x))) # <class 'type'>
Определение

Утиная типизация (duck typing): «Если это выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, и есть утка». Python не проверяет тип объекта — он проверяет, поддерживает ли объект нужный протокол (набор методов).

Переменные и присваивание

В Python переменная — это метка (label), которая ссылается на объект в памяти. Переменная не хранит значение напрямую.

assignment.py
# label = object
x = 42 # имя x ссылается на объект int(42)
y = x # y ссылается на тот же объект
print(id(x) == id(y)) # True — один и тот же объект

Walrus-оператор (:=)

Начиная с Python 3.8 появился моржовый оператор (walrus operator), который совмещает присваивание и выражение:

walrus.py
# Без walrus
command = input("> ")
while command != "quit":
print("You entered:", command)
command = input("> ")

# С walrus
while (command := input("> ")) != "quit":
print("You entered:", command)
# Ещё пример
if (n := len(a)) > 10:
print(f"List is too long ({n} elements, expected <= 10)")

Идентичность и равенство (is vs ==)

Для сравнения объектов в Python есть два механизма:

  • == — проверка равенства значений (вызывает __eq__)
  • is — проверка идентичности (один и тот же объект в памяти)
identity.py
i, j = 100, 100
print(i == j, i is j) # True True — кэширование малых чисел (-5..256)

x, y = 10**100, 10**100
print(x == y, x is y) # True False — разные объекты с одинаковым значением
Частая ошибка

Не используйте is для сравнения значений! is проверяет идентичность объекта (один id), а не равенство. Исключение — сравнение с None: if x is None.

Изменяемые и неизменяемые типы

Immutable (неизменяемые)Mutable (изменяемые)
str, byteslist, bytearray
int, float, complexdict
boolset
tuplecollections.deque
frozensetcollections.Counter
Практический совет

Неизменяемые типы можно использовать как ключи словарей и элементы множеств, потому что их хэш не меняется. Если нужен «замороженный» список — используйте tuple.

Целочисленные и вещественные литералы

literals.py
# Системы счисления: 2-, 8-, 10- и 16-ричные литералы
print(0b11001100, 0o314, 204, 0xCC) # 204 204 204 204
print(bin(204), oct(204), str(204), hex(204))

Вещественные числа (float)

64-разрядные числа двойной точности (аналог double в C):

floats.py
print(1., .1, 1.1)          # 1.0 0.1 1.1
print(.1e1, 1e-1, 1.1e0) # 1.0 0.1 1.1 (экспоненциальная запись)
print(5. / .3, 5. // .3, 5. % .3) # деление, нацело, остаток
Точность вычислений

float имеет ограниченную точность (sys.float_info.epsilon ~ 2.2e-16). Для финансовых расчётов используйте decimal.Decimal, для точных дробей — fractions.Fraction.

Логические операторы

bool_ops.py
print(bool(5))        # True  — любое ненулевое число
print(bool(0)) # False
print(bool('')) # False — пустая строка
print(bool([])) # False — пустой список
print(True == 1) # True — bool наследуется от int
print(True + False) # 1
Определение

Операторы or и not возвращают операнды, а не булевы значения:

  • x or y возвращает x, если x истинно, иначе y
  • x and y возвращает x, если x ложно, иначе y

Pattern matching (Python 3.10+)

match.py
match command:
case "quit":
quit_game()
case "go" as direction:
go(direction)
case ["drop", *objects]:
drop(objects)
case _:
unknown_command()

Обработка исключений

exceptions.py
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Ошибка: {e}")
except (TypeError, ValueError):
print("Ошибка типа или значения")
else:
print("Успех") # выполняется если не было исключений
finally:
print("Всегда выполняется")
Практический совет

Используйте блок else для кода, который должен выполниться только при отсутствии исключений. Блок finally идеален для освобождения ресурсов.

Как работает интерпретатор CPython

CPython обрабатывает исходный код в несколько этапов:

  1. Лексер (tokenizer) — разбивает текст на токены
  2. Парсер — строит абстрактное синтаксическое дерево (AST)
  3. Компилятор — транслирует AST в байткод
  4. Виртуальная машина (PVM) — исполняет байткод
bytecode.py
import dis

def add(a, b):
return a + b

dis.dis(add)
# LOAD_FAST 0 (a)
# LOAD_FAST 1 (b)
# BINARY_ADD
# RETURN_VALUE

Реализация списков и словарей

Списки

Список в CPython — это массив указателей на объекты. При добавлении элементов используется стратегия over-allocation (выделяется больше памяти, чем нужно) для амортизации вставок.

Словари

Словарь основан на хэш-таблице. Начиная с Python 3.6, словари сохраняют порядок вставки (это стало гарантией в 3.7).

hash_example.py
# __hash__ определяет позицию в хэш-таблице
print(hash("hello")) # фиксированный для неизменяемых типов
print(hash(42)) # для int: hash(x) == x (обычно)
Частая ошибка

Нельзя изменять словарь во время итерации по нему — это вызовет RuntimeError:

a = {'a': 1, 'b': 2}
for i in a:
a['c'] = 3 # RuntimeError: dictionary changed size during iteration

Immutable > Mutable по памяти

memory.py
import sys

print(sys.getsizeof([1])) # 64 байта — список
print(sys.getsizeof((1,))) # 48 байт — кортеж (экономнее!)

Работа с памятью в CPython

CPython использует трёхуровневую систему управления памятью:

  1. Арены (256 КБ) — крупные блоки, запрашиваемые у ОС
  2. Пулы (4 КБ) — блоки внутри арены для объектов одного размера
  3. Блоки — фактические ячейки памяти для объектов
Определение

Small object allocator — оптимизация CPython для объектов размером до 512 байт. Такие объекты не выделяются через malloc напрямую, а берутся из пулов, что существенно ускоряет аллокацию.

Подсчёт ссылок и сборщик мусора

Reference counting

Основной механизм управления памятью — подсчёт ссылок. Каждый объект хранит счётчик ob_refcnt. Когда счётчик достигает 0, память освобождается немедленно.

refcount.py
import sys

a = []
print(sys.getrefcount(a)) # 2 (a + аргумент getrefcount)
b = a
print(sys.getrefcount(a)) # 3
del b
print(sys.getrefcount(a)) # 2

Циклический сборщик мусора (GC)

Reference counting не может обнаружить циклические ссылки. Для этого CPython использует отдельный сборщик мусора на основе алгоритма поколений (generational GC):

  • Поколение 0 — новые объекты, проверяются часто
  • Поколение 1 — пережили одну сборку
  • Поколение 2 — долгоживущие объекты, проверяются редко
gc_example.py
import gc

# Информация о порогах сборки
print(gc.get_threshold()) # (700, 10, 10)

# Принудительная сборка
gc.collect()
Практический совет

Для критичного по производительности кода можно временно отключить GC (gc.disable()) и запускать сборку вручную. Но это нужно делать осознанно, иначе утечки памяти неизбежны.