Перейти к содержимому
Ступень 00.2 Python с нуля

Переменные, типы, условия, циклы

Второй шаг в Python: числа и строки подробнее, четыре способа хранить много значений (список, кортеж, словарь, множество), условия if и циклы for и while — с мини-задачей к каждому понятию и разбором ловушек новичка.

УРОКstage-0/0-2-python/01-basics.mdx2 чУрок 2 из 7
После урока вы сможете
  • Хранить данные в списках, кортежах, словарях и множествах и выбирать подходящую коллекцию под задачу
  • Писать условия и циклы, считать частоты слов словарём и печатать красивые логи обучения f-строками
  • Предсказать, что произойдёт после b = a со списком, и объяснить, почему переменная — это наклейка-имя на объекте
  • Узнавать в чужом коде 10 типичных ошибок новичка

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.

В уроке 0.2.0 вы познакомились с двумя видами чисел: целыми (int) и дробными (float). Разберём их подробнее, а заодно третий тип — логический bool. Тип значения, напомним, показывает функция type.

int — целые числа. В Python они могут быть сколь угодно большими: 2 ** 100 — точное число из 31 цифры. (Во многих других языках у целых есть предел, после которого число «переполняется» и становится неверным; в Python такого нет.) Подчёркивания внутри числа, как вы помните, нужны только для удобства чтения:

params = 671_000_000_000 # столько параметров у DeepSeek-V3
print(f"{params:,}") # → 671,000,000,000 (:, — разделить разряды запятыми; о форматах — ниже)
print(2 ** 100) # → 1267650600228229401496703205376

float — дробное число с плавающей точкойчисло с плавающей точкой (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.30000000000000004
print(0.1 + 0.2 == 0.3) # → False
print(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 / 23.5обычное деление, всегда float
7 // 23деление с округлением вниз
-7 // 2-4вниз — значит к минус бесконечности, а не к нулю
-7 % 21остаток имеет знак делителя
2 ** 101024возведение в степень
divmod(10_000, 32)(312, 16)частное и остаток сразу

Почему -7 // 2 равно −4, а не −3? Точное частное −3.5 лежит между −4 и −3, а // всегда округляет вниз, то есть к меньшему числу, — к −4. Остаток % подбирается так, чтобы сходилось школьное равенство «делимое = частное · делитель + остаток»: −7=(−4)⋅2+1-7 = (-4) \cdot 2 + 1, поэтому -7 % 2 равно 1. Для положительных чисел всё как в школе: 7=3⋅2+17 = 3 \cdot 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: 7⋅1097 \cdot 10^9, семь миллиардов. Формат :.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). Кавычки '...' и "..." равноправны, """...""" — многострочная строка. У каждого символа есть номер позиции — индекс. Индексы начинаются с нуля, а отрицательные считаются с конца:

СимволПривет
Индекс012345
Индекс с конца−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 "котлета") # → True
print("Привет".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-строки (вы видели их в уроке 0.1.3): перед кавычкой ставится f, выражения пишутся в фигурных скобках, а после двоеточия внутри скобок можно указать формат — как показать число. Например, {loss:.4f} значит «loss с 4 знаками после точки». Первая строка примера раскладывает четыре значения по четырём именам сразу (подробно — в разделе о кортежах), 3e-4 — научная запись числа 0.0003:

step, loss, acc, lr = 100, 2.302585, 0.125, 3e-4
print(f"step {step:>5} | loss {loss:.4f} | acc {acc:.1%} | lr {lr:.0e}")
# → step 100 | loss 2.3026 | acc 12.5% | lr 3e-04
print(f"{loss=}") # → loss=2.302585 (удобно для отладки)
ФорматПримерРезультат
.4ff"{2.302585:.4f}"2.3026
>5f"{100:>5}" 100 — поле из 5 символов, число прижато вправо
.1%f"{0.125:.1%}"12.5%
,f"{671000000000:,}"671,000,000,000
.0ef"{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 — ошибку, и это хорошо: меньше незаметных багов.

Коллекция — объект, который хранит много значений сразу. Четыре встроенные коллекции покрывают почти все нужды 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 = 10
print(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]"]
После b = a оба имени указывают на один объект; copy() создаёт новый

Оператор is проверяет, тот же ли это объект, а == — равны ли значения. Для сравнения с None пишут x is None, в остальных случаях почти всегда нужен ==.

Та же ловушка прячется в записи [[0] * 3] * 2 — «таблица из двух строк по три нуля». Здесь * 2 повторяет не список, а наклейку на один и тот же внутренний список. grid[0] — первая «строка» (внутренний список), grid[0][0] — её первый элемент. Меняем его — и меняются обе строки, ведь это один объект:

grid = [[0] * 3] * 2 # две наклейки на ОДИН список [0, 0, 0]
grid[0][0] = 1
print(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 = shape
print(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["кот"]) # → 1
print("кот" 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)
1
True
0
<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) + 1
print(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 == 4True
!=не равно"кот" != "Кот"True
<, >меньше, больше0.5 > 1e-3True
<=, >=меньше или равно, больше или равно3 <= 2False

Одиночный = — это присваивание, а не сравнение. Тело условия (блок) в Python задаётся отступом в 4 пробела — фигурных скобок, как в C или JavaScript, нет. Логические операторы пишутся словами: and (и), or (или), not (не). Ветки проверяются сверху вниз, и выполняется только первая подходящая; elif — «иначе если», else — «во всех остальных случаях». Learning rateскорость обученияEN learning rate (скорость обучения) — число, которое задаёт размер шага при обучении модели:

lr = 0.5
if 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, 0
while n > 1:
n = n // 2 # 100 → 50 → 25 → 12 → 6 → 3 → 1
steps += 1
print(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 — алгоритм, которым обучают все нейросети. Здесь впервые появляется формула, поэтому разберём её словами.

Запись f(w)=(w−3)2f(w) = (w - 3)^2 — это функция: правило «возьми число ww, вычти из него 3 и возведи результат в квадрат». Например, f(0)=(0−3)2=9f(0) = (0 - 3)^2 = 9, f(5)=(5−3)2=4f(5) = (5 - 3)^2 = 4, f(3)=0f(3) = 0. Квадрат не бывает меньше нуля, поэтому самое маленькое значение функции — 0, при w=3w = 3. Мы это видим, но алгоритм не знает. Он умеет только мерить наклон графика функции в текущей точке. Наклон даёт производнаяпроизводнаяEN derivativeМгновенная скорость изменения функции — f′(x) = lim (f(x+h) − f(x))/h при h → 0. Геометрически — наклон касательной. f′(w)=2(w−3)f'(w) = 2(w - 3) (её ещё называют градиентом; откуда берётся эта формула, разберём в модуле 1.2). В точке w=0w = 0 наклон равен 2⋅(0−3)=−62 \cdot (0 - 3) = -6. Минус значит, что вправо функция убывает, и идти нужно вправо; плюс — что идти нужно влево. Отсюда шаг w -= lr * grad: сдвигаемся против наклона на долю lr от его величины и повторяем, пока наклон не станет почти нулевым. Первые шаги при lr = 0.1 вручную:

Шагwwнаклон 2(w−3)2(w - 3)новое ww = w−0.1⋅w - 0.1 \cdot наклон
10−60 + 0.6 = 0.6
20.6−4.80.6 + 0.48 = 1.08
31.08−3.841.08 + 0.384 = 1.464

ww ползёт к 3, а шаги становятся всё мельче, потому что наклон уменьшается. Тот же процесс в коде:

w, lr, steps = 0.0, 0.1, 0
grad = 2 * (w - 3) # наклон в стартовой точке: -6
while abs(grad) > 1e-6: # abs — модуль числа
w -= lr * grad # то же, что w = w - lr * grad
grad = 2 * (w - 3)
steps += 1
print(f"w = {w:.6f} за {steps} шагов") # → w = 3.000000 за 70 шагов

Условие abs(grad) > 1e-6 значит «пока наклон по модулю больше одной миллионной» (1e-6 = 0.000001). Производные подробно разберём в модуле 1.2, градиентный спуск — в модуле 1.4, а пока важна форма: цикл, условие остановки, лог.

Для любопытных: почему ровно 70 шагов

После каждого шага расстояние до ответа w−3w - 3 умножается на 1−2⋅0.1=0.81 - 2 \cdot 0.1 = 0.8 (проверьте по таблице: −3→−2.4→−1.92-3 \to -2.4 \to -1.92), а вместе с ним и наклон. Начальный наклон равен −6-6. После 69 шагов он по модулю равен 6⋅0.869≈1.2⋅10−66 \cdot 0.8^{69} \approx 1.2 \cdot 10^{-6} — ещё больше порога, а после 70 — 6⋅0.870≈9.9⋅10−76 \cdot 0.8^{70} \approx 9.9 \cdot 10^{-7}, уже меньше. Запись 0.8700.8^{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 или TabError4 пробела, редактор с подсветкой
if x = 5:SyntaxErrorсравнение — ==
b = a для списка, потом b.append(...)меняется и ab = 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.3Falsemath.isclose
list = [...], sum = 0TypeError: 'int' object is not callable при вызове sum(...)не называйте переменные как встроенные функции
d["ключ"] для отсутствующего ключаKeyErrord.get("ключ", default) или in
range(1, 10) «до 10 включительно»последний элемент 9range(1, 11)

Пять маленьких функций, в каждой — одна идея урока: подсчёт частот слов словарём, FizzBuzz в обличье лога обучения (FizzBuzz — классическая учебная задача: числа, кратные 3, заменить словом Fizz, кратные 5 — Buzz, кратные обоим — FizzBuzz), построение словаря токенов с <unk>, кодирование текста в номера и f-строка для лога. Сначала нажмите «Запустить», чтобы увидеть, что печатает заготовка, потом «Проверить».

Задания оформлены как функции, их вы видели в уроке 0.1.3: строка def имя(аргументы): открывает функцию, тело пишется с отступом, а return возвращает результат. Ваша задача — заменить ... в теле на код. Подробно о функциях — в следующем уроке.

Словари, циклы и f-строки7 тестов
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))
Проверь себя6 вопросов
1 Что напечатает код a = [1, 2]; b = a; b.append(3); print(len(a))?
2 Какие значения в условии if считаются ложными?Можно выбрать несколько ответов
3 Чему равно -7 // 2 в Python?
4 В словаре токенизатора 150 000 токенов. Нужно много раз проверять, есть ли в нём слово. Что выбрать?
5 Чему равно len("🐉🐟, мир")?
6 Что получится после grid = [[0] * 3] * 2; grid[0][0] = 1?
  • Переменная в 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.