Справочник

Справочник по Python

Синтаксис, примеры и короткие пояснения — от основ языка до алгоритмических приёмов, классов и тестов.

Основы языка13

Переменные и типы
n = 42
ratio = 1.5
name = "Ada"
flag = True
nothing = None

Тип выводится из значения, объявлять его не нужно. Проверить: type(x), isinstance(x, int).

f-строки
f"{name} solved {n} tasks"
f"{ratio:.2f}"
f"{n:>5}"
f"{n=}"

Подстановка выражений в строку. :.2f — два знака после точки, :>5 — выравнивание, {n=} печатает и имя, и значение.

Истинность значений
if items:
    ...
if not s:
    ...
bool(0), bool(""), bool([])   # False

Пустые коллекции, 0, "" и None ложны. Пишут if items, а не if len(items) > 0.

Множественное присваивание
a, b = b, a
first, *rest = [1, 2, 3, 4]
x = y = 0

Обмен значений без временной переменной; *rest забирает остаток последовательности.

Тернарный оператор
sign = "+" if n >= 0 else "-"

Условие в одну строку. Возвращает значение, поэтому его можно присвоить или вернуть.

Целочисленное деление и остаток
7 // 2          # 3
7 % 2           # 1
divmod(7, 2)    # (3, 1)
-7 // 2         # -4

// округляет вниз, а не к нулю: -7 // 2 это -4. divmod отдаёт частное и остаток сразу.

Длина через len
len([10, 20, 30])  # 3

`len()` — встроенная функция, которая использует протокол `__len__`, поэтому одинаково работает с разными контейнерами. Метода `.len()` в Python нет.

Ввод целого числа
n = int(input())

`input()` всегда возвращает строку, даже если пользователь ввёл число. `int()` выбросит `ValueError`, если строка не является корректным целым числом.

Преобразование типов и основание системы
s = str(42)
n = int("42")
x = float("3.5")
h = int("ff", 16)

`str`, `int` и `float` создают значение нужного типа, а второй аргумент `int()` задаёт основание системы счисления. Некорректная строка приводит к `ValueError`.

Проверка типа через isinstance
isinstance(True, int)  # True
type(True) is int  # False

`isinstance()` учитывает наследование и обычно подходит для проверки типа лучше. `type(x) is T` проверяет только точное совпадение типа.

None как значение по умолчанию
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

`None` используют как безопасный маркер, когда изменяемый объект нужно создавать отдельно при каждом вызове. Проверять его следует через `is None`, а не через `== None`.

Тождественность и равенство
a = [1]
b = [1]
a == b  # True
a is b  # False
x = 1
y = 1
x is y  # True

`==` сравнивает значения, а `is` проверяет, являются ли ссылки одним объектом. Малые целые числа могут переиспользоваться интерпретатором, поэтому `is` нельзя применять для сравнения чисел.

Условия и циклы10

Условия if / elif / else
if score >= 90:
    grade = "A"
elif score >= 75:
    grade = "B"
else:
    grade = "C"

Условия проверяются сверху вниз, выполняется первая подходящая ветка. Более общий случай может скрыть расположенный ниже частный.

Сравнения и цепочки сравнений
a == b
a != b
a < b
a <= b
a is None
0 <= x < n
a < b == c
x not in blocked

Цепочка сравнений эквивалентна нескольким проверкам через and, но среднее выражение вычисляется один раз. С None обычно сравнивают через is или is not.

Логические операторы
value = cached or compute()
result = ready and data
flag = not items

and и or используют короткое замыкание и возвращают один из операндов, а не обязательно bool. Оператор not всегда возвращает bool.

Циклы for и while
for item in items:
    process(item)

while left < right:
    left += 1

for перебирает элементы итерируемого объекта, а while повторяется, пока условие истинно. В while состояние должно изменяться, иначе цикл может стать бесконечным.

Диапазон range
range(stop)
range(start, stop)
range(start, stop, step)
range(n - 1, -1, -1)

range создаёт ленивую целочисленную последовательность и не включает stop. Для движения назад нужен отрицательный step, а нулевой шаг запрещён.

Прерывание цикла break
for x in items:
    if x == target:
        break

break немедленно завершает ближайший цикл. Во вложенных циклах внешний цикл продолжит работу.

Пропуск итерации continue
for x in items:
    if x < 0:
        continue
    process(x)

continue пропускает остаток текущей итерации и переходит к следующей. В while перед continue нельзя забывать обновлять переменные цикла.

Блок else у цикла
for x in items:
    if x == target:
        found = True
        break
else:
    found = False

else у цикла выполняется, только если цикл завершился без break. Для пустого итерируемого объекта блок else тоже выполняется.

Сопоставление match / case
match token:
    case 0:
        kind = "zero"
    case int() if token > 0:
        kind = "positive"
    case _:
        kind = "other"

match выбирает первый подходящий шаблон, а _ служит универсальным случаем. Перехода в следующую ветку после совпадения нет.

Распаковка в цикле
for key, value in pairs:
    process(key, value)

for i, (x, y) in enumerate(points):
    process(i, x, y)

Цель цикла может сразу распаковывать вложенную структуру. Количество и форма элементов должны совпадать, если не используется цель со звёздочкой.

Функции10

Функция и возврат нескольких значений
def bounds(values):
    return min(values), max(values)

low, high = bounds(items)

Несколько возвращаемых значений фактически упаковываются в кортеж. При распаковке количество переменных должно совпадать с количеством элементов.

Аргументы по умолчанию
def power(base, exponent=2):
    return base ** exponent

Значение по умолчанию используется, когда аргумент не передан. Выражение значения по умолчанию вычисляется один раз при создании функции.

Изменяемый аргумент по умолчанию
def add_item(item, items=[]):
    items.append(item)
    return items

Изменяемый объект по умолчанию сохраняется между вызовами, поэтому данные неожиданно накапливаются. Обычно вместо него используют None и создают объект внутри функции.

Аргументы *args и **kwargs
def collect(first, *args, **kwargs):
    return first, args, kwargs

*args собирает лишние позиционные аргументы в кортеж, а **kwargs — именованные в словарь. Обычные параметры связываются раньше этих коллекций.

Только именованные аргументы
def clamp(value, *, low=0, high=100):
    return max(low, min(value, high))

Параметры после * можно передавать только по имени. Это предотвращает ошибки из-за неочевидного порядка нескольких похожих аргументов.

Лямбда-функция
key = lambda item: item[1]
result = sorted(items, key=key)

lambda создаёт небольшую функцию из одного выражения и не поддерживает обычные инструкции. Для сложной логики читаемее использовать def.

Замыкание
def make_adder(delta):
    def add(value):
        return value + delta
    return add

Замыкание сохраняет доступ к именам из внешней функции после её завершения. Имена захватываются по ссылке, поэтому при создании функций в цикле возможна ловушка позднего связывания.

Области видимости global и nonlocal
count = 0

def outer():
    total = 0

    def update():
        global count
        nonlocal total
        count += 1
        total += 1
        return total

    return update

global изменяет имя уровня модуля, а nonlocal — имя из ближайшей внешней области видимости, где это имя уже определено. Без этих объявлений присваивание создаёт локальное имя.

Кеширование lru_cache
from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

lru_cache сохраняет результаты вызовов и особенно полезен для повторяющихся рекурсивных подзадач. Аргументы должны быть хешируемыми, а maxsize=None создаёт неограниченный кеш.

Аннотации типов
def binary_search(a: list[int], target: int) -> int | None:
    ...

Аннотации описывают ожидаемые типы параметров и результата, но сами по себе не проверяют их во время выполнения. Объединение X | None означает, что результат может отсутствовать.

Строки7

Срезы строк
s[1:4]
s[:3]
s[-2:]
s[::-1]
s[::2]

Срез не включает правую границу. s[::-1] разворачивает строку, s[::2] берёт каждый второй символ, отрицательные индексы считают с конца.

split / join
"a,b,c".split(",")     # ["a", "b", "c"]
"a b  c".split()
",".join(["a", "b"])   # "a,b"

split() без аргумента режет по любым пробелам и отбрасывает пустые куски. join собирает обратно.

Методы строк
s.strip()
s.lower()
s.upper()
s.replace("a", "b")
s.startswith("ab")
s.endswith(".py")
s.count("a")
s.find("a")   # -1

Строки неизменяемы: методы возвращают новую строку. find отдаёт -1, если не нашёл, index бросает ошибку.

Проверка символов
s.isdigit()
s.isalpha()
s.isalnum()
c.isupper()
c.islower()
"ABC123".isupper()   # True
"123".isupper()      # False
"ab" in s

isdigit/isalpha/isalnum истинны, только если строка непустая и подходит целиком. isupper/islower проверяют все символы с регистром: цифры и знаки игнорируются, но хотя бы один такой символ нужен. in проверяет, есть ли символ или подстрока.

Сравнение по символам
sorted(s)
sorted(s1) == sorted(s2)
set("aab") == set("ab")        # True
sorted("aab") == sorted("ab")  # False

sorted(s) даёт отсортированный список символов: равенство таких списков — обычная проверка на анаграмму. Множество для этого не годится: оно теряет количество повторов. Оба варианта различают регистр, пробелы и знаки.

Накопление строки
parts = []
for w in words:
    parts.append(w)
result = "".join(parts)

Не складывайте строки в цикле через += — каждая операция копирует всё. Копите в список и склейте один раз.

ord / chr
ord('a')            # 97
chr(97)             # 'a'
ord(c) - ord('a')

Код символа и обратно. ord(c) - ord("a") даёт позицию буквы — частый приём для счётчика из 26 ячеек.

Строки: продвинутое10

Форматирование строк
"{name}: {score:.2f}".format(name="Ada", score=9.876)
score = 9.876
f"{score:.2f}"

`str.format` подставляет значения в шаблон, а f-строка вычисляет выражения непосредственно в текущей области видимости. Спецификаторы управляют точностью, знаком и представлением, а литеральные фигурные скобки записываются как `{{` и `}}`.

Выравнивание и заполнение строк
x = "42"
f"{x:>8}"
f"{x:<8}"
f"{x:*^8}"

Спецификаторы `<`, `>` и `^` выравнивают значение влево, вправо и по центру, а символ перед ними задает заполнение. Ширина является минимальной и не обрезает слишком длинное значение.

Таблица замены символов
table = str.maketrans({"a": "@", "e": "3"})
"peace".translate(table)

`str.maketrans` создает таблицу преобразований, а `translate` применяет ее ко всем символам за один проход. Значение `None` в таблице удаляет соответствующий символ.

Разделение строки на три части
head, sep, tail = "key=value".partition("=")

`partition` делит строку только по первому вхождению и всегда возвращает три элемента, включая сам разделитель. Если разделитель не найден, исходная строка находится в первом элементе, а два остальных пусты.

Разделение текста на строки
"a\nb\r\nc".splitlines()  # ["a", "b", "c"]

`splitlines` распознает разные виды переносов строк и по умолчанию удаляет их. В отличие от `split("\n")`, метод корректно обрабатывает несколько стандартных разделителей строк.

Дополнение строки нулями
"42".zfill(5)  # "00042"
"-42".zfill(5)  # "-0042"

`zfill` дополняет строку нулями слева до минимальной ширины и размещает нули после знака числа. Метод не обрезает строку, если она уже длиннее указанной ширины.

Удаление точного префикса или суффикса
"unhappy".removeprefix("un")  # "happy"
"report.csv".removesuffix(".csv")  # "report"

`removeprefix` и `removesuffix` удаляют одно точное совпадение только с соответствующего края строки. В отличие от `strip`, их аргумент не интерпретируется как набор отдельных символов.

Основы регулярных выражений
import re
re.match(r"\d+", text)
re.search(r"\d+", text)
re.findall(r"\d+", text)
re.sub(r"\s+", " ", text)

`match` проверяет совпадение только с начала строки, `search` ищет первое совпадение в любом месте, `findall` собирает все совпадения, а `sub` заменяет их. Для многократно используемого шаблона выгодно один раз вызвать `re.compile`.

Кодирование и декодирование текста
data = "café".encode("utf-8")
text = data.decode("utf-8")

`encode` преобразует `str` в `bytes`, а `decode` выполняет обратное преобразование с указанной кодировкой. Использование разных кодировок может вызвать ошибку или испортить текст.

Регистронезависимое сравнение Unicode
"Straße".lower()  # "straße"
"Straße".casefold()  # "strasse"

`lower` выполняет обычное преобразование в нижний регистр, а `casefold` агрессивнее свёртывает регистр (ß → ss) для регистронезависимого сравнения. Это не нормализация Unicode: `é` одним символом и `e` с комбинирующим ударением после `casefold` по-прежнему не равны — для этого нужен `unicodedata.normalize`.

Списки и срезы13

Срезы списка
a = [0, 1, 2, 3, 4]
a[1:4]    # [1, 2, 3]
a[:3]
a[-2:]
a[::-1]
b = a[:]

Срез создаёт новый список. a[:] копирует верхний уровень — вложенные списки останутся общими.

append / extend / insert / pop
a.append(x)        # O(1)
a.extend([x, y])
a.insert(0, x)     # O(n)
a.pop()            # O(1)
a.pop(0)           # O(n)

Вставка и удаление в начале списка стоят O(n). Если нужны обе стороны — берите collections.deque.

enumerate
for i, x in enumerate(a):
    ...
for i, x in enumerate(a, start=1):
    ...

Индекс и значение сразу. Заменяет for i in range(len(a)) и не даёт промахнуться на границе.

zip
for x, y in zip(a, b):
    ...
list(zip(a, b))
zip(*matrix)

Идёт по нескольким последовательностям параллельно и останавливается на самой короткой. zip(*matrix) транспонирует матрицу.

Списковые включения
[x for x in a if x > 0]
[x * 2 for x in a]
[y for row in m for y in row]
{x: x ** 2 for x in a}

Короче и быстрее цикла с append. Два for подряд разворачивают вложенный список в плоский, фигурные скобки с двоеточием дают словарь. Порядок вложенных for такой же, как в обычном цикле.

any / all / sum / min / max
any(x > 0 for x in a)
all(x > 0 for x in a)
sum(a)
min(a)
max(a)
max(a, key=abs)
max(a, default=0)

any/all останавливаются на первом решающем элементе. key задаёт, по чему сравнивать; default спасает от ошибки на пустом списке.

Создание двумерной сетки
grid = [[0] * m for _ in range(n)]   # ✓
grid = [[0] * m] * n                # ✗

Второй вариант неверен: умножение списка копирует ссылку, а не содержимое, и все строки окажутся одной и той же. Стройте через включение.

Создание и повторение списка
a = [1, 2, 3]
b = list(range(3))
zeros = [0] * 5
rows = [[0] * 3] * 2

Литерал и `list()` создают список, а умножение повторяет его элементы. При повторении вложенного изменяемого объекта копируются ссылки, поэтому строки в `rows` связаны между собой.

Индекс и количество элементов
a = [4, 2, 4]
i = a.index(4)
n = a.count(4)

`index()` возвращает позицию первого совпадения, а `count()` — число совпадений. Если значения нет, `index()` выбрасывает `ValueError`.

Удаление элементов списка
a = [10, 20, 30, 20]
a.remove(20)
del a[0]
x = a.pop()

`remove()` удаляет первое совпавшее значение, `del` удаляет по индексу или срезу, а `pop()` удаляет и возвращает элемент. Отсутствующее значение для `remove()` и неверный индекс приводят к исключению.

Поверхностная и глубокая копии списка
from copy import deepcopy

a = [[1], [2]]
b = a.copy()
c = deepcopy(a)
a[0].append(9)

`copy()` создаёт новый внешний список, но сохраняет ссылки на вложенные объекты. `deepcopy()` рекурсивно копирует вложенное содержимое, но обычно требует больше времени и памяти.

Распаковка списка в аргументы
def add(a, b):
    return a + b

nums = [2, 3]
result = add(*nums)

Оператор `*` передаёт элементы списка как отдельные позиционные аргументы. Их количество должно подходить сигнатуре функции, если она не принимает `*args`.

Кортеж как неизменяемая последовательность
point = (2, 3)
seen = {point}

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

Словари и множества13

Словарь: доступ и значения по умолчанию
d[k]                          # KeyError
d.get(k)                      # None
d.get(k, 0)
d.setdefault(k, []).append(x)

get не бросает исключение. setdefault создаёт значение, если ключа ещё нет, и сразу возвращает его.

Обход словаря
for k in d:
    ...
for k, v in d.items():
    ...
d.keys()
d.values()

Перебор по словарю идёт по ключам. Порядок вставки сохраняется начиная с Python 3.7.

Counter
from collections import Counter
c = Counter(a)
c[x]                        # 0
c.most_common(2)
Counter(s1) == Counter(s2)

Готовый счётчик частот. Отсутствующий ключ даёт 0, а не ошибку; most_common отдаёт топ по убыванию.

defaultdict
from collections import defaultdict
g = defaultdict(list)
g[k].append(x)
cnt = defaultdict(int)
cnt[k] += 1

Значение создаётся само при первом обращении. Удобно для группировки и списков смежности графа.

Множества
s = set(a)
s.add(x)
s.discard(x)
x in s          # O(1)
a_set & b_set
a_set | b_set
a_set - b_set

Проверка вхождения за O(1) вместо O(n) у списка. & это пересечение, | объединение, - разность. Множество не хранит порядок и не допускает повторов.

Приём «видели раньше»
seen = set()
for x in a:
    if x in seen:
        return True
    seen.add(x)

Поиск дубликата за один проход и O(n). Тот же приём даёт «два слагаемых»: искать target - x в seen.

Создание словаря
d1 = {"a": 1}
d2 = dict(a=1)
d3 = dict([("a", 1)])
d4 = {k: 0 for k in ("a", "b")}

Словарь можно создать литералом, через `dict()`, из последовательности пар или словарным включением. Именованные аргументы `dict()` подходят только для строковых ключей, являющихся допустимыми идентификаторами.

Проверка и удаление ключа
d = {"a": 1, "c": 3}
"a" in d  # True
x = d.pop("a", 0)
y = d.pop("b", 0)
del d["c"]

Оператор `in` для словаря проверяет ключи, а не значения. `del` выбрасывает `KeyError` для отсутствующего ключа, тогда как `pop(key, default)` возвращает запасное значение.

Представления словаря
d = {"a": 1, "b": 2}
keys = d.keys()
values = d.values()
pairs = d.items()
d["c"] = 3
list(keys)  # ["a", "b", "c"]

Методы `keys()`, `values()` и `items()` возвращают динамические представления, а не списки. Они отражают последующие изменения словаря; для независимого снимка нужна явная конвертация в `list`.

Объединение словарей
a = {"x": 1}
b = {"x": 2, "y": 3}
c = a | b
a.update(b)

Оператор `|` создаёт новый словарь, а `update()` изменяет существующий. При совпадении ключей побеждает значение из правого словаря или переданного источника.

Сортировка словаря по значению
d = {"a": 3, "b": 1, "c": 2}
pairs = sorted(d.items(), key=lambda item: item[1])

Для сортировки по значениям сортируют пары из `items()` с ключом по второму элементу. Результатом является список кортежей, а не автоматически отсортированный словарь.

Подсчёт частот словарём
freq = {}
for x in [2, 1, 2]:
    freq[x] = freq.get(x, 0) + 1

Частоты можно считать обычным словарём, получая ноль для ещё не встречавшегося ключа. Без значения по умолчанию `get()` вернёт `None`, которое нельзя складывать с числом.

Frozenset как ключ словаря
edge = frozenset({2, 5})
weights = {edge: 7}

`frozenset` — неизменяемое и хешируемое множество, поэтому его можно использовать как ключ словаря или элемент другого множества. Обычный `set` для этого не подходит.

Коллекции стандартной библиотеки10

Операции с концами deque
from collections import deque
q = deque([2, 3])
q.appendleft(1)
q.append(4)
left = q.popleft()
right = q.pop()

deque позволяет добавлять и удалять элементы с обоих концов за O(1). Для очереди используй append и popleft, а не pop(0) у списка.

Ограниченный deque и поворот
from collections import deque
q = deque([1, 2, 3], maxlen=3)
q.append(4)
q.rotate(1)
q.rotate(-1)

maxlen автоматически удаляет элементы с противоположного конца при переполнении. rotate сдвигает элементы вправо при положительном аргументе и влево при отрицательном.

Минимальная куча
import heapq
h = [4, 1, 7, 2]
heapq.heapify(h)
heapq.heappush(h, 0)
smallest = heapq.heappop(h)

heapq реализует минимальную кучу: на вершине всегда находится наименьший элемент. heapify перестраивает список за O(n), а push и pop работают за O(log n).

Выбор элементов и максимальная куча
import heapq
nums = [4, 1, 7, 2]
largest = heapq.nlargest(2, nums)
smallest = heapq.nsmallest(2, nums)
h = [-x for x in nums]
heapq.heapify(h)
maximum = -heapq.heappop(h)

nlargest и nsmallest удобны для выбора k крайних элементов без полной сортировки. Максимальную кучу обычно имитируют, сохраняя числа с противоположным знаком.

Операции Counter
from collections import Counter
a = Counter("banana")
a.update("band")
top = a.most_common(2)
b = Counter("an")
added = a + b
common = a & b

Counter хранит частоты элементов и поддерживает most_common, update и операции между счетчиками. Арифметические операции создают новый Counter и обычно исключают нулевые и отрицательные результаты.

Словарь со значением по умолчанию
from collections import defaultdict
groups = defaultdict(list)
counts = defaultdict(int)
seen = defaultdict(set)
groups["a"].append(1)
value = counts["missing"]

defaultdict создаёт значение через фабрику при обращении к отсутствующему ключу. Простое чтение через квадратные скобки изменяет словарь, поэтому для проверки существования используй in или get.

Упорядоченный словарь
from collections import OrderedDict
d = {"a": 1, "b": 2}
od = OrderedDict(d)
od.move_to_end("a")
first = od.popitem(last=False)

Обычный dict уже сохраняет порядок вставки, поэтому OrderedDict нужен главным образом для явного перемещения и удаления с нужного конца. Сравнение двух OrderedDict также учитывает порядок элементов.

Именованный кортеж
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(2, 3)
x = p.x

namedtuple создаёт компактный неизменяемый тип с доступом к полям по имени и индексу. Для сложных моделей с методами и значениями по умолчанию обычно удобнее dataclass.

Типизированный массив и байты
from array import array
nums = array("i", [1, 2, 3])
raw = bytes([65, 66, 67])
mutable = bytearray(raw)
mutable[0] = 90

array хранит значения одного примитивного типа плотнее обычного списка, но редко нужен в алгоритмических задачах. bytes неизменяем, а bytearray допускает изменение отдельных байтов.

Цепочка словарей
from collections import ChainMap
defaults = {"timeout": 30}
overrides = {"timeout": 10}
config = ChainMap(overrides, defaults)
value = config["timeout"]

ChainMap выполняет поиск по нескольким словарям слева направо без их копирования и объединения. Запись по умолчанию изменяет только первый словарь цепочки.

Итерация и генераторы10

Генераторное и списковое выражения
squares_list = [x * x for x in values]
squares_gen = (x * x for x in values)

Списковое включение сразу создаёт весь список, а генераторное выражение выдаёт элементы по запросу. Генератор экономит память, но обычно проходит только один раз.

Генератор yield
def countdown(n):
    while n > 0:
        yield n
        n -= 1

Наличие yield превращает функцию в генераторную: вызов возвращает генератор, а тело выполняется при переборе. После каждого yield выполнение продолжается с сохранённого места.

Делегирование yield from
def flatten(chunks):
    for chunk in chunks:
        yield from chunk

yield from передаёт элементы вложенного итерируемого объекта без ручного внутреннего цикла. Он разворачивает только один уровень за каждое применение.

Итератор, next и StopIteration
it = iter([10])
first = next(it)
missing = next(it, None)

try:
    next(it)
except StopIteration:
    ...

iter получает итератор, а next извлекает следующий элемент и сдвигает его состояние. Значение по умолчанию предотвращает StopIteration при исчерпании.

Итераторы count, cycle и islice
from itertools import count, cycle, islice

numbers = list(islice(count(10, 2), 4))
pattern = list(islice(cycle("ab"), 5))

count и cycle создают потенциально бесконечные итераторы, а islice ограничивает их без материализации всей последовательности. islice не поддерживает отрицательные индексы и отрицательный шаг.

Объединение итераторов chain
from itertools import chain

merged = list(chain([1, 2], [3, 4]))
flat = list(chain.from_iterable([[1, 2], [3, 4]]))

chain лениво перебирает несколько итерируемых объектов один за другим. chain.from_iterable принимает внешний итерируемый объект и разворачивает один уровень.

Произведение, перестановки и сочетания
from itertools import combinations, permutations, product

pairs = list(product([0, 1], repeat=2))
orders = list(permutations([1, 2, 3], 2))
choices = list(combinations([1, 2, 3], 2))

product строит декартово произведение, permutations учитывает порядок, а combinations — нет. Все функции возвращают итераторы, но число результатов может расти комбинаторно.

Группировка groupby
from itertools import groupby

items = [1, 1, 2, 2, 2, 3]
groups = [(key, list(group)) for key, group in groupby(items)]

groupby объединяет только соседние элементы с одинаковым ключом. Для общей группировки данные обычно сначала сортируют по тому же ключу.

Обратный обход reversed
a = [1, 2, 3]
it = reversed(a)
b = list(it)

reversed возвращает итератор обратного обхода и не изменяет исходную последовательность. Объект должен поддерживать обратную итерацию или доступ по индексам и длину.

Агрегация генераторов
total = sum(x * x for x in values)
has_negative = any(x < 0 for x in values)
all_even = all(x % 2 == 0 for x in values)

sum потребляет все значения генератора, а any и all завершаются при первом достаточном результате. Для пустого источника any возвращает False, а all — True.

Числа и математика10

Целые числа произвольной точности
n = 10**100
n.bit_length()

Тип `int` хранит целые числа произвольного размера, поэтому переполнения фиксированной разрядности нет. Ограничения определяются памятью и временем вычислений, но начиная с Python 3.11 преобразование между `int` и `str` ограничено 4300 знаками и сверх этого вызывает `ValueError`.

Точность чисел с плавающей точкой
0.1 + 0.2 == 0.3  # False
x = float("inf")

Тип `float` использует двоичное приближение, поэтому многие десятичные дроби хранятся неточно. Для сравнений обычно применяют `math.isclose`, а для точной десятичной арифметики — `decimal.Decimal`.

Банковское округление
round(2.5)  # 2
round(3.5)  # 4
round(12.345, 2)

`round` округляет точную середину к ближайшему четному значению, а не всегда вверх. Результат для `float` также зависит от погрешности двоичного представления.

Модуль и возведение в степень
abs(-7)  # 7
2**10  # 1024
pow(2, 10)  # 1024
pow(2, 10, 1000)  # 24

`abs` возвращает модуль, а `**` и `pow` возводят число в степень. Трехаргументный `pow(a, b, mod)` эффективно вычисляет степень по модулю без создания огромного промежуточного числа.

Корни и целочисленная математика
from math import sqrt, isqrt, gcd, factorial
sqrt(81)  # 9.0
isqrt(80)  # 8
gcd(18, 24)  # 6
factorial(5)  # 120

`sqrt` возвращает вещественный корень, а `isqrt` — точную целую часть корня без ошибок `float`. `gcd` вычисляет НОД, а `factorial` принимает только неотрицательные целые числа.

Округление вниз, вверх и бесконечность
from math import ceil, floor, inf
ceil(2.1)  # 3
floor(2.9)  # 2
inf > 10**100  # True

`ceil` округляет к положительной бесконечности, а `floor` — к отрицательной, что особенно важно для отрицательных чисел. `math.inf` сравнивается как бесконечность, но не возникает автоматически при делении на ноль.

Деление на ноль
try:
    1 / 0
except ZeroDivisionError:
    ...

Деление, целочисленное деление и остаток с нулевым делителем вызывают `ZeroDivisionError`. Python не превращает такое деление в бесконечность.

Преобразования чисел и системы счисления
int("101", 2)  # 5
int("0xff", 0)  # 255
int(3.9)  # 3
float("2.5")  # 2.5
str(42)  # "42"
bin(10)  # "0b1010"
hex(255)  # "0xff"
oct(8)  # "0o10"

`int` может разбирать строки с основанием от 2 до 36, а основание `0` распознает стандартный префикс автоматически. Преобразование `float` в `int` отбрасывает дробную часть к нулю, а `bin`, `hex` и `oct` возвращают строки с префиксами.

Побитовые операции
x = 0b1100
y = 0b1010
x & y           # 8
x | y           # 14
x ^ y           # 6
~x              # -13
x << 2          # 48
x >> 2          # 3
x.bit_count()   # 2

Побитовые операции работают с двоичным представлением целых чисел и часто используются для масок и множеств состояний. Целые числа не имеют фиксированной ширины, а `~x` всегда равно `-x - 1`.

Псевдослучайные значения
import random
rng = random.Random(42)
rng.randrange(1, 10)
rng.randint(1, 10)
rng.choice([1, 2, 3])
a = [1, 2, 3]
rng.shuffle(a)

`random` генерирует воспроизводимые псевдослучайные значения: `randrange` исключает правую границу, а `randint` включает ее. Модуль не подходит для паролей и криптографии.

Сортировка и поиск6

sorted / list.sort
b = sorted(a)
a.sort()
sorted(a, reverse=True)

sorted возвращает новый список, sort меняет исходный и возвращает None. Сортировка устойчивая.

Ключ сортировки
sorted(words, key=len)
sorted(pairs, key=lambda p: p[1])
sorted(a, key=lambda x: (-x[1], x[0]))

key вызывается по разу на элемент. Кортеж задаёт несколько уровней, минус разворачивает числовое поле.

Устойчивость сортировки
a.sort(key=lambda x: x.age)
a.sort(key=lambda x: x.name)

Равные элементы сохраняют исходный порядок. Поэтому сортировать можно последовательно, начиная с младшего ключа.

itemgetter
from operator import itemgetter
sorted(rows, key=itemgetter(1, 0))

То же, что lambda, но быстрее и читается лучше при сортировке по нескольким полям.

bisect
from bisect import bisect_left, bisect_right, insort
i = bisect_left(a, x)
j = bisect_right(a, x)
insort(a, x)

Двоичный поиск по отсортированному списку за O(log n). bisect_left даёт первую позицию x, bisect_right — позицию сразу за ним, поэтому j - i это количество вхождений. insort вставляет, сохраняя порядок.

Двоичный поиск вручную
lo, hi = 0, len(a) - 1
while lo <= hi:
    mid = (lo + hi) // 2
    if a[mid] == x:
        return mid
    if a[mid] < x:
        lo = mid + 1
    else:
        hi = mid - 1
return -1

Список должен быть отсортирован. Границы двигают за mid, иначе цикл зациклится.

Стек, очередь и два указателя9

Список как стек
stack = []
stack.append(1)
stack.append(2)
top = stack.pop()

Список эффективно реализует стек LIFO через append и pop с конца за амортизированное O(1). Удаление с начала списка требует O(n).

Deque как очередь
from collections import deque
q = deque([1, 2])
q.append(3)
first = q.popleft()

Очередь FIFO удобно реализуется через deque: append добавляет справа, а popleft удаляет слева за O(1). Список с pop(0) для большой очереди не подходит из-за сдвига элементов.

Монотонный стек
def next_greater(nums):
    result = [-1] * len(nums)
    stack = []
    for i, x in enumerate(nums):
        while stack and nums[stack[-1]] < x:
            result[stack.pop()] = x
        stack.append(i)
    return result

Монотонный стек хранит элементы или индексы в отсортированном по стеку порядке и находит ближайший больший или меньший элемент за O(n). В стек часто нужно класть индексы, чтобы вычислять расстояния и позиции.

Проверка скобок
def is_balanced(s):
    pairs = {")": "(", "]": "[", "}": "{"}
    stack = []
    for ch in s:
        if ch in "([{":
            stack.append(ch)
        elif ch in pairs and (not stack or stack.pop() != pairs[ch]):
            return False
    return not stack

Открывающие скобки кладутся в стек, а закрывающая должна совпасть с его вершиной. После обработки стек обязан быть пустым, иначе остались незакрытые скобки.

Два указателя навстречу
def has_pair(nums, target):
    left, right = 0, len(nums) - 1
    while left < right:
        total = nums[left] + nums[right]
        if total == target:
            return True
        if total < target:
            left += 1
        else:
            right -= 1
    return False

Указатели с двух концов сокращают область поиска, когда направление движения можно определить по текущему результату. Для поиска пары по сумме массив должен быть отсортирован.

Быстрый и медленный указатели
def middle_node(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    return slow

Быстрый указатель движется быстрее медленного, что позволяет искать середину или цикл без дополнительной памяти. Условия цикла должны проверять и fast, и следующий узел перед двойным переходом.

Удаление дубликатов на месте
def remove_duplicates(nums):
    if not nums:
        return 0
    write = 1
    for read in range(1, len(nums)):
        if nums[read] != nums[write - 1]:
            nums[write] = nums[read]
            write += 1
    return write

Указатель чтения просматривает массив, а указатель записи формирует корректный префикс без дополнительного массива. Этот вариант предполагает, что одинаковые элементы стоят рядом, обычно после сортировки.

Разворот на месте
def reverse_in_place(nums):
    left, right = 0, len(nums) - 1
    while left < right:
        nums[left], nums[right] = nums[right], nums[left]
        left += 1
        right -= 1

Обмен элементов с двух концов разворачивает изменяемую последовательность за O(n) времени и O(1) дополнительной памяти. Строку так изменить нельзя, потому что она неизменяема.

Слияние отсортированных последовательностей
def merge_sorted(a, b):
    i = j = 0
    result = []
    while i < len(a) and j < len(b):
        if a[i] <= b[j]:
            result.append(a[i])
            i += 1
        else:
            result.append(b[j])
            j += 1
    result.extend(a[i:])
    result.extend(b[j:])
    return result

Два указателя сливают отсортированные последовательности за O(n + m), каждый раз выбирая меньший текущий элемент. После завершения основного цикла нужно добавить оставшийся хвост.

Скользящее окно и префиксные суммы9

Окно фиксированного размера
def window_sums(nums, k):
    if k > len(nums):
        return []
    current = sum(nums[:k])
    result = [current]
    for right in range(k, len(nums)):
        current += nums[right] - nums[right - k]
        result.append(current)
    return result

Фиксированное окно обновляет состояние, добавляя новый элемент и удаляя вышедший, поэтому все окна обрабатываются за O(n). Нужно отдельно определить поведение для k, равного нулю или превышающего длину массива.

Окно переменного размера
def min_length(nums, target):
    left = 0
    total = 0
    answer = len(nums) + 1
    for right, x in enumerate(nums):
        total += x
        while total >= target:
            answer = min(answer, right - left + 1)
            total -= nums[left]
            left += 1
    return 0 if answer > len(nums) else answer

Переменное окно расширяется справа и сжимается слева, пока выполняется нужное условие. Метод с суммой обычно требует неотрицательных элементов, иначе монотонность окна нарушается.

Счётчик элементов в окне
from collections import Counter

def anagram_windows(s, pattern):
    need = Counter(pattern)
    window = Counter()
    result = []
    k = len(pattern)
    for right, ch in enumerate(s):
        window[ch] += 1
        if right >= k:
            left_ch = s[right - k]
            window[left_ch] -= 1
            if window[left_ch] == 0:
                del window[left_ch]
        if window == need:
            result.append(right - k + 1)
    return result

Частоты в окне обновляются только для входящего и выходящего элементов, что избавляет от повторного подсчёта. Нулевые значения лучше удалять, иначе сравнение Counter может стать менее очевидным.

Максимальная сумма окна
def max_window_sum(nums, k):
    current = sum(nums[:k])
    best = current
    for right in range(k, len(nums)):
        current += nums[right] - nums[right - k]
        best = max(best, current)
    return best

Сумма следующего окна получается из предыдущей вычитанием левого и добавлением правого элемента. Инициализируй максимум первым полным окном, а не нулём, если числа могут быть отрицательными.

Префиксные суммы
def range_sum(prefix, left, right):
    return prefix[right] - prefix[left]

nums = [2, 4, 1]
prefix = [0]
for x in nums:
    prefix.append(prefix[-1] + x)
value = range_sum(prefix, 1, 3)

Префиксный массив с начальным нулём позволяет получать сумму полуинтервала [left, right) за O(1). Дополнительный ноль упрощает границы и запросы, начинающиеся с первого элемента.

Разности соседних элементов
nums = [3, 8, 10, 15]
diffs = [b - a for a, b in zip(nums, nums[1:])]

Разности соседних элементов описывают изменения между позициями и помогают восстанавливать значения через накопление. Результат короче исходной последовательности на один элемент.

Накопление значений
from itertools import accumulate
prefix = list(accumulate([2, 4, 1], initial=0))
products = list(accumulate([2, 3, 4], lambda a, b: a * b))

accumulate лениво выдаёт промежуточные результаты накопления и поддерживает пользовательскую бинарную функцию. initial добавляет начальное значение в результат и влияет на его длину.

Подотрезок с заданной суммой
def has_subarray_sum(nums, target):
    seen = {0}
    prefix = 0
    for x in nums:
        prefix += x
        if prefix - target in seen:
            return True
        seen.add(prefix)
    return False

Подотрезок имеет сумму target, если два префикса отличаются на target. Множество префиксов даёт O(n) и работает с отрицательными числами, в отличие от обычного окна по сумме.

Двоичный поиск по ответу
def first_true(low, high, feasible):
    while low < high:
        mid = (low + high) // 2
        if feasible(mid):
            high = mid
        else:
            low = mid + 1
    return low

Двоичный поиск по ответу ищет границу, где монотонный предикат меняется с False на True. Главная сложность — правильно выбрать включительные границы и доказать монотонность feasible.

Рекурсия и динамическое программирование10

Базовый случай и рекурсивный шаг
def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)

Базовый случай останавливает рекурсию, а рекурсивный шаг сводит задачу к меньшему экземпляру. Без достижимого базового случая вызовы закончатся RecursionError.

Лимит глубины рекурсии
import sys
sys.setrecursionlimit(200000)

Python ограничивает глубину стека и при превышении выбрасывает RecursionError. Повышать лимит нужно осторожно: слишком глубокая рекурсия может исчерпать системный стек.

Хвостовая рекурсия
def factorial_tail(n, acc=1):
    if n <= 1:
        return acc
    return factorial_tail(n - 1, acc * n)

Хвостовой вызов является последней операцией функции, но Python не оптимизирует такие вызовы. Поэтому хвостовая рекурсия все равно расходует стек и обычно заменяется циклом.

Декораторы мемоизации
from functools import cache, lru_cache

@lru_cache(maxsize=None)
def fib_lru(n):
    return n if n < 2 else fib_lru(n - 1) + fib_lru(n - 2)

@cache
def fib_cache(n):
    return n if n < 2 else fib_cache(n - 1) + fib_cache(n - 2)

Декораторы сохраняют результаты вызовов по аргументам и устраняют повторные вычисления. cache эквивалентен lru_cache с неограниченным размером, а аргументы должны быть хешируемыми.

Динамическое программирование снизу вверх
def fib(n):
    if n < 2:
        return n
    dp = [0] * (n + 1)
    dp[1] = 1
    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    return dp[n]

Подход снизу вверх вычисляет состояния от простых к сложным и хранит их в массиве. Он не использует рекурсивный стек, но иногда массив можно сократить до нескольких переменных.

Двумерное динамическое программирование
def grid_paths(rows, cols):
    dp = [[0] * cols for _ in range(rows)]
    for r in range(rows):
        dp[r][0] = 1
    for c in range(cols):
        dp[0][c] = 1
    for r in range(1, rows):
        for c in range(1, cols):
            dp[r][c] = dp[r - 1][c] + dp[r][c - 1]
    return dp[-1][-1]

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

Восстановление ответа в динамике
def min_coins(coins, amount):
    inf = amount + 1
    dp = [0] + [inf] * amount
    prev = [-1] * (amount + 1)
    for total in range(1, amount + 1):
        for coin in coins:
            if coin <= total and dp[total - coin] + 1 < dp[total]:
                dp[total] = dp[total - coin] + 1
                prev[total] = coin
    if dp[amount] == inf:
        return None
    answer = []
    while amount > 0:
        coin = prev[amount]
        answer.append(coin)
        amount -= coin
    return answer

Помимо оптимального значения DP может хранить выбор, из которого получено каждое состояние. Затем ответ восстанавливается обратным проходом от конечного состояния.

Разделяй и властвуй
def merge_sort(a):
    if len(a) <= 1:
        return a
    mid = len(a) // 2
    left = merge_sort(a[:mid])
    right = merge_sort(a[mid:])
    result = []
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    return result + left[i:] + right[j:]

Задача делится на независимые подзадачи, их решения вычисляются рекурсивно и затем объединяются. Эффективность зависит от сбалансированности разбиения и стоимости объединения.

Перестановки с возвратом
def permutations(a):
    result = []

    def backtrack(start):
        if start == len(a):
            result.append(a.copy())
            return
        for i in range(start, len(a)):
            a[start], a[i] = a[i], a[start]
            backtrack(start + 1)
            a[start], a[i] = a[i], a[start]

    backtrack(0)
    return result

Backtracking выбирает вариант, запускает рекурсию и обязательно откатывает измененное состояние. Полная генерация перестановок требует факториального времени.

Подмножества с возвратом
def subsets(a):
    result = []
    current = []

    def backtrack(index):
        if index == len(a):
            result.append(current.copy())
            return
        backtrack(index + 1)
        current.append(a[index])
        backtrack(index + 1)
        current.pop()

    backtrack(0)
    return result

Для каждого элемента рекурсия рассматривает два выбора: взять его или пропустить. Список текущего ответа нужно копировать при сохранении и откатывать после рекурсивного вызова.

Графы и деревья10

Список смежности
edges = [(0, 1), (0, 2), (1, 2)]
graph = {0: [], 1: [], 2: []}
for u, v in edges:
    graph[u].append(v)
    graph[v].append(u)

Список смежности хранит для каждой вершины ее соседей и обычно требует O(V + E) памяти. Для ориентированного графа обратное ребро добавлять не нужно.

Кратчайший путь в невзвешенном графе
from collections import deque

def shortest_path(graph, start, target):
    queue = deque([start])
    parent = {start: None}
    while queue:
        node = queue.popleft()
        if node == target:
            break
        for neighbor in graph[node]:
            if neighbor not in parent:
                parent[neighbor] = node
                queue.append(neighbor)
    if target not in parent:
        return None
    path = []
    node = target
    while node is not None:
        path.append(node)
        node = parent[node]
    return path[::-1]

BFS первым достигает вершины по пути с минимальным числом ребер. Для восстановления самого пути достаточно сохранить родителя каждой впервые посещенной вершины.

Обходы дерева в глубину
def preorder(node, result):
    if node is None:
        return
    result.append(node.val)
    preorder(node.left, result)
    preorder(node.right, result)

def inorder(node, result):
    if node is None:
        return
    inorder(node.left, result)
    result.append(node.val)
    inorder(node.right, result)

def postorder(node, result):
    if node is None:
        return
    postorder(node.left, result)
    postorder(node.right, result)
    result.append(node.val)

Preorder обрабатывает узел до детей, inorder между левым и правым поддеревьями, а postorder после детей. Inorder дает отсортированный порядок только для корректного бинарного дерева поиска.

Обход дерева по уровням
from collections import deque

def level_order(root):
    if root is None:
        return []
    queue = deque([root])
    levels = []
    while queue:
        level = []
        for _ in range(len(queue)):
            node = queue.popleft()
            level.append(node.val)
            if node.left is not None:
                queue.append(node.left)
            if node.right is not None:
                queue.append(node.right)
        levels.append(level)
    return levels

Обход по уровням является BFS по дереву и использует очередь. Размер очереди в начале итерации фиксирует число узлов текущего уровня.

Поиск цикла в ориентированном графе
def has_cycle(graph):
    state = {}

    def dfs(node):
        state[node] = 1
        for neighbor in graph[node]:
            if state.get(neighbor, 0) == 1:
                return True
            if state.get(neighbor, 0) == 0 and dfs(neighbor):
                return True
        state[node] = 2
        return False

    return any(state.get(node, 0) == 0 and dfs(node) for node in graph)

В ориентированном графе ребро в вершину текущего стека DFS означает цикл. Обычно используются три состояния: не посещена, обрабатывается и полностью обработана.

Топологическая сортировка
from collections import deque

def topological_sort(graph):
    indegree = {node: 0 for node in graph}
    for node in graph:
        for neighbor in graph[node]:
            indegree[neighbor] += 1
    queue = deque(node for node in graph if indegree[node] == 0)
    order = []
    while queue:
        node = queue.popleft()
        order.append(node)
        for neighbor in graph[node]:
            indegree[neighbor] -= 1
            if indegree[neighbor] == 0:
                queue.append(neighbor)
    return order if len(order) == len(graph) else None

Алгоритм Кана последовательно удаляет вершины с нулевой входящей степенью. Если обработаны не все вершины, граф содержит цикл и топологического порядка не существует.

Система непересекающихся множеств
class DSU:
    def __init__(self, n):
        self.parent = list(range(n))
        self.size = [1] * n

    def find(self, x):
        while x != self.parent[x]:
            self.parent[x] = self.parent[self.parent[x]]
            x = self.parent[x]
        return x

    def union(self, a, b):
        a = self.find(a)
        b = self.find(b)
        if a == b:
            return False
        if self.size[a] < self.size[b]:
            a, b = b, a
        self.parent[b] = a
        self.size[a] += self.size[b]
        return True

Union-Find быстро проверяет принадлежность вершин одной компоненте и объединяет компоненты. Сжатие путей и объединение по размеру дают почти постоянное амортизированное время операции.

Исключения10

Полная конструкция обработки исключений
try:
    value = int(text)
except ValueError:
    value = 0
else:
    value += 1
finally:
    closed = True

except выполняется при исключении, else только при успешном завершении try, а finally выполняется всегда. В блок try лучше помещать только операции, исключения которых действительно ожидаются.

Перехват конкретного исключения
try:
    item = data[key]
except KeyError:
    item = None

Перехватывайте конкретный тип исключения, чтобы не скрывать посторонние ошибки программы. Голый except допустим только в редких случаях, когда исключение затем корректно обрабатывается или повторно выбрасывается.

Перехват нескольких типов исключений
try:
    result = values[index] / divisor
except (IndexError, ZeroDivisionError):
    result = None

Несколько типов с одинаковой обработкой перечисляются кортежем в одном except. Если обработка различается, следует использовать отдельные блоки except.

Создание и повторный выброс исключения
def load_number(text):
    try:
        return int(text)
    except ValueError as exc:
        raise ValueError("invalid number") from exc

def parse_number(text):
    try:
        return load_number(text)
    except ValueError:
        raise

raise создает исключение, а raise без аргументов повторно выбрасывает текущее. Конструкция raise ... from ... сохраняет явную причинную цепочку между исключениями.

Собственное исключение
class InvalidStateError(Exception):
    pass

def require_ready(ready):
    if not ready:
        raise InvalidStateError("not ready")

Собственные исключения наследуются от Exception и позволяют отличать ошибки предметной области от встроенных ошибок. Обычно достаточно пустого класса с понятным названием.

Основные встроенные исключения
def built_in_errors(data, key, index, value, divisor):
    if not isinstance(value, int):
        raise TypeError
    if divisor == 0:
        raise ZeroDivisionError
    if key not in data:
        raise KeyError(key)
    if not -len(data) <= index < len(data):
        raise IndexError(index)
    if value < 0:
        raise ValueError(value)

ValueError означает неподходящее значение корректного типа, TypeError — неподходящий тип, KeyError и IndexError — отсутствующий ключ или индекс. ZeroDivisionError возникает при делении или взятии остатка по нулю.

Проверка assert
def divide(a, b):
    assert b != 0
    return a / b

assert предназначен для проверки внутренних инвариантов и при нарушении вызывает AssertionError. При запуске Python с оптимизацией -O проверки assert удаляются, поэтому ими нельзя валидировать пользовательские данные.

Контекстный менеджер with
with open("data.txt", encoding="utf-8") as file:
    text = file.read()

Контекстный менеджер гарантирует освобождение ресурса при обычном завершении и при исключении. Для файлов это означает автоматическое закрытие после выхода из блока with.

Подавление исключений
from contextlib import suppress
from pathlib import Path

path = Path("data.txt")
with suppress(FileNotFoundError):
    path.unlink()

contextlib.suppress игнорирует только явно перечисленные типы исключений и подходит для коротких ожидаемых случаев. Его не следует использовать для сокрытия ошибок, которые требуют обработки или журналирования.

Подходы EAFP и LBYL
def eafp(data, key):
    try:
        return data[key]
    except KeyError:
        return None

def lbyl(data, key):
    if key in data:
        return data[key]
    return None

EAFP сначала выполняет операцию и обрабатывает ожидаемую ошибку, а LBYL сначала проверяет условие. EAFP часто естественнее для Python и избегает расхождения между проверкой и самой операцией.

Классы и объекты10

Класс, инициализатор и метод экземпляра
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, dx, dy):
        self.x += dx
        self.y += dy

Класс описывает структуру объектов, а __init__ заполняет состояние нового экземпляра. self передается методу автоматически и указывает на конкретный объект.

Атрибуты экземпляра и класса
class Counter:
    total = 0

    def __init__(self):
        self.value = 0
        Counter.total += 1

Атрибут класса общий для всех экземпляров, а атрибут self принадлежит отдельному объекту. Изменение изменяемого атрибута класса может неожиданно затронуть все экземпляры.

Свойство
class Account:
    def __init__(self, balance):
        self.balance = balance

    @property
    def balance(self):
        return self._balance

    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError
        self._balance = value

@property позволяет обращаться к методу как к обычному атрибуту и добавлять проверку чтения или записи. Внутри свойства обычно используют отдельный атрибут, иначе возникнет бесконечная рекурсия.

Статические методы и методы класса
class Temperature:
    def __init__(self, value):
        self.value = value

    @staticmethod
    def is_valid(value):
        return value >= -273.15

    @classmethod
    def zero(cls):
        return cls(0)

@staticmethod не получает ни экземпляр, ни класс, а @classmethod получает класс через cls. Методы класса удобны для альтернативных конструкторов и корректно работают с наследниками.

Наследование и super
class Base:
    def __init__(self, value):
        self.value = value

class Child(Base):
    def __init__(self, value, extra):
        super().__init__(value)
        self.extra = extra

Наследник получает поведение базового класса и может расширять или переопределять его. super() вызывает следующую реализацию по порядку разрешения методов, что особенно важно при множественном наследовании.

Строковые представления объекта
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f'Point(x={self.x}, y={self.y})'

    def __str__(self):
        return f'{self.x}, {self.y}'

__repr__ предназначен для разработчика и отладки, а __str__ — для читаемого вывода пользователю. Если __str__ отсутствует, str() использует __repr__.

Равенство и хеширование
class User:
    def __init__(self, user_id):
        self.user_id = user_id

    def __eq__(self, other):
        return isinstance(other, User) and self.user_id == other.user_id

    def __hash__(self):
        return hash(self.user_id)

__eq__ определяет логическое равенство, а __hash__ позволяет использовать объект как ключ словаря или элемент множества. Равные объекты обязаны иметь одинаковый хеш, а поля хеширования не должны меняться.

Сортировка объектов через __lt__
class Item:
    def __init__(self, score, name):
        self.score = score
        self.name = name

    def __lt__(self, other):
        return (self.score, self.name) < (other.score, other.name)

items = sorted([Item(2, 'b'), Item(1, 'a')])

__lt__ определяет оператор < и позволяет sorted() сравнивать объекты напрямую. Сравнение кортежей удобно задает основной и дополнительные критерии сортировки.

Замороженный dataclass со значениями по умолчанию
from dataclasses import dataclass, field

@dataclass(frozen=True)
class Node:
    value: int
    tags: tuple[str, ...] = ()

@dataclass
class Basket:
    items: list[str] = field(default_factory=list)

@dataclass автоматически создаёт методы вроде __init__ и __repr__, а frozen=True запрещает обычное изменение полей. Неизменяемое значение по умолчанию задаётся напрямую, а изменяемое, например [], вызывает ValueError при создании класса и требует default_factory.

Слоты
class Point:
    __slots__ = ('x', 'y')

    def __init__(self, x, y):
        self.x = x
        self.y = y

__slots__ ограничивает допустимые атрибуты экземпляра и часто уменьшает расход памяти. Без включения __dict__ объекту нельзя динамически добавлять другие атрибуты.

Аннотации типов10

Аннотации переменных и функций
count: int = 3

def repeat(text: str, times: int) -> str:
    return text * times

count = '3'

Аннотации описывают ожидаемые типы для анализаторов, IDE и документации. Python не проверяет их автоматически во время выполнения и допускает присваивание значения другого типа.

Аннотации коллекций
items: list[int] = [1, 2]
scores: dict[str, int] = {'a': 1}
point: tuple[int, int] = (2, 3)

Встроенные коллекции параметризуются типами элементов, ключей и значений. tuple[int, int] описывает фиксированную структуру, а tuple[int, ...] — кортеж произвольной длины.

Необязательные значения
from typing import Optional

left: Optional[int] = None
right: int | None = None

Optional[int] и int | None означают, что значение может быть int или None. Optional не означает необязательный аргумент: для этого у параметра должно быть значение по умолчанию.

Объединение типов
from typing import Union

value_a: Union[int, str] = 1
value_b: int | str = 'x'

Union[int, str] и int | str разрешают один из нескольких типов. Слишком широкие объединения усложняют сужение типов и обработку всех вариантов.

Тип Any
from typing import Any

value: Any = 1
value = 'x'
value = [1, 2]

Any отключает статическую проверку операций со значением и совместим с любым типом в обе стороны. Его избыток скрывает ошибки и сводит пользу аннотаций к минимуму.

Тип вызываемого объекта
from collections.abc import Callable

def apply(fn: Callable[[int, int], int], a: int, b: int) -> int:
    return fn(a, b)

Callable[[A, B], R] описывает вызываемый объект с двумя аргументами и возвращаемым типом R. Callable[..., R] используют, когда точный набор аргументов неизвестен.

Iterable и Sequence
from collections.abc import Iterable, Sequence

def total(values: Iterable[int]) -> int:
    return sum(values)

def first(values: Sequence[int]) -> int:
    return values[0]

Iterable гарантирует возможность обхода, но не индексирование и не повторный проход. Sequence дополнительно поддерживает длину и доступ по индексу, поэтому является более строгим контрактом.

Псевдонимы типов и Literal
from typing import Literal, TypeAlias

UserId: TypeAlias = int
Mode: TypeAlias = Literal['fast', 'safe']

TypeAlias дает сложному типу понятное имя, но не создает новый несовместимый тип. Literal ограничивает значение конкретным набором литералов и полезен для режимов и состояний.

Типизированный словарь
from typing import TypedDict

class UserData(TypedDict):
    name: str
    age: int

user: UserData = {'name': 'a', 'age': 20}

TypedDict описывает обязательные ключи и типы значений обычного словаря. Во время выполнения объект остается dict, а структура проверяется только статическими инструментами.

Обобщенные типы и TypeVar
from typing import Generic, TypeVar

T = TypeVar('T')

class Box(Generic[T]):
    def __init__(self, value: T):
        self.value = value

    def get(self) -> T:
        return self.value

TypeVar связывает типы в разных частях сигнатуры, а Generic создает класс, параметризованный этим типом. В отличие от Any, анализатор сохраняет конкретный тип для каждого использования.

Тесты и отладка10

Тест pytest и assert
def add(a, b):
    return a + b

def test_add():
    assert add(2, 3) == 5

pytest обнаруживает функции, имя которых начинается с test (по умолчанию python_functions = test*, обычно пишут test_), и считает упавший assert ошибкой теста. В assert лучше сравнивать конкретный результат, чтобы отчёт показывал полезную разницу.

Ожидаемое исключение через pytest.raises
import pytest

def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0

pytest.raises проверяет, что блок выбрасывает исключение нужного типа. Если исключение не возникло или его тип отличается, тест падает.

Параметризованный тест
import pytest

@pytest.mark.parametrize('x, expected', [(1, 1), (2, 4), (3, 9)])
def test_square(x, expected):
    assert x * x == expected

parametrize запускает одну тестовую функцию для каждого набора входных данных. Наборы остаются независимыми, поэтому отчет показывает конкретный упавший случай.

Фикстура pytest
import pytest

@pytest.fixture
def sample():
    return [1, 2, 3]

def test_length(sample):
    assert len(sample) == 3

Фикстура подготавливает данные или ресурсы и передается тесту по имени аргумента. Избыточно общие фикстуры создают скрытые зависимости и усложняют понимание теста.

Приблизительное сравнение чисел
import pytest

def test_ratio():
    assert 0.1 + 0.2 == pytest.approx(0.3)

pytest.approx сравнивает числа с допустимой абсолютной или относительной погрешностью. Прямое равенство float ненадежно из-за двоичного представления дробей.

Тестовый класс unittest.TestCase
import unittest

class MathTest(unittest.TestCase):
    def test_add(self):
        self.assertEqual(2 + 3, 5)

unittest.TestCase — встроенная классовая модель тестирования с методами assertEqual, setUp и tearDown. Имя тестового метода должно начинаться с префикса test (TestLoader.testMethodPrefix), иначе стандартный загрузчик его не обнаружит; по соглашению пишут test_.

Тесты в документации
def square(x):
    """
    >>> square(3)
    9
    """
    return x * x

if __name__ == '__main__':
    import doctest
    doctest.testmod()

doctest выполняет примеры из docstring и сравнивает вывод с записанным результатом. Он удобен для коротких примеров, но хрупок при изменениях форматирования.

Замер производительности через timeit
from timeit import timeit

elapsed = timeit('sum(range(100))', number=10_000)

timeit многократно выполняет небольшой фрагмент и уменьшает влияние случайного шума замера. Сравнивать нужно эквивалентные операции в одинаковом окружении, а не делать вывод по одному запуску.

Вывод трассировки исключения
import traceback

try:
    1 / 0
except ZeroDivisionError:
    traceback.print_exc()

traceback.print_exc() выводит текущую трассировку внутри обработчика исключения. Перехватывать ошибку без логирования или повторного выброса опасно, потому что причина сбоя теряется.

Ввод, вывод и файлы10

Чтение одной строки ввода
name = input().strip()

`input` читает одну строку из стандартного ввода, удаляет завершающий перенос и всегда возвращает `str`. Пробелы по краям остаются, если отдельно не вызвать `strip`.

Чтение всего стандартного ввода
import sys
tokens = sys.stdin.buffer.read().split()

`sys.stdin.buffer.read` быстро считывает весь ввод как `bytes`, а `split` разбивает его по любым пробельным символам. Такой подход удобен в алгоритмических задачах, но хранит весь ввод в памяти.

Построчное чтение стандартного ввода
import sys
while line := sys.stdin.readline():
    line = line.rstrip("\r\n")

`sys.stdin.readline` сохраняет завершающий перенос строки и возвращает пустую строку при достижении конца ввода. Построчное чтение не требует загружать все данные в память.

Настройка печати
import sys
print(a, b, sep=",", end="\n", file=sys.stderr)

`sep` задает разделитель между аргументами, `end` — окончание вывода, а `file` — целевой текстовый поток. Большое количество вызовов `print` может быть медленнее формирования одной строки через `join`.

Чтение и запись текстовых файлов
with open("input.txt", "r", encoding="utf-8") as f:
    text = f.read()
with open("output.txt", "w", encoding="utf-8") as f:
    f.write(text)

`with open` гарантирует закрытие файла даже при исключении, а явная кодировка делает поведение переносимым. Режим `w` полностью очищает существующий файл перед записью.

Сериализация JSON
import json
data = json.loads(text)
text = json.dumps(data, ensure_ascii=False)

`json.loads` разбирает JSON-строку в объекты Python, а `json.dumps` выполняет обратное преобразование. JSON не сохраняет различие между списками и кортежами, а ключи объектов после чтения являются строками.

Чтение и запись CSV
import csv
with open("data.csv", newline="", encoding="utf-8") as f:
    rows = list(csv.reader(f))
with open("out.csv", "w", newline="", encoding="utf-8") as f:
    csv.writer(f).writerows(rows)

`csv.reader` и `csv.writer` корректно обрабатывают разделители, кавычки и переносы внутри полей. Файлы CSV следует открывать с `newline=""`, а прочитанные значения по умолчанию остаются строками.

Объекты путей
from pathlib import Path
path = Path("data") / "input.txt"
text = path.read_text(encoding="utf-8")
path.write_text(text, encoding="utf-8")

`pathlib.Path` создает переносимые объекты путей и объединяет части оператором `/`. `read_text` и `write_text` удобны для небольших файлов, но загружают или записывают содержимое целиком.

Переменные окружения
import os
token = os.environ.get("API_TOKEN")

`os.environ` предоставляет переменные окружения процесса как изменяемое отображение строк. Доступ через квадратные скобки вызывает `KeyError` при отсутствии ключа, а `get` возвращает `None` или заданное значение по умолчанию.

Аргументы командной строки
import sys
script, *args = sys.argv

`sys.argv` содержит аргументы командной строки как строки, причем элемент с индексом `0` обычно является именем запущенного скрипта. Модуль не выполняет проверку и преобразование аргументов; для сложного интерфейса используют `argparse`.