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

1.1 · Python для тех, кто уже программирует

Неделя 1 · Подготовка · ⏱ ~4 ч · Теория, Практика

Почти весь AI-стек — SDK Anthropic и OpenAI, Pydantic, LangGraph, LlamaIndex, evals-фреймворки, примеры в документации — написан на Python. Код нам часто будет генерировать модель, но отвечаем за него мы: надо с первого взгляда видеть изменяемый default, проглоченное исключение или float там, где нужны деньги. На собеседованиях live-coding для AI-инженера почти всегда на Python. Цель главы — не выучить язык с нуля, а перенести то, что мы уже умеем в другом языке (JavaScript, Java, Go, C# — не важно), и разобрать только то, чем Python от них отличается.

Сквозной пример курса — учебный интернет-магазин «Полка»: кружки, лампы, чай на развес; заказы, товары, оплаты. На нём построены примеры кода в этой главе и все четыре проекта дальше.

Глава устроена от общего к частному: сначала таблица главных отличий Python от других языков, затем по порядку синтаксис, числа, коллекции, функции, классы, исключения, модули и типы. Каждую тему стоит сразу пробовать в интерактивной консоли: команда python3 без аргументов открывает REPL, где выражение выполняется сразу после Enter.

1. Ментальная модель: чем Python отличается от других языков

Заголовок раздела «1. Ментальная модель: чем Python отличается от других языков»
Во многих языках Python
Запуск компиляция или сборка перед запуском python file.py — интерпретатор, без сборки
Типы проверяет компилятор type hints вообще не проверяются в рантайме; проверяют mypy/pyright/IDE
Блоки { } отступ (4 пробела), двоеточие в конце заголовка
Пустое значение null, иногда ещё и undefined один None
Конкурентность потоки или встроенный event loop синхронно по умолчанию; event loop — явно через asyncio (глава гл. 1.3)
Пакеты npm, Maven, Go modules и т.п. uv/pip, pyproject.toml, виртуальное окружение .venv (гл. 1.3)
Числа целые фиксированного размера + double int произвольной длины + float (double) + Decimal

Главное следствие второй строки: аннотация def f(x: int) — это документация и подсказка для IDE. f("abc") выполнится без ошибки, пока что-то внутри не сломается. Валидацию данных в рантайме делает Pydantic — это тема гл. 1.3. Остальные строки таблицы разбираем ниже по порядку.

Блок кода задаётся не скобками, а отступом: заголовок if, for, def заканчивается двоеточием, а всё, что сдвинуто на 4 пробела под ним, — тело блока. Функция, которая по остатку на складе возвращает статус товара:

def stock_status(quantity: int) -> str:
if quantity > 20:
return "in_stock"
elif quantity < 1: # elif, not "else if"
return "sold_out"
else:
return "low"

Логические операторы — словами: and, or, not. Тернарный оператор читается наоборот: x if cond else y (в C-подобных языках cond ? x : y). Оператора «значение по умолчанию только для пустого значения» (как ?? в JavaScript и C#) нет; a or b срабатывает на любое falsy, включая 0.

Кроме True и False, в условии можно проверять любое значение: каждое считается либо «истинным» (truthy), либо «ложным» (falsy). Falsy в Python: None, False, 0, 0.0, "", [], {}, set(), (). Важная особенность: пустой список и пустой словарь — falsy (в JavaScript, например, [] и {} truthy). Поэтому идиома if not orders: значит «список пуст».

== против is. == сравнивает значения, для списков и словарей — поэлементно: [1, 2] == [1, 2] → True (во многих языках такое сравнение проверяет ссылки и даёт false). is сравнивает идентичность объекта. is используем только с None: if x is None:. Сравнение строк или чисел через is иногда «работает» из-за кеширования интерпретатора и однажды перестаёт — это классический баг.

В магазине почти всё — числа: количество, цены, скидки. Здесь Python ведёт себя не так, как многие языки:

  • int не переполняется: 2**100 — точное число.
  • / всегда даёт float: 7 / 2 == 3.5, 4 / 2 == 2.0.
  • // — деление с округлением вниз (к минус бесконечности!): 7 // 2 == 3, но -7 // 2 == -4.
  • ** — степень, % — остаток (знак как у делителя: -7 % 2 == 1).
  • Для денег и всего, где важна точность, — decimal.Decimal("0.1") (строкой, не float). 0.1 + 0.2 == 0.30000000000000004 здесь так же, как в любом языке с double.

Списки заказов, словари «товар → цена», множества категорий — основная рабочая форма данных в любом скрипте. Встроенных коллекций четыре:

Привычное название Python Заметки
массив, список list[T]: [1, 2, 3] изменяемый
кортеж tuple: (1, "a") неизменяемый, может быть ключом словаря
словарь, map, хеш-таблица dict[K, V]: {"mug": 1} порядок вставки сохраняется
множество set[T]: {1, 2} пустое — только set(), {} это пустой dict

Обращения:

prices = {"mug": 590.0, "notebook": 320.0}
prices["mug"] # KeyError if missing — not a silent empty value
prices.get("pen") # None
prices.get("pen", 0.0) # default value
"mug" in prices # key membership
for product, price in prices.items():
print(product, price)

Срезы: a[start:stop:step], stop не включается, отрицательные индексы считают с конца.

xs = [10, 20, 30, 40, 50]
xs[1:3] # [20, 30]
xs[-1] # 50
xs[:2] # [10, 20]
xs[::-1] # reversed copy

Comprehensions заменяют цепочки map/filter: список, словарь или множество собирается одним выражением «что взять — откуда — при каком условии». В примере orders — список заказов-словарей вида {"id": 1, "category": "mugs", "quantity": 2}:

# "keep the positive ones and double them" in one expression
doubled = [x * 2 for x in xs if x > 0]
by_id = {o["id"]: o for o in orders} # dict comprehension
categories = {o["category"] for o in orders} # set comprehension
total = sum(o["quantity"] for o in orders) # generator, no list built

Распаковка — то же, что деструктуризация в других языках: a, b = b, a; first, *rest = xs; в цикле for i, x in enumerate(xs):. zip(a, b) идёт по двум спискам парами.

Строки: f-строки подставляют значения прямо в текст — f"{product}: {discount:.1f}%". Строки неизменяемы; склейка списка строк — "".join(parts), метод вызывается у разделителя.

def order_preview(product: str, quantity: int, *, delivery: str = "pickup", **extra) -> dict:
return {"product": product, "quantity": quantity, "delivery": delivery, **extra}
order_preview("mug", 2, delivery="courier", note="gift")
  • Позиционные и именованные аргументы: f(1, delivery="courier"). Это не объект с параметрами, а настоящая часть языка.
  • * в сигнатуре: всё после него — только по имени (keyword-only). В SDK это встречается постоянно: client.messages.create(model=..., max_tokens=..., messages=...).
  • *args собирает лишние позиционные в кортеж, **kwargs — лишние именованные в словарь (в примере выше это **extra: note="gift" попадёт в extra). При вызове f(*xs, **opts) — распаковка списка и словаря в аргументы; внутри литерала {..., **extra} — распаковка словаря в новый словарь.
  • lambda x: x * 2 — только одно выражение.

Ловушка изменяемого default. Значение по умолчанию вычисляется один раз при определении функции:

def add_item(item, cart=[]): # BUG: one list shared by all calls
cart.append(item)
return cart
add_item("mug") # ["mug"]
add_item("notebook") # ["mug", "notebook"] <- surprise
def add_item(item, cart=None): # correct
if cart is None:
cart = []
cart.append(item)
return cart

Области видимости. Присваивание внутри функции создаёт локальную переменную. Чтобы изменить переменную внешней функции — nonlocal, модуля — global (почти никогда не нужно). Блоки if/for области видимости не создают: переменная из цикла живёт после цикла.

Классы устроены привычно — конструктор, атрибуты, методы, — но с явным self. Пример — платёжный провайдер, который берёт комиссию с суммы оплаты:

class PaymentProvider:
fee_rate: float = 0.025 # class attribute
def __init__(self, name: str): # constructor
self.name = name # instance attribute, "this" is explicit
def fee(self, amount: float) -> float:
return amount * self.fee_rate
def __repr__(self) -> str: # like toString() for debugging
return f"PaymentProvider({self.name!r})"

self явно передаётся первым параметром каждого метода. Приватности нет: _name — соглашение «не трогать снаружи», __name — искажение имени, не защита. Наследование: class CardProvider(PaymentProvider):, вызов родителя — super().__init__(name).

Для «просто данных» берём @dataclass — он генерирует __init__, __repr__, __eq__. Строка с @ над классом или функцией — декоратор: функция, которая получает определение и возвращает его изменённую версию (здесь — класс с готовыми методами). Декораторы встретятся постоянно: @field_validator в Pydantic, @pytest.mark.parametrize в тестах.

from dataclasses import dataclass, field
@dataclass(frozen=True) # frozen = immutable, like a readonly type
class OrderItem:
product: str
quantity: int
price: float
tags: list[str] = field(default_factory=list) # never "= []"

Dunder-методы (от double underscore: __init__, __len__, __eq__, __enter__) — это «протоколы»: Python вызывает их сам, когда мы пишем len(x), a == b, with x: (о with — в следующем разделе).

Ошибки в Python — исключения, как в Java или C#. Для своего кода заводим иерархию: базовый класс ошибок слоя данных и конкретные подклассы, а чужие исключения при необходимости заворачиваем в свои:

class ShopError(Exception):
"""Base error for the shop data layer."""
class OutOfStock(ShopError):
pass
try:
data = load(path)
except FileNotFoundError:
data = []
except (ValueError, KeyError) as e:
raise ShopError(f"bad file {path}") from e # keeps the cause
else:
print("loaded") # runs only if no exception
finally:
print("done")

Исключения ловятся по типу (except ValueError), порядок веток важен: срабатывает первая подходящая. except Exception: без повторного raise — это почти всегда проглоченная ошибка. Голый except: ловит даже Ctrl+C — так не пишем.

Контекстный менеджер with — гарантированное освобождение ресурса (то, что в других языках делают через try/finally, using или defer):

with open("orders.csv", encoding="utf-8") as f:
text = f.read()
# file is closed here even on exception

Как только кода становится больше одного файла, встаёт вопрос, как файлы видят друг друга. Файл = модуль, папка с файлами = пакет. import json, from pathlib import Path, from .utils import order_total (относительный импорт внутри пакета). Импортировать модуль — значит выполнить его верхний уровень один раз. Отсюда идиома:

def main() -> None:
...
if __name__ == "__main__": # true only when run as a script, not when imported
main()

Тестам и ноутбукам это даёт импортировать функции, не запуская скрипт.

Аннотации типов уже встречались во всех примерах выше; соберём их формы в одном месте. Напомним: рантайм их не проверяет, но их читают IDE, линтеры, Pydantic и SDK, которые строят по ним схемы данных.

from typing import Optional, Literal, Any
from collections.abc import Iterable
def average(a: float, b: float) -> float: ...
def find(product: str) -> dict[str, float] | None: ... # modern form (3.10+)
def find_old(product: str) -> Optional[dict[str, float]]: ... # same thing
Status = Literal["new", "paid", "shipped"] # only these three strings are allowed
def total(xs: Iterable[float]) -> float: ...

Optional[X] значит «X или None», а не «необязательный аргумент». list[str], dict[str, int] пишутся со строчной буквы (с Python 3.9); List, Dict из typing — старый стиль, он встречается в чужом коде. Any отключает проверку типа для значения.

10. Стандартная библиотека, которую стоит знать в лицо

Заголовок раздела «10. Стандартная библиотека, которую стоит знать в лицо»

pathlib (пути), json, csv, datetime, collections (Counter, defaultdict), statistics (mean, median, stdev), itertools, logging, os.environ. Учить наизусть не надо — достаточно знать, что это есть, и спросить модель или документацию.

Для этой главы провайдер почти не важен: Python один и тот же. Основные модели курса — DeepSeek (облако, дёшево) и локальная Ollama (модели на своём компьютере), а Claude и OpenAI остаются для сравнения (подробно — в главе 1.5 · Видео Карпатого + ключи API). И DeepSeek, и Ollama принимают запросы в формате OpenAI, поэтому оба подключаются одной библиотекой openai — меняется только адрес сервера. Ключ API (пароль для доступа к облачной модели) берём из переменной окружения через os.environ, а не пишем в коде. Вызывать модели будем с главы 1.5, а прочитать такой код и увидеть в нём темы этой главы полезно уже сейчас:

# illustrative: one SDK, two providers — only keyword arguments differ
import os
from openai import OpenAI
deepseek = OpenAI(api_key=os.environ["DEEPSEEK_API_KEY"], base_url="https://api.deepseek.com")
local = OpenAI(api_key="ollama", base_url="http://localhost:11434/v1") # Ollama ignores the key
  • Именованные аргументы (base_url=..., раздел 5) — то, на чём держится вся совместимость: клиент не нужно наследовать или переписывать, достаточно передать другой адрес. В SDK почти всё keyword-only (* в сигнатуре).
  • os.environ["X"] против os.environ.get("X"). Первое бросает KeyError, если переменной нет (громкая ошибка сразу), второе возвращает None, и непонятная ошибка авторизации появится позже. В официальном примере DeepSeek стоит .get — в своём коде предпочитаем [].
  • Что можно отправлять в облако. Попросить модель сгенерировать учебный скрипт можно у любого провайдера: в запросе нет ничего личного. Рабочий код, данные клиентов и всё, что нельзя показывать посторонним, в облачные модели не отправляем — для этого есть локальная модель в Ollama. Это правило действует весь курс.
  1. Открываем Learn X in Y minutes: Python → проходим страницу целиком, запуская каждый блок в python3 REPL или в файле (≈90 мин). Отмечаем всё, что ведёт себя не так, как в привычном языке, — это сырьё для шпаргалки.
  2. Открываем официальный туториал → главы 3–9 (≈90 мин, бегло). Подробно: 4.8 «More on Defining Functions» (аргументы, ловушка default), 5 «Data Structures» (comprehensions, словари), 8 «Errors and Exceptions», 9 «Classes» до раздела 9.8. Главы 6 (модули) и 7 (ввод-вывод, f-строки) — по диагонали.
  3. Открываем документацию dataclasses → первые примеры и field(default_factory=...) (≈10 мин).
  4. Открываем модуль csv → csv.DictReader (≈10 мин) — пригодится в практике.
  5. PEP 8 → раздел Naming Conventions (≈10 мин): snake_case для функций и переменных, PascalCase для классов, UPPER_CASE для констант. Остальное сделает форматтер Ruff (гл. 1.3).
  6. Real Python — не читаем подряд, а ищем по мере вопросов («python args kwargs», «python decorators»). Одна статья — один вопрос.
  7. (≈5 мин, упражнение на чтение) Открываем DeepSeek Quick Start → пример на Python. Читаем его как Python-код: какие аргументы именованные, откуда берётся ключ, что вернёт response.choices[0].message.content. Запускать пока не нужно — ключ появится в главе 1.5.
  • Изменяемый default (def f(x=[]), x={}) — самый частый баг в сгенерированном коде. В dataclass — field(default_factory=list).
  • float для денег. Сумма по 1000 заказов в float разойдётся с отчётом платёжной системы на копейки. Либо Decimal из строк, либо осознанное округление при выводе.
  • except Exception: pass — молча скрывает сбой. В AI-коде так часто «обрабатывают» ошибки API; на ревью требуем логирование или повторный raise.
  • Отступы. Смешение табов и пробелов даёт TabError; лишний отступ у return внутри цикла — функция выходит после первой итерации.
  • Копирование. b = a для списка — та же ссылка, а не копия. Копия — a.copy() или list(a); глубокая — copy.deepcopy(a).
  • dict[key] бросает KeyError, а не возвращает пустое значение. Для данных от API — .get().
  • Переменная цикла живёт после цикла, а лямбды в цикле захватывают переменную, не значение: все они увидят её последнее значение.
  • Type hints не защищают. Если функция объявлена -> float, а возвращает None, рантайм промолчит.
  • / в индексах. xs[len(xs) / 2] упадёт: индекс должен быть int, нужен //.

Учебный репозиторий ai-engineering-journey заведём в следующей главе, 1.2 (гл. 1.2); пока создаём обычную папку с его будущей структурой — mkdir -p ~/ai-engineering-journey/exercises/week-01 ~/ai-engineering-journey/notes — и в 1.2 превращаем её в git-репозиторий. Пока uv не настроен (это гл. 1.3), хватит системного python3 (python3 --version — нужен 3.10+, для X | None).

  • Learn X in Y minutes с запуском. Создаём exercises/week-01/learn_x_in_y.py и перепечатываем (не копируем) блоки, которые удивили. Минимум 10 «сюрпризов».

  • Три чистые функции для магазина. Пишем в exercises/week-01/pricing.py функции без ввода-вывода: сумма заказа со скидкой, наценка в процентах, процентное изменение цены. Каркас:

    def order_total(unit_price: float, quantity: int, discount_pct: float = 0.0) -> float:
    """Order amount after a percent discount."""
    ...
    def markup_pct(price: float, cost: float) -> float:
    """How much the price exceeds the cost, in percent of the cost."""
    ...
    if __name__ == "__main__":
    print(order_total(100.0, 3, discount_pct=10)) # expected: 270.0
    print(markup_pct(150.0, 100.0)) # expected: 50.0

    Если под рукой есть свой проект на другом языке, вместо этих функций можно перенести 2–3 чистые функции из него: так отличия Python видны лучше всего.

  • CSV руками, без AI. Создаём вымышленный exercises/week-01/orders.csv (20 строк, одна строка — один заказ; колонки created_at,category,status,quantity,unit_price,discount; статусы paid, refunded, cancelled; discount — скидка в рублях, сумма заказа = quantity × unit_price − discount). Пишем stats.py, который:

    1. читает файл через csv.DictReader;
    2. переводит quantity, unit_price, discount в числа (что делать со строкой, которая не парсится? — пропустить с предупреждением);
    3. отбрасывает отменённые заказы;
    4. считает по каждой категории (через collections.defaultdict или Counter): количество заказов n (оплаченные и возвращённые), выручку — сумму только оплаченных, потому что возвращённые деньги ушли обратно покупателю, средний чек — выручку на один оплаченный заказ, долю возвратов среди n;
    5. печатает таблицу, отсортированную по выручке (sorted(..., key=lambda r: r["revenue"], reverse=True)).

    Ожидаемый результат: несколько строк вида mugs n=7 revenue=8420.00 avg=1403.33 refunded=14% (7 заказов: 6 оплаченных на 8420 ₽ и 1 возврат).

  • Шпаргалка. notes/python-cheatsheet.md — таблица «как я привык → как в Python», минимум 25 строк: пустое значение, сравнение, тернарник, map/filter → comprehensions, деструктуризация → распаковка, обработка ошибок, классы и self, структуры данных → dataclass/Pydantic, импорты, разбор JSON → json.loads, обход словаря → .items(), длина → len(x) и т.д. Левую колонку заполняем на своём основном языке.

  • Чтение чужого кода. Просим модель (любой чат: DeepSeek, Claude, ChatGPT — в запросе нет ничего личного) сгенерировать «скрипт на 100 строк, который анализирует лог-файл веб-сервера» и объясняем вслух (или письменно в notes/) каждую функцию, не запуская. Потом запускаем и проверяем себя.

  • Можем прочитать незнакомый Python-скрипт на 100 строк и объяснить, что он делает, включая comprehensions, *args/**kwargs, with и обработку исключений.
  • pricing.py и stats.py работают, stats.py написан без AI.
  • notes/python-cheatsheet.md написан; в главе 1.2 он станет частью первого коммита.

Вопрос 1

Что выведет print(7 / 2, 7 // 2, -7 // 2)?

  • a) 3 3 -3
  • b) 3.5 3 -4
  • c) 3.5 3 -3
  • d) 3.5 3.5 -3.5

b. / всегда float, // округляет вниз, к минус бесконечности, поэтому -7 // 2 == -4.

Вопрос 2

Какие из значений falsy: [], "0", 0.0, {}, " ", None?

[], 0.0, {}, None. Строки "0" и " " непустые, значит truthy.

Вопрос 3

Чем Optional[int] отличается от «необязательного аргумента»?

Optional[int] — это тип «int или None» (то же, что int | None). Необязательным аргумент делает значение по умолчанию (= None), а не аннотация.

Вопрос 4

Что не так в этом коде?

def collect(category, seen=set()):
seen.add(category)
return len(seen)

Изменяемый default: одно множество создаётся при определении функции и копится между вызовами. Нужно seen=None и if seen is None: seen = set().

Вопрос 5

Когда правильно использовать is, а не ==?

Только для синглтонов, на практике — x is None / x is not None. Для значений (строки, числа, списки) — ==.

Вопрос 6

Что делает if __name__ == "__main__": и зачем он нужен, если файл будут импортировать тесты?

Блок выполняется только при запуске файла как скрипта. При import из тестов или ноутбука __name__ равен имени модуля, и main() не запускается.

Вопрос 7

Что вернёт {r["sku"]: r["price"] for r in [{"sku": "MUG-NORTH", "price": 590}, {"sku": "MUG-NORTH", "price": 650}]}?

  • a) {"MUG-NORTH": 590}
  • b) {"MUG-NORTH": 650}
  • c) {"MUG-NORTH": [590, 650]}
  • d) ошибку

b. Ключи словаря уникальны, последнее присваивание побеждает.

Вопрос 8

Функция объявлена def average(a: float, b: float) -> float. Что произойдёт при вызове average("1", "2"), если внутри return (a + b) / 2?

Аннотации не проверяются: "1" + "2" даст строку "12", а деление строки на 2 бросит TypeError. Ошибку покажет только статический анализатор или IDE.

Вопрос 9

Что не так в обработке ошибки?

try:
price = float(row["price"])
except Exception:
pass
total += price

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

Вопрос 10

Что такое * в сигнатуре def create(*, model, max_tokens)?

Всё после * передаётся только по имени: create(model="m", max_tokens=100); create("m", 100) даст TypeError. Так устроены SDK, чтобы порядок аргументов можно было менять без поломок.

Вопрос 11

Как скопировать список a, чтобы изменения копии не меняли a, если внутри списка лежат словари?

copy.deepcopy(a). a.copy() и list(a) создают поверхностную копию: словари внутри будут общими.

Вопрос 12

Почему {} нельзя использовать для создания пустого множества?

{} — литерал пустого словаря (так исторически сложилось). Пустое множество — set().

Вопрос 13

Чем отличаются OpenAI() и OpenAI(base_url="https://api.deepseek.com", api_key=os.environ["DEEPSEEK_API_KEY"]) с точки зрения Python и с точки зрения того, куда уходят данные?

Для Python это один и тот же класс, разница только в именованных аргументах. Для данных — принципиальная: первый клиент шлёт запросы в OpenAI (ключ из OPENAI_API_KEY), второй — в DeepSeek. В обоих случаях данные уходят на чужие серверы, поэтому рабочие и личные данные туда не отправляем.

Вопрос 14

Что не так в этом коде, если в .env переменная называется DEEPSEEK_API_KEY?

key = os.environ.get("DEEPSEK_API_KEY")
client = OpenAI(api_key=key, base_url="https://api.deepseek.com")

Опечатка в имени переменной: .get молча вернёт None, и ошибка проявится позже и непонятно (SDK пожалуется на отсутствие ключа или сервер вернёт 401). os.environ["DEEPSEEK_API_KEY"] упал бы сразу с KeyError и правильным именем в сообщении.

Дальше глава 1.2 · Репозиторий для учёбы: git, notes/, структура: заведём репозиторий, куда положим шпаргалку и упражнения. Затем глава 1.3 · Python: uv, async, Pydantic — настоящее окружение проекта, асинхронные запросы и валидация данных, без которых не обходится ни один LLM-проект.