Python — база, ООП, веб, DevOps
Практический раздел для backend-разработчика (PHP/Laravel) — быстрый вход в Python: синтаксис, ООП, веб-фреймворки (Django/FastAPI/Flask), инструменты. С акцентом на «что здесь по-другому чем в PHP» и «где Python сегодня реально используют».
Python — второй по популярности язык backend-мира после JavaScript/PHP. Для PHP-разработчика это дешёвая инвестиция: половина концепций (ООП, ассоциативные массивы, интерполяция строк, декораторы, замыкания) уже знакомы, различия в основном в синтаксисе. Дальнейшие 20% — асинхронность, type hints и pythonic-стиль — усваиваются за пару недель практики.
| Блок | Что внутри |
|---|---|
| Основы | Установка, синтаксис, типы, строки, условия, циклы, функции, comprehensions. |
| Продвинуто | ООП + dunder, модули, venv/poetry, исключения, файлы, декораторы, type hints, async, генераторы. |
| Веб-фреймворки | Django (батарейки в комплекте), FastAPI (современный async), Flask (микро), сравнение. |
| Данные и HTTP | psycopg2 / SQLAlchemy, requests / httpx, Pandas базово. |
| Инструменты | pytest, logging, ruff/black/mypy. |
| Для PHP-разработчика | Мосты Python ↔ PHP по типовым задачам, частые вопросы. |
ML / AI / Data Science
Абсолютный монополист. NumPy, Pandas, scikit-learn, PyTorch, TensorFlow, LangChain, Anthropic SDK. Если ты пишешь что-то с AI — почти наверняка это Python (или JavaScript-обёртка вокруг Python-модели на сервере).
Backend web / API
Django (Instagram, Pinterest, Reddit — начинали с него), FastAPI (Netflix, Uber — для новых сервисов), Flask (маленькие сервисы, прототипы). Для микросервисов и AI-бэков — FastAPI сейчас де-факто стандарт.
DevOps + автоматизация
Ansible, SaltStack, скрипты деплоя, обёртки над AWS/GCP CLI, парсинг логов, генерация конфигов. В любой команде DevOps — Python как «второй bash».
Data engineering + ETL
Airflow, dbt, Prefect. Загрузка данных из десятков источников в хранилище — почти всегда Python-пайплайны.
Скрипты, парсеры, крон-задачи
Beautiful Soup, Scrapy, requests. Небольшие «одноразовые» задачи — почти всегда Python, потому что быстрее написать, чем на любом другом языке.
{}, плюс лучшая экосистема для AI/Data и async-первый веб через FastAPI. Синтаксис читается как псевдокод. Порог входа для PHP-разработчика — 2 недели чтобы стать продуктивным.
Актуальная стабильная — Python 3.13 (вышла в 2024). Минимум для нового кода — 3.11 (значительный прирост скорости + улучшенные ошибки). Всё что 3.10 и старше — legacy. Python 2 мёртв с 2020 — не трогай, если только не поддерживаешь очень старый код.
# Через Homebrew
brew install python@3.13
python3 --version # Python 3.13.x
# Или через pyenv — если нужно держать несколько версий
brew install pyenv
pyenv install 3.13.1
pyenv install 3.11.9
pyenv global 3.13.1 # default для системы
pyenv local 3.11.9 # для конкретного проекта (создаст .python-version)
/usr/bin/python3) не трогай — это OS-owned интерпретатор для скриптов Apple. Всегда ставь свой через brew / pyenv.
# Ubuntu 24.04 — уже есть Python 3.12 из коробки
python3 --version
# Если нужна другая версия — PPA от deadsnakes
sudo add-apt-repository ppa:deadsnakes/ppa -y
sudo apt update
sudo apt install python3.13 python3.13-venv python3.13-dev -y
pip — Python-аналог composer в PHP или npm в JS. Ставит библиотеки из PyPI (Python Package Index).
# Установить пакет
pip install requests
# Конкретная версия
pip install django==5.0.1
# Список установленных
pip list
# Заморозить зависимости → requirements.txt
pip freeze > requirements.txt
# Восстановить из requirements.txt
pip install -r requirements.txt
# Обновить сам pip
python -m pip install --upgrade pip
vendor/ в PHP, только на уровне интерпретатора. В Ubuntu 24.04 pip install в глобал вообще запрещён по умолчанию (externally-managed-environment).uvuv — новый (2024) менеджер от Astral (авторы ruff). Написан на Rust, в 10-100 раз быстрее pip. Заменяет pip + venv + pip-tools одной утилитой. Стандарт де-факто в новых проектах.
# Установка
curl -LsSf https://astral.sh/uv/install.sh | sh
# Создать проект
uv init my-app && cd my-app
# Добавить зависимость (сам создаст venv, обновит pyproject.toml)
uv add fastapi uvicorn
# Запустить
uv run python main.py
uv run uvicorn main:app --reload
brew/pyenv (macOS) или apt/deadsnakes (Linux). Для проектов используй uv в новых, venv+pip в старых. Никогда не ставь пакеты в глобальный интерпретатор.
{ }В Python отступы — часть синтаксиса. Блок кода определяется одинаковым уровнем отступа. Классические { } отсутствуют. Стандарт: 4 пробела (не таб, никогда не смешивай).
# Python
if user.age >= 18:
print("adult")
send_email(user)
else:
print("minor")
# Аналог в PHP
# if ($user->age >= 18) {
# echo "adult";
# send_email($user);
# } else {
# echo "minor";
# }
TabError: inconsistent use of tabs and spaces. Настрой IDE, чтобы Tab превращался в 4 пробела. В PyCharm/VS Code с плагином Python — по умолчанию так.# Просто присваиваем — тип выводится автоматически
name = "Alice" # str
age = 30 # int
salary = 1500.50 # float
is_admin = True # bool (с большой буквы!)
tags = ["php", "python"] # list
user = {"name": "Alice", "age": 30} # dict
# Constants — просто UPPER_CASE конвенция, языком не защищено
MAX_RETRIES = 3
# Type hints (опционально, для читаемости и mypy)
name: str = "Alice"
age: int = 30
Ключевые отличия от PHP:
- Нет
$перед именем — простоname, не$name. True/False/None— с большой буквы. В PHP былоtrue/false/null.- Нет объявления типа обязательно — можно опционально через
:(type hints). - Констант в языке нет — только конвенция
UPPER_CASE. Реальную неизменяемость даёт толькоFinal[int]изtyping(проверяется mypy, но не runtime).
print() — вывод в консольprint("Hello") # Hello
print("Hello", name) # Hello Alice (через пробел)
print("a", "b", "c", sep="-") # a-b-c
print("no newline", end="") # без переноса строки
print(f"Name: {name}, age: {age}") # f-string — интерполяция
input() — чтение по одной строкеОдин вызов input() читает ровно одну строку — до перевода строки — и возвращает её всегда как str, даже если там одни цифры. Поэтому для чисел результат сразу оборачивают в int(). Если на вход подано несколько строк, вызовы разбирают их по очереди: первый — первую, второй — вторую.
# на вход подано:
# 3
# 6200
# 2150
count = int(input()) # 3
price1 = int(input()) # 6200
price2 = int(input()) # 2150
print(count, price1, price2) # 3 6200 2150
В интерактивном запуске программа на каждом input() останавливается и ждёт ввода с клавиатуры. Когда ввод подаётся из файла или через конвейер, ожидания нет — строки просто разбираются подряд. Для кода разницы никакой: читается следующая непрочитанная строка.
input() убирает, пробелы оставляет. Строка " 42 " вернётся как ' 42 ' целиком. Для int() это неважно — он сам обрезает пробелы по краям, — а вот при сравнении строк пробелы выйдут боком, поэтому текстовый ввод обычно сразу пропускают через .strip().# 1. количество задано первой строкой, дальше по числу в строке
n = int(input())
prices = [int(input()) for _ in range(n)]
print(sum(prices))
# 2. несколько чисел в одной строке через пробел: "6200 2150 11800"
a, b, c = map(int, input().split())
prices = list(map(int, input().split())) # когда их неизвестно сколько
# 3. весь ввод разом, без оглядки на то, как он разбит на строки
import sys
nums = list(map(int, sys.stdin.read().split()))
split() без аргументов. Он режет по любому количеству пробелов и переводов строк сразу и выбрасывает пустые куски. Поэтому третий способ разбирает ввод одинаково, как бы он ни был разложен по строкам: 3 / 6200 2150 / 11800 даст тот же список, что и четыре отдельные строки.| Что произошло | Исключение |
|---|---|
| В строке не число: пустая строка, буквы, запятая вместо точки | ValueError: invalid literal for int() with base 10: 'abc' |
Вызовов input() больше, чем строк во вводе | EOFError: EOF when reading a line |
EOFError почти всегда означает рассогласование с форматом ввода. Лишний input(), неверно понятое количество строк, или первая строка прочитана дважды. Отлаживать это удобнее третьим способом: прочитать всё сразу в список и работать с индексами — тогда видно, сколько значений пришло на самом деле.float(), не int(). int("3.5") завершится той же ValueError: функция не округляет текст, а требует целое. Нужно либо float(input()), либо int(float(input())), если дробную часть действительно надо отбросить.# Однострочный (как // в PHP)
"""
Многострочный (docstring).
Стандарт для документации функций и классов.
"""
def divide(a, b):
"""Делит a на b. Бросает ZeroDivisionError если b == 0."""
return a / b
Тройные кавычки — не «многострочный комментарий» (такого понятия нет), а обычная строка, которую можно не присваивать. Стандартное использование — как первая строка функции/класса — docstring. IDE и help(func) его показывают.
| PHP | Python | Заметка |
|---|---|---|
. (конкатенация) | + | "a" + "b". Только для одинаковых типов — "a" + 1 упадёт. |
===, !== | ==, != | В Python нет loose comparison — обычные операторы уже строгие |
&&, ||, ! | and, or, not | Буквенные ключевые слова |
/ (int div) | // | 7 // 2 == 3. Обычный / всегда возвращает float: 7 / 2 == 3.5 |
** (power) | ** | 2 ** 10 == 1024 |
% (mod) | % | Так же |
| — | in | "a" in ["a", "b"] → True. Работает для list/dict/str/set |
| — | is | Сравнение по идентичности (тот же объект). a is None — правильная проверка на None |
В Python «пустое» = False:
if not users: # True если users — пустой list, dict, str, 0, None, False
print("nothing to do")
# Явно проверить на None (правильный питонический способ)
if user is not None:
...
if x == None — работает, но не питонично. Правильно: if x is None. Причина: is сравнивает идентичность (быстрее и точнее для singletons), None — синглтон.# hello.py
def main():
name = input("Твоё имя: ")
print(f"Привет, {name}!")
if __name__ == "__main__":
main()
$ python hello.py
Твоё имя: Санжар
Привет, Санжар!
Конструкция if __name__ == "__main__": — питонический шаблон. Она выполняется, только когда файл запущен напрямую как скрипт, но не при import. Подробнее — в секции Модули + импорты.
| Python | Пример | PHP-аналог |
|---|---|---|
int | 42 | int |
float | 3.14 | float |
str | "hello" | string |
bool | True / False | bool |
None | None | null |
list | [1, 2, 3] | Индексированный array |
tuple | (1, 2, 3) | Неизменяемый array (нет прямого аналога) |
dict | {"a": 1, "b": 2} | Ассоциативный array |
set | {1, 2, 3} | Нет прямого — используют array_unique |
list — упорядоченный, изменяемыйusers = ["alice", "bob", "charlie"]
# Доступ по индексу
users[0] # "alice"
users[-1] # "charlie" (с конца)
# Slicing — срезы
users[1:3] # ["bob", "charlie"]
users[:2] # ["alice", "bob"]
users[::2] # каждый второй → ["alice", "charlie"]
users[::-1] # reverse
# Модификация
users.append("dave") # в конец
users.insert(0, "zoe") # в начало
users.remove("bob") # удалить по значению
del users[0] # удалить по индексу
users.pop() # удалить и вернуть последний
# Проверка
"alice" in users # True
len(users) # 3
# Сортировка
users.sort() # in-place, вернёт None
sorted(users) # возвращает НОВЫЙ отсортированный, не меняет
sorted(users, reverse=True)
tuple — упорядоченный, неизменяемыйcoords = (55.75, 37.61)
x, y = coords # unpacking — очень частый паттерн
# Неизменяемый:
coords[0] = 99 # TypeError
# Функции часто возвращают tuple — сразу распаковываем:
name, age = get_user()
# Именованные — namedtuple (легковесная замена классу)
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(1, 2)
print(p.x, p.y) # 1 2
Зачем tuple, если есть list: tuple немного быстрее, может быть ключом словаря (list — нельзя), и семантически говорит «эти данные вместе, порядок фиксирован» (координаты, RGB-цвет, дата разбитая на y/m/d).
dict — ассоциативный массив (hash map)user = {
"name": "Alice",
"age": 30,
"roles": ["admin", "editor"],
}
# Доступ
user["name"] # "Alice"
user["phone"] # KeyError!
user.get("phone") # None — безопасно
user.get("phone", "unknown") # дефолт если нет
# Модификация
user["email"] = "a@b.c" # добавили
del user["age"] # удалили
# Обход
for key in user: # обход ключей
print(key, user[key])
for key, value in user.items(): # pythonic — пара сразу
print(key, value)
for value in user.values():
print(value)
# Слияние — Python 3.9+
merged = defaults | overrides # справа побеждает
# Проверка ключа
"name" in user # True — работает по ключам
set — множество без дубликатовtags = {"php", "python", "js", "php"}
print(tags) # {"php", "python", "js"} — дубликат ушёл
# Операции над множествами (математические!)
a = {1, 2, 3}
b = {2, 3, 4}
a | b # объединение → {1, 2, 3, 4}
a & b # пересечение → {2, 3}
a - b # разность → {1}
a ^ b # симметрическая разность → {1, 4}
# Убрать дубликаты из списка
unique_users = list(set(users))
| Тип | Изменяемый? |
|---|---|
int, float, str, bool, None, tuple, frozenset | Immutable |
list, dict, set | Mutable |
Immutable-объекты нельзя менять «на месте» — методы возвращают новую копию. У str: s.upper() вернёт новую строку, оригинал не тронется. Это как в PHP: str — value type.
def add_item(item, items=[]): # ← [] создаётся ОДИН раз при определении функции!
items.append(item)
return items
add_item("a") # ["a"]
add_item("b") # ["a", "b"] !!! копится между вызовами
Правильно: items=None и внутри if items is None: items = [].
| PHP | Python | Комментарий |
|---|---|---|
<?php | Ничего | Python-файл — сразу код, без открывающего тега |
$name | name | Без $ |
echo, print | print() | Функция, всегда со скобками |
{ ... } | Отступы (4 пробела) | Отступ — часть синтаксиса |
; в конце | — | Не нужен |
true/false/null | True/False/None | С большой буквы! |
&&, ||, ! | and, or, not | Буквенные |
. (concat) | + | Только для одинаковых типов |
foreach ($arr as $v) | for v in arr: | Прямой обход значений |
foreach ($arr as $k => $v) | for k, v in arr.items(): | Для dict — метод .items() |
function | def | |
use, namespace | import, from ... import | |
class Foo extends Bar | class Foo(Bar): | Скобки вместо extends |
$this->name | self.name | self — первый аргумент метода явно |
__construct | __init__ | |
const FOO = 1 | FOO = 1 (конвенция) | Реальных констант нет |
?string $name | name: str | None | Type hints |
array_map | list comprehension / map() | [x*2 for x in arr] |
array_filter | list comprehension | [x for x in arr if x > 0] |
compact('user') | {"user": user} | Аналога нет — dict вручную |
json_encode, json_decode | json.dumps, json.loads | Из stdlib import json |
composer | pip / uv / poetry | |
vendor/ | .venv/ или site-packages/ | |
composer.json | requirements.txt / pyproject.toml | |
artisan | manage.py (Django) / скрипты | |
| Laravel | Django (batteries included) / FastAPI (микро+async) |
1. Всё — объект, всё передаётся по ссылке на объект
В PHP массив передаётся по значению (копируется). В Python list и dict — mutable объекты, передаются как ссылки. Функция может изменить исходный список. Похоже на объекты в PHP, но касается вообще всех collections.
2. Асинхронность — первого класса
В PHP async/await — экзотика (Swoole, ReactPHP). В Python — стандарт стандартной библиотеки: asyncio, aiohttp, FastAPI полностью на нём построен. Изучить сразу, если планируешь микросервисы.
3. Type hints — опциональные, но важные
В PHP 7.4+ типы обязательны для сериозного кода. В Python — опциональны, работают через отдельный tool (mypy), рантайм их игнорирует. Но в новом коде почти всегда пишут — читаемость и IDE-подсказки в разы лучше.
4. Duck typing и «easier to ask forgiveness than permission»
В PHP чаще проверяют «есть ли метод» через method_exists. В Python — идиома «просто вызови, и обработай исключение». try: obj.method(); except AttributeError: .... Это питонично.
5. Один способ сделать вещь
Zen of Python: «There should be one — and preferably only one — obvious way to do it». В PHP часто 3-4 способа (array_map / foreach / array_walk). В Python — почти всегда один канонический (list comprehension), остальные — «можно, но обычно не нужно».
6. Многопоточность и GIL
У Python есть Global Interpreter Lock (GIL) — только один поток выполняет Python-код в один момент. Для CPU-bound нагрузки → multiprocessing (отдельные процессы). Для I/O-bound → asyncio. С Python 3.13+ GIL постепенно ослабляется, но пока правило работает.
a = "hello" # двойные
b = 'hello' # одинарные — эквивалентны
c = """multi
line string""" # тройные — многострочная
d = r"C:\path\file" # raw — не экранирует \n \t \
e = b"bytes" # bytes (не str)
f = f"Hi {name}" # f-string — интерполяция
name = "Alice"
age = 30
price = 1234.5678
f"Hi, {name}!" # "Hi, Alice!"
f"{name} is {age} years old" # подстановка нескольких
f"{name.upper()}" # выражения — не только имена
f"total: {a + b}" # арифметика внутри
# Форматирование
f"{price:.2f}" # "1234.57" — округление до 2
f"{price:,.2f}" # "1,234.57" — с разделителем
f"{price:10.2f}" # " 1234.57" — ширина 10, padding
f"{price:>10}" # выравнивание вправо
f"{price:<10}" # влево
f"{price:^10}" # по центру
f"{age:03d}" # "030" — паддинг нулями
f"{price:.0%}" # проценты
f"{now:%Y-%m-%d %H:%M}" # дата (datetime)
# Debug (Python 3.8+) — печатает имя + значение
f"{name=}" # "name='Alice'"
f"{price=:.2f}" # "price=1234.57"
# repr vs str внутри f-string
f"{name!r}" # "'Alice'" — через repr()
f"{name!s}" # "Alice"
.format() и %-форматирование — устаревшее, встречается только в legacy. f-strings быстрее (в 2-3 раза), читаемее, поддерживают все выражения.
Способов встретится четыре, и выглядят они взаимозаменяемыми. На деле первые три дают строку, а четвёртый строку не создаёт вовсе.
text = 'привет'
# 1. f-строка — основной способ
print(f"Он зашёл в чат и написал: {text}!")
# Он зашёл в чат и написал: привет!
# 2. метод .format()
print("Скажи мне {} ещё раз.".format(text))
# Скажи мне привет ещё раз.
# 3. сложение через +
print("Она ответила: " + text)
# Она ответила: привет
# 4. перечисление через запятую в print()
print("Новое сообщение:", text, "всем")
# Новое сообщение: привет всем
| Способ | Что получается | Когда уместен |
|---|---|---|
f"..." | str | Всегда, если текст известен в момент написания кода |
.format() | str | Когда шаблон приходит извне: из настроек, из файла переводов, из базы |
+ | str | Две-три части, и все уже строки. Для длинной сборки нечитаемо |
print(a, b) | None | Только для вывода на экран. Строки на выходе нет |
print сам печатает их подряд, вставляя пробел между соседними. Результата в переменную не положить — print возвращает None. Разделитель меняется параметром: print("a", "b", sep=" | ") напечатает a | b, а end="" уберёт перевод строки в конце."сообщений: " + 5 завершится ошибкой TypeError: can only concatenate str (not "int") to str — неявного приведения, как в PHP, в Python нет. Пришлось бы писать str(5), и это ещё одна причина предпочитать f-строку: внутри {} преобразование выполняется само..format() не исчез совсем. f-строка вычисляется там, где написана, и шаблоном быть не может: её нельзя сохранить в настройках и подставить значения позже. А "Привет, {name}!" — обычная строка, её можно хранить где угодно и применить к данным в нужный момент: template.format(name=user.name). Для переводимых сообщений берут именно этот способ.logger.info("user %s вошёл", name) с подстановкой через %s и отдельными аргументами: сообщение собирается, только если уровень логирования включён. f-строка собралась бы всегда, даже когда запись будет отброшена. Подробнее — в разделе Логирование."".join(parts) или ", ".join(names): склейка списка одним вызовом вместо цепочки плюсов.str| Метод | Что делает | Пример |
|---|---|---|
.upper(), .lower(), .title(), .capitalize() | Регистр | "hello".upper() → "HELLO" |
.strip(), .lstrip(), .rstrip() | Убрать whitespace (или указанные символы) | " hi ".strip() → "hi" |
.split(sep) | Разбить на list по разделителю | "a,b,c".split(",") → ["a","b","c"] |
sep.join(iterable) | Собрать iterable в строку | ",".join(["a","b"]) → "a,b" |
.replace(old, new) | Заменить подстроку | "a-b".replace("-", "_") |
.startswith(x), .endswith(x) | Проверка префикса/суффикса | "file.jpg".endswith(".jpg") |
.find(x), .index(x) | Позиция первого вхождения (find → -1, index → ValueError) | "hello".find("l") → 2 |
x in string | Проверка вхождения | "lo" in "hello" → True |
.count(x) | Число вхождений | "aaa".count("a") → 3 |
.isdigit(), .isalpha(), .isalnum() | Проверка типа | "123".isdigit() → True |
.zfill(n), .center(n), .ljust(n), .rjust(n) | Паддинг | "5".zfill(3) → "005" |
.splitlines() | Разбить по \n / \r\n | Кросс-платформенно |
.encode(enc) | str → bytes | "hi".encode("utf-8") |
Все методы возвращают НОВУЮ строку — str immutable. s.upper() не меняет s, а возвращает новую.
s = "hello world"
s[0] # "h"
s[-1] # "d"
s[0:5] # "hello"
s[:5] # "hello" — с начала
s[6:] # "world" — до конца
s[::-1] # "dlrow olleh" — reverse
s[::2] # "hlowrd" — каждый второй
len(s) # 11
reimport re
# Основные функции
re.search(pattern, string) # найти первое → Match | None
re.match(pattern, string) # только с НАЧАЛА строки
re.fullmatch(pattern, string) # вся строка целиком
re.findall(pattern, string) # все совпадения → list
re.finditer(pattern, string) # все → iterator Match'ей (для больших текстов)
re.sub(pattern, repl, string) # замена
re.split(pattern, string) # split по regex
# Пример: проверить email
if re.fullmatch(r"[\w.+-]+@[\w-]+\.[\w.-]+", email):
print("looks like email")
# Захват групп
m = re.search(r"(\d+)-(\d+)", "order 42-100")
if m:
print(m.group(0)) # "42-100" — вся сматченная строка
print(m.group(1)) # "42"
print(m.group(2)) # "100"
print(m.groups()) # ("42", "100")
# Именованные группы
m = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})", "2026-09")
m.group("year") # "2026"
# Компиляция для повторного использования (быстрее)
EMAIL_RE = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+")
EMAIL_RE.fullmatch(email)
r"..."), иначе \d, \s, \n будут интерпретироваться Python-ом до regex-движка. Флаг re.IGNORECASE для case-insensitive, re.MULTILINE — ^/$ на каждой строке.
В Python 3 str — это Unicode (последовательность code point'ов). bytes — сырые байты. Между ними — явная конвертация через кодировку.
s = "Привет" # str (unicode)
b = s.encode("utf-8") # bytes: b'\xd0\x9f\xd1\x80\xd0\xb8...'
s2 = b.decode("utf-8") # обратно в str
# Файлы
with open("file.txt", encoding="utf-8") as f: # явно всегда utf-8
text = f.read()
# Бинарные — bytes
with open("image.png", "rb") as f:
data = f.read() # bytes
open("f.txt") без явного encoding="utf-8" — на Windows возьмёт cp1251, на Linux — utf-8. Один и тот же код будет ломаться на разных ОС.+ в цикле — медленно. Каждый + создаёт новую строку. Для N-строк — "".join(list_of_strings), работает за O(n) вместо O(n²)..replace() не regex. Убрать все пробелы — s.replace(" ", ""); но убрать любые whitespace — re.sub(r"\s+", "", s)."".split(",") → [""], не []. Проверяй if s перед split.str.format() с dict — точки не работают. "{d.name}".format(d=obj) — обращение по атрибуту, не по ключу dict. Используй f-string."\d+" в Python 3.12+ выдаёт DeprecationWarning. Всегда r"\d+".if age >= 18:
status = "adult"
elif age >= 13:
status = "teen"
else:
status = "child"
# Тернарник: value_if_true if condition else value_if_false
status = "adult" if age >= 18 else "minor"
# Не путать с C-style — синтаксис Python другой:
# status = age >= 18 ? "adult" : "minor" — SyntaxError
# Chained comparisons — красивая питоническая фича
if 18 <= age < 65: # эквивалент age >= 18 and age < 65
...
# Walrus operator := (Python 3.8+) — присвоить внутри выражения
if (n := len(items)) > 10:
print(f"too many: {n}")
for и while# for — обход iterable (list/tuple/dict/str/set/generator)
for user in users:
print(user)
# range(start, stop, step)
for i in range(10): # 0..9
...
for i in range(1, 11): # 1..10
...
for i in range(0, 100, 10): # 0, 10, 20, ..., 90
...
# while — пока условие true
while queue:
task = queue.pop()
process(task)
# break / continue — как везде
for item in items:
if item.is_bad:
continue
if item.is_stop:
break
process(item)
# enumerate — индекс + значение
for i, user in enumerate(users):
print(f"{i}: {user}")
for i, user in enumerate(users, start=1): # с 1
...
# zip — параллельный обход двух iterables
names = ["Alice", "Bob"]
ages = [30, 25]
for name, age in zip(names, ages):
print(name, age)
# dict — .items(), .keys(), .values()
for key, value in user.items():
...
# reversed — обход в обратном порядке
for item in reversed(items):
...
# sorted — обход отсортированного
for user in sorted(users, key=lambda u: u.age):
...
for i in range(len(items)) — это анти-паттерн. Питонично:
- нужно только значение →
for item in items - нужен индекс + значение →
for i, item in enumerate(items) - обход двух списков —
for a, b in zip(list1, list2)
else в цикле — редкая, но живая конструкция# else срабатывает если цикл завершился БЕЗ break
for item in items:
if item.is_target:
print("found!")
break
else:
print("not found") # сработает только если target не встретился
Кто это придумал — вопрос философский. Практика — заменяют flag-переменной для читаемости.
match / case — pattern matching (Python 3.10+)Мощная замена switch — не просто сравнение по значению, а деструктуризация:
# Простой — как switch
match status:
case "active":
handle_active()
case "paused" | "stopped": # несколько значений
handle_off()
case _: # default
handle_unknown()
# Деструктуризация — вот где magic
match event:
case {"type": "order.created", "data": {"id": order_id}}:
process_order(order_id)
case {"type": "payment.received", "data": {"amount": amount, "currency": cur}}:
record_payment(amount, cur)
case {"type": "error", "message": msg}:
logger.error(msg)
# Классы + деструктуризация
match shape:
case Circle(radius=r):
return 3.14 * r ** 2
case Rectangle(width=w, height=h):
return w * h
# Guard — доп условие
match point:
case (x, y) if x == y:
print("on diagonal")
for x in list: list.remove(x) — пропустит элементы. Обходи копию (list[:]) или собери в новый.range() — не список. В Python 3 это ленивый iterator. Хочешь список — list(range(10)). Для миллионных диапазонов важно.zip() обрезает до самого короткого. Для строгой проверки — zip(a, b, strict=True) (Python 3.10+), кинет ValueError.match ≠ switch. case <VAR>: без точки — это захват (bind), а не сравнение. case value: просто присвоит что угодно в value. Для сравнения с константой класса — case Color.RED:.defdef greet(name):
"""Docstring — первая строка, доступна через help(greet)."""
return f"Hello, {name}!"
greet("Alice") # "Hello, Alice!"
# С type hints (рекомендуется в новом коде)
def greet(name: str) -> str:
return f"Hello, {name}!"
# Без явного return — функция вернёт None
def log_it(msg):
print(msg) # возвращает None автоматически
def create_user(name, age, role="user", active=True):
return {"name": name, "age": age, "role": role, "active": active}
# Позиционные — по порядку
create_user("Alice", 30)
# Keyword — по имени, порядок не важен
create_user(age=30, name="Alice", role="admin")
# Смешанно — сначала позиционные, потом keyword
create_user("Alice", 30, role="admin")
# Keyword-only — принудительно через *
def create_order(items, *, currency="USD", express=False):
...
create_order([...], currency="EUR") #
create_order([...], "EUR") # TypeError
# Positional-only — до /
def divmod_(a, b, /): # a и b нельзя передать как keyword
return a // b, a % b
*args и **kwargs — «собирают» и «раскрывают»* и ** — одна операция в две стороны:
- В
def— СОБИРАЮТ много аргументов в один (много → одно) - При вызове — РАСКРЫВАЮТ одну коллекцию во много аргументов (одно → много)
* — работает с позиционными. Два ** — с именованными.
Сторона 1: в def — собирают
# *args — собрать все позиционные в кортеж
def sum_all(*args):
return sum(args)
sum_all(1, 2, 3, 4)
# sum_all(1, 2, 3, 4)
# └──┬───────┘
# *args собирает всё это
# ↓
# args = (1, 2, 3, 4) ← кортеж
# **kwargs — собрать все именованные в словарь
def make_dict(**kwargs):
return kwargs
make_dict(name="Alice", age=30)
# kwargs = {"name": "Alice", "age": 30} ← словарь
# Комбинация — обычная / позиционные / именованные
def log(level, *messages, **extras):
print(level, messages, extras)
log("INFO", "user", "created", user_id=42, ip="1.1.1.1")
# ↓ └────┬─────┘ └──────┬───────────┘
# level *messages **extras
# ("user","created") {"user_id":42,"ip":"1.1.1.1"}
Сторона 2: при вызове — раскрывают (обратная операция)
# * при вызове — раскрыть список в позиционные
def add(a, b, c):
return a + b + c
nums = [1, 2, 3]
add(nums) # TypeError — передался ОДИН список
add(*nums) # = add(1, 2, 3) → 6
# ** при вызове — раскрыть словарь в именованные
def create_user(name, age): ...
kwargs = {"name": "Alice", "age": 30}
create_user(**kwargs) # = create_user(name="Alice", age=30)
Одна таблица — обе стороны:
| Место | Символ | Что делает | Направление |
|---|---|---|---|
в def | *args | СОБИРАЕТ позиционные → кортеж | много → один |
в def | **kwargs | СОБИРАЕТ именованные → словарь | много → один |
| при вызове | f(*list) | РАСКРЫВАЕТ список → позиционные | один → много |
| при вызове | f(**dict) | РАСКРЫВАЕТ словарь → именованные | один → много |
Мнемоника: в def — «звёздочка собирает» (много в одно). При вызове — «звёздочка раскрывает» (одно в много). Это обратные операции одного механизма.
def f(*args, **kwargs):
print("args =", args, type(args).__name__)
print("kwargs =", kwargs, type(kwargs).__name__)
f(1, 2, x=10, y=20)
# args = (1, 2) tuple ← позиционные → КОРТЕЖ
# kwargs = {'x': 10, 'y': 20} dict ← именованные → СЛОВАРЬ
# Раскрыть их и передать в другую функцию
def g(a, b, x, y): return a + b + x + y
args = [1, 2]
kwargs = {"x": 10, "y": 20}
g(*args, **kwargs) # = g(1, 2, x=10, y=20) → 33
print("args =", args, type(args).__name__)Три аргумента через запятую — print напечатает их через пробел:
"args ="— обычная строка-подписьargs— переменная (кортеж, собранный*args)type(args).__name__— цепочка:type(args)→<class 'tuple'>(объект-класс).__name__→'tuple'(имя класса как строка, без обёртки<class ...>)
Итого — «напечатай подпись, значение и красивое имя типа». Часто используется в отладке.
type(42).__name__ # 'int'
type("hi").__name__ # 'str'
type([1, 2]).__name__ # 'list'
type({"a": 1}).__name__ # 'dict'
type(None).__name__ # 'NoneType'
type(True).__name__ # 'bool'
В f-string короче и читаемее:
print(f"args = {args} ({type(args).__name__})")
# args = (1, 2) (tuple)
Декоратор не знает, какие аргументы принимает оборачиваемая функция. Ловит «всё» через *args, **kwargs и прокидывает дальше:
def logged(func):
def wrapper(*args, **kwargs): # СОБИРАЕТ что угодно
print("вызов:", func.__name__)
return func(*args, **kwargs) # РАСКРЫВАЕТ и передаёт
return wrapper
@logged
def add(a, b): return a + b
add(1, 2)
# вызов: add
# 3
def строго фиксирован: def f(обычные, *args, keyword_only=X, **kwargs). Обратный порядок — SyntaxError.args/kwargs — просто конвенция. Работает и def f(*nums, **opts). Важны звёздочки, не имена.* для словаря раскроет КЛЮЧИ:
d = {"a": 1, "b": 2}
f(*d) # = f("a", "b") — раскрылись ключи, не значения!
f(**d) # = f(a=1, b=2) — вот так правильно
args — кортеж (неизменяемый). args[0] = 99 → TypeError. Хочешь менять — list(args).* в def и * при вызове. Один и тот же символ, но противоположные операции. Смотри по позиции: def f(*args) — собирает; f(*[1,2,3]) — раскрывает.def add(item, items=[]): # [] создаётся ОДИН раз при определении!
items.append(item)
return items
add("a") # ["a"]
add("b") # ["a", "b"] — не пустой!
# Правильно:
def add(item, items=None):
if items is None:
items = []
items.append(item)
return items
def divmod_(a, b):
return a // b, a % b # на самом деле — tuple (a//b, a%b)
quotient, remainder = divmod_(10, 3) # unpacking
# Часто возвращают dict / dataclass для читаемости
def get_user_stats(user_id):
return {"orders": 42, "total": 1500.0, "last_order_at": ...}
lambda — анонимные функцииОднострочные функции без имени. Полезны для sorted, map, filter. Не для сложной логики — там обычная def.
# Сортировка по полю
users = [{"name": "Bob", "age": 25}, {"name": "Alice", "age": 30}]
sorted(users, key=lambda u: u["age"])
# Максимум по критерию
max(users, key=lambda u: u["age"])
# Фильтр
list(filter(lambda u: u["age"] >= 18, users))
# Питоничнее — comprehension:
[u for u in users if u["age"] >= 18]
def. Читаемость важнее краткости.
Внутренняя функция запоминает переменные внешней. Классический паттерн — фабрика функций:
def make_multiplier(factor):
def multiply(x):
return x * factor # factor — из внешней области
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
double(5) # 10
triple(5) # 15
# Изменение внешней переменной — через nonlocal
def counter():
count = 0
def inc():
nonlocal count # без этого count = local, будет UnboundLocalError
count += 1
return count
return inc
next_id = counter()
next_id(), next_id(), next_id() # 1, 2, 3
funcs = [lambda: i for i in range(3)]
[f() for f in funcs] # [2, 2, 2] — не [0, 1, 2]!
# Все lambda ссылаются на ОДНУ и ту же i, к моменту вызова = 2
# Фикс через default:
funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs] # [0, 1, 2]
global x и nonlocal x — модификаторы «пиши в верхний scope».help(), документация (Sphinx) используют её. Пиши всегда для публичных функций.-> None для void, -> list[str] для типизированной коллекции. mypy проверит.def append_id(items, id): items.append(id) — сюрприз для вызывающего. Возвращай новый список либо явно назови *_inplace.def — стандарт, lambda — для одноразовых key-функций. *args/**kwargs для гибких API. Никогда mutable default arg — только None + проверка. Closures работают, но осторожно с late binding.
# [expression for item in iterable if condition]
# Простое преобразование
squared = [x ** 2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# С фильтром
positives = [x for x in nums if x > 0]
# Оба сразу
even_squares = [x ** 2 for x in range(20) if x % 2 == 0]
# Извлечение из объектов
emails = [user.email for user in users if user.is_active]
# if/else в expression — тернарник (тогда if НЕ фильтр, а часть выражения)
labels = ["adult" if u.age >= 18 else "minor" for u in users]
map()/filter(). В новом коде — только comprehensions. map/filter остались в лямбда-мире функционального программирования.
# {key_expr: value_expr for item in iterable if condition}
# Из пары списков
names = ["Alice", "Bob", "Charlie"]
ages = [30, 25, 35]
user_ages = {n: a for n, a in zip(names, ages)}
# {"Alice": 30, "Bob": 25, "Charlie": 35}
# Инверсия dict
inverted = {v: k for k, v in d.items()}
# Трансформация значений
upper_d = {k: v.upper() for k, v in d.items()}
# Индекс по id
users_by_id = {u.id: u for u in users}
# С фильтром
active = {k: v for k, v in d.items() if v.is_active}
unique_tags = {tag for post in posts for tag in post.tags}
# Уникальные домены email-ов
domains = {email.split("@")[1] for email in emails}
Просто скобки (...) вместо [...] — но получается генератор, а не список. Не занимает память под все элементы сразу, вычисляет по одному.
# List — сразу все 10^9 чисел в памяти. Убьёт RAM.
sum([x ** 2 for x in range(10 ** 9)])
# Generator — по одному, память O(1)
sum(x ** 2 for x in range(10 ** 9))
# При передаче в функцию скобки comprehension'а можно опустить
any(u.is_admin for u in users)
max(o.total for o in orders)
len) → list comprehension. Однократный обход + большая последовательность → generator expression.
forfor идут в том же порядке, что и в обычном коде: первый for — внешний, второй — внутренний. Никакой магии — просто одна строка вместо трёх.
Универсальная формула перевода:
# Comprehension
[expr for A in X for B in Y if cond]
# Эквивалент через обычный код
result = []
for A in X: # ← первый for (внешний)
for B in Y: # ← второй for (внутренний)
if cond:
result.append(expr)
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [x for row in matrix for x in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Разворот в обычный цикл:
flat = []
for row in matrix: # row = [1,2,3], потом [4,5,6], потом [7,8,9]
for x in row: # x = 1, 2, 3 потом 4, 5, 6 потом 7, 8, 9
flat.append(x)
«Прочитай вслух» — [x for row in matrix for x in row] → «возьми x — для каждой row в matrix — для каждого x в row». Порядок for = порядок вложенности.
unique_tags = {tag for post in posts for tag in post.tags}
# Развёрнуто:
unique_tags = set()
for post in posts: # каждый пост
for tag in post.tags: # каждый его тег
unique_tags.add(tag) # set сам убирает дубли
Второй for использует post, объявленный в первом → порядок нельзя менять. post.tags — коллекция внутри объекта.
for)domains = {email.split("@")[1] for email in emails}
# Развёрнуто:
domains = set()
for email in emails:
domains.add(email.split("@")[1])
Один for — потому что вход плоский (список строк). В прошлых примерах был двумерный (список списков / список объектов с вложенными коллекциями) — оттуда два for.
pairs = [(a, b) for a in range(3) for b in range(3) if a != b]
# Развёрнуто:
pairs = []
for a in range(3): # a = 0, 1, 2
for b in range(3): # b = 0, 1, 2 (для КАЖДОГО a)
if a != b:
pairs.append((a, b))
# Результат:
# a=0: (0,1) (0,2)
# a=1: (1,0) (1,2)
# a=2: (2,0) (2,1)
# → 6 пар
Разница с уплощением:
| Тип | Пример | Второй for |
|---|---|---|
| Уплощение (зависимые) | [x for row in matrix for x in row] | зависит от первого (row оттуда) |
| Комбинации (независимые) | [(a,b) for a in range(3) for b in range(3)] | независим — свой итератор |
- Найди первый
for— это внешний цикл. - Найди второй
for— внутренний (вложен в первый). - Всё, что стоит в начале (до первого
for) — что собираем. ifв конце — фильтр (можно несколько).ifпослеforприменяется к последнему циклу.
[ ВЫРАЖЕНИЕ for A in X for B in Y if УСЛОВИЕ ]
# ↑ ↑ ↑ ↑
# собираем внешний внутренний фильтр
for:
[x for row in matrix for x in row] #
[x for x in row for row in matrix] # NameError: row не объявлен
Первый for должен объявить переменную, которую использует второй.if в начале и в конце:
[x if x > 0 else 0 for x in nums] # ← тернарник (преобразование каждого)
[x for x in nums if x > 0] # ← фильтр (отбор)
if после for — фильтр. if...else перед for — часть выражения.[x for a in A for b in a for c in b for d in c] — разверни в циклы или вспомогательные функции.# Все слова из списка предложений (плоский)
[word for sentence in text for word in sentence.split()]
# Все числа из вложенного списка
[n for group in groups for n in group]
# Все теги из постов (уникальные)
{tag for post in posts for tag in post.tags}
# Все комбинации цвет × размер
[(c, s) for c in colors for s in sizes]
# Пары без повторов (i < j)
[(a, b) for a in range(5) for b in range(5) if a < b]
# Все ключи из списка словарей
[k for d in dicts for k in d.keys()]
# Транспонировать матрицу
[[row[i] for row in matrix] for i in range(len(matrix[0]))]
for = вложенные циклы, записанные в одну строку. Порядок for — как в обычном коде (первый внешний, второй внутренний). Второй for может зависеть от первого (уплощение) или быть независимым (комбинации). Если больше двух уровней — пиши обычными циклами, не мучай читателя.
| PHP | Python |
|---|---|
array_map(fn($x) => $x * 2, $nums) | [x * 2 for x in nums] |
array_filter($nums, fn($x) => $x > 0) | [x for x in nums if x > 0] |
array_combine($keys, $values) | {k: v for k, v in zip(keys, values)} |
array_column($users, 'email') | [u["email"] for u in users] |
array_unique($items) | list({x for x in items}) |
Самый частый паттерн в реальном коде — обработка данных из БД, API-ответов, JSON. Держи как справочник:
users = [{"name": "Bob", "age": 25}, {"name": "Alice", "age": 30}]
# ─── Сортировка по полю ──────────────────────────────────────
sorted(users, key=lambda u: u["age"])
sorted(users, key=lambda u: u["age"], reverse=True)
# По нескольким полям (сначала age, потом name)
sorted(users, key=lambda u: (u["age"], u["name"]))
# Через itemgetter — быстрее lambda
from operator import itemgetter
sorted(users, key=itemgetter("age"))
# ─── Максимум / минимум ──────────────────────────────────────
max(users, key=lambda u: u["age"]) # {"name": "Alice", "age": 30}
min(users, key=lambda u: u["age"])
# ─── Фильтр (питонично) ──────────────────────────────────────
[u for u in users if u["age"] >= 18]
# ─── Извлечь одно поле (аналог array_column PHP) ─────────────
[u["name"] for u in users] # ["Bob", "Alice"]
# ─── Сумма / среднее по полю ─────────────────────────────────
sum(u["age"] for u in users) # 55
sum(u["age"] for u in users) / len(users) # 27.5
# ─── Логика по коллекции ─────────────────────────────────────
any(u["age"] > 28 for u in users) # True — есть хоть один
all(u["age"] >= 18 for u in users) # True — все
# ─── Найти первого по условию (безопасно) ────────────────────
next((u for u in users if u["name"] == "Bob"), None)
# default None — если не нашли (иначе StopIteration)
# ─── Индекс по ключу (dict из списка) ────────────────────────
users_by_name = {u["name"]: u for u in users}
users_by_name["Alice"] # O(1) доступ
# ─── Группировка по полю ─────────────────────────────────────
from collections import defaultdict
by_age = defaultdict(list)
for u in users:
by_age[u["age"]].append(u["name"])
# {25: ["Bob"], 30: ["Alice"]}
# itertools.groupby — работает ТОЛЬКО с отсортированными по ключу
from itertools import groupby
sorted_users = sorted(users, key=itemgetter("age"))
for age, group in groupby(sorted_users, key=itemgetter("age")):
print(age, list(group))
# ─── Добавить / изменить поле у каждого ──────────────────────
for u in users:
u["is_adult"] = u["age"] >= 18 # мутация in-place
# Или через comprehension — новый список из новых dict'ов
enriched = [{**u, "is_adult": u["age"] >= 18} for u in users]
# ─── Удалить дубликаты по полю (сохранить первое) ────────────
seen = set()
unique = []
for u in users:
if u["name"] not in seen:
seen.add(u["name"])
unique.append(u)
# ─── Разбить на два по условию (partition) ───────────────────
adults = [u for u in users if u["age"] >= 18]
minors = [u for u in users if u["age"] < 18]
# ─── Топ-N по полю ───────────────────────────────────────────
import heapq
top_3 = heapq.nlargest(3, users, key=itemgetter("age"))
bottom_3 = heapq.nsmallest(3, users, key=itemgetter("age"))
# быстрее sorted(...)[:3] для больших списков
# ─── Есть ли dict с таким полем ──────────────────────────────
"Bob" in {u["name"] for u in users} # True
# ─── Слить два списка dict'ов по ключу ───────────────────────
orders = [{"user": "Bob", "total": 100}, {"user": "Alice", "total": 200}]
merged = [{**u, **next((o for o in orders if o["user"] == u["name"]), {})}
for u in users]
# Для больших данных — переходи на pandas.merge
GROUP BY/ORDER BY/JOIN.
u["nmae"] кинет KeyError при доступе. Безопаснее u.get("name") — вернёт None. Ещё безопаснее — @dataclass или Pydantic: IDE и mypy подсветят опечатку.for u in users: users.remove(u) — пропустит элементы. Собирай новый список или итерируй по копии users[:].Python предоставляет ~70 функций без импорта — они всегда доступны из любого модуля. Живут в модуле builtins, автоматически загружаемом. Полный список: dir(__builtins__) (там ~150 имён, включая исключения типа ValueError).
print, input, len, range, enumerate, zip, sorted, min, max, sum, int, str, list, isinstance, open. Остальные — по мере необходимости.
| Функция | Что делает | Пример |
|---|---|---|
print(*args, sep=' ', end='\n') | Вывод в консоль | print("a", "b", sep="-") → a-b |
input(prompt="") | Читает строку от пользователя (всегда str) | name = input("Имя: ") |
open(path, mode, encoding) | Открыть файл (использовать через with) | with open("f.txt", "r", encoding="utf-8") as f: |
| Функция | Что делает | Пример |
|---|---|---|
len(x) | Длина коллекции / строки | len("hello") → 5 |
type(x) | Класс объекта | type(42) → <class 'int'> |
isinstance(x, T) | Экземпляр класса T (учитывает наследников) — предпочтительнее type(x) == T | isinstance(42, (int, str)) → True (можно кортеж типов) |
callable(x) | Можно ли вызвать x() | callable(print) → True; callable(42) → False |
issubclass(A, B) | A — подкласс B | issubclass(bool, int) → True |
isinstance(x, T) — всегда вместо type(x) == T. Первое учитывает наследование (isinstance(True, int) → True, потому что bool — наследник int). Второе — строгое сравнение классов.| Функция | Что делает | Пример |
|---|---|---|
int(x) | В целое (обрезает дробь, НЕ округляет) | int("42") → 42; int(3.9) → 3 |
int(x, base) | Парсинг с системой счисления | int("ff", 16) → 255 |
float(x) | В float | float("3.14") → 3.14 |
str(x) | В строку (через __str__) | str([1, 2]) → "[1, 2]" |
bool(x) | В True/False (falsy: 0, "", [], {}, None) | bool([]) → False; bool("hi") → True |
list(x) | Список из iterable | list("abc") → ['a', 'b', 'c'] |
tuple(x) | Кортеж | tuple([1, 2]) → (1, 2) |
set(x) | Множество (уникальные) | set([1, 1, 2]) → {1, 2} |
dict(x) | Словарь | dict(a=1, b=2) → {'a': 1, 'b': 2} |
bytes(x), bytearray(x) | Бинарные типы | bytes("hi", "utf-8") |
frozenset(x) | Неизменяемое множество (hashable — можно как ключ dict) | frozenset([1, 2]) |
int(3.9) = 3, не 4 — обрезает к нулю. Для правильного округления — round(3.9).| Функция | Что делает | Пример |
|---|---|---|
range(stop), range(start, stop, step) | Ленивая последовательность чисел | list(range(0, 10, 2)) → [0, 2, 4, 6, 8] |
enumerate(x, start=0) | Пары (index, value) | list(enumerate("ab")) → [(0, 'a'), (1, 'b')] |
zip(a, b, ..., strict=False) | Параллельный обход. strict=True (3.10+) кинет ошибку при разной длине | list(zip([1, 2], ["a", "b"])) → [(1, 'a'), (2, 'b')] |
reversed(x) | Итератор в обратном порядке (без изменения оригинала) | list(reversed([1, 2, 3])) → [3, 2, 1] |
iter(x) | Получить iterator | it = iter([1, 2, 3]); next(it) → 1 |
next(it, default) | Следующий элемент или default вместо StopIteration | next(it, "!") → "!" |
map(f, x) | Применить f к каждому (ленивый) | list(map(str, [1, 2, 3])) → ['1', '2', '3'] |
filter(f, x) | Оставить элементы, где f(x) истинно | list(filter(lambda n: n > 0, [1, -2, 3])) → [1, 3] |
map/filter в новом коде обычно заменяют comprehension — короче и читаемее:
list(map(str, nums)) → [str(n) for n in nums]
list(filter(lambda n: n > 0, nums)) → [n for n in nums if n > 0]
| Функция | Что делает | Пример |
|---|---|---|
sorted(x, key=None, reverse=False) | Отсортированная копия (в отличие от list.sort()) | sorted(users, key=lambda u: u.age) |
min(x, key=None, default=...) | Минимум | min(users, key=lambda u: u.age) |
max(x, key=None, default=...) | Максимум | max([1, 2, 3]) → 3 |
| Функция | Что делает | Пример |
|---|---|---|
sum(x, start=0) | Сумма | sum([1, 2, 3]) → 6; sum([1, 2], 10) → 13 |
abs(x) | Модуль | abs(-5) → 5 |
round(x, ndigits=None) | Округление (bankers' rounding — половина к чётному) | round(3.14159, 2) → 3.14; round(2.5) → 2 (!!!) |
pow(base, exp, mod=None) | Степень. С mod — эффективно для больших чисел (крипта) | pow(2, 10, 100) → 24 |
divmod(a, b) | Частное + остаток одним вызовом | divmod(7, 2) → (3, 1) |
bin(x), oct(x), hex(x) | В строковое представление системы счисления | hex(255) → '0xff' |
ord(char) | Char → int (Unicode code point) | ord("A") → 65 |
chr(n) | Int → char (Unicode) | chr(65) → 'A' |
round(2.5) → 2, не 3. Python использует bankers' rounding (half to even) — половина округляется к чётному. Это стандарт IEEE 754 для избежания статистического смещения. Не баг — фича.| Функция | Что делает | Пример |
|---|---|---|
all(x) | Все элементы truthy? Пустой → True | all([True, True]) → True; all([]) → True |
any(x) | Хоть один truthy? Пустой → False | any([False, True]) → True; any([]) → False |
Реальный use-case:
# Все users имеют email?
if all(u.email for u in users):
send_bulk(users)
# Есть хоть один админ?
if any(u.is_admin for u in users):
...
# Ленивое короткое замыкание — как в SQL EXISTS
# При первом True в any / False в all — обход прекращается
| Функция | Что делает | Пример |
|---|---|---|
help(x) | Показать docstring / документацию | help(str.split) |
dir(x) | Список атрибутов и методов объекта | dir("hi") → [..., 'split', 'upper', ...] |
repr(x) | «Отладочное» представление (видны escape-символы, кавычки) | repr("hi\n") → "'hi\\n'" |
id(x) | Адрес объекта в памяти (для is) | id(42) |
hash(x) | Хеш immutable-объекта. Для list — ошибка (unhashable) | hash((1, 2)) — да; hash([1, 2]) — нет |
getattr(obj, name, default) | Атрибут по имени (динамически), с дефолтом | getattr(user, "email", "none") |
setattr(obj, name, value) | Установить атрибут по имени | setattr(user, "role", "admin") |
hasattr(obj, name) | Есть ли атрибут | hasattr(user, "email") |
delattr(obj, name) | Удалить атрибут | — |
| Функция | Что делает | Пример |
|---|---|---|
vars(x) | __dict__ объекта (атрибуты как словарь) | vars(user) → {'name': 'Alice', ...} |
vars() (без аргумента) | То же что locals() | — |
globals() | Словарь глобальных переменных модуля | Отладка, метапрограммирование |
locals() | Словарь локальных переменных (для чтения; изменения не сохранятся) | Отладка |
Используются редко — в основном для отладки и метапрограммирования (декораторы, ORM, тестовые фреймворки).
eval / execeval("2 + 3") # 5 — выполнить ВЫРАЖЕНИЕ, вернуть значение
eval("len('hi')") # 2
exec("x = 5\nprint(x)") # выполнить ИНСТРУКЦИИ, вернуть None
eval/exec на пользовательских данных = RCE. eval(user_input) с вводом "__import__('os').system('rm -rf /')" — уничтожит сервер. Использовать только для доверенных строк (свои шаблоны, тесты), никогда для внешнего ввода.Безопасные альтернативы:
- Парсинг чисел из строки →
int(s)/float(s), а неeval(s) - Парсинг литералов (числа, строки, списки, dict) →
ast.literal_eval(s)— только литералы, никаких вызовов функций - Парсинг математических выражений → библиотека
sympyили свой AST-парсер - Динамическое выполнение кода из БД → крайне пересматривай архитектуру, обычно есть безопасное решение
| Категория | Функции |
|---|---|
| Ввод/вывод | print, input, open |
| Длина / тип | len, type, isinstance, issubclass, callable |
| Приведение | int, float, str, bool, list, tuple, set, dict, bytes, frozenset |
| Итерация | range, enumerate, zip, reversed, iter, next, map, filter |
| Сортировка | sorted, min, max |
| Математика | sum, abs, round, pow, divmod, bin, hex, oct, ord, chr |
| Логика | all, any |
| Интроспекция | help, dir, repr, id, hash, getattr/setattr/hasattr, vars, globals, locals |
| Опасное | eval, exec, compile |
- 90% кода использует ~15 функций:
print,input,len,range,enumerate,zip,sorted,min/max/sum,int/str/list,isinstance,open iter/next/map/filter— низкоуровневые, чаще заменяются comprehensionseval/exec— почти никогда не нужны и опасны; для литералов —ast.literal_evalvars/globals/locals/getattr/setattr— метапрограммирование и отладка- Полный список —
dir(__builtins__), ~150 имён с исключениями (ValueError, TypeError...)
class User:
# Class-атрибут — общий для всех экземпляров
role = "guest"
# Конструктор — __init__
def __init__(self, name: str, age: int):
self.name = name # instance-атрибуты
self.age = age
# Обычный метод — первый аргумент всегда self
def greet(self) -> str:
return f"Hi, I'm {self.name}"
user = User("Alice", 30)
user.greet() # "Hi, I'm Alice"
user.name # "Alice"
user.role # "guest" (из class-атрибута)
self — явный первый аргумент каждого метода (в PHP было неявное $this). Никаких public/private/protected в языке — только конвенция: _name = «внутреннее» (soft), __name = name-mangling (hard, редко). Всё по умолчанию публично.
super()class Admin(User): # extends User
role = "admin" # переопределили class-атрибут
def __init__(self, name, age, permissions):
super().__init__(name, age) # вызов конструктора родителя
self.permissions = permissions
def greet(self) -> str:
base = super().greet() # вызов метода родителя
return f"{base} (admin)"
admin = Admin("Bob", 40, ["read", "write"])
admin.greet() # "Hi, I'm Bob (admin)"
isinstance(admin, User) # True — Admin наследник User
Множественное наследование возможно (в отличие от PHP). Порядок разрешения — MRO (Method Resolution Order) по алгоритму C3:
class A: def hi(self): print("A")
class B(A): def hi(self): print("B")
class C(A): def hi(self): print("C")
class D(B, C): pass
D().hi() # "B" — MRO: D → B → C → A → object
D.__mro__ # покажет полный порядок
@property — getter/setter без синтаксиса скобокclass Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self) -> float:
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("radius must be positive")
self._radius = value
@property
def area(self) -> float: # вычисляемое поле
return 3.14159 * self._radius ** 2
c = Circle(5)
c.radius # 5 (вызовет getter, но выглядит как атрибут)
c.radius = 10 # вызовет setter
c.radius = -1 # ValueError
c.area # 314.159 — без скобок!
Питоническое решение: начинай с обычного публичного атрибута; когда потребуется валидация/вычисление — превращай в @property, интерфейс наружу не меняется.
@classmethod и @staticmethodclass User:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def from_dict(cls, data: dict) -> "User":
# cls = сам класс. Работает и в наследниках.
return cls(data["name"], data["age"])
@staticmethod
def is_valid_email(email: str) -> bool:
# Ни self, ни cls — обычная функция в namespace класса
return "@" in email
user = User.from_dict({"name": "Alice", "age": 30})
User.is_valid_email("a@b.c") # True
Различие: @classmethod получает класс — используется для альтернативных конструкторов (from_dict, from_json). @staticmethod — просто функция, помещённая в namespace класса для семантики.
Специальные методы с двойным подчёркиванием — «крючки» в поведение языка. Питон вызывает их сам при определённых операциях.
| Dunder | Когда вызывается | Пример |
|---|---|---|
__init__ | Создание объекта | Конструктор |
__str__ | str(obj), print(obj), f"{obj}" | Человекочитаемое представление |
__repr__ | repr(obj), в REPL, в debug | Однозначное представление для разработчика |
__eq__, __ne__ | a == b, a != b | Сравнение на равенство |
__lt__, __le__, __gt__, __ge__ | <, <=, >, >= | Для сортировки |
__hash__ | hash(obj), использование в set/dict-ключах | Обязателен если __eq__ |
__len__ | len(obj) | Для коллекций |
__iter__, __next__ | for x in obj: | Iterator protocol |
__getitem__, __setitem__ | obj[key], obj[key] = v | Индексация |
__call__ | obj(...) | Объект как функция |
__enter__, __exit__ | with obj as x: | Context manager |
class Money:
def __init__(self, amount, currency):
self.amount = amount
self.currency = currency
def __repr__(self) -> str:
return f"Money({self.amount}, {self.currency!r})"
def __str__(self) -> str:
return f"{self.amount} {self.currency}"
def __eq__(self, other) -> bool:
return self.amount == other.amount and self.currency == other.currency
def __hash__(self) -> int:
return hash((self.amount, self.currency))
def __add__(self, other):
if self.currency != other.currency:
raise ValueError("cannot add different currencies")
return Money(self.amount + other.amount, self.currency)
Money(100, "USD") + Money(50, "USD") # Money(150, 'USD')
В Python есть два вида dunder-имён (double underscore, «двойное подчёркивание с двух сторон»). Их часто путают:
| Dunder-атрибут (данные) | Dunder-метод (функция) | |
|---|---|---|
| Что это | Просто значение | Функция, которую можно вызвать |
| Обращение | Без скобок: obj.__name__ | Со скобками: obj.__str__() |
| Примеры | __name__, __doc__, __dict__, __class__, __module__, __file__ | __init__, __str__, __len__, __eq__, __add__, __call__ |
| Кто устанавливает | Python — сам, читаешь готовое | Ты — определяешь в своём классе, Python вызывает |
| Проверка | callable(x.__name__) → False | callable(x.__len__) → True |
__что-то__ зарезервировано для языка. Некоторые из них — атрибуты-данные (Python их устанавливает автоматически, ты просто читаешь). Другие — методы-хуки: ты пишешь их в своём классе, а Python вызывает сам, когда пользователь делает обычные операции (+, len(), print, for, with).
Не методы, а готовая информация об объекте. Читаешь без скобок.
| Атрибут | Что содержит | Пример |
|---|---|---|
__name__ | Имя объекта как строка | greet.__name__ → 'greet' |
__doc__ | Docstring (первая строка в """) функции/класса/модуля | str.__doc__ |
__dict__ | Словарь атрибутов объекта | user.__dict__ → {'name': 'Alice'} |
__class__ | Класс объекта (= type(obj)) | user.__class__.__name__ → 'User' |
__module__ | Модуль, где определён | User.__module__ → 'app.users' |
__file__ | Путь к файлу модуля | my_module.__file__ |
__all__ | Список публичных имён при from x import * | Задаётся вручную в модуле |
__slots__ | Разрешённые атрибуты (экономит память) | Задаётся вручную в классе |
__annotations__ | Type hints как dict | func.__annotations__ → {'x': int, 'return': str} |
Особый случай — __name__ у разных объектов:
| У чего | Что содержит |
|---|---|
Модуль (файл .py) | Имя модуля ('mymodule'), либо '__main__' при прямом запуске |
| Функция | Имя функции: greet.__name__ → 'greet' |
| Класс | Имя класса: User.__name__ → 'User' |
Тип (через type(...)) | Имя типа: type(42).__name__ → 'int' |
Отсюда идиома:
# mymodule.py
def main(): ...
if __name__ == "__main__":
main() # сработает только при `python mymodule.py`
# НЕ при `import mymodule`
Ты определяешь в своём классе, Python сам вызывает когда пользователь делает обычную вещь. В этом и есть «магия».
1. Жизненный цикл
| Метод | Когда вызывается |
|---|---|
__init__(self, ...) | При создании: User("Alice") |
__new__(cls, ...) | Ещё раньше — создаёт сам объект. Редко нужен (immutable-типы, метаклассы) |
__del__(self) | При удалении объекта GC. Ненадёжно, лучше использовать with |
2. Строковое представление
| Метод | Для чего |
|---|---|
__str__(self) | str(x), print(x), f-строка — «для людей» |
__repr__(self) | repr(x), REPL, debug — «для программиста», желательно чтобы можно было eval(repr(x)) |
__format__(self, spec) | f"{x:spec}", format(x, spec) |
3. Сравнение
| Метод | Оператор |
|---|---|
__eq__ / __ne__ | == / != |
__lt__ / __le__ | < / <= |
__gt__ / __ge__ | > / >= |
__hash__(self) | hash(x) — обязателен если __eq__, иначе объект unhashable |
Хочешь все 6 операторов сравнения сразу — @functools.total_ordering сгенерирует их из __eq__ + __lt__.
4. Арифметика
| Метод | Оператор |
|---|---|
__add__ | + |
__sub__ | - |
__mul__ | * |
__truediv__ | / |
__floordiv__ | // |
__mod__ | % |
__pow__ | ** |
__matmul__ | @ (матрицы, NumPy/PyTorch) |
__neg__, __pos__, __abs__ | -x, +x, abs(x) |
__iadd__, __isub__, ... | +=, -=, ... (in-place) |
5. Контейнеры (list/dict-подобное поведение)
| Метод | Оператор |
|---|---|
__len__(self) | len(x) |
__getitem__(self, key) | x[key] |
__setitem__(self, key, val) | x[key] = val |
__delitem__(self, key) | del x[key] |
__contains__(self, item) | item in x |
__iter__(self) | for i in x: |
__next__(self) | next(x) — для iterator'ов |
__reversed__(self) | reversed(x) |
6. Context managers (with)
| Метод | Когда |
|---|---|
__enter__(self) | Вход в with. Возвращаемое значение = as var |
__exit__(self, exc_type, exc_val, tb) | Выход, даже при exception |
7. Другие важные хуки
| Метод | Оператор |
|---|---|
__call__(self, ...) | obj(...) — объект вызывается как функция |
__bool__(self) | bool(x), if x: |
__getattr__(self, name) | Атрибут не найден — fallback |
__setattr__(self, name, val) | При любом присваивании атрибута (осторожно с рекурсией!) |
__getattribute__(self, name) | При любом доступе (даже к существующему) — очень опасно |
__slots__ = ("x", "y") | Ограничение разрешённых атрибутов, экономия памяти |
class Basket:
def __init__(self, items=None): # создание
self._items = items or []
def __repr__(self): # debug
return f"Basket({self._items!r})"
def __str__(self): # print
return f"Корзина: {len(self._items)} товаров"
def __len__(self): # len()
return len(self._items)
def __getitem__(self, i): # basket[i]
return self._items[i]
def __contains__(self, item): # item in basket
return item in self._items
def __iter__(self): # for x in basket
return iter(self._items)
def __bool__(self): # if basket:
return len(self._items) > 0
def __add__(self, other): # basket1 + basket2
return Basket(self._items + other._items)
def __eq__(self, other): # basket1 == basket2
return self._items == other._items
# Использование — Python сам дёргает нужные хуки
b = Basket(["хлеб", "молоко"])
print(b) # __str__ → "Корзина: 2 товаров"
len(b) # __len__ → 2
b[0] # __getitem__ → "хлеб"
"молоко" in b # __contains__ → True
bool(b) # __bool__ → True
for item in b: # __iter__
print(item)
# Что даёт Python сам как атрибуты — без определения:
b.__class__.__name__ # 'Basket'
b.__dict__ # {'_items': ['хлеб', 'молоко']}
x = "hello"
callable(x.__str__) # True — метод, можно вызвать
callable(x.__class__) # True — класс сам callable (создаёт объект)
callable(x.__doc__) # False — атрибут (строка)
# Ещё способ — type
type(x.__str__) # <class 'method-wrapper'>
type(x.__doc__) # <class 'str'>
Хуки — «Python дёрнет когда пользователь напишет
+/len()/print()».
Атрибуты — «Python сохранил информацию, ты её читаешь».
@dataclass — DTO без boilerplate (must-have Python 3.7+)from dataclasses import dataclass, field
@dataclass
class User:
name: str
age: int
role: str = "user"
tags: list[str] = field(default_factory=list) # для mutable default
user = User("Alice", 30)
print(user) # User(name='Alice', age=30, role='user', tags=[])
user == User("Alice", 30) # True — __eq__ автоматом
# Опции:
@dataclass(frozen=True) # иммутабельный
@dataclass(slots=True) # меньше памяти, быстрее (3.10+)
@dataclass(kw_only=True) # только keyword-arguments
Автоматически генерирует __init__, __repr__, __eq__. Аналог PHP readonly class или Java records. Используй по умолчанию для всех классов-DTO. Для валидации данных из внешнего мира (API/форм) — Pydantic.
self — не ключевое слово. Можно назвать хоть this, но никогда так не делай — это конвенция сообщества. IDE и линтеры на неё завязаны.instance.attr без переприсваивания повлияет на всех._x — «руками не трогать». __x — name-mangling (внутри класса Foo превращается в _Foo__x), только для избежания коллизий в наследовании. Не защита.__eq__ без __hash__ — объект становится unhashable. После переопределения __eq__ обязательно определи __hash__ (или @dataclass(frozen=True) сделает автоматом).collections.UserDict / UserList, иначе внутренние методы могут не звать твои переопределения.abc. from abc import ABC, abstractmethod; class Repo(ABC): @abstractmethod def save(self): .... Экземпляр создать нельзя, пока не реализуешь метод.@dataclass. Для сервисов — обычный класс с DI через __init__. Для валидации внешних данных — Pydantic. Наследование — сдержанно, композиция чаще лучше. Dunder-методы — при необходимости (__eq__, __hash__, __repr__ — почти всегда стоит). Множественное наследование — только для mixin.
Любой .py-файл — модуль. Импорт делает его переменные/функции/классы доступными в другом файле.
# math_utils.py
PI = 3.14159
def area(radius): return PI * radius ** 2
# app.py
import math_utils # модуль целиком
math_utils.area(5)
math_utils.PI
from math_utils import area, PI # только нужное
area(5)
from math_utils import area as circle_area # переименовать
circle_area(5)
from math_utils import * # анти-паттерн, засоряет namespace
__init__.pymyapp/
├── __init__.py # делает директорию пакетом
├── main.py
├── users/
│ ├── __init__.py
│ ├── models.py # class User
│ └── service.py # def register(...)
└── orders/
├── __init__.py
└── models.py
# В main.py — абсолютные импорты (рекомендуется)
from myapp.users.models import User
from myapp.users.service import register
# Внутри users/service.py — относительные (только между модулями пакета)
from .models import User # тот же пакет
from ..orders.models import Order # родительский пакет
__init__.py может быть пустым (просто маркер пакета) или содержать re-exports:
# myapp/users/__init__.py
from .models import User
from .service import register
# Теперь можно короче:
from myapp.users import User, register
if __name__ == "__main__":Каждый модуль имеет специальную переменную __name__:
- Если файл запущен напрямую (
python app.py) →__name__ == "__main__" - Если файл импортирован (
import app) →__name__ == "app"
# cli.py
def main():
print("Running CLI")
if __name__ == "__main__":
main() # сработает только при `python cli.py`,
# не при `import cli`
Позволяет один файл использовать и как скрипт, и как импортируемый модуль без побочных эффектов при импорте.
a.py импортирует b, b.py импортирует a → ImportError. Решения: разбить общий код в третий модуль c.py; отложенный импорт внутри функции; type hints через TYPE_CHECKING.sys.modules. Повторный import — no-op. Перезагрузка — importlib.reload(m) (редко нужно, только для интерактивной разработки).if __name__ == "__main__":.from X import * — не используй. Ломает читаемость (непонятно откуда символ), может перекрыть built-ins. Исключение — __all__ в __init__.py пакета, если сам это контролируешь.from myapp.users.models import User) — предпочтительнее. Относительные (from ..models import User) — только внутри пакета, экономят на длине.Проект A требует Django 4.2, проект B — Django 5.1. Оба ставить глобально нельзя. Virtual environment — изолированная копия интерпретатора со своим site-packages/. Аналог vendor/ в PHP, но на уровне интерпретатора.
pip install глобально по умолчанию (PEP 668, externally-managed-environment). Только через venv или --break-system-packages (не делай так). macOS через brew — та же ситуация.venv + pip# Создать окружение в текущей директории
python -m venv .venv
# Активировать
source .venv/bin/activate # macOS/Linux
.venv\Scripts\activate # Windows PowerShell
# Теперь python/pip = из .venv/
which python # /path/to/project/.venv/bin/python
pip install fastapi uvicorn sqlalchemy
# Заморозить
pip freeze > requirements.txt
# Восстановить в новом окружении
pip install -r requirements.txt
# Деактивировать
deactivate
Добавь .venv/ в .gitignore — окружение локально, восстанавливается из requirements.txt. По той же логике, что vendor/ в PHP.
uvuv (Astral, 2024) — Rust-based замена pip + venv + pip-tools. В 10-100 раз быстрее. Управляет всем через pyproject.toml + uv.lock — воспроизводимые сборки как composer.lock.
# Установка (один раз)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Создать проект
uv init my-api && cd my-api
# Установить конкретную версию Python (если её нет — скачает)
uv python install 3.13
# Добавить зависимости — сам создаст .venv, обновит pyproject.toml + uv.lock
uv add fastapi uvicorn sqlalchemy
uv add --dev pytest ruff mypy
# Запустить — не нужно `source .venv/bin/activate`
uv run python app.py
uv run uvicorn main:app --reload
# Установить из lock-файла (для CI/деплоя)
uv sync # reproducible install
Poetry (2018) — до uv был стандартом. Тот же принцип (pyproject.toml + lock), но написан на Python и в 100 раз медленнее.
poetry init
poetry add fastapi
poetry install
poetry run pytest
Для нового проекта в 2026 — uv. Poetry — только если проект уже на нём.
| Инструмент | Скорость | Lock-файл | Управляет Python-версией | Когда выбирать |
|---|---|---|---|---|
pip + venv | Медленно | requirements.txt (плоский) | Нет | Legacy, простые скрипты |
pip-tools | Медленно | requirements.lock | Нет | Есть pip, нужен lock |
Poetry | Медленно | Да | Нет (нужен pyenv) | Существующие проекты |
pipenv | Очень медленно | Да | Нет | Не выбирать |
uv | Мгновенно | Да | Да | Новые проекты (2026) |
pyproject.toml — современный конфиг# pyproject.toml — как composer.json в PHP
[project]
name = "my-api"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.110",
"sqlalchemy>=2.0",
"asyncpg",
]
[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy"]
[tool.ruff]
line-length = 100
target-version = "py313"
[tool.mypy]
strict = true
uv. Для legacy/скриптов — venv + pip. Всегда .venv/ в .gitignore. Файл pyproject.toml с pin-версиями + lock-файл в git — воспроизводимая сборка на CI и в проде.
try:
data = json.loads(payload)
result = data["user"]["id"]
except json.JSONDecodeError as e:
logger.error(f"Invalid JSON: {e}")
result = None
except KeyError as e:
logger.warning(f"Missing key: {e}")
result = None
except Exception:
logger.exception("Unexpected") # exception() = error() + traceback
raise # перебросить дальше
else:
# выполнится только если НЕ было исключения в try
logger.info(f"Got user_id={result}")
finally:
# выполнится ВСЕГДА — успех, исключение или return/break
connection.close()
except Exception без raise — почти всегда баг: скрывает реальные ошибки.
Не ловить BaseException напрямую — там KeyboardInterrupt (Ctrl+C) и SystemExit. Пользователь не сможет прервать программу.
raise — бросить исключениеdef divide(a, b):
if b == 0:
raise ValueError("cannot divide by zero")
return a / b
# Перебросить с сохранением контекста
try:
do_something()
except Exception as e:
raise RuntimeError("failed") from e
# traceback покажет ОБА — оригинал не потерян
# Голый raise — только внутри except, перебрасывает текущее
try:
...
except Exception:
log("got exception")
raise
class OrderError(Exception):
"""Base для всех доменных ошибок заказов."""
class OrderNotFound(OrderError):
pass
class InsufficientStock(OrderError):
def __init__(self, sku, requested, available):
super().__init__(f"SKU {sku}: need {requested}, have {available}")
self.sku = sku
self.requested = requested
self.available = available
# Ловим одним except по базовому классу
try:
place_order(...)
except OrderError as e:
logger.error(f"Order failed: {e}")
Свой класс — когда обработчик выше по стеку должен различать типы ошибок. Внутри одного модуля обычные ValueError/RuntimeError сойдут.
with — context managersГарантированное освобождение ресурсов даже при исключении. Аналог try/finally, только короче:
# Без with — легко забыть закрыть
f = open("data.txt")
data = f.read()
f.close() # не выполнится если read() кинет
# С with — close автоматически
with open("data.txt") as f:
data = f.read()
# Несколько ресурсов сразу
with open("in.txt") as src, open("out.txt", "w") as dst:
dst.write(src.read())
Свой context manager — через декоратор @contextmanager:
from contextlib import contextmanager
@contextmanager
def db_transaction(conn):
conn.begin()
try:
yield conn
conn.commit()
except:
conn.rollback()
raise
with db_transaction(conn) as tx:
tx.execute(...)
Два подхода к «а вдруг что-то пойдёт не так»:
| LBYL (PHP-стиль) | EAFP (питонично) | |
|---|---|---|
| Расшифровка | Look Before You Leap | Easier to Ask Forgiveness than Permission |
| Подход | Проверить условие ДО операции | Попробовать, поймать исключение |
| Пример | |
|
Питоничнее — EAFP, но не догматично. d.get("key") ещё короче. Правило: EAFP когда «в обычном случае получится, ошибка — редкая»; LBYL когда «проверка дешёвая, ошибка ожидаема».
except: pass — почти всегда баг. Проглатывает всё, включая KeyboardInterrupt. Минимум — except Exception: logger.exception(...).except имеет значение. Специфичные — выше, общие — ниже. except Exception первым съест всё.for/break лучше, чем try/except StopIteration.get_user_or_raise() ок; но не find_user() которая всегда кидает при отсутствии — верни None.raise ... from None — скрывает оригинальный контекст. Используй только когда специально хочешь заменить traceback (redacting чувствительных данных).Exception. Используй raise ... from e для chaining. with для ресурсов — файлы, БД-соединения, блокировки. EAFP — питоничный дефолт.
open() и режимы# Всегда через with — гарантированное закрытие
with open("data.txt", encoding="utf-8") as f:
content = f.read()
# Режимы
open(path, "r") # чтение (default) — если файла нет, FileNotFoundError
open(path, "w") # запись — создаст / ПЕРЕЗАПИШЕТ
open(path, "a") # append — создаст / допишет в конец
open(path, "x") # exclusive create — упадёт если файл есть
open(path, "r+") # чтение + запись
open(path, "rb") # binary read (для картинок, PDF)
open(path, "wb") # binary write
# Три способа чтения
with open("log.txt") as f:
all_at_once = f.read() # одна строка
# или
lines = f.readlines() # список строк (все в память)
# или (лучший для больших файлов) — построчно
for line in f:
process(line.rstrip()) # O(1) память
# Запись
with open("out.txt", "w", encoding="utf-8") as f:
f.write("one line\n")
f.writelines(["a\n", "b\n"]) # не добавит \n сам!
encoding="utf-8". Без него берётся дефолтный OS — на Windows это cp1251, на Linux utf-8. Один код будет ломаться на разных ОС.pathlib.Path — современная работа с путямиЗабудь про os.path.join, os.path.exists, os.listdir. pathlib (Python 3.4+) — ООП-обёртка, чище и переносимо.
from pathlib import Path
p = Path("/var/log/app.log")
p = Path("data") / "users.json" # склейка через /
p = Path.home() / ".config" / "app" # $HOME/.config/app
# Свойства
p.name # "app.log"
p.stem # "app" (без расширения)
p.suffix # ".log"
p.parent # Path("/var/log")
p.parts # ("/", "var", "log", "app.log")
p.absolute() # абсолютный путь
# Проверки
p.exists()
p.is_file()
p.is_dir()
# Операции
p.mkdir(parents=True, exist_ok=True) # создать директорию (рекурсивно, no-op если есть)
p.unlink(missing_ok=True) # удалить файл
p.rename(Path("new_name.log"))
p.rmdir() # удалить пустую директорию
# Обход директории
for item in Path(".").iterdir(): # прямые потомки
print(item)
for py in Path("src").rglob("*.py"): // рекурсивно по glob
print(py)
# Чтение / запись — методы прямо на Path
text = Path("config.txt").read_text(encoding="utf-8")
Path("out.txt").write_text("hello", encoding="utf-8")
data = Path("image.png").read_bytes()
jsonimport json
# Из строки → Python-объект
data = json.loads('{"name": "Alice", "age": 30}')
# data = {"name": "Alice", "age": 30}
# Python-объект → строку
s = json.dumps(data)
s = json.dumps(data, indent=2, ensure_ascii=False)
# ensure_ascii=False — не эскейпить кириллицу в \uXXXX
# Из / в файл — load/dump без "s"
with open("data.json", encoding="utf-8") as f:
data = json.load(f)
with open("out.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
# Соответствия типов
# JSON → Python
# object → dict
# array → list
# string → str
# number → int / float
# true → True
# null → None
# Ошибки
try:
data = json.loads(payload)
except json.JSONDecodeError as e:
logger.error(f"bad JSON: {e}")
datetime не сериализуется по умолчанию. json.dumps({"created": datetime.now()}) → TypeError. Решения: конвертировать в строку заранее (.isoformat()), либо json.dumps(..., default=str), либо использовать Pydantic.csvimport csv
# Чтение — обычные списки-строки
with open("users.csv", encoding="utf-8") as f:
reader = csv.reader(f)
header = next(reader) # первая строка — обычно шапка
for row in reader:
print(row) # ['Alice', '30', 'a@b.c']
# Чтение как dict — DictReader (питоничнее)
with open("users.csv", encoding="utf-8") as f:
for row in csv.DictReader(f):
print(row["name"], row["email"])
# Запись
with open("out.csv", "w", encoding="utf-8", newline="") as f:
# newline="" — обязательно, иначе на Windows \r\r\n
writer = csv.DictWriter(f, fieldnames=["name", "age"])
writer.writeheader()
writer.writerow({"name": "Alice", "age": 30})
writer.writerows([{"name": "Bob", "age": 25}])
# Кастомный разделитель — TSV, semicolon
reader = csv.reader(f, delimiter=";")
"00123" станет 123. Не проблема Python, но пользователи ругаются. Для reliable выгрузки в Excel — .xlsx через openpyxl.with. f = open(path) без close() — файл висит открытым, на Windows его нельзя переименовать, на Linux — деcкриптор течёт.f.read() прочтёт всё сразу. Для больших — for line in f или чтение чанками f.read(8192).json.dumps без ensure_ascii=False. Кириллица превратится в При... — читабельно, но раздутый файл. Всегда ensure_ascii=False для внутреннего использования.csv без newline="". На Windows будут двойные переносы строк.open("data.txt") откроет от текущей рабочей директории, а не от файла со скриптом. Лучше: Path(__file__).parent / "data.txt".@Декоратор — функция, принимающая другую функцию и возвращающая новую (обычно с дополнительным поведением до/после вызова). Синтаксис @decorator — просто сахар над func = decorator(func).
# Простой декоратор — измеряет время выполнения
import time
from functools import wraps
def timed(func):
@wraps(func) # сохраняет __name__/__doc__ оригинала
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.3f}s")
return result
return wrapper
@timed
def slow_calc(n):
time.sleep(1)
return n * 2
slow_calc(5)
# slow_calc took 1.001s
# 10
Эквивалент без синтаксиса @:
def slow_calc(n): ...
slow_calc = timed(slow_calc) # ровно то же самое
# Retry с настраиваемым числом попыток
def retry(times=3, delay=1.0):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_error = None
for attempt in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
last_error = e
time.sleep(delay)
raise last_error
return wrapper
return decorator
@retry(times=5, delay=0.5)
def fetch_url(url):
return requests.get(url)
Три уровня функций: retry(times=3) возвращает decorator, тот принимает func, а wrapper уже делает работу. Синтаксический сахар разворачивается в fetch_url = retry(times=5, delay=0.5)(fetch_url).
| Декоратор | Что делает |
|---|---|
@property | Getter, вызываемый как атрибут |
@staticmethod | Метод без self/cls |
@classmethod | Метод, получающий cls вместо self |
@dataclass | Автогенерирует __init__/__repr__/__eq__ |
@functools.cache | Мемоизация — кеш результата по аргументам |
@functools.lru_cache(maxsize=128) | Мемоизация с ограничением размера |
@functools.wraps(func) | Сохраняет метаданные оригинала — используй в своих |
@abstractmethod | Абстрактный метод (в ABC-классе) |
@contextmanager | Превращает генератор в context manager |
from functools import cache
@cache
def fib(n):
if n < 2: return n
return fib(n-1) + fib(n-2)
fib(100) # мгновенно — благодаря кешу
# Flask
@app.route("/users/<int:id>")
def get_user(id): ...
# FastAPI
@app.get("/users/{id}")
def get_user(id: int): ...
# pytest — параметризация тестов
@pytest.mark.parametrize("a, expected", [(1, 2), (2, 4)])
def test_double(a, expected):
assert double(a) == expected
# Django — авторизация
@login_required
def profile(request): ...
@wraps. wrapper.__name__ станет "wrapper", а не имя оригинальной функции. Ломает help(), debug, некоторые фреймворки (Flask ругается на конфликт имён при регистрации роутов).@a @b def f() = f = a(b(f)). Ближний к функции — применяется первым. Для маршрутов часто важно: @app.route @login_required — сначала auth-check.@retry и @retry() — не одно и то же. Первый передаст retry(func), второй — retry()(func).ParamSpec. Для нового кода — ParamSpec из typing (Python 3.10+).@wraps — обязательно. Встроенные (@property, @dataclass, @cache) — используй сразу. Свои — для сквозных обязанностей: логирование, retry, кеш, auth-check в фреймворке.
# Переменные
name: str = "Alice"
age: int = 30
is_active: bool = True
tags: list[str] = ["php", "python"]
user: dict[str, int] = {"age": 30}
# Функции
def greet(name: str, times: int = 1) -> str:
return (f"Hi {name}! ") * times
# Возврат None (void в других языках)
def log_it(msg: str) -> None:
print(msg)
# Классы
class User:
name: str
age: int
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
mypy. IDE использует их для автокомплита и подсветки ошибок.
|# X | None — может быть X или None (Python 3.10+)
def find_user(id: int) -> User | None:
...
# Legacy до 3.10 (ещё встречается)
from typing import Optional
def find_user(id: int) -> Optional[User]:
...
# Union — X или Y или Z
def parse(value: str | int | float) -> float:
...
# Коллекции с несколькими типами внутри
users: list[User | Admin] = [...]
| Legacy (до Python 3.9) | Modern (3.9+) |
|---|---|
List[int] | list[int] |
Dict[str, int] | dict[str, int] |
Tuple[int, str] | tuple[int, str] |
Set[int] | set[int] |
Optional[X] | X | None (3.10+) |
Union[X, Y] | X | Y (3.10+) |
В новом коде — только list/dict/|. Импорты из typing для этих типов больше не нужны.
from typing import Any, Callable, Literal, Final, TypedDict, Protocol
# Any — «выключить» type-check (использовать в крайних случаях)
def parse_json(s: str) -> Any:
return json.loads(s)
# Callable — функция как аргумент
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
# Literal — конкретные значения
def set_status(status: Literal["active", "paused", "stopped"]) -> None:
...
set_status("active") #
set_status("deleted") # mypy error
# Final — константа, mypy запретит перезаписать
MAX_RETRIES: Final[int] = 3
# TypedDict — структура словаря
class UserDict(TypedDict):
name: str
age: int
email: str | None
def save_user(u: UserDict) -> None: ...
# Protocol — duck typing со статической проверкой
class HasName(Protocol):
name: str
def greet(obj: HasName) -> str:
return f"Hi {obj.name}"
# Работает с любым классом у которого есть поле name — независимо от наследования
# Установка
uv add --dev mypy
# Запуск
mypy app.py
mypy src/
# Конфиг — pyproject.toml
[tool.mypy]
python_version = "3.13"
strict = true # все проверки на максимум
warn_unused_ignores = true
disallow_untyped_defs = true
# Игнорировать одну строку
result = something_weird() # type: ignore[assignment]
# Игнорировать модуль без типов
[[tool.mypy.overrides]]
module = "legacy_lib.*"
ignore_missing_imports = true
x: int = "hello" запустится без ошибки в рантайме. Только mypy заметит. Для проверки в рантайме — Pydantic (см. FastAPI).def foo(self) -> "User" или from __future__ import annotations в начале файла.Any — «выключение» типизации. Использовать как крайнюю меру. mypy пропускает через Any любые операции.List/Dict. В новом коде — list/dict. Не смешивай.func.__annotations__ — dict имя→тип. Используется Pydantic, FastAPI, dataclasses для генерации кода.list[int], X | None, TypedDict, Protocol. Настрой mypy strict в pyproject.toml и запусти в CI.
HTTP-запрос к БД, вызов внешнего API, чтение файла — во всех случаях процесс ждёт I/O. Синхронный код в этот момент блокирует поток. Async позволяет одному потоку обслуживать сотни одновременных операций I/O — пока один запрос ждёт БД, поток обрабатывает другие.
Когда нужен async:
- HTTP API с тысячами RPS (FastAPI async — стандарт)
- WebSocket-серверы, real-time
- Web-scrapers — тысячи параллельных запросов
- Микросервисы с множеством внешних вызовов
Когда НЕ нужен: CPU-heavy вычисления (тут async не помогает — нужен multiprocessing), простые CRUD с 5 RPS, скрипты. Не тащить async туда, где он не нужен — иначе получишь сложный код без выигрыша.
import asyncio
# async def создаёт корутину, не исполняет функцию сразу
async def fetch_user(id: int) -> dict:
await asyncio.sleep(1) # эмуляция I/O
return {"id": id, "name": "Alice"}
# Обычный вызов вернёт корутину, не результат:
fetch_user(1) # <coroutine object fetch_user at 0x...>
# Правильный запуск — только через event loop:
result = asyncio.run(fetch_user(1))
# await — только внутри async def
async def main():
user = await fetch_user(1) # дождались, получили dict
print(user)
asyncio.run(main())
gatherasync def fetch_user(id):
await asyncio.sleep(1)
return {"id": id}
# Последовательно — 3 секунды
async def bad():
a = await fetch_user(1)
b = await fetch_user(2)
c = await fetch_user(3)
# Параллельно — 1 секунда
async def good():
a, b, c = await asyncio.gather(
fetch_user(1),
fetch_user(2),
fetch_user(3),
)
# Обработка ошибок в gather
results = await asyncio.gather(*tasks, return_exceptions=True)
# исключения будут В списке, не бросят весь gather
# Timeout — Python 3.11+
async def get_with_timeout():
async with asyncio.timeout(5.0):
return await slow_operation()
# Legacy до 3.11 — wait_for
result = await asyncio.wait_for(slow_operation(), timeout=5.0)
# Создать задачу в фоне — не блокировать текущую функцию
task = asyncio.create_task(background_job())
print("main continues...")
await task # дождаться когда потребуется
task.cancel() # можно отменить
# HTTP через httpx (async-first, поддерживает и sync)
import httpx
async def fetch_all(urls):
async with httpx.AsyncClient() as client:
tasks = [client.get(url) for url in urls]
responses = await asyncio.gather(*tasks)
return [r.json() for r in responses]
# БД через asyncpg (PostgreSQL)
import asyncpg
async def get_users():
conn = await asyncpg.connect("postgresql://...")
rows = await conn.fetch("SELECT id, name FROM users")
await conn.close()
return rows
# SQLAlchemy 2.0 async
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
engine = create_async_engine("postgresql+asyncpg://...")
async with AsyncSession(engine) as session:
result = await session.execute(select(User))
users = result.scalars().all()
| asyncio | threading | multiprocessing | |
|---|---|---|---|
| Для чего | I/O-bound (HTTP, БД) | I/O-bound + legacy code | CPU-bound |
| GIL мешает? | Нет (один поток) | Да, но I/O освобождает | Нет (разные процессы) |
| Overhead | Минимальный | Средний | Высокий |
| Общая память | Да (тот же процесс) | Да, нужны locks | Нет (IPC) |
| Тысячи задач | Легко | Кончится память | Overhead убьёт |
await. result = fetch_user(1) — в result корутина, а не dict. IDE подсветит; при запуске упадёт с RuntimeWarning: coroutine was never awaited.time.sleep(5) внутри async def — заблокирует ВЕСЬ сервер на 5 секунд. Используй await asyncio.sleep(5). Синхронный requests.get() — тоже блокирует; вместо него httpx.AsyncClient.asyncio.to_thread(func) или в отдельный процесс.await. Приходится asyncio.run(async_func()), но нельзя вложенно (если уже в event loop). Правило: если проект async — весь стек async.logger.exception внутри corountine.async def + await = concurrent I/O в одном потоке. Для тысяч одновременных операций — идеально. Для CPU — не поможет (нужен multiprocessing). Правило проекта: либо весь стек async, либо весь sync — смешивание болит. FastAPI + httpx + asyncpg/SQLAlchemy async — стандартная async-связка backend 2026.
Iterable — объект, у которого есть __iter__(). Возвращает iterator. Iterator — объект с __next__(), возвращает следующий элемент, кидает StopIteration когда всё кончилось. for x in obj: — сахар над этим протоколом.
# Внутри for крутится примерно вот это:
it = iter(users) # получить iterator из iterable
while True:
try:
user = next(it) # next() зовёт __next__()
except StopIteration:
break
process(user)
# Свой iterator — редко, обычно используем генератор (см. ниже)
class Counter:
def __init__(self, limit): self.n, self.limit = 0, limit
def __iter__(self): return self
def __next__(self):
if self.n >= self.limit: raise StopIteration
self.n += 1
return self.n
for i in Counter(3): print(i) # 1, 2, 3
yieldФункция с yield — генератор. При вызове не выполняется сразу, возвращает generator-объект. Каждый next() исполняет до следующего yield, потом заморозит состояние.
def count_up_to(limit):
n = 1
while n <= limit:
yield n
n += 1
gen = count_up_to(3) # НИЧЕГО не выполнилось; gen — генератор-объект
next(gen) # 1
next(gen) # 2
next(gen) # 3
next(gen) # StopIteration
# Обычно через for
for n in count_up_to(10):
print(n)
# Или превратить в list (загрузит всё в память)
list(count_up_to(100))
Главный use-case: обработка больших потоков данных без загрузки всего в память.
# Загрузим весь 10GB лог в память — OOM
def read_all(path):
with open(path) as f:
return f.readlines() # list всех строк
# Читаем по строке, память O(1)
def read_lazy(path):
with open(path) as f:
for line in f:
yield line.rstrip()
# Пайплайн — можно накладывать фильтры/преобразования лениво
def errors_only(lines):
for line in lines:
if "ERROR" in line:
yield line
def parse_json(lines):
for line in lines:
yield json.loads(line)
# Читаем 10GB, фильтруем, парсим — всё лениво, память O(1)
for event in parse_json(errors_only(read_lazy("app.log"))):
save_to_db(event)
yield from — делегированиеdef flatten(matrix):
for row in matrix:
yield from row # yield каждого элемента row
# Эквивалент:
# for row in matrix:
# for x in row:
# yield x
list(flatten([[1, 2], [3, 4]])) # [1, 2, 3, 4]
itertools — стандартная библиотека| Функция | Что делает |
|---|---|
chain(a, b, c) | Соединяет несколько iterables в один |
islice(it, start, stop, step) | Срез iterator'а (нельзя срезать generator обычным [:]) |
count(start, step) | Бесконечный счётчик start, start+step, ... |
cycle(iterable) | Бесконечно повторяет |
repeat(x, n) | Повторяет x N раз (или бесконечно) |
groupby(iterable, key) | Группировка подряд идущих одинаковых (не как SQL GROUP BY) |
combinations(it, r) | Все сочетания по r элементов |
permutations(it, r) | Все перестановки |
product(a, b) | Декартово произведение |
batched(it, n) (Python 3.12+) | Разбить на батчи размером n |
takewhile(pred, it), dropwhile | Пока условие true / после того как false |
from itertools import chain, islice, batched
# Соединить несколько источников
for item in chain(users_db, users_api, users_cache):
...
# Взять первые 100 из бесконечного generator
for line in islice(read_lazy("huge.log"), 100):
...
# Батчи по 100 — для bulk-INSERT в БД
for chunk in batched(records, 100):
User.bulk_create(chunk)
list(gen) дважды — второй раз получишь []. Хочешь повторный обход — заверни в list.len(generator). Длина неизвестна. sum(1 for _ in gen) посчитает (но выпьет генератор).gen[5]. Только последовательный доступ. Для 5-го элемента — next(islice(gen, 5, 6)).gen = (fn(x) for x in items) — fn ещё не вызвана. Ошибки внутри fn вылезут только при обходе, не при создании.yield внутри with-блока и досрочный exit. Если генератор бросили не дочитав — __exit__ вызовется через GC (не сразу). Для файлов/соединений лучше contextmanager обёртка.yield — стандартный питонический способ обработки потоков без загрузки в память. Read → filter → transform → save — пайплайн из генераторов. Для обычных задач хватает yield в def + for; itertools — когда нужны chain/batched/groupby.
Django (с 2005) — full-stack framework: ORM, миграции, admin-панель, auth, templates, sessions, forms, i18n. По философии — batteries included: почти всё что нужно для сайта, уже внутри.
На чём построены: Instagram, Pinterest, Disqus, Bitbucket, Mozilla, Washington Post. К 2026 — по-прежнему стандарт для «сайта с админкой», но для чистых API проиграл FastAPI.
uv add django
uv run django-admin startproject myapp .
uv run python manage.py startapp users
uv run python manage.py migrate
uv run python manage.py runserver
# http://localhost:8000
Структура:
myapp/
├── manage.py # CLI как artisan
├── myapp/
│ ├── settings.py # конфиг всего
│ ├── urls.py # роуты
│ ├── wsgi.py / asgi.py # entry points для сервера
├── users/ # app
│ ├── models.py # модели БД
│ ├── views.py # контроллеры
│ ├── urls.py
│ ├── admin.py # регистрация в admin-панели
│ ├── migrations/
│ └── templates/
Django-версия MVC. «View» здесь — не то же, что вью в Laravel: это контроллер. «Template» — HTML-шаблон (Blade-аналог).
# users/models.py
from django.db import models
class User(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
# users/views.py
from django.shortcuts import render, get_object_or_404
from .models import User
def user_list(request):
users = User.objects.all()
return render(request, "users/list.html", {"users": users})
def user_detail(request, pk):
user = get_object_or_404(User, pk=pk)
return render(request, "users/detail.html", {"user": user})
# users/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("", views.user_list, name="user_list"),
path("<int:pk>/", views.user_detail, name="user_detail"),
]
Template (аналог Blade):
<!-- users/templates/users/list.html -->
{% extends "base.html" %}
{% block content %}
<ul>
{% for user in users %}
<li><a href="{% url 'user_detail' user.pk %}">{{ user.name }}</a></li>
{% empty %}
<li>Пользователей нет</li>
{% endfor %}
</ul>
{% endblock %}
# CRUD
u = User.objects.create(name="Alice", email="a@b.c")
User.objects.get(pk=1) # найти один или DoesNotExist
User.objects.filter(name__startswith="A") # QuerySet
User.objects.exclude(is_active=False)
User.objects.filter(age__gte=18, name__icontains="ali")
# Обновление
u.name = "Bob"
u.save()
# Bulk update
User.objects.filter(is_active=False).update(archived=True)
# Удаление
u.delete()
# Связи (аналог relations в Eloquent)
class Post(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="posts")
title = models.CharField(max_length=200)
# Обход:
user.posts.all() # все посты юзера
post.author.name # автор поста
# Оптимизация N+1
User.objects.prefetch_related("posts") # посты одним запросом
Post.objects.select_related("author") # join с author
# Агрегация
from django.db.models import Count, Sum, Avg
User.objects.annotate(posts_count=Count("posts"))
User.objects.aggregate(total_age=Sum("age"))
# Сгенерировать по изменениям в models.py
python manage.py makemigrations
# Migrations for 'users':
# users/migrations/0002_user_phone.py
# Применить
python manage.py migrate
# Откатить
python manage.py migrate users 0001
# SQL который выполнится
python manage.py sqlmigrate users 0002
В отличие от Laravel — ORM автогенерит миграции сам. Меняешь models.py, запускаешь makemigrations — Django поймёт диф и напишет миграцию.
# users/admin.py
from django.contrib import admin
from .models import User
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
list_display = ["name", "email", "created_at"]
list_filter = ["is_active"]
search_fields = ["name", "email"]
ordering = ["-created_at"]
Одна регистрация — и в /admin/ получаешь готовую CRUD-панель с фильтрами, поиском, пермишенами. Это причина почему Django до сих пор берут для внутренних систем и админок.
Для API поверх Django ставят DRF — отдельный пакет:
uv add djangorestframework
# users/serializers.py — аналог Pydantic-моделей
from rest_framework import serializers
from .models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "name", "email", "created_at"]
# users/views.py
from rest_framework import viewsets
from .models import User
from .serializers import UserSerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
# Всё — 5 CRUD endpoints готовы (GET list, POST create, GET/PUT/DELETE detail)
| Задача | Django | FastAPI |
|---|---|---|
| Сайт с админкой | Идеально | Не подходит |
| Внутренняя система с CRUD-формами | Огромная экономия времени | Больше кода писать |
| Публичный REST API | DRF — работает, но многословно | Стандарт 2026 |
| Микросервис / AI-backend | Тяжеловат | Быстро и легко |
| WebSocket / real-time | Django Channels — через костыли | Async из коробки |
| Стартап-прототип с сайтом | Мгновенно рабочий MVP | Придётся собирать всё |
User.register(...)), а не в view. Обратно от Laravel «тонкий контроллер + Service».for post in posts: print(post.author.name) — запрос на каждый автор. Спасение — select_related/prefetch_related.QuerySet ленивый. qs = User.objects.filter(...) — SQL ещё не выполнен. Вызовется при for, list(), len(). Кешируется после первого обхода.FastAPI — современный async-first веб-фреймворк на type hints (Python 3.7+, актив с 2018, к 2026 — #1 выбор для нового Python-API). Основан на Starlette (ASGI) + Pydantic (валидация). Даёт из коробки: async endpoints, автоматическая валидация вход/выхода по type hints, автогенерация OpenAPI и Swagger UI, DI через Depends().
uv init my-api && cd my-api
uv add fastapi uvicorn[standard]
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"status": "ok"}
@app.get("/users/{user_id}")
async def get_user(user_id: int):
return {"id": user_id}
uv run uvicorn main:app --reload
# http://localhost:8000
# http://localhost:8000/docs — Swagger UI автоматом
# http://localhost:8000/redoc — ReDoc
user_id: int — не просто аннотация. FastAPI сам конвертирует path-параметр в int, валидирует, при ошибке возвращает 422 с понятным JSON. Никакого (int)$request->input() и валидатора руками.
from pydantic import BaseModel, Field, EmailStr
class CreateUserRequest(BaseModel):
name: str = Field(min_length=2, max_length=50)
email: EmailStr
age: int = Field(ge=18, le=120)
role: Literal["user", "admin"] = "user"
class UserResponse(BaseModel):
id: int
name: str
email: str
@app.post("/users", response_model=UserResponse, status_code=201)
async def create_user(payload: CreateUserRequest) -> UserResponse:
# payload — уже провалидирован, все типы правильные
user = await user_service.create(payload)
return user # автоматом сериализуется по UserResponse (hide-hostile поля)
Что делает Pydantic:
- Приходит
{"age": "30"}(string) → сконвертит вintавтоматически - Приходит
{"age": "abc"}→ 422 с"value is not a valid integer" - Приходит
{"email": "not-email"}→ 422 с"value is not a valid email" - Приходит лишние поля — игнорируются (или бросается ошибка если
model_config = {"extra": "forbid"}) response_modelобрезает вывод —passwordне утечёт даже если в объекте есть
Laravel-аналог: Pydantic-модель ≈ FormRequest (валидация) + Eloquent (типы) + JsonResource (сериализация) в одном классе.
from fastapi import Query, Path
@app.get("/users/{user_id}/orders")
async def list_orders(
user_id: int = Path(gt=0), # из URL
limit: int = Query(10, ge=1, le=100), # ?limit=10
status: str | None = None, # ?status=paid (опц.)
tag: list[str] = Query(default_factory=list), # ?tag=a&tag=b
):
...
@app.post("/orders")
async def create_order(payload: OrderRequest): # Pydantic — из body
...
Правило: FastAPI сам различает по типу. Path-параметр {user_id} в URL — берёт оттуда. Простые типы (int, str, bool) — из query. Pydantic-модель — из body JSON.
from fastapi import Depends, HTTPException, status
# Общий provider — сессия БД
async def get_db() -> AsyncSession:
async with AsyncSession(engine) as session:
yield session
# Auth-проверка
async def current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db),
) -> User:
user = await verify_jwt(token, db)
if not user:
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
return user
# Использование — DI по типу параметра
@app.get("/me")
async def read_me(user: User = Depends(current_user)) -> User:
return user
# Depends можно повесить на роут — auth ко всем эндпоинтам роутера
router = APIRouter(dependencies=[Depends(current_user)])
Как это работает: FastAPI смотрит на аннотации параметров и рекурсивно резолвит зависимости. get_db возвращает через yield → это context manager, сессия закроется после ответа. Кешируется в рамках запроса (одна БД-сессия на весь request).
myapi/
├── pyproject.toml
├── uv.lock
├── .env # DATABASE_URL, SECRET_KEY, ...
├── main.py # FastAPI() + include_router
├── src/
│ ├── config.py # pydantic-settings — типизированный env
│ ├── database.py # engine, get_db()
│ ├── auth/
│ │ ├── router.py # /login, /register
│ │ ├── schemas.py # Pydantic-модели входа/выхода
│ │ ├── service.py # бизнес-логика
│ │ ├── models.py # SQLAlchemy-модели
│ │ └── deps.py # current_user, etc
│ ├── users/
│ │ └── ...
│ └── orders/
│ └── ...
└── tests/
└── ...
# main.py
from fastapi import FastAPI
from src.auth.router import router as auth_router
from src.users.router import router as users_router
app = FastAPI(title="My API", version="1.0")
app.include_router(auth_router, prefix="/auth", tags=["auth"])
app.include_router(users_router, prefix="/users", tags=["users"])
from fastapi import HTTPException
@app.get("/users/{id}")
async def get_user(id: int):
user = await find(id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
# Глобальный обработчик кастомного исключения
from fastapi.responses import JSONResponse
@app.exception_handler(OrderError)
async def order_error_handler(request, exc: OrderError):
return JSONResponse(
status_code=400,
content={"error": str(exc), "type": exc.__class__.__name__},
)
| FastAPI | Django | Flask | |
|---|---|---|---|
| Тип | Micro/API-first, async | Batteries-included | Micro |
| Async | Первого класса | Частично (3.1+) | Нет (только через расширения) |
| ORM | Нет — свой выбор (SQLAlchemy) | Свой мощный ORM | Нет — Flask-SQLAlchemy |
| Admin panel | Нет из коробки | Мощная | Нет |
| Валидация | Pydantic — на type hints | Django Forms / DRF Serializers | Отдельные libs |
| OpenAPI/Swagger | Автоматически | Через DRF + пакеты | Нужны пакеты |
| Кривая обучения | Быстро — type hints и всё | Долго — много концепций | Быстро — но всё собирать |
| Когда выбирать | HTTP-API, микросервис, AI-backend | Сайт с админкой, монолит | Скрипт, прототип, legacy |
requests.get(), time.sleep(), синхронный SQLAlchemy — все убивают производительность. Используй httpx.AsyncClient, await asyncio.sleep, SQLAlchemy async.await asyncio.to_thread(func) или в отдельный воркер (Celery/RQ).MissingGreenlet. Используй response_model с Pydantic-схемой или model.model_dump().@app.on_event устарел. В новых версиях FastAPI — lifespan context manager: app = FastAPI(lifespan=my_lifespan).Depends — не singleton по умолчанию. Кешируется в рамках одного запроса. Хочешь real singleton — оборачивай в @lru_cache.app.add_middleware(CORSMiddleware, allow_origins=[...]).Depends() — DI. Deploy — uvicorn в Docker + reverse-proxy Nginx. Для сайта с админкой — Django; для микросервиса/AI-бэка/API — FastAPI.
Flask (с 2010) — микрофреймворк: даёт роутинг + шаблоны + request/response. Всё остальное (ORM, форма, auth) — плагины по выбору. Долгое время был «PHP для Python» — быстро написать что-то маленькое.
К 2026: уступает FastAPI. Для нового кода выбирают FastAPI (async + type hints + автодоки). Flask остался в: legacy-системах, обучении (простой для входа), небольших скриптах-веб-инструментах.
uv add flask
# app.py
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/")
def home():
return "Hello!"
@app.route("/users/<int:user_id>", methods=["GET"])
def get_user(user_id):
return jsonify({"id": user_id, "name": "Alice"})
@app.route("/users", methods=["POST"])
def create_user():
data = request.get_json() # ручной парсинг
if not data or "name" not in data:
return jsonify({"error": "name required"}), 400
return jsonify({"created": data}), 201
if __name__ == "__main__":
app.run(debug=True)
python app.py
# http://localhost:5000
Обрати внимание — валидация вручную (сравни с Pydantic в FastAPI, где просто аннотация типа). Это основной минус.
| Задача | Плагин |
|---|---|
| ORM | Flask-SQLAlchemy |
| Миграции | Flask-Migrate (Alembic) |
| Формы + CSRF | Flask-WTF |
| Auth (сессии) | Flask-Login |
| JWT | Flask-JWT-Extended |
| REST | Flask-RESTful, Flask-Smorest |
| Admin-панель | Flask-Admin |
В сумме получается тот же Django, только собираешь его сам из кубиков.
- Мини-скрипт с 3-5 endpoint'ами (webhook receiver, health-check server)
- Проект на legacy Python 2 / 3.6 (FastAPI требует 3.7+)
- Обучение — синтаксис проще чем у FastAPI, меньше «магии»
- Работаешь в существующем Flask-проекте
Для нового REST-API в 2026 — берут FastAPI, не Flask. Скорость разработки выше за счёт Pydantic и автодоки.
| Аспект | Django | FastAPI | Flask |
|---|---|---|---|
| Тип | Full-stack | Micro / API-first | Micro |
| Год | 2005 | 2018 | 2010 |
| Философия | Batteries included | Type-driven, async-first | Do-it-yourself |
| Async | Частично (4.1+) | Первого класса | (только Quart-fork) |
| ORM в комплекте | Django ORM | (SQLAlchemy отдельно) | (Flask-SQLAlchemy отдельно) |
| Admin-панель | Из коробки | Flask-Admin (отдельно) | |
| Auth-система | Полная | Собирать (FastAPI Users / OAuth2) | Flask-Login |
| Валидация | Django Forms / DRF Serializers | Pydantic на type hints | Вручную / Flask-WTF |
| Автодоки OpenAPI | Через DRF + drf-spectacular | Автоматически | Через плагин Flask-Smorest |
| Templates | Django templates | Jinja2 через Starlette | Jinja2 |
| Миграции | Автогенерация | Alembic отдельно | Flask-Migrate |
| Скорость (RPS) | Средняя | Высокая (async) | Средняя |
| Кривая обучения | Крутая — много концепций | Пологая, но нужны type hints | Очень пологая |
| Комьюнити 2026 | Огромное, зрелое | Взрывной рост, активное | Стабильное, но растёт медленно |
| Кто использует | Instagram, Pinterest, Reddit | Netflix, Uber, Microsoft | Небольшие сервисы, скрипты |
Сайт с админкой, внутренняя система, e-commerce
→ Django. Batteries included окупятся за 1-2 недели: admin, auth, ORM, миграции — всё уже настроено. Не изобретай стек.
Публичный REST API, микросервис, AI-backend
→ FastAPI. Async из коробки, Pydantic для валидации, автогенерация OpenAPI. Стандарт нового Python-backend в 2026.
Мини-скрипт, webhook receiver, prototype
→ Flask — если проект действительно минимальный (3-5 endpoints, никаких перспектив роста). Иначе всё равно FastAPI.
Работа с существующим кодом
→ что уже стоит. Переписывать legacy Flask на FastAPI ради «модно» — редко того стоит. Мигрируй только новые модули.
Крупные компании часто смешивают:
- Django — «основной сайт» с админкой + бизнес-логика
- FastAPI — новые публичные API-эндпоинты, микросервисы, ML-inference
- Общая база данных через одну ORM (обычно SQLAlchemy)
Так на разные задачи используется правильный инструмент, без религиозных войн.
| Уровень | Инструменты | PHP-аналог |
|---|---|---|
| 1. Сырые SQL | psycopg (PG), mysql-connector, sqlite3 (stdlib) | PDO |
| 2. Query Builder + Core ORM | SQLAlchemy Core | Laravel Query Builder |
| 3. Full ORM | SQLAlchemy ORM, Django ORM | Eloquent, Doctrine |
Для нового backend-проекта на FastAPI — стандарт SQLAlchemy 2.0 async. Для Django-проекта — Django ORM (входит).
psycopgpsycopg (v3, 2021) — актуальный драйвер PostgreSQL. Старая версия psycopg2 — legacy, но ещё популярна. Аналог PDO — прямой доступ к SQL.
uv add "psycopg[binary]"
import psycopg
with psycopg.connect("postgresql://user:pass@localhost/mydb") as conn:
with conn.cursor() as cur:
# SELECT с параметрами (защита от SQL-injection)
cur.execute("SELECT id, name FROM users WHERE age > %s", (18,))
rows = cur.fetchall() # list of tuples
for id, name in rows:
print(id, name)
# INSERT + RETURNING
cur.execute(
"INSERT INTO users (name, email) VALUES (%s, %s) RETURNING id",
("Alice", "a@b.c"),
)
new_id = cur.fetchone()[0]
conn.commit()
# Row factory — получить dict вместо tuple
from psycopg.rows import dict_row
with psycopg.connect(DSN, row_factory=dict_row) as conn:
with conn.cursor() as cur:
cur.execute("SELECT id, name FROM users")
for row in cur:
print(row["name"])
%s, никогда f-string / concat. cur.execute(f"SELECT ... WHERE id = {user_input}") — SQL-инъекция. Всегда cur.execute("... WHERE id = %s", (user_input,)) — драйвер эскейпит.SQLAlchemy — de-facto ORM в Python-экосистеме (кроме Django-проектов). Версия 2.0 (2023) кардинально переработана: новый API, полноценный async, отличные type hints.
uv add "sqlalchemy[asyncio]" "asyncpg"
# models.py — декларация моделей
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String, ForeignKey
from datetime import datetime
class Base(DeclarativeBase): pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100))
email: Mapped[str] = mapped_column(String(200), unique=True)
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
# database.py — async engine + session
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/mydb")
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
async def get_db(): # для FastAPI Depends()
async with AsyncSessionLocal() as session:
yield session
# Использование в FastAPI
from sqlalchemy import select, insert, update, delete
@app.get("/users/{id}")
async def get_user(id: int, db: AsyncSession = Depends(get_db)):
# SELECT one
user = await db.get(User, id)
if not user:
raise HTTPException(404)
return user
@app.get("/users")
async def list_users(db: AsyncSession = Depends(get_db)):
# SELECT many
stmt = select(User).where(User.name.ilike("a%")).limit(100)
result = await db.execute(stmt)
return result.scalars().all()
@app.post("/users")
async def create_user(payload: CreateUserRequest, db: AsyncSession = Depends(get_db)):
user = User(name=payload.name, email=payload.email)
db.add(user)
await db.commit()
await db.refresh(user) # подтянуть id, created_at
return user
# Связь один-ко-многим
class Post(Base):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(primary_key=True)
author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
title: Mapped[str]
author: Mapped["User"] = relationship(back_populates="posts")
class User(Base):
...
posts: Mapped[list["Post"]] = relationship(back_populates="author")
# Оптимизация — selectinload = prefetch, joinedload = JOIN
from sqlalchemy.orm import selectinload
stmt = select(User).options(selectinload(User.posts))
# Один запрос за юзерами + один за всеми постами (через IN)
# Без — N+1: SELECT users, потом на каждого — SELECT posts
Стандарт для миграций SQLAlchemy. Работает как artisan migrate в Laravel — генерация SQL по diff моделей.
uv add alembic
alembic init migrations # создать конфиг
# Настроить alembic/env.py — указать target_metadata = Base.metadata
# Автогенерация по diff моделей
alembic revision --autogenerate -m "add users table"
# Применить все миграции
alembic upgrade head
# Откатить одну
alembic downgrade -1
# Показать текущую версию
alembic current
# История
alembic history
session.query(...)) не работает в async-сессии. Один стек — либо весь sync, либо весь async.await db.commit(). Изменения не сохранятся. В отличие от Django/Laravel, где транзакция автозакрывается — SQLAlchemy требует явного commit.user.posts без предварительной selectinload в async — MissingGreenlet exception. Async не умеет ленивую подгрузку — грузи заранее.response_model.AsyncSession() в каждой функции — используй Depends() и один shared через request. Иначе connection pool не окупится.psycopg напрямую. Всегда параметризованные запросы. В async — selectinload для relations, чтобы избежать MissingGreenlet.
requests | httpx | |
|---|---|---|
| Год | 2011 | 2019 |
| API | Простой, стандартный | Такой же — можно мигрировать с sed |
| Async | Нет | Sync + Async в одном пакете |
| HTTP/2 | Нет | Да |
| Timeout по умолчанию | Нет (висит навсегда) | 5 секунд |
| Проверка типов | Слабая | Полные type hints |
Для нового кода — httpx. requests — для legacy или совсем маленьких скриптов.
uv add httpx
import httpx
# GET
r = httpx.get("https://api.example.com/users/1")
r.status_code # 200
r.text # raw body как строка
r.json() # JSON → dict
r.headers["content-type"]
# Query params
r = httpx.get("https://api.ex.com/search", params={"q": "python", "limit": 10})
# автосклейка → ?q=python&limit=10
# POST JSON
r = httpx.post("https://api.ex.com/users", json={"name": "Alice"})
# Content-Type: application/json автоматически
# POST form-data
r = httpx.post(url, data={"name": "Alice"})
# Content-Type: application/x-www-form-urlencoded
# POST file (multipart)
with open("img.png", "rb") as f:
r = httpx.post(url, files={"image": f})
# Headers
r = httpx.get(url, headers={"Authorization": f"Bearer {token}"})
# Timeout
r = httpx.get(url, timeout=10.0) # обязательно ставить!
# Обработка ошибок HTTP-статусов
r.raise_for_status() # бросит HTTPStatusError если 4xx/5xx
Прямой вызов httpx.get() — каждый раз новое TCP-соединение. Для 10+ запросов к одному хосту используй Client:
# Sync client
with httpx.Client(
base_url="https://api.example.com",
headers={"Authorization": f"Bearer {token}"},
timeout=10.0,
) as client:
r1 = client.get("/users/1")
r2 = client.get("/users/2")
r3 = client.post("/users", json={...})
# Соединение переиспользуется, ~10× быстрее
AsyncClientimport asyncio, httpx
async def fetch_all(urls):
async with httpx.AsyncClient(timeout=10.0) as client:
tasks = [client.get(u) for u in urls]
responses = await asyncio.gather(*tasks)
return [r.json() for r in responses]
data = asyncio.run(fetch_all(["url1", "url2", "url3"]))
# Три запроса параллельно
В httpx/requests нет retry из коробки. Стандарт — библиотека tenacity:
uv add tenacity
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10),
)
def fetch(url):
r = httpx.get(url, timeout=5.0)
r.raise_for_status()
return r.json()
uv add --dev respx pytest
import respx, httpx, pytest
@respx.mock
def test_fetch_user():
route = respx.get("https://api.ex.com/users/1").mock(
return_value=httpx.Response(200, json={"id": 1, "name": "Alice"})
)
result = fetch_user(1)
assert result["name"] == "Alice"
assert route.called
timeout. В requests нет дефолта — запрос висит навсегда. В httpx дефолт 5 сек. Всегда указывай явно.r = httpx.get(...) с 500 не выкинет exception, только r.status_code == 500. Явно — r.raise_for_status().r.text может не быть JSON. Сначала проверь r.headers.get("content-type"), потом r.json(). Или try/except JSONDecodeError.httpx.get() внутри async def блокирует event loop. Используй httpx.AsyncClient.r.content прочитает весь response. Для 10GB-файла — httpx.stream("GET", url) as r: for chunk in r.iter_bytes().httpx.get(url, verify=False) — отключает проверку сертификата. НИКОГДА в проде, только для тестового localhost.NumPy — библиотека для работы с многомерными массивами (ndarray) и векторизованных вычислений. На нём построены Pandas, scikit-learn, PyTorch, TensorFlow, SciPy. Для ML — обязательный фундамент.
a * 2 в 50-100 раз быстрее [x*2 for x in list].
uv add numpy
import numpy as np # общепринятый alias, всегда np
ndarray)# Из Python-списка
a = np.array([1, 2, 3, 4]) # 1D — вектор
b = np.array([[1, 2, 3], [4, 5, 6]]) # 2D — матрица
c = np.array([[[1], [2]], [[3], [4]]]) # 3D — тензор
# Из «шаблонов»
np.zeros((3, 4)) # матрица 3×4 из нулей
np.ones((2, 3)) # из единиц
np.full((2, 2), 7) # заполнить константой
np.eye(3) # единичная 3×3
np.arange(0, 10, 2) # [0, 2, 4, 6, 8] — как range
np.linspace(0, 1, 11) # 11 точек от 0 до 1
# Случайные
np.random.rand(3, 4) # uniform [0, 1)
np.random.randn(3, 4) # normal (0, 1)
np.random.randint(0, 10, size=(2, 3)) # целые
# Свойства
a.shape # (4,) — размеры
b.shape # (2, 3)
a.ndim # 1 — размерность
b.ndim # 2
a.dtype # int64 — тип элементов
a.size # 4 — общее число элементов
np.array([1, "hello"]) сконвертит всё в строки. Задавать явно: np.array([1, 2], dtype=np.float32).a = np.array([1, 2, 3, 4])
# Арифметика — поэлементно, БЕЗ циклов
a * 2 # [2, 4, 6, 8]
a + 10 # [11, 12, 13, 14]
a ** 2 # [1, 4, 9, 16]
a > 2 # [False, False, True, True] — булева маска
# Между массивами — тоже поэлементно
b = np.array([10, 20, 30, 40])
a + b # [11, 22, 33, 44]
a * b # [10, 40, 90, 160] — НЕ матричное умножение
a @ b # 300 — скалярное произведение (dot product)
# Универсальные функции (ufuncs) — векторизованные
np.sqrt(a) # [1., 1.41, 1.73, 2.]
np.exp(a) # e^x
np.log(a) # ln
np.sin(a)
np.abs(a)
a * 2 — ~2ms. Разница 100×. Не пиши for i in range(len(a)): a[i] += 1 — пиши a += 1.
NumPy автоматически «растягивает» меньший массив, если операция это допускает. Одно из самых мощных свойств.
a = np.array([[1, 2, 3], # shape (2, 3)
[4, 5, 6]])
row = np.array([10, 20, 30]) # shape (3,)
a + row
# [[11, 22, 33],
# [14, 25, 36]]
# row «растянут» на 2 строки, потом поэлементно
# Классический use-case: нормализация features
# X shape (n_samples, n_features), mean shape (n_features,)
X_normalized = (X - X.mean(axis=0)) / X.std(axis=0)
Правила broadcasting (упрощённо):
- Сравнивают формы с конца.
- Размерности совпадают → ок.
- Одна из размерностей = 1 → «растягивается» до другой.
- Иначе — ошибка.
# Совместимые
(3, 4) + (4,) # (3,4) + (1,4) → (3,4)
(3, 4) + (3, 1) → (3,4)
(2, 1, 4) + (3, 4) → (2,3,4)
# Несовместимые — ValueError
(3, 4) + (3,) операнды не совпадают
a = np.arange(10).reshape(2, 5)
# [[0, 1, 2, 3, 4],
# [5, 6, 7, 8, 9]]
# Обычный slicing
a[0] # [0, 1, 2, 3, 4] — вся строка 0
a[0, 2] # 2 — элемент (row 0, col 2)
a[:, 1] # [1, 6] — колонка 1 целиком
a[:, 1:3] # колонки 1..2
a[0, ::2] # [0, 2, 4] — каждый второй в строке 0
# Fancy indexing — по массиву индексов
a[[0, 1], [2, 3]] # [2, 8] — a[0,2], a[1,3]
# Булева маска — самый частый паттерн
mask = a > 3 # маска той же формы, что и a
a[mask] # все элементы > 3 — [4, 5, 6, 7, 8, 9]
a[a % 2 == 0] # все чётные
# Модификация через маску
a[a < 5] = 0 # все < 5 → 0
b = a[:, 1:3]; b[0, 0] = 99 изменит оригинал a. Хочешь копию — явно a[:, 1:3].copy(). В Pandas та же ловушка (SettingWithCopyWarning).Ось (axis) — направление, вдоль которого агрегируем. Для 2D-матрицы: axis=0 — по столбцам (сжимает строки), axis=1 — по строкам (сжимает столбцы). Классическая путаница — запоминай: axis = «размерность, которая исчезает».
a = np.array([[1, 2, 3],
[4, 5, 6]]) # shape (2, 3)
a.sum() # 21 — сумма всего
a.sum(axis=0) # [5, 7, 9] — по столбцам (shape (3,))
a.sum(axis=1) # [6, 15] — по строкам (shape (2,))
# Все агрегации имеют axis
a.mean(axis=0) # среднее по столбцам
a.max(axis=1) # max в каждой строке
a.std(axis=0) # стандартное отклонение
a.argmax(axis=1) # индекс максимума в строке
a.cumsum(axis=0) # накопительная сумма
a = np.arange(12) # [0, 1, ..., 11]
a.reshape(3, 4) # (3, 4)
a.reshape(2, 2, 3) # (2, 2, 3)
a.reshape(-1, 3) # -1 = «посчитай сам» → (4, 3)
# Транспонирование
b = a.reshape(3, 4)
b.T # shape (4, 3)
# Уплощение
b.flatten() # копия, 1D
b.ravel() # view, 1D (быстрее)
# Добавить размерность
a[np.newaxis, :] # (12,) → (1, 12)
a[:, np.newaxis] # (12,) → (12, 1)
a[None, :] # то же — None = np.newaxis
# Склейка
np.concatenate([a, a], axis=0) # по оси 0
np.vstack([a, a]) # вертикально (по строкам)
np.hstack([a, a]) # горизонтально (по столбцам)
np.stack([a, a]) # добавляет новую ось
# Split — обратная операция
np.split(a, 3) # на 3 равные части
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
A @ B # матричное умножение (Python 3.5+)
np.dot(A, B) # то же, для legacy
A * B # поэлементно (Hadamard), НЕ матричное
np.linalg.inv(A) # обратная матрица
np.linalg.det(A) # определитель
np.linalg.solve(A, b) # решить систему A·x = b
np.linalg.eig(A) # собственные значения/векторы
np.linalg.norm(a) # норма (по умолчанию L2)
import numpy as np
# 100 samples × 4 features
X = np.random.rand(100, 4)
# Z-score normalization: (x - mean) / std
X_norm = (X - X.mean(axis=0)) / X.std(axis=0)
# mean/std shape (4,) → broadcasting до (100, 4)
# Min-max normalization: (x - min) / (max - min)
X_scaled = (X - X.min(axis=0)) / (X.max(axis=0) - X.min(axis=0))
# Проверка
X_norm.mean(axis=0) # ≈ [0, 0, 0, 0]
X_norm.std(axis=0) # ≈ [1, 1, 1, 1]
# Разделение на train/test
np.random.seed(42)
indices = np.random.permutation(len(X))
train_idx = indices[:80]
test_idx = indices[80:]
X_train = X[train_idx]
X_test = X[test_idx]
.copy() если нужен независимый массив.a * b vs a @ b. Первое — поэлементное умножение (Hadamard product), второе — матричное. Классическая ошибка при переходе с MATLAB.np.int8 вмещает -128..127. np.array([120], dtype=np.int8) + 100 → -36. В ML обычно float32/float64.axis=0 vs axis=1. Путаница почти у всех новичков. Запомни: axis = «размерность, которая исчезает». axis=0 в матрице (rows, cols) — исчезают rows, остаются cols.numba с @jit.np.array([1, np.nan, 3]).sum() → nan. Для игнорирования: np.nansum, np.nanmean, np.nanstd.ndarray, shape/dtype/axis, векторизация (a * 2 вместо цикла), broadcasting (a + row), reshape/transpose. Pandas и PyTorch — надстройки над этим фундаментом.
Pandas — DataFrame (таблица) + Series (колонка) на основе NumPy. Стандарт для анализа данных, ETL, подготовки датасетов под ML. Умеет читать всё (CSV/Excel/JSON/SQL/Parquet), фильтровать, группировать, объединять, обрабатывать пропуски. Обязательный инструмент data-стека.
- Backend-разработчику: конвертация CSV↔JSON↔Excel, разовый ETL, отчёты, миграции
- Для ML: подготовка признаков перед обучением — очистка, преобразование, agrgегация, объединение источников
- Data-инженеру: пайплайны обработки, Airflow-задачи
Когда НЕ Pandas: production real-time API (тяжёлая инициализация), потоки >RAM (используй Polars / Dask / DuckDB / чанки).
Полноценный курс по Pandas — отдельная тема (это data-science стандарт). Для backend-разработчика полезно знать минимум — для разовых задач:
- «Конвертнуть CSV в JSON / из Excel в БД / из БД в отчёт»
- Быстрое исследование данных перед миграцией
- Мини-ETL — раз в сутки достать данные, обработать, сохранить
- Ad-hoc отчёты — «сгруппируй по клиенту, посчитай сумму, отсортируй»
Не для: production real-time API — pandas тяжёлый, для этого SQL/SQLAlchemy.
uv add pandas openpyxl # openpyxl — для xlsx
import pandas as pd
# Из dict / list
df = pd.DataFrame({
"name": ["Alice", "Bob", "Charlie"],
"age": [30, 25, 35],
"city": ["NY", "LA", "NY"],
})
# Из файлов
df = pd.read_csv("data.csv")
df = pd.read_excel("data.xlsx", sheet_name="Users")
df = pd.read_json("data.json")
df = pd.read_sql("SELECT * FROM users", connection)
# Выгрузка
df.to_csv("out.csv", index=False)
df.to_excel("out.xlsx", index=False)
df.to_json("out.json", orient="records")
df.to_sql("users", connection, if_exists="append")
df.head(10) # первые 10
df.tail(5) # последние 5
df.sample(3) # случайные 3
df.shape # (rows, cols) — (3, 3)
df.columns # Index(['name', 'age', 'city'])
df.dtypes # типы колонок
df.info() # сводка + типы + non-null count
df.describe() # статистика по числовым колонкам
len(df) # число строк
# Выбрать колонку → Series
df["age"]
# Несколько колонок → DataFrame
df[["name", "age"]]
# Фильтр по условию — как WHERE в SQL
df[df["age"] > 30]
df[(df["age"] > 25) & (df["city"] == "NY")] # & — AND
df[df["city"].isin(["NY", "LA"])] # IN
df[df["name"].str.startswith("A")] # LIKE
# .query() — SQL-подобный синтаксис
df.query("age > 25 and city == 'NY'")
# Доступ по индексу
df.iloc[0] # первая строка (по позиции)
df.loc[0, "name"] # cell — строка 0, колонка name
# Сортировка
df.sort_values("age", ascending=False)
&/|/~, не and/or/not. Из-за перегрузки операторов Pandas. И скобки вокруг каждого условия обязательны.# Сколько людей в каждом городе
df.groupby("city").size()
# city
# LA 1
# NY 2
# Средний возраст по городам
df.groupby("city")["age"].mean()
# Несколько агрегатов сразу
df.groupby("city").agg({
"age": ["mean", "max", "count"],
"name": "count",
})
# Двойная группировка
df.groupby(["city", "department"])["salary"].sum()
users = pd.DataFrame({"id": [1, 2], "name": ["Alice", "Bob"]})
orders = pd.DataFrame({"user_id": [1, 1, 2], "total": [100, 200, 300]})
# INNER JOIN
pd.merge(users, orders, left_on="id", right_on="user_id")
# LEFT JOIN
pd.merge(users, orders, left_on="id", right_on="user_id", how="left")
# Все виды: 'inner' (default), 'left', 'right', 'outer', 'cross'
# NaN — как NULL в SQL
df.isna() # булевая маска — где NaN
df.isna().sum() # сколько NaN по каждой колонке
df.dropna() # удалить строки с NaN
df.fillna(0) # заменить на 0
df["age"].fillna(df["age"].mean()) # заменить на среднее
# Новая колонка на основе других
df["is_adult"] = df["age"] >= 18
df["full"] = df["name"] + " (" + df["city"] + ")"
# apply — применить функцию
df["age_group"] = df["age"].apply(lambda x: "adult" if x >= 18 else "minor")
# Удалить колонку
df = df.drop(columns=["city"])
df = df.drop([0, 2]) # удалить строки 0 и 2
# Переименовать
df = df.rename(columns={"age": "years"})
df[df.age > 18]["name"] = "X" — модифицирует копию, не оригинал. Пиши через .loc: df.loc[df.age > 18, "name"] = "X".pd.read_csv(..., chunksize=100_000) — генератор по чанкам.df.iterrows() — медленно. Обход по строкам в 100 раз медленнее векторных операций. Используй apply или векторизацию.pd.to_datetime(). После read_csv даты часто строки. Явно приведи: df["date"] = pd.to_datetime(df["date"]).read_csv/to_csv, фильтрацию, groupby, merge, fillna. Хватит на 95% разовых задач. Для ML — обязательно + concat, pivot, работа с датами, оптимизация типов. Для production real-time — оставь Pandas data-инженерам.
В ML/DS 90% работы — понимать данные. Гистограмма распределения, scatter корреляций, heatmap важностей, learning curves — без графиков вслепую. Стандартные библиотеки:
- matplotlib (с 2003) — фундамент, низкоуровневый API, полный контроль. Все другие библиотеки под капотом рисуют через matplotlib.
- seaborn — обёртка над matplotlib, красивые дефолты, статистические графики (regplot, boxplot, heatmap) из коробки.
- plotly — интерактивные графики (zoom, hover), для веба и дашбордов.
uv add matplotlib seaborn
import matplotlib.pyplot as plt # стандартный alias
import seaborn as sns
import numpy as np
# Простой line-plot
x = np.linspace(0, 10, 100)
y = np.sin(x)
plt.plot(x, y)
plt.xlabel("x")
plt.ylabel("sin(x)")
plt.title("Sine wave")
plt.grid(True)
plt.show()
# Несколько линий
plt.plot(x, np.sin(x), label="sin")
plt.plot(x, np.cos(x), label="cos")
plt.legend()
# Гистограмма — распределение
data = np.random.randn(1000)
plt.hist(data, bins=30, edgecolor="black")
# Scatter — корреляции
plt.scatter(x, y, c="red", alpha=0.5, s=30)
# Bar chart
plt.bar(["A", "B", "C"], [10, 20, 15])
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
axes[0, 0].plot(x, np.sin(x))
axes[0, 0].set_title("sin")
axes[0, 1].plot(x, np.cos(x))
axes[0, 1].set_title("cos")
axes[1, 0].hist(data)
axes[1, 1].scatter(x, y)
plt.tight_layout() # auto-spacing
plt.savefig("plots.png", dpi=150)
plt.show()
import seaborn as sns
import pandas as pd
sns.set_theme(style="whitegrid") # красивая сетка
# Данные
tips = sns.load_dataset("tips") # встроенный dataset
# Scatter с регрессией одной строкой
sns.regplot(data=tips, x="total_bill", y="tip")
# Boxplot — распределение по категориям
sns.boxplot(data=tips, x="day", y="total_bill")
# Violin plot — boxplot + KDE
sns.violinplot(data=tips, x="day", y="tip", hue="sex")
# Heatmap — корреляция признаков (ML-must)
sns.heatmap(df.corr(), annot=True, cmap="coolwarm", fmt=".2f")
# Pairplot — все пары признаков сразу
sns.pairplot(tips, hue="sex")
# Гистограмма + KDE
sns.histplot(tips, x="total_bill", kde=True, bins=30)
# Countplot — счёт по категориям (как bar от Pandas)
sns.countplot(data=tips, x="day", hue="sex")
| График | Зачем в ML |
|---|---|
| Histogram / KDE | Распределение признака. Гауссово? Skewed? Нужна ли нормализация / логарифмирование? |
Heatmap correlations (df.corr()) | Найти мультиколлинеарность — если два признака сильно коррелируют, один можно выкинуть |
| Scatter target vs feature | Есть ли линейная зависимость? Выбросы? |
| Learning curves (train/val loss vs epoch) | Overfit? Underfit? Пора остановить обучение? |
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
df = pd.read_csv("data.csv")
# 1. Обзор данных
df.info()
df.describe()
# 2. Распределения всех числовых
df.hist(figsize=(12, 8), bins=30)
plt.tight_layout()
# 3. Heatmap корреляций
plt.figure(figsize=(10, 8))
sns.heatmap(df.corr(numeric_only=True), annot=True, cmap="coolwarm", center=0)
# 4. Отношения target ↔ признаки
sns.pairplot(df, hue="target", diag_kind="kde")
# 5. Проверка баланса классов
sns.countplot(data=df, x="target")
plt.show() в скрипте / %matplotlib inline в ноутбуке. В Jupyter графики рендерятся inline автоматически с 3+, показ plt.show() необязателен.plt.figure(figsize=...) — иначе всё будет квадратные микро-графики 6×4.hist, scatter, heatmap, pairplot. Для production-дашбордов — plotly. Инлайн-графики в Jupyter — стандартный workflow ML.
Jupyter Notebook — интерактивная среда, где код разбит на ячейки, каждая выполняется отдельно, состояние переменных сохраняется между ячейками. Файл .ipynb — JSON с ячейками (код + markdown + результаты выполнения). Для ML — стандартная среда, а не .py-скрипты.
Почему в ML именно так:
- Загрузка данных занимает 10-60 секунд — глупо перезапускать при каждой правке
- Обучение модели — минуты/часы, тоже не хочешь повторять при исследовании результата
- Графики видны сразу под кодом, легко сравнивать эксперименты
- Комбинация кода + markdown + графиков = живой отчёт для команды
uv add jupyter
# Классический Jupyter
uv run jupyter notebook
# JupyterLab (современный UI)
uv add jupyterlab
uv run jupyter lab
# Или просто открой .ipynb в VS Code — встроенная поддержка
# PyCharm Professional тоже умеет
Cloud-варианты (без установки):
- Google Colab (colab.research.google.com) — бесплатно, GPU/TPU по подписке. Стандарт для learning.
- Kaggle Notebooks — с встроенным доступом к датасетам
- Deepnote, Databricks — enterprise-платформы
| Клавиша | Что делает |
|---|---|
Shift + Enter | Выполнить ячейку, перейти к следующей |
Ctrl + Enter | Выполнить, остаться на этой |
Alt + Enter | Выполнить + вставить новую ячейку снизу |
Esc | Выйти из режима редактирования (command mode) |
A / B (в command mode) | Вставить ячейку выше / ниже |
DD | Удалить ячейку |
M / Y | Переключить в markdown / в code |
Z | Undo удаления ячейки |
Ctrl + / | Комментировать/раскомментировать строку |
Tab | Автокомплит |
Shift + Tab | Показать docstring функции под курсором |
# %% — cell magic, применяется к всей ячейке
# % — line magic, к одной строке
%time expensive_function() # замерить время одной операции
%%time # замерить время ВСЕЙ ячейки
stuff()
more_stuff()
%timeit expensive() # усреднение по N запусков (для микро-бенчей)
%matplotlib inline # графики inline (default в новых Jupyter)
%matplotlib widget # интерактивные
%load_ext autoreload # авто-перезагрузка своих модулей
%autoreload 2
%who # список переменных в namespace
%whos # с типами и деталями
%reset # очистить весь namespace
# Запуск shell-команд — префикс !
!ls -la
!pip list
!git status
# Присвоить результат shell в переменную
files = !ls *.csv
# Ячейка 1 — импорты (один раз)
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# Ячейка 2 — загрузка данных (тяжёлая, один раз)
df = pd.read_csv("train.csv")
df.shape
# Ячейка 3 — беглый обзор
df.head()
# Ячейка 4 — распределения
df.hist(figsize=(12, 8))
plt.tight_layout()
# Ячейка 5 — корреляции
sns.heatmap(df.corr(numeric_only=True), annot=True)
# Ячейка 6 — feature engineering
df["age_group"] = pd.cut(df["age"], bins=[0, 18, 65, 100])
# Ячейка 7 — обучение модели
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier()
model.fit(X_train, y_train)
# Ячейка 8 — оценка
score = model.score(X_test, y_test)
print(f"Accuracy: {score:.3f}")
Данные загружены один раз (ячейка 2). Экспериментируешь в ячейках 4-8 без повторной загрузки. Обучил модель раз — тестируешь на разных данных, не переобучая.
x может быть от ячейки 5, а не от ячейки 3. Заканчивая работу: Kernel → Restart & Run All — проверить что notebook воспроизводим сверху вниз.del df; import gc; gc.collect() — если нужно освободить.nbstripout (убирает outputs при commit), jupytext (парная .py-версия для diff), ReviewNB..py-модули, notebook оставь для experiments. Не запускать jupyter nbconvert --execute prod.ipynb в cron — хрупко.from tqdm.auto import tqdm; for x in tqdm(items): — прогресс-бар..env: from dotenv import load_dotenv; load_dotenv(); os.getenv("API_KEY").Shift+Enter, magic-команды (%time, %matplotlib inline), tqdm для прогресса. Для production — переноси в модули. Git через nbstripout / jupytext.
В алгоритмических задачах (особенно ML/DS) и в тестах — обязательный навык. Задача: «найди пару чисел с суммой X», «максимальную подстроку без повторов», «скользящее окно из K чисел». Без знания паттернов пишешь O(n²) там, где нужен O(n).
Считать что-то по мере прохода: сумму, произведение, максимум, счётчик.
# Задача: сумма всех положительных
def sum_positive(nums):
total = 0
for n in nums:
if n > 0:
total += n
return total
# Задача: максимальная сумма подряд идущих (Kadane's algorithm)
def max_subarray_sum(nums):
max_sum = current = nums[0]
for n in nums[1:]:
current = max(n, current + n)
max_sum = max(max_sum, current)
return max_sum
max_subarray_sum([-2, 1, -3, 4, -1, 2, 1, -5, 4]) # 6 (от [4,-1,2,1])
Два индекса, движущиеся навстречу или в одном направлении. Заменяет вложенный цикл O(n²) на O(n).
# Задача: есть ли в отсортированном массиве пара с суммой = target
def has_pair_sum(nums, target):
left, right = 0, len(nums) - 1
while left < right:
s = nums[left] + nums[right]
if s == target:
return True
elif s < target:
left += 1 # нужна большая сумма → сдвиг вправо
else:
right -= 1 # нужна меньшая → сдвиг влево
return False
has_pair_sum([1, 2, 4, 7, 11, 15], 9) # True (2 + 7)
# Задача: удалить дубликаты in-place
def remove_duplicates(nums):
if not nums:
return 0
write = 1
for read in range(1, len(nums)):
if nums[read] != nums[read - 1]:
nums[write] = nums[read]
write += 1
return write
# Задача: разворот массива in-place
def reverse(nums):
left, right = 0, len(nums) - 1
while left < right:
nums[left], nums[right] = nums[right], nums[left]
left += 1
right -= 1
Два указателя движутся в одном направлении, поддерживая «окно» — подмассив/подстроку. Классика для «максимум/минимум/сумма подмассива длины K», «подстрока с условием».
# Задача: максимальная сумма подмассива длины K
def max_sum_window(nums, k):
window = sum(nums[:k])
max_sum = window
for i in range(k, len(nums)):
window += nums[i] - nums[i - k] # сдвинули окно на 1
max_sum = max(max_sum, window)
return max_sum
max_sum_window([1, 4, 2, 10, 23, 3, 1, 0, 20], 4) # 39
# Задача: длина максимальной подстроки без повторов
def longest_unique(s):
seen = {} # char → last index
left = 0
best = 0
for right, ch in enumerate(s):
if ch in seen and seen[ch] >= left:
left = seen[ch] + 1 # сдвинуть окно после дубликата
seen[ch] = right
best = max(best, right - left + 1)
return best
longest_unique("abcabcbb") # 3 (abc)
longest_unique("pwwkew") # 3 (wke)
Заранее считаем массив префиксных сумм. Потом сумма любого подмассива [i..j] — за O(1) вместо O(j-i).
nums = [1, 2, 3, 4, 5]
prefix = [0] # prefix[i] = sum(nums[:i])
for n in nums:
prefix.append(prefix[-1] + n)
# prefix = [0, 1, 3, 6, 10, 15]
# Сумма nums[1..3] (индексы включительно) — за O(1)
prefix[4] - prefix[1] # 10 - 1 = 9 (2+3+4)
# Полезно если запросов сумм МНОГО — предвычислили один раз
Классический паттерн «two sum» — за один проход через словарь. Меняет O(n²) на O(n) за счёт O(n) памяти.
# Задача: индексы двух чисел с суммой = target (LeetCode #1)
def two_sum(nums, target):
seen = {} # value → index
for i, n in enumerate(nums):
need = target - n
if need in seen:
return [seen[need], i]
seen[n] = i
return []
two_sum([2, 7, 11, 15], 9) # [0, 1]
# Задача: подсчёт частот
from collections import Counter
freq = Counter(["a", "b", "a", "c", "a"])
# Counter({'a': 3, 'b': 1, 'c': 1})
freq.most_common(2) # [('a', 3), ('b', 1)]
Два указателя с разной скоростью. Найти середину списка, обнаружить цикл в связном списке, найти дубликат в массиве.
# Задача: найти дубликат в массиве где числа 1..n, размер n+1
def find_duplicate(nums):
slow = fast = nums[0]
while True:
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast:
break
# Найти вход в цикл
slow = nums[0]
while slow != fast:
slow = nums[slow]
fast = nums[fast]
return slow
find_duplicate([1, 3, 4, 2, 2]) # 2
# Индекс в отсортированном массиве
def bsearch(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
# Готовое из stdlib — bisect
import bisect
bisect.bisect_left([1, 3, 5, 7], 3) # 1 — куда вставить 3
bisect.insort(a, x) # вставить с сохранением сортировки
| Задача | Паттерн | Сложность |
|---|---|---|
| «Есть ли пара с суммой X» (сортированный) | Два указателя | O(n) |
| «Есть ли пара с суммой X» (произвольный) | Hash-map | O(n) time, O(n) memory |
| «Максимум/минимум подмассива длины K» | Sliding window | O(n) |
| «Подстрока/подмассив с условием» | Sliding window + hash | O(n) |
| «Сумма любого подмассива» (много запросов) | Prefix sum | O(n) prep + O(1) query |
| «Топ-K частых» | Counter.most_common | O(n log k) |
| «Индекс в сортированном» | Binary search / bisect | O(log n) |
| «Цикл в связном списке / дубликат» | Fast/slow pointers | O(n) time, O(1) memory |
| «Максимальная сумма подряд» | Kadane (running max) | O(n) |
# Обмен переменных без временной
a, b = b, a
# Развёртка сложных данных
first, *rest = [1, 2, 3, 4] # first=1, rest=[2,3,4]
first, *middle, last = range(10) # first=0, middle=[1..8], last=9
# Одна строка для условной вставки
result = [x if x > 0 else 0 for x in nums]
# zip для параллельного обхода
for a, b in zip(nums, nums[1:]): # пары соседей
...
# Обход в обратном
for i in range(len(nums) - 1, -1, -1):
...
# или
for x in reversed(nums):
...
# Проверка на палиндром одной строкой
s == s[::-1]
# Уникальные + отсортированные + сохранить порядок первых вхождений
from collections import OrderedDict
list(OrderedDict.fromkeys([3, 1, 3, 2, 1])) # [3, 1, 2]
# Инициализация словаря значениями по умолчанию
from collections import defaultdict
counts = defaultdict(int) # без KeyError
groups = defaultdict(list) # группировка
for item in items:
groups[item.category].append(item)
nums[:] создаёт новый список. Для больших массивов дорого. Итерируй через индексы или используй view (numpy).for x in nums: nums.remove(x) — пропустит элементы. Итерируй по копии либо строй новый список.mid = (l + r) // 2 — двойной слэш. Одинарный / в Python 3 всегда float.(l + r) / 2 не переполнится. Всё равно пиши l + (r - l) // 2 для переноса кода на другие языки.if not nums: return. Ошибки типа max([]) = ValueError.# Time: O(n), Space: O(1). Показывает что понимаешь что написал.zip, enumerate, Counter, defaultdict, bisect) экономят строки и делают решение читаемее.
# unittest — многословно import unittest class TestMath(unittest.TestCase): def test_add(self): self.assertEqual(1 + 1, 2) # pytest — просто def test_add(): assert 1 + 1 == 2
uv add --dev pytest pytest-cov
# Структура проекта
myapp/
├── src/
│ └── calc.py
└── tests/
├── test_calc.py # файлы test_*.py или *_test.py
└── conftest.py # общие fixtures
# Запуск
uv run pytest # все тесты
uv run pytest tests/test_calc.py # конкретный файл
uv run pytest -k "test_add" # по имени
uv run pytest -v # verbose
uv run pytest -x # остановиться на первой ошибке
uv run pytest --pdb # дебаггер при падении
# tests/test_calc.py
from src.calc import add, divide
def test_add_positive():
assert add(2, 3) == 5
def test_add_negative():
assert add(-2, 3) == 1
# Ожидание исключения
import pytest
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(10, 0)
def test_divide_by_zero_message():
with pytest.raises(ValueError, match="cannot divide"):
divide(10, 0)
# Приблизительное сравнение float
def test_float():
assert 0.1 + 0.2 == pytest.approx(0.3)
# Пропуск тестов
@pytest.mark.skip(reason="not implemented yet")
def test_todo(): ...
@pytest.mark.skipif(sys.version_info < (3, 11), reason="needs 3.11+")
def test_new_syntax(): ...
# tests/conftest.py — общие fixtures
import pytest
@pytest.fixture
def sample_user():
return {"name": "Alice", "age": 30}
@pytest.fixture
def db_session():
# setup
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
session = Session(engine)
yield session # тут выполняется тест
# teardown
session.close()
engine.dispose()
# Тест получает fixture через параметр — DI
def test_user_name(sample_user):
assert sample_user["name"] == "Alice"
def test_db(db_session):
user = User(name="Bob")
db_session.add(user)
db_session.commit()
assert user.id is not None
# Scope — как долго живёт fixture
@pytest.fixture(scope="function") # default — на каждый тест
@pytest.fixture(scope="module") # один раз на модуль
@pytest.fixture(scope="session") # один раз на всю сессию
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
])
def test_add(a, b, expected):
assert add(a, b) == expected
# Запустится 4 раза, каждый — отдельный тест в отчёте
# С id для читаемых имён
@pytest.mark.parametrize("email, valid", [
("a@b.c", True),
("nope", False),
("", False),
], ids=["valid", "no-at", "empty"])
def test_email(email, valid):
assert is_valid_email(email) == valid
unittest.mockfrom unittest.mock import Mock, MagicMock, patch
# Простой mock
mock = Mock()
mock.some_method.return_value = "hello"
mock.some_method() # "hello"
mock.some_method.assert_called_once_with()
# patch — подменить объект в модуле
@patch("src.services.email.send")
def test_register(mock_send):
mock_send.return_value = True
register_user("Alice")
mock_send.assert_called_once()
# patch как context manager
def test_register_ctx():
with patch("src.services.email.send") as mock_send:
register_user("Alice")
mock_send.assert_called_once()
register_user внутри делает from src.email import send; send(...) — @patch("src.email.send") НЕ сработает. Патчи @patch("src.users.send") — там где импортировано.uv add --dev pytest-asyncio httpx
# tests/test_api.py
import pytest
from httpx import AsyncClient
from main import app
@pytest.mark.asyncio
async def test_create_user():
async with AsyncClient(app=app, base_url="http://test") as client:
r = await client.post("/users", json={"name": "Alice", "email": "a@b.c"})
assert r.status_code == 201
assert r.json()["name"] == "Alice"
В pyproject.toml:
[tool.pytest.ini_options]
asyncio_mode = "auto" # тогда @mark.asyncio не нужен
uv run pytest --cov=src --cov-report=term-missing
# покажет процент покрытия + строки без тестов
uv run pytest --cov=src --cov-report=html
# сгенерит htmlcov/ с интерактивным HTML-отчётом
test_ префикс обязателен. pytest автоматически находит функции test_*. Опечатался в tests_* — тест не запустится.def test_x(): без параметра sample_user — fixture не будет использована.respx для HTTP, @patch для остального.tmp_path fixture — встроенная в pytest, даёт временную директорию, автоматически чистится. Не создавай файлы в /tmp руками.test_*.py, функции test_*, обычный assert. @parametrize для табличных тестов. @patch/respx для моков. Плюс pytest-cov для покрытия.
loggingprint() в продеprint() ходит в stdout без метаданных (когда? что за модуль? какой уровень?). В проде — обязательно logging: уровни, timestamps, форматирование, разные handlers (файл + консоль + Sentry), config без правки кода.
import logging
logger = logging.getLogger(__name__) # имя = имя модуля
logger.debug("detailed info") # 10 — только для разработки
logger.info("user created") # 20 — обычные события
logger.warning("deprecated call") # 30 — стоит обратить внимание
logger.error("failed to save") # 40 — операция провалилась
logger.critical("db down") # 50 — сервис умирает
# Форматирование через параметры (не f-string!)
logger.info("user %s created with id %d", name, user_id)
# Исключение — с traceback
try:
something()
except Exception:
logger.exception("unexpected") # == error() + traceback
logger.info(f"user {name}") сформирует строку ВСЕГДА, даже если этот уровень отключён. logger.info("user %s", name) — форматирование только когда handler решил залогировать. На горячем пути экономия времени.dictConfigimport logging.config
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
},
"json": {
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
"format": "%(asctime)s %(levelname)s %(name)s %(message)s",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "standard",
"level": "INFO",
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "/var/log/app.log",
"maxBytes": 10 * 1024 * 1024, # 10 MB
"backupCount": 5, # хранить 5 файлов
"formatter": "json",
"level": "WARNING",
},
},
"loggers": {
"": { # root logger
"handlers": ["console", "file"],
"level": "INFO",
},
"myapp.db": { # специфичный логгер
"level": "DEBUG",
},
},
}
logging.config.dictConfig(LOGGING)
Для сборки в ELK / Loki / Datadog — логи в JSON. Каждая строка — валидный JSON, поля удобно фильтровать.
uv add python-json-logger
# Уже настроен в LOGGING выше (formatters.json)
# Дополнительные поля — через extra:
logger.info("user created", extra={"user_id": 42, "ip": "1.1.1.1"})
# {"asctime": "...", "levelname": "INFO", "name": "myapp",
# "message": "user created", "user_id": 42, "ip": "1.1.1.1"}
richuv add rich
from rich.logging import RichHandler
logging.basicConfig(
level="INFO",
format="%(message)s",
handlers=[RichHandler()],
)
# В консоли — цветные уровни, красивые traceback, таблицы для extra полей
logger = logging.getLogger(__name__) — во ВСЕХ модулях. Так каждый модуль пишет со своим именем, и можно фильтровать в конфиге myapp.users.service.logger.debug() на горячем пути. Даже если DEBUG отключён — Python вычислит аргументы. Оборачивай в if logger.isEnabledFor(logging.DEBUG):.RotatingFileHandler и Docker. В контейнерах пиши в stdout/stderr — Docker/K8s сами соберут. Файлы = нет reproducibility.| Инструмент | Роль |
|---|---|
ruff | Линтер + форматтер. На Rust, 10-100× быстрее flake8. К 2026 заменил flake8, isort, pylint, pyupgrade. |
mypy | Статический тайп-чекер. Проверяет type hints, находит ошибки до runtime. |
black | Автоформатирование «no options». Стандарт много лет — но ruff format его вытесняет (совместимый). |
pre-commit | Хуки для запуска ruff/mypy/black перед git commit. |
Мнемоника: ruff — стилистика + мелкие баги; mypy — типы; black/ruff format — форматирование.
uv add --dev ruff
# Линт
uv run ruff check src/
uv run ruff check src/ --fix # автофиксы
# Форматирование (замена black)
uv run ruff format src/
# Конфиг — pyproject.toml
[tool.ruff]
line-length = 100
target-version = "py313"
extend-exclude = ["migrations"]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # pyflakes
"I", # isort — сортировка импортов
"UP", # pyupgrade — новый синтаксис
"B", # bugbear — типовые баги
"SIM", # simplify
"N", # PEP 8 naming
]
ignore = ["E501"] # line too long — ruff format сам разрулит
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"] # asserts разрешены в тестах
uv add --dev mypy
uv run mypy src/ # проверить
# Конфиг — pyproject.toml
[tool.mypy]
python_version = "3.13"
strict = true # включает все проверки
warn_unused_ignores = true
warn_return_any = true
# Для сторонних библиотек без стабов
[[tool.mypy.overrides]]
module = ["legacy_lib.*", "untyped_pkg.*"]
ignore_missing_imports = true
На существующем проекте strict = true сразу — больно. Включай постепенно: сначала для новых модулей через per-module overrides, потом расширяй.
uv add --dev pre-commit
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
# Установить хуки
uv run pre-commit install
# Теперь при каждом `git commit` — прогонятся автоматом
# .github/workflows/lint.yml
name: lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- run: uv sync --dev
- run: uv run ruff check src/
- run: uv run ruff format --check src/
- run: uv run mypy src/
- run: uv run pytest --cov
ruff format написан как замена black с той же логикой. Держи одно (лучше ruff).# noqa: E501 для ruff, # type: ignore[assignment] для mypy. Всегда указывай КОД ошибки, не голое noqa — иначе спрячешь новые баги.ruff --fix — автозамены. Просмотри diff перед commit — иногда меняет семантику (например, UP-правила модернизирующие синтаксис).ruff + mypy + pre-commit в pyproject.toml. В CI — те же три команды. Всё быстро (ruff на Rust), настройка в одном файле, стандарт индустрии 2026.
1. Что такое GIL?
Global Interpreter Lock — mutex в CPython, разрешающий выполнять Python-bytecode только одному потоку в один момент. Из-за него threading не даёт истинного параллелизма для CPU-bound задач (для этого — multiprocessing). Для I/O-bound работает (поток отпускает GIL на I/O). В Python 3.13 экспериментально можно собирать без GIL, но production ещё далеко.
2. Mutable vs immutable — почему важно
Immutable: int, float, str, tuple, bool, None, frozenset. Mutable: list, dict, set. Immutable — можно использовать как ключи в dict/set (потому что есть __hash__). Mutable default arg в функции — классический баг (создаётся ОДИН раз при определении, разделяется между вызовами).
3. list vs tuple
Tuple неизменяем, немного быстрее, может быть ключом dict. Семантически — «фиксированный набор» (координаты, RGB). List — «однородная коллекция, длина меняется» (список пользователей).
4. == vs is
== — сравнение значений (__eq__). is — сравнение идентичности (тот же объект в памяти). Для None — только x is None. Small int'ы (-5..256) и короткие строки кешируются, поэтому a = 5; b = 5; a is b — True, но это implementation detail.
5. *args и **kwargs
Переменное число аргументов. *args — tuple позиционных, **kwargs — dict именованных. Также используются для «прозрачного прокси»: def wrapper(*a, **kw): return func(*a, **kw).
6. Что такое декоратор
Функция, оборачивающая другую функцию. @decorator — сахар над func = decorator(func). Внутри своего декоратора обязательно @functools.wraps(func) — сохраняет метаданные оригинала. Используется для сквозных обязанностей: логирование, retry, кеш, auth.
7. Генератор vs list — когда что
Генератор ленивый — не хранит все элементы в памяти. Для миллионных потоков (лог-файлы, БД-выборки) — генератор. Для повторного обхода / индексации / len() — list. yield в функции превращает её в генератор.
8. Замыкания (closures) и late binding
Внутренняя функция запоминает переменные внешней. Late binding: [lambda: i for i in range(3)] — все lambda ссылаются на ОДНУ i, к моменту вызова = 2. Фикс: lambda i=i: i (захват через default).
9. __init__ vs __new__
__new__ — создаёт объект (возвращает instance). __init__ — инициализирует уже созданный. 95% времени пишешь только __init__. __new__ нужен для immutable-типов (наследники str/tuple) и метаклассов.
10. MRO и множественное наследование
Method Resolution Order — порядок, в котором Python ищет метод в родителях. Алгоритм C3 linearization. Проверить: ClassName.__mro__. При diamond inheritance (D наследует B и C, обе наследуют A) — MRO гарантирует, что A встретится один раз.
11. asyncio vs threading vs multiprocessing
asyncio — concurrent I/O в одном потоке, тысячи «задач» без overhead. threading — I/O-bound + legacy, GIL мешает CPU. multiprocessing — отдельные процессы, обходит GIL, для CPU-heavy (обработка изображений, ML-inference).
12. @classmethod vs @staticmethod
@classmethod — первый аргумент cls, знает свой класс. Используется для альтернативных конструкторов (User.from_dict(...)). @staticmethod — обычная функция в namespace класса, ни self, ни cls.
13. @property
Метод, вызываемый как атрибут. Позволяет добавить валидацию/вычисление без изменения интерфейса. Питонический подход: начинай с публичного атрибута, при необходимости — превращай в property.
14. dataclass vs namedtuple vs Pydantic
dataclass — стандартный способ создать DTO с __init__/__eq__/__repr__. namedtuple — легковесный immutable, доступ по атрибуту и по индексу. Pydantic — тот же dataclass + валидация в runtime, конвертация типов, JSON serialization. Для внешних данных (API-вход) — Pydantic.
15. Type hints — enforcement?
Нет. Python не проверяет их в runtime — только подсказки. Проверяет отдельный tool (mypy). В runtime валидацию делает Pydantic (использует те же аннотации).
16. Оптимизация N+1 в SQLAlchemy / Django
SQLAlchemy: selectinload (2 запроса через IN) / joinedload (JOIN одним запросом). Django: prefetch_related / select_related. Правило: selectinload/prefetch_related для 1-N, joinedload/select_related для N-1 (FK).
17. EAFP vs LBYL
Easier to Ask Forgiveness than Permission — попробуй, поймай исключение. Look Before You Leap — проверь условие ДО. Питонично — EAFP. Пример: try: v = d[k] except KeyError: ... вместо if k in d: v = d[k]. Но d.get(k) ещё лучше.
18. Context manager — with
Гарантированное освобождение ресурсов через __enter__/__exit__. Свой — либо класс с этими методами, либо декоратор @contextmanager над функцией с yield.
19. venv зачем — если можно ставить пакеты глобально
Разные проекты требуют разных версий одних и тех же библиотек. venv — изолированный интерпретатор со своим site-packages/. С 2024 Ubuntu 24.04 глобально ставить вообще запрещено (PEP 668). В 2026 — стандарт uv.
20. FastAPI vs Django — когда что
Django — сайт с админкой, batteries included. FastAPI — REST API / микросервис / AI-backend, async first, автогенерация OpenAPI. Flask — legacy или мини-скрипт. Гибрид тоже норма: Django для сайта + FastAPI для новых API.
21. Duck typing и Protocol
«Если крякает как утка — то утка». Не проверяется isinstance, работает если у объекта есть нужные методы. typing.Protocol — «структурный интерфейс», проверяемый статически (mypy) без наследования.
22. Как сериализовать datetime в JSON
json.dumps({"created": datetime.now()}) кинет TypeError. Решения: default=str в dumps, или предварительно .isoformat(), или использовать Pydantic (сам сериализует).
23. Разница между Exception и BaseException
BaseException — корень всей иерархии, содержит KeyboardInterrupt и SystemExit. Exception — базовый для обычных ошибок. Никогда не ловить BaseException — заблокируешь Ctrl+C.
24. Что такое __slots__
Класс-атрибут, ограничивающий разрешённые instance-атрибуты. Экономит память (нет __dict__), быстрее доступ. Полезно для классов с миллионами инстансов. Ломает multiple inheritance и dynamic attributes. В @dataclass(slots=True) — из коробки.
25. WSGI vs ASGI
WSGI (2003) — sync-интерфейс для веб-серверов (Flask, Django до 3.0). ASGI (2018) — async-версия (FastAPI, Django 3+). ASGI умеет WebSocket, long-polling, HTTP/2. Uvicorn — популярный ASGI-сервер, gunicorn+uvicorn workers — производственная связка.
- «Как проверить» → всегда пишу тест на pytest, для API —
httpx.AsyncClient(app=app). - «Как деплоить» → Docker с multi-stage build; runtime — Python slim, зависимости через uv.
- «Где хранить конфиг» →
pydantic-settings: типизированный класс, читает.envи env-переменные. - «Как логировать» →
logging.getLogger(__name__), JSON-formatter для сбора в ELK/Loki. - «Что улучшить в существующем коде» → type hints, ruff+mypy, замена
requests→httpx, sync-код блокирующий event loop.
uv + ruff + mypy + FastAPI + SQLAlchemy 2 async + Pydantic + pytest + httpx + Docker. Django — если нужен сайт с админкой. AI/ML — отдельная тропа (PyTorch, LangChain, Anthropic SDK).