1.1 · Python для тех, кто уже программирует
Неделя 1 · Подготовка · ⏱ ~4 ч · Теория, Практика
Зачем это AI-инженеру
Заголовок раздела «Зачем это AI-инженеру»Почти весь 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. Остальные строки таблицы разбираем ниже по порядку.
2. Синтаксис: отступы, None, truthy/falsy
Заголовок раздела «2. Синтаксис: отступы, None, truthy/falsy»Блок кода задаётся не скобками, а отступом: заголовок 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 иногда «работает» из-за кеширования интерпретатора и однажды перестаёт — это классический баг.
3. Числа
Заголовок раздела «3. Числа»В магазине почти всё — числа: количество, цены, скидки. Здесь 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.
4. Коллекции
Заголовок раздела «4. Коллекции»Списки заказов, словари «товар → цена», множества категорий — основная рабочая форма данных в любом скрипте. Встроенных коллекций четыре:
| Привычное название | 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 valueprices.get("pen") # Noneprices.get("pen", 0.0) # default value"mug" in prices # key membershipfor 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] # 50xs[:2] # [10, 20]xs[::-1] # reversed copyComprehensions заменяют цепочки map/filter: список, словарь или множество собирается одним выражением «что взять — откуда — при каком условии». В примере orders — список заказов-словарей вида {"id": 1, "category": "mugs", "quantity": 2}:
# "keep the positive ones and double them" in one expressiondoubled = [x * 2 for x in xs if x > 0]by_id = {o["id"]: o for o in orders} # dict comprehensioncategories = {o["category"] for o in orders} # set comprehensiontotal = 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), метод вызывается у разделителя.
5. Функции
Заголовок раздела «5. Функции»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 области видимости не создают: переменная из цикла живёт после цикла.
6. Классы и dataclass
Заголовок раздела «6. Классы и dataclass»Классы устроены привычно — конструктор, атрибуты, методы, — но с явным 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 typeclass 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 — в следующем разделе).
7. Исключения
Заголовок раздела «7. Исключения»Ошибки в 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 causeelse: print("loaded") # runs only if no exceptionfinally: 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 exception8. Модули и импорты
Заголовок раздела «8. Модули и импорты»Как только кода становится больше одного файла, встаёт вопрос, как файлы видят друг друга. Файл = модуль, папка с файлами = пакет. 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()Тестам и ноутбукам это даёт импортировать функции, не запуская скрипт.
9. Type hints
Заголовок раздела «9. Type hints»Аннотации типов уже встречались во всех примерах выше; соберём их формы в одном месте. Напомним: рантайм их не проверяет, но их читают IDE, линтеры, Pydantic и SDK, которые строят по ним схемы данных.
from typing import Optional, Literal, Anyfrom 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 thingStatus = Literal["new", "paid", "shipped"] # only these three strings are alloweddef 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. Учить наизусть не надо — достаточно знать, что это есть, и спросить модель или документацию.
DeepSeek и Ollama: как это сделать у нас
Заголовок раздела «DeepSeek и Ollama: как это сделать у нас»Для этой главы провайдер почти не важен: 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 differimport osfrom 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. Это правило действует весь курс.
Маршрут изучения
Заголовок раздела «Маршрут изучения»- Открываем Learn X in Y minutes: Python → проходим страницу целиком, запуская каждый блок в
python3REPL или в файле (≈90 мин). Отмечаем всё, что ведёт себя не так, как в привычном языке, — это сырьё для шпаргалки. - Открываем официальный туториал → главы 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-строки) — по диагонали.
- Открываем документацию dataclasses → первые примеры и
field(default_factory=...)(≈10 мин). - Открываем модуль csv →
csv.DictReader(≈10 мин) — пригодится в практике. - PEP 8 → раздел Naming Conventions (≈10 мин):
snake_caseдля функций и переменных,PascalCaseдля классов,UPPER_CASEдля констант. Остальное сделает форматтер Ruff (гл. 1.3). - Real Python — не читаем подряд, а ищем по мере вопросов («python args kwargs», «python decorators»). Одна статья — один вопрос.
- (≈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.0print(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, который:- читает файл через
csv.DictReader; - переводит
quantity,unit_price,discountв числа (что делать со строкой, которая не парсится? — пропустить с предупреждением); - отбрасывает отменённые заказы;
- считает по каждой категории (через
collections.defaultdictилиCounter): количество заказовn(оплаченные и возвращённые), выручку — сумму только оплаченных, потому что возвращённые деньги ушли обратно покупателю, средний чек — выручку на один оплаченный заказ, долю возвратов средиn; - печатает таблицу, отсортированную по выручке (
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)?
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}]}?
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: passtotal += 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-проект.