Переменные, типы, условия, циклы
Второй шаг в Python: числа и строки подробнее, четыре способа хранить много значений (список, кортеж, словарь, множество), условия if и циклы for и while — с мини-задачей к каждому понятию и разбором ловушек новичка.
Зачем это нужно
Заголовок раздела «Зачем это нужно»Python — рабочий язык всей современной LLMбольшая языковая модель (БЯМ, LLM)EN large language model (LLM)В китайском «большая модель» («да мосин») — разговорное общее название для LLM и вообще foundation models.-индустрии. PyTorch, Hugging Face transformers,
vLLM, обучающие скрипты DeepSeek и Qwen, учебный проект MiniMind, который мы разберём на ступени 5, — всё это Python.
Даже когда «тяжёлая» часть написана на C++ или CUDA, управляет ею код на Python: циклы обучения, загрузка данных,
конфиги, логи.
В уроке 0.2.0 вы написали первые строки: print, арифметику, переменные и строки. Теперь соберём полный набор базовых
инструментов: типы данных, четыре встроенные коллекции, условия и циклы. Из них состоит любая программа — от подсчёта
слов в тексте до цикла обучения нейросети.
Урок плотный, поэтому не спешите: разбейте его на три вечера.
| Вечер | Разделы | Что научитесь делать |
|---|---|---|
| 1 | «Числа», «Строки» | считать и округлять, резать и склеивать текст, печатать красивые логи |
| 2 | «Коллекции» | хранить много значений сразу: списки, кортежи, словари, множества |
| 3 | «Условия», «Циклы», «Десять ошибок новичка», практика | принимать решения и повторять действия |
Иногда мы замечаем, что в других языках (C, Java, JavaScript) то же самое устроено иначе. Это подсказка для тех, кто их знает. Если вы никогда не программировали, просто пропускайте такие замечания.
Каждое понятие сопровождает мини-задача: попробуйте решить её в голове или в интерпретаторе, а потом раскройте ответ.
Запускайте примеры там, где вам удобно: в интерактивном интерпретаторе (python в терминале, приглашение >>>),
файлом (python script.py) или в Jupyter Lab — окружение вы настроили в уроке 0.1.2. Комментарий # → в примерах
показывает, что напечатает код. Все примеры проверены на Python 3.11.
Числа: int, float, bool
Заголовок раздела «Числа: int, float, bool»В уроке 0.2.0 вы познакомились с двумя видами чисел: целыми (int) и дробными (float).
Разберём их подробнее, а заодно третий тип — логический bool. Тип значения, напомним, показывает функция type.
int — целые числа. В Python они могут быть сколь угодно большими: 2 ** 100 — точное число из 31 цифры. (Во многих
других языках у целых есть предел, после которого число «переполняется» и становится неверным; в Python такого нет.)
Подчёркивания внутри числа, как вы помните, нужны только для удобства чтения:
params = 671_000_000_000 # столько параметров у DeepSeek-V3print(f"{params:,}") # → 671,000,000,000 (:, — разделить разряды запятыми; о форматах — ниже)print(2 ** 100) # → 1267650600228229401496703205376float — дробное число с плавающей точкойчисло с плавающей точкой (float)EN floating-point numberСпособ хранить дробные числа как «мантисса × 2 в степени». Форматы: FP32 (4 байта), FP16 и BF16 (2 байта), FP8 (1 байт).. Как вы помните из урока 0.0.2, оно хранится в двоичном виде в 8 байтах (по стандарту IEEE 754, которым пользуются почти все компьютеры). Десятичная дробь 0.1 в двоичном виде бесконечна, поэтому хранится с крошечной ошибкой округления:
import math # подключить модуль math с математическими функциями (об import — урок 0.2.4)print(0.1 + 0.2) # → 0.30000000000000004print(0.1 + 0.2 == 0.3) # → Falseprint(math.isclose(0.1 + 0.2, 0.3)) # → True — «примерно равны?», сравнение с крошечным допускомНикогда не сравнивайте дробные числа через ==, пользуйтесь math.isclose. В ML это особенно важно: модели считают
в ещё менее точных форматах (BF16, FP8 из урока 0.0.2), и о них будет отдельный урок 5.4.1.
Деление в Python бывает двух видов, вы видели их в уроке 0.2.0. С отрицательными числами у // и % есть
особенность, которую стоит запомнить (в C и Java здесь ответ другой — частый источник ошибок у тех, кто переходит
с этих языков):
| Выражение | Результат | Что делает |
|---|---|---|
7 / 2 | 3.5 | обычное деление, всегда float |
7 // 2 | 3 | деление с округлением вниз |
-7 // 2 | -4 | вниз — значит к минус бесконечности, а не к нулю |
-7 % 2 | 1 | остаток имеет знак делителя |
2 ** 10 | 1024 | возведение в степень |
divmod(10_000, 32) | (312, 16) | частное и остаток сразу |
Почему -7 // 2 равно −4, а не −3? Точное частное −3.5 лежит между −4 и −3, а // всегда округляет вниз,
то есть к меньшему числу, — к −4. Остаток % подбирается так, чтобы сходилось школьное равенство
«делимое = частное · делитель + остаток»: , поэтому -7 % 2 равно 1. Для положительных чисел
всё как в школе: . divmod возвращает пару «частное, остаток» в круглых скобках — это кортеж,
о нём в разделе «Коллекции».
Пример из жизни. Модель учится не на всех примерах сразу, а порциями — батчами (batch). В датасете 10 000 примеров,
батч — 32 примера. Полных батчей 10_000 // 32 = 312, в последнем неполном — 10_000 % 32 = 16 примеров, всего батчей
math.ceil(10_000 / 32) = 313 (math.ceil округляет вверх; import math подключает модуль с математическими
функциями, подробно об import — в уроке 0.2.4).
bool — логический тип с двумя значениями, True (истина) и False (ложь). Его дают сравнения: 3 > 2 — это True.
Внутри Python bool — разновидность int: True ведёт себя как 1, False — как 0. Поэтому True + True == 2, а сумма
списка результатов сравнений — это число верных. Ниже checks — список: значения в квадратных скобках по порядку
(подробно — в разделе «Коллекции»); sum складывает его элементы, len считает, сколько их:
checks = [True, False, True, True] # совпал ли ответ модели с правильным?print(sum(checks), sum(checks) / len(checks)) # → 3 0.75 — три верных ответа из четырёхДоля верных ответов называется accuracy (точность): здесь 0.75, то есть 75 %.
Преобразования типов вы видели в уроке 0.2.0: int("42"), float("3.14"), str(42). Две тонкости. int(3.99) даёт 3:
дробная часть отбрасывается, а не округляется. А round(2.5) даёт 2, потому что round округляет «по-банковски»:
ровно половинка округляется к ближайшему чётному числу. Поэтому round(2.5) == 2, но round(3.5) == 4,
а round(2.6) == 3 — как обычно.
Особое значение None означает «ничего», его тип — NoneType. Функция без return возвращает None.
Мини-задача: сколько гигабайт займут веса модели на 7 млрд параметров в формате BF16 (2 байта на параметр)?
size = 7e9 * 2 # 14 000 000 000 байтprint(f"{size / 1e9:.0f} GB") # → 14 GB (десятичные гигабайты)print(f"{size / 2**30:.2f} GiB") # → 13.04 GiB (двоичные, как показывает nvidia-smi)7e9 — научная запись из урока 0.0.2: , семь миллиардов. Формат :.0f значит «без знаков после точки»,
:.2f — «два знака» (подробно — в разделе о f-строках ниже). 2**30 = 1 073 741 824 — «двоичный» гигабайт (GiB),
его показывают некоторые программы. Такую прикидку «параметры × байты» вы будете делать постоянно, выбирая видеокарту
под модель.
СтрокастрокаEN string (str)В Python 3 строка — неизменяемая последовательность символов Юникода; длина считается в символах, а не в байтах. (str) — последовательность символов ЮникодаЮникодEN UnicodeЕдиная таблица символов всех письменностей; каждому символу соответствует номер (code point), например «я» = U+044F, «λ» = U+03BB. (урок 0.0.2).
Кавычки '...' и "..." равноправны, """...""" — многострочная строка. У каждого символа есть номер позиции —
индекс. Индексы начинаются с нуля, а отрицательные считаются с конца:
| Символ | П | р | и | в | е | т |
|---|---|---|---|---|---|---|
| Индекс | 0 | 1 | 2 | 3 | 4 | 5 |
| Индекс с конца | −6 | −5 | −4 | −3 | −2 | −1 |
s[i] достаёт один символ. СрезсрезEN slicingВыборка вида a[start:stop:step]. В NumPy срез — это view: он смотрит на те же данные, без копирования. s[start:stop:step] достаёт кусок: символы с индекса start
до stop, не включая stop, с шагом step. Любую часть можно пропустить: пропущенный start — «с начала»,
stop — «до конца», step — «шаг 1».
s = "Привет"print(s[0], s[-1]) # → П т первый и последний символprint(s[1:4]) # → рив индексы 1, 2, 3 (4 не входит)print(s[:2], s[2:]) # → Пр ивет первые два и всё остальноеprint(s[::-1]) # → тевирП шаг -1 — задом наперёдprint(len(s), len("€₽")) # → 6 2# s[0] = "п" # TypeError: 'str' object does not support item assignmentПоследняя строка показывает, что строка неизменяема: поменять букву внутри неё нельзя, можно только создать
новую строку, например "п" + s[1:].
len считает символы, а не байты. Вы помните из урока 0.0.2, что в кодировке UTF-8 «Привет» занимает 12 байт,
а «€₽» — 6. Как получить эти байты в Python и что это значит для токенизации, разберём в уроке 0.2.4.
У строки есть встроенные операции — методыметодEN methodФункция, привязанная к объекту и вызываемая через точку: s.upper(), tokens.append(x).. Метод — это функция, которая «принадлежит» объекту
и вызывается через точку: s.upper() значит «возьми строку s и верни её заглавными буквами». Строки неизменяемы,
поэтому методы строк возвращают новую строку, а исходная не меняется:
print(" кот ".strip()) # → кот (срезать пробелы по краям)print("«кот!»".strip("«»!")) # → кот (срезать перечисленные символы по краям)print("мама мыла раму".split()) # → ['мама', 'мыла', 'раму'] — список слов (о списках ниже)print("-".join(["а", "б", "в"])) # → а-б-в (склеить список строк через разделитель)print("кот" in "котлета") # → Trueprint("Привет".lower(), "Привет".upper()) # → привет ПРИВЕТЕщё несколько методов, которые встретятся в обработке текста:
print("мама мыла раму".replace("мыла", "моет")) # → мама моет раму (заменить кусок)print("train.py".endswith(".py")) # → True (кончается ли на…; startswith — начинается ли с…)print("котлета".find("лет"), "котлета".find("пёс")) # → 3 -1 (индекс начала куска; -1 — не нашлось)print("мама мыла раму".count("м")) # → 4 (сколько раз встречается)print("2026".isdigit(), "кот3".isalpha()) # → True False (только цифры? только буквы?)f-строки
Заголовок раздела «f-строки»Логи обучения печатают через f-строки (вы видели их в уроке 0.1.3): перед кавычкой ставится f, выражения
пишутся в фигурных скобках, а после двоеточия внутри скобок можно указать формат — как показать число.
Например, {loss:.4f} значит «loss с 4 знаками после точки». Первая строка примера раскладывает четыре значения
по четырём именам сразу (подробно — в разделе о кортежах), 3e-4 — научная запись числа 0.0003:
step, loss, acc, lr = 100, 2.302585, 0.125, 3e-4print(f"step {step:>5} | loss {loss:.4f} | acc {acc:.1%} | lr {lr:.0e}")# → step 100 | loss 2.3026 | acc 12.5% | lr 3e-04print(f"{loss=}") # → loss=2.302585 (удобно для отладки)| Формат | Пример | Результат |
|---|---|---|
.4f | f"{2.302585:.4f}" | 2.3026 |
>5 | f"{100:>5}" | 100 — поле из 5 символов, число прижато вправо |
.1% | f"{0.125:.1%}" | 12.5% |
, | f"{671000000000:,}" | 671,000,000,000 |
.0e | f"{0.0003:.0e}" | 3e-04 |
Мини-задача: почему "шаг " + 5 падает и как это исправить?
Python не приводит типы молча: str + int — это TypeError: can only concatenate str (not "int") to str.
Пишите f"шаг {5}" или "шаг " + str(5). В JavaScript вы получили бы "шаг 5", в Python — ошибку, и это хорошо:
меньше незаметных багов.
Коллекции: list, tuple, dict, set
Заголовок раздела «Коллекции: list, tuple, dict, set»Коллекция — объект, который хранит много значений сразу. Четыре встроенные коллекции покрывают почти все нужды ML-кода. Главное — понимать их изменяемостьизменяемость (mutable / immutable)EN mutabilityИзменяемые: list, dict, set. Неизменяемые: int, float, str, tuple. От этого зависят «эффекты» при копировании и аргументы по умолчанию. (можно ли менять объект «на месте») и то, как быстро в них искать.
Скорость поиска в таблице ниже записана в O-нотациисложность алгоритма (O-нотация)EN time complexity, big-O notationКак растёт время работы с размером данных n. O(n) — пропорционально n (поиск в list), O(1) — не зависит от n (поиск в dict и set).. O(n) читается «о от эн»
и значит: время растёт пропорционально числу элементов n, в десять раз больше элементов — примерно в десять раз
дольше. O(1) значит: время не зависит от размера коллекции, что в ней десять элементов, что миллион.
| Коллекция | Как записать | Изменяемая | Порядок | x in ... | Типичное применение в ML |
|---|---|---|---|---|---|
listсписокEN listИзменяемая упорядоченная последовательность. Проверка x in list перебирает элементы — O(n). | [1, 2, 3] | да | да | O(n), перебор | батч примеров, история loss |
| tupleкортежEN tupleНеизменяемая последовательность. Формы тензоров (shape) в NumPy и PyTorch — кортежи. | (32, 128) | нет | да | O(n) | форма тензора, пара токенов |
| dictсловарьEN dictionary (dict)Отображение «ключ → значение» на хеш-таблице; поиск по ключу в среднем O(1). С Python 3.7 сохраняет порядок вставки. | {"кот": 1} | да | порядок вставки | O(1) по ключу | словарь токенизатора, конфиг |
setмножествоEN setНеупорядоченный набор уникальных элементов. Пустое множество — set(), а {} — пустой словарь. | {"кот", "пёс"} | да | нет | O(1) | уникальные символы, стоп-слова |
Подробнее об оценке сложности и о рабочем минимуме алгоритмов для собеседования — в уроке 11.3.5.
Список — упорядоченный набор значений в квадратных скобках. Элементы нумеруются с нуля, а индексы и срезы работают так же, как у строк. В отличие от строки, список можно менять: добавлять, удалять и заменять элементы.
tokens = ["я", "люблю", "python"]tokens.append("и") # добавить в конецtokens.extend(["ИИ", "!"]) # добавить несколькоprint(tokens[1:3], tokens[-1]) # → ['люблю', 'python'] !tokens[0] = "мы" # заменить элемент по индексуlast = tokens.pop() # убрать и вернуть последнийprint(tokens, last, len(tokens)) # → ['мы', 'люблю', 'python', 'и', 'ИИ'] ! 5Мини-задача: что напечатает nums = [3, 1, 2]; nums.append(5); print(nums[-2], len(nums))?
2 4. После append список стал [3, 1, 2, 5]: в нём 4 элемента, а второй с конца — 2.
Имя — наклейка, а не коробка
Заголовок раздела «Имя — наклейка, а не коробка»В уроке 0.2.0 переменная была «подписанной коробкой». Для чисел и строк этой картинки хватает, но со списками нужна точнее. ПеременнаяпеременнаяEN variableВ Python переменная — это имя, привязанное к объекту, а не «коробка» с фиксированным типом. — это наклейка с именем, которую прикрепили к объекту. Сам объект (число, строка, список) живёт в памяти отдельно, а имя только на него указывает. У объекта есть тип, у имени — нет, поэтому одно и то же имя можно переклеить на объект другого типа:
x = 10print(type(x)) # → <class 'int'>x = "десять" # то же имя теперь указывает на строкуprint(type(x)) # → <class 'str'>Пока объекты неизменяемые (числа, строки), разница между «коробкой» и «наклейкой» незаметна. Но список можно менять
«на месте»: append дописывает элемент в тот же самый список. И тут разница становится главной ловушкой новичка:
a = [1, 2]b = a # НЕ копия: вторая наклейка на тот же списокb.append(3)print(a, a is b) # → [1, 2, 3] True — изменили «через b», а видно и «через a»c = a.copy() # настоящая копия: новый список с теми же элементамиc.append(4)print(a, c) # → [1, 2, 3] [1, 2, 3, 4]flowchart LR A["имя a"] --> L["объект list: [1, 2, 3]"] B["имя b"] --> L C["имя c"] --> M["другой объект list: [1, 2, 3, 4]"]
Оператор is проверяет, тот же ли это объект, а == — равны ли значения. Для сравнения с None пишут
x is None, в остальных случаях почти всегда нужен ==.
Та же ловушка прячется в записи [[0] * 3] * 2 — «таблица из двух строк по три нуля». Здесь * 2 повторяет не список,
а наклейку на один и тот же внутренний список. grid[0] — первая «строка» (внутренний список), grid[0][0] —
её первый элемент. Меняем его — и меняются обе строки, ведь это один объект:
grid = [[0] * 3] * 2 # две наклейки на ОДИН список [0, 0, 0]grid[0][0] = 1print(grid) # → [[1, 0, 0], [1, 0, 0]] — изменились обе строки!Как построить такую таблицу правильно, покажет урок 0.2.2, а готовый ответ есть в таблице ошибок в конце урока.
Две полезные записи присваивания: множественное h, w = 480, 640 (в h попадёт 480, в w — 640) и обмен значениями
без временной переменной a, b = b, a — вспомните задачу про чай и кофе из урока 0.2.0. Имена переменных пишут
в стиле snake_case: строчными буквами, слова через подчёркивание — learning_rate, num_layers.
Мини-задача: что напечатает x = [0]; y = x; x = x + [1]; print(y)?
[0]. Знак + склеивает два списка в новый (как строки), и имя x переклеивается на этот новый список; y
по-прежнему указывает на старый. А вот x += [1] или x.append(1) изменили бы общий объект, и y стал бы [0, 1].
Точка с запятой ; просто разделяет несколько команд в одной строке.
Кортеж — «замороженный» список в круглых скобках: после создания его нельзя изменить. Кортеж удобно распаковывать — раскладывать элементы по нескольким именам одним присваиванием:
shape = (32, 128, 768) # 32 текста в батче, по 128 токенов, каждый токен — вектор из 768 чиселbatch, seq_len, d_model = shapeprint(type((5,)), type((5))) # → <class 'tuple'> <class 'int'>Кортеж из одного элемента пишется с запятой: (5,). Без запятой скобки — просто скобки вокруг числа.
Именно кортежами будут формыформа массиваEN shapeКортеж размеров по осям, например (batch, seq_len, d_model). тензоров (многомерных таблиц чисел) в NumPy и PyTorch — модуль 0.3.
Словарь
Заголовок раздела «Словарь»Словарь хранит пары «ключ → значение» на хеш-таблицехеш-таблицаEN hash tableСтруктура данных, на которой построены dict и set; ключи должны быть хешируемыми (неизменяемыми).. По ключу Python вычисляет число-«адрес»
(хеш) и сразу идёт в нужную ячейку, не перебирая остальные пары. Поэтому поиск по ключу не зависит от размера словаря.
Типичный пример — словарь токенизаторатокенизаторEN tokenizer, который переводит токенытокенEN tokenВ разговорной речи и статьях китайцы чаще всего пишут просто «token».
(кусочки текста) в номера. В примерах ниже встретится цикл for x in коллекция: — он выполняет блок с отступом по разу
для каждого элемента; подробно циклы разберём в конце урока.
vocab = {"<unk>": 0, "кот": 1, "собака": 2} # <unk> — «неизвестный токен»print(vocab["кот"]) # → 1print("кот" in vocab) # → True (in проверяет ключи, а не значения)print(vocab.get("попугай", 0)) # → 0 (значение по умолчанию вместо ошибки)# vocab["попугай"] # KeyError: 'попугай' — такого ключа нетvocab["попугай"] = len(vocab) # добавить: id = 3 (в словаре было 3 пары)for word, idx in vocab.items(): # .items() выдаёт пары «ключ, значение» print(word, "->", idx)1True0<unk> -> 0кот -> 1собака -> 2попугай -> 3С Python 3.7 словарь помнит порядок вставки. Ключом может быть только неизменяемый объект: строка, число, кортеж.
(По-научному такие объекты называют хешируемыми: из них можно вычислить тот самый хеш-адрес, и он не поменяется.) Кортеж-ключ ("т", "о") — ровно то, что понадобится для подсчёта пар символов в алгоритме BPE
(урок 4.2.2). Список ключом быть не может: TypeError: unhashable type: 'list'.
Главная идиома словаря — подсчёт частот:
counts = {}for w in "мама мыла раму мама мыла пол".split(): counts[w] = counts.get(w, 0) + 1print(counts) # → {'мама': 2, 'мыла': 2, 'раму': 1, 'пол': 1}Для нового слова counts.get(w, 0) вернёт 0, и в словарь запишется 1; для знакомого — прибавится единица к старому счёту.
Мини-задача: постройте обратный словарь «номер → токен» для vocab = {"<unk>": 0, "кот": 1, "собака": 2}.
vocab = {"<unk>": 0, "кот": 1, "собака": 2}itos = {}for word, idx in vocab.items(): itos[idx] = word # номер становится ключом, слово — значениемprint(itos) # → {0: '<unk>', 1: 'кот', 2: 'собака'}Модель выдаёт номера токенов, и обратный словарь превращает их снова в текст. В следующем уроке вы запишете его одной строкой.
Множество
Заголовок раздела «Множество»Множество хранит только уникальные значения и не помнит порядок: повторы выбрасываются сами. Операции — как в школьной
математике: пересечение &, разность -, объединение |.
print(set("мама")) # → {'м', 'а'} (порядок может отличаться)a, b = {"кот", "пёс", "ёж"}, {"пёс", "ёж", "конь"}print(a & b, a - b) # пересечение {'пёс', 'ёж'} и разность {'кот'}Пустое множество — set(), потому что {} уже занято пустым словарём.
Почему это важно для скорости. Проверка x in list перебирает элементы, x in set — один поиск в хеш-таблице.
В наших замерах поиск последнего элемента в списке из миллиона чисел занимал около 10 мс, а в множестве — около
0.1 мкс: разница примерно в сто тысяч раз (точные цифры зависят от компьютера). Проверить каждое слово корпуса из 10 миллионов
слов по словарю из 150 тысяч токенов со списком займёт часы, а с множеством — около секунды.
Мини-задача: сколько уникальных слов во фразе «мама мыла раму мама»?
len(set("мама мыла раму мама".split())) → 3.
Условия
Заголовок раздела «Условия»Условие выполняет кусок кода, только если что-то верно. Проверку записывают операторами сравнения, каждый из них
даёт True или False:
| Оператор | Значение | Пример | Результат |
|---|---|---|---|
== | равно | 2 + 2 == 4 | True |
!= | не равно | "кот" != "Кот" | True |
<, > | меньше, больше | 0.5 > 1e-3 | True |
<=, >= | меньше или равно, больше или равно | 3 <= 2 | False |
Одиночный = — это присваивание, а не сравнение. Тело условия (блок) в Python задаётся отступом в 4 пробела —
фигурных скобок, как в C или JavaScript, нет. Логические операторы пишутся словами: and (и), or (или), not (не).
Ветки проверяются сверху вниз, и выполняется только первая подходящая; elif — «иначе если», else — «во всех
остальных случаях». Learning rateскорость обученияEN learning rate (скорость обучения) — число, которое задаёт размер шага при обучении модели:
lr = 0.5if lr > 0.1: print("слишком большой")elif lr > 1e-5: print("нормальный")else: print("слишком маленький")Полезные особенности:
- Цепочки сравнений:
0 <= x < 10работает так, как пишут в математике. - Истинность: в
ifложныFalse,None,0,0.0и пустые коллекции. Поэтому пишутif not batch:вместоif len(batch) == 0:. Строка"0"и список[0]— истинны: они не пустые. - Условное выражение (его ещё называют тернарным оператором) выбирает одно из двух значений в одной строке:
label = "чётное" if x % 2 == 0 else "нечётное"— «если x чётно, то “чётное”, иначе “нечётное”». orвозвращает первое истинное из двух значений: вname = user_name or "аноним"при пустомuser_name(пустая строка ложна) вnameпопадёт"аноним"— так подставляют значение по умолчанию.
Мини-задача: что напечатает print(bool([0]), bool(""), bool(" "))?
True False True. Список из одного нуля не пуст; пустая строка ложна; строка из пробела не пуста.
Цикл повторяет блок кода много раз: for — для каждого элемента коллекции, while — пока условие истинно.
for перебирает сами элементы по очереди: на каждом повторе имя после for указывает на следующий элемент
(в C и Java обычно перебирают номера-индексы, в Python — сразу элементы). Для чисел есть range(start, stop, step) —
«числа от start до stop с шагом step»; как и срез, он не включает stop. range(5) — это 0, 1, 2, 3, 4,
а range(1, 10, 3) — 1, 4, 7. Сам range числа не показывает, поэтому, чтобы посмотреть на них, его превращают
в список: list(range(...)).
for ch in "я€": print(ch, ord(ch)) # → я 1103, затем € 8364 — номера символов в Юникодеprint(list(range(1, 10, 3))) # → [1, 4, 7]for i in range(3): print("шаг", i) # → шаг 0, шаг 1, шаг 2 — каждое на своей строкеwhile подходит, когда число повторов заранее неизвестно. Сколько раз нужно поделить 100 пополам (нацело), чтобы
осталась единица?
n, steps = 100, 0while n > 1: n = n // 2 # 100 → 50 → 25 → 12 → 6 → 3 → 1 steps += 1print(steps) # → 6Следите, чтобы условие когда-нибудь стало ложным: если забыть строку n = n // 2, цикл никогда не закончится
(остановить программу можно клавишами Ctrl+C). break прерывает цикл досрочно, continue сразу переходит
к следующему повтору (итерации). Например, так выглядит простейшая «ранняя остановка» обучения:
for loss in [2.5, 1.9, 1.2, 0.8, 0.9]: if loss < 1.0: print("цель достигнута:", loss) # → цель достигнута: 0.8 break # дальше не идём, 0.9 не проверяетсяА вот самый настоящий градиентный спускградиентный спускEN gradient descent — алгоритм, которым обучают все нейросети. Здесь впервые появляется формула, поэтому разберём её словами.
Запись — это функция: правило «возьми число , вычти из него 3 и возведи результат в квадрат».
Например, , , . Квадрат не бывает меньше нуля, поэтому самое
маленькое значение функции — 0, при . Мы это видим, но алгоритм не знает. Он умеет только мерить наклон
графика функции в текущей точке. Наклон даёт производнаяпроизводнаяEN derivativeМгновенная скорость изменения функции — f′(x) = lim (f(x+h) − f(x))/h при h → 0. Геометрически — наклон касательной. (её ещё называют
градиентом; откуда берётся эта формула, разберём в модуле 1.2). В точке наклон равен .
Минус значит, что вправо функция убывает, и идти нужно вправо; плюс — что идти нужно влево. Отсюда шаг
w -= lr * grad: сдвигаемся против наклона на долю lr от его величины и повторяем, пока наклон не станет почти
нулевым. Первые шаги при lr = 0.1 вручную:
| Шаг | наклон | новое = наклон | |
|---|---|---|---|
| 1 | 0 | −6 | 0 + 0.6 = 0.6 |
| 2 | 0.6 | −4.8 | 0.6 + 0.48 = 1.08 |
| 3 | 1.08 | −3.84 | 1.08 + 0.384 = 1.464 |
ползёт к 3, а шаги становятся всё мельче, потому что наклон уменьшается. Тот же процесс в коде:
w, lr, steps = 0.0, 0.1, 0grad = 2 * (w - 3) # наклон в стартовой точке: -6while abs(grad) > 1e-6: # abs — модуль числа w -= lr * grad # то же, что w = w - lr * grad grad = 2 * (w - 3) steps += 1print(f"w = {w:.6f} за {steps} шагов") # → w = 3.000000 за 70 шаговУсловие abs(grad) > 1e-6 значит «пока наклон по модулю больше одной миллионной» (1e-6 = 0.000001).
Производные подробно разберём в модуле 1.2, градиентный спуск — в модуле 1.4, а пока важна форма: цикл, условие
остановки, лог.
Для любопытных: почему ровно 70 шагов
После каждого шага расстояние до ответа умножается на (проверьте по таблице: ), а вместе с ним и наклон. Начальный наклон равен . После 69 шагов он по модулю равен — ещё больше порога, а после 70 — , уже меньше. Запись — «0.8, умноженное само на себя 70 раз».
Мини-задача: напечатайте номера шагов от 1 до 6, пропуская кратные трём, в одну строку.
for step in range(1, 7): if step % 3 == 0: continue print(step, end=" ") # → 1 2 4 5Аргумент end=" " заменяет перевод строки в print пробелом.
Десять ошибок новичка
Заголовок раздела «Десять ошибок новичка»Попавшись на одну из них, запишите её в свой тетрадь ошибоктетрадь ошибокEN mistake notebookКитайская школьная практика — переписывать каждую ошибку с правильным решением и регулярно к ней возвращаться. На сайте это страница «Тетрадь ошибок», куда сами попадают ошибки из квизов. из урока 0.0.3: записанная ошибка повторяется заметно реже.
| Ошибка | Симптом | Как правильно |
|---|---|---|
| Смешаны табы и пробелы, «съехал» отступ | IndentationError или TabError | 4 пробела, редактор с подсветкой |
if x = 5: | SyntaxError | сравнение — == |
b = a для списка, потом b.append(...) | меняется и a | b = a.copy() |
grid = [[0] * 3] * 2, потом grid[0][0] = 1 | меняются обе строки: [[1, 0, 0], [1, 0, 0]] | [[0] * 3 for _ in range(2)] (генератор списка, урок 0.2.2) |
удаление из списка внутри for по нему | элементы пропускаются | новый список |
s = {} как пустое множество | получился словарь | set() |
0.1 + 0.2 == 0.3 | False | math.isclose |
list = [...], sum = 0 | TypeError: 'int' object is not callable при вызове sum(...) | не называйте переменные как встроенные функции |
d["ключ"] для отсутствующего ключа | KeyError | d.get("ключ", default) или in |
range(1, 10) «до 10 включительно» | последний элемент 9 | range(1, 11) |
Практика
Заголовок раздела «Практика»Пять маленьких функций, в каждой — одна идея урока: подсчёт частот слов словарём, FizzBuzz в обличье лога обучения
(FizzBuzz — классическая учебная задача: числа, кратные 3, заменить словом Fizz, кратные 5 — Buzz, кратные обоим — FizzBuzz),
построение словаря токенов с <unk>, кодирование текста в номера и f-строка для лога. Сначала нажмите «Запустить»,
чтобы увидеть, что печатает заготовка, потом «Проверить».
Задания оформлены как функции, их вы видели в уроке 0.1.3: строка def имя(аргументы): открывает функцию, тело пишется
с отступом, а return возвращает результат. Ваша задача — заменить ... в теле на код. Подробно о функциях — в следующем
уроке.
PUNCT = '.,!?:;«»"()…—-' # эти символы срезаются с краёв слова
def count_words(text):
"""Частоты слов в тексте.
Правила: текст приводится к нижнему регистру и режется по пробельным символам (str.split()),
у каждого слова с краёв срезаются знаки из PUNCT (str.strip(PUNCT)), пустые слова не считаются.
count_words("Кот и кот, и КОТ!") -> {"кот": 3, "и": 2}
"""
# TODO: план — 1) пустой словарь counts = {};
# 2) цикл по словам text.lower().split();
# 3) у слова срезать знаки PUNCT с краёв (метод strip), пустое слово пропустить (continue);
# 4) прибавить слову 1: counts.get(слово, 0) + 1; 5) после цикла return counts.
...
def training_log(n_steps, eval_every, save_every):
"""FizzBuzz для цикла обучения: список меток для шагов 1..n_steps включительно.
Шаг делится и на eval_every, и на save_every -> "eval+save"
делится только на eval_every -> "eval"
делится только на save_every -> "save"
иначе -> номер шага строкой, например "7"
training_log(6, 2, 3) -> ["1", "eval", "save", "eval", "5", "eval+save"]
"""
# TODO: пустой список меток; цикл по шагам от 1 до n_steps ВКЛЮЧИТЕЛЬНО (range не включает правую границу);
# «делится на k» — это step % k == 0; if / elif / else; после цикла return список.
...
def build_vocab(tokens):
"""Словарь токен -> id. Первым всегда идёт "<unk>" с id 0,
остальные токены получают id по порядку ПЕРВОГО появления, повторы пропускаются.
build_vocab(["я", "люблю", "я"]) -> {"<unk>": 0, "я": 1, "люблю": 2}
"""
# TODO: начните со словаря {"<unk>": 0}; новый токен получает id, равный текущему размеру словаря: len(vocab).
...
def encode(tokens, vocab):
"""Список токенов -> список id. Неизвестный токен получает id токена "<unk>".
encode(["я", "кот"], {"<unk>": 0, "я": 1}) -> [1, 0]
"""
# TODO: подсказка — у словаря есть метод .get(ключ, значение_по_умолчанию)
...
def report(step, loss, acc):
"""Строка лога: номер шага выровнен вправо в поле ширины 5, loss — 4 знака после точки,
acc — проценты с одним знаком после точки.
report(100, 2.302585, 0.125) -> "step 100 | loss 2.3026 | acc 12.5%"
"""
# TODO: используйте f-строку
...
# Проверьте себя кнопкой «Запустить»:
print(count_words("Кот и кот, и КОТ!"))
print(training_log(6, 2, 3))
vocab = build_vocab("я люблю я и ты".split())
print(vocab)
print(encode(["я", "кот"], vocab) if vocab else None)
print(report(100, 2.302585, 0.125))
Проверь себя
Заголовок раздела «Проверь себя»- Переменная в Python — имя объекта.
b = aне копирует список; копия —a.copy(). intне переполняется,floatнеточен (math.isclose),//округляет вниз,/всегда даёт float.listиtuple— последовательности (поиск O(n)),dictиset— хеш-таблицы (поиск O(1)); порядокsetне гарантирован, поэтомуsorted(set(...)).- Пустые коллекции,
0иNoneложны;forперебирает элементы,rangeне включает правую границу. - f-строки
f"{loss:.4f}"— стандартный способ печатать логи обучения.
Дальше: урок 0.2.2 — функции, аргументы *args и **kwargs, генераторы списков, sorted
с key, zip и рекурсия: всё, из чего состоит обработка данных для LLM.