Функции, генераторы списков, лямбды
Как писать свои функции и вызывать чужие с именованными аргументами, как собрать новый список одной строкой (генераторы списков), как сортировать по своему правилу и как функция может вызывать саму себя — на примерах из кода обучения LLM.
Зачем это нужно
Заголовок раздела «Зачем это нужно»До сих пор вы писали программы сверху вниз, строка за строкой. Но если одно и то же действие нужно в десяти местах,
копировать его десять раз — путь к ошибкам: исправите в одном месте и забудете в другом. Функция решает это так же,
как кнопка на калькуляторе: один раз описали действие, дали ему имя — и вызываете сколько угодно раз с разными
числами. Готовыми функциями (print, len, round) вы пользуетесь с урока 0.2.0, а свою первую написали в уроке 0.1.3.
Откройте любой скрипт обучения LLM, и вы увидите три вещи на каждом шагу. Сейчас эти примеры могут выглядеть незнакомо — к концу урока вы сможете их прочитать.
- Вызовы с именованными аргументами:
F.cross_entropy(logits, targets, ignore_index=-100, label_smoothing=0.1). Что такое logits и cross-entropy, вы узнаете в модуле 1.3, а пока важна форма вызова. Чтобы понимать такой код, нужно читать сигнатуры — заголовки функций: какие аргументы обязательны, какие имеют значение по умолчанию и какое. - Однострочная обработка данных:
ids = [vocab[t] for t in tokens],itos = {i: s for s, i in stoi.items()}. - Передача функций как данных:
sorted(..., key=...),DataLoader(..., collate_fn=...), планировщики learning rate.
Этот урок — про всё это. В конце вы напишете пять функций, которые позже встретятся вам в токенизаторе и в загрузке конфигов.
Функция: def, return, docstring
Заголовок раздела «Функция: def, return, docstring»ФункцияфункцияEN functionВ Python функция — обычный объект, её можно передать как аргумент или вернуть из другой функции. — именованный кусок кода, который можно вызывать много раз с разными данными.
В уроке 0.1.3 вы уже писали def double(x): return x * 2. Разберём устройство функции целиком:
def mean(xs): """Среднее арифметическое списка чисел.""" total = 0 for x in xs: total += x return total / len(xs)
losses = [2.5, 2.1, 1.9, 1.5] # ошибка модели (loss) на четырёх шагахprint(mean(losses)) # → 2.0print(mean([1, 2])) # → 1.5def mean(xs):— заголовок: имя функции и параметры в скобках. Двоеточие в конце обязательно.- Тело пишется с отступом. Строка в тройных кавычках сразу после заголовка — docstring, документация,
которую показывают
help(mean)и редакторы кода. returnвозвращает результат и сразу завершает функцию. Функция безreturnвозвращаетNone.- Вызов
mean(losses)выполняет тело, подставив вместоxsпереданный список. Значение, переданное при вызове, называют аргументом. Параметр — «пустое место с именем» в рецепте, аргумент — то, что на это место положили.
Проследим вызов mean([1, 2]) по шагам, как в таблице трассировки из урока 0.2.0:
| Шаг | Что выполняется | xs | x | total |
|---|---|---|---|---|
| 1 | вызов: xs указывает на [1, 2] | [1, 2] | — | — |
| 2 | total = 0 | [1, 2] | — | 0 |
| 3 | первый повтор цикла: total += x | [1, 2] | 1 | 1 |
| 4 | второй повтор цикла: total += x | [1, 2] | 2 | 3 |
| 5 | return total / len(xs) → 3 / 2 | возвращается 1.5 |
Переменные, созданные внутри функции (total, x), — локальные: они существуют только во время вызова, и снаружи
их не видно. После вызова print(total) даст NameError: name 'total' is not defined.
Вспомните урок 0.2.1: переменная — это имя-наклейка. Параметр функции — такая же наклейка на тот же объект, который передали. Поэтому если функция меняет список «на месте», изменение увидит и тот, кто её вызвал:
def add_bos(tokens): tokens.insert(0, "<bos>") # вставить в начало; <bos> — метка «начало текста»
my_tokens = ["я", "люблю"]add_bos(my_tokens)print(my_tokens) # → ['<bos>', 'я', 'люблю'] — список изменился снаружиЕщё три факта, которые понадобятся в этом уроке:
- «Несколько значений» возвращаются кортежем:
return acc, errors, а при вызове распаковываются:acc, errors = evaluate(...). - Аннотации типов вроде
def mean(xs: list[float]) -> float:подсказывают людям и редактору, что функция принимает и что возвращает. Python их не проверяет. В чужом коде они встречаются постоянно. - Функция — обычный объект. Её можно положить в переменную, передать аргументом, вернуть из другой функции:
f = mean(без скобок!) — иf([1, 2])даст1.5. На этом держится половина урока.
Аргументы: позиционные, именованные, по умолчанию
Заголовок раздела «Аргументы: позиционные, именованные, по умолчанию»Параметру можно задать значение по умолчанию: если аргумент не передан, возьмётся оно. Пример из настоящего обучения:
learning rateскорость обученияEN learning rate первые warmup шагов линейно растёт, а потом держится постоянным. Такой
«разогрев» используют почти во всех LLM, подробно о нём — в уроке 4.6.3.
def lr_at(step, base_lr=3e-4, warmup=100): """Линейный разогрев: lr растёт от base_lr / warmup до base_lr за warmup шагов.""" if step < warmup: return base_lr * (step + 1) / warmup return base_lr
print(f"{lr_at(0):.1e} {lr_at(49):.1e} {lr_at(500):.1e}") # → 3.0e-06 1.5e-04 3.0e-04print(lr_at(9, warmup=10)) # → 0.0003 step по позиции, base_lr по умолчаниюprint(lr_at(step=9, base_lr=1e-3, warmup=10)) # → 0.001Проверим первую строку вывода руками. lr_at(0): шаг 0 меньше warmup = 100, поэтому
, то есть 3.0e-06 в научной записи. lr_at(49):
(1.5e-04). lr_at(500): разогрев закончился, возвращается сам base_lr = 0.0003. Формат :.1e значит «научная
запись с одной цифрой после точки».
Аргумент можно передать по позиции или по имени. Именованные аргументы можно писать в любом порядке,
и код с ними читается без заглядывания в документацию. В ML-коде так и пишут: lr_at(step, warmup=2000).
Ловушка: изменяемое значение по умолчанию
Заголовок раздела «Ловушка: изменяемое значение по умолчанию»Значение по умолчанию вычисляется один раз — когда Python выполняет строку def, а не при каждом вызове.
Если это список или словарь, он будет общим для всех вызовов:
def add_end(L=[]): L.append("END") return L
print(add_end()) # → ['END']print(add_end()) # → ['END', 'END'] — список «помнит» прошлый вызов!Правильно — использовать None как метку «аргумент не передан»:
def add_end(L=None): if L is None: L = [] L.append("END") return LПравило: значение по умолчанию должно быть неизменяемым (число, строка, None, кортеж). На этой ловушке
спотыкаются все, и она пригодится вам ещё раз в следующем уроке, в датаклассах.
Только именованные аргументы: звёздочка в сигнатуре
Заголовок раздела «Только именованные аргументы: звёздочка в сигнатуре»Посмотрите на настоящую сигнатуру (заголовок функции) из PyTorch 2.x. Это AdamW — оптимизатор, то есть та часть
обучения, которая подкручивает веса модели (ступень 1). Что значат все эти параметры, сейчас неважно, смотрите на форму:
AdamW(params, lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0.01, amsgrad=False, *, maximize=False, foreach=None, capturable=False, differentiable=False, fused=None)Одинокая * значит: всё, что после неё, можно передать только по имени. AdamW(params, 3e-4) сработает
(lr — позиционный), а флаг fused придётся писать явно: fused=True. Так авторы библиотек защищают пользователей
от путаницы в длинных списках флагов.
def scale(x, *, factor): return x * factor
print(scale(3, factor=2)) # → 6# scale(3, 2) # TypeError: scale() takes 1 positional argument but 2 were given*args и **kwargs
Заголовок раздела «*args и **kwargs»Звёздочки в определении функции собирают «лишние» аргументы: *args — позиционные в кортеж,
**kwargs — именованные в словарь. Имена args и kwargs — соглашение, важны только звёздочки.
def show(*args, **kwargs): print(args, kwargs)
show(1, 2, lr=3e-4, warmup=100) # → (1, 2) {'lr': 0.0003, 'warmup': 100}В вызове звёздочки работают наоборот — распаковывают:
params = {"base_lr": 1e-3, "warmup": 10}print(lr_at(9, **params)) # → 0.001 то же, что lr_at(9, base_lr=1e-3, warmup=10)
shape = (2, 5)def area(h, w): return h * wprint(area(*shape)) # → 10Самый частый сценарий в ML — конфиг со значениями по умолчанию, которые можно переопределить:
def make_config(**overrides): defaults = {"hidden_size": 768, "num_hidden_layers": 8, "vocab_size": 6400} return {**defaults, **overrides} # ** раскладывает словари в новый; правый перекрывает левый
print(make_config(hidden_size=512, dropout=0.1))# → {'hidden_size': 512, 'num_hidden_layers': 8, 'vocab_size': 6400, 'dropout': 0.1}Сводка видов параметров в порядке, в котором они обязаны идти в def:
| Вид | Пример в def | Как передаётся |
|---|---|---|
| позиционный | step | по позиции или по имени |
| со значением по умолчанию | warmup=100 | можно не передавать |
| сборщик позиционных | *args | лишние позиционные → кортеж |
| только именованный | fused=None после * или *args | только fused=... |
| сборщик именованных | **kwargs | лишние именованные → словарь |
Мини-задача: что напечатает def f(a, *rest, sep="-", **opts): print(a, rest, sep, opts) при вызове f(1, 2, 3, sep="+", lr=0.1)?
1 (2, 3) + {'lr': 0.1}. Первый позиционный идёт в a, остальные — в кортеж rest; sep после *rest можно передать
только по имени; незнакомое имя lr попадает в opts.
Генераторы списков и словарей
Заголовок раздела «Генераторы списков и словарей»Очень частая задача — пройти по коллекции и собрать новый список: из каждого элемента что-то вычислить, а лишние пропустить. Циклом это четыре строки:
words = "Мама мыла раму , мама !".split()clean = []for w in words: if w.isalpha(): # только слова из букв: «,» и «!» пропускаем clean.append(w.lower())print(clean) # → ['мама', 'мыла', 'раму', 'мама']Генератор спискагенератор списка (списковое включение)EN list comprehensionЗапись вида [f(x) for x in xs if cond]. В популярном китайском учебнике Python Ляо Сюэфэна это называется «выражение, порождающее список». (list comprehension) записывает то же самое одной строкой:
clean = [w.lower() for w in words if w.isalpha()] # тот же результатЧитается слева направо как фраза: «w.lower() для каждого w из words, если w.isalpha()». Часть с if
необязательна: [len(w) for w in words] — длины всех слов. Те же скобки, но фигурные и с двоеточием, дают
генератор словаря; без двоеточия — множества. Функция enumerate, которая нумерует элементы, подробно разобрана
ниже:
stoi = {w: i for i, w in enumerate(sorted(set(clean)))} # строка → iditos = {i: w for w, i in stoi.items()} # id → строка (обратный словарь)print(stoi) # → {'мама': 0, 'мыла': 1, 'раму': 2}print(itos) # → {0: 'мама', 1: 'мыла', 2: 'раму'}Пара stoi / itos (string-to-index и обратно) — словарьсловарь (токенизатора)EN vocabulary (vocab)Множество всех токенов с их номерами (id). Размер словаря V задаёт размер матрицы эмбеддингов и выходного слоя (V × d). токенизатора. Сравните
с мини-задачей урока 0.2.1: обратный словарь, который там занимал цикл из трёх строк, здесь — одна строка. В учебных
проектах Карпатого точно так же устроен словарь символов, и такой же вы напишете в модуле 4.1.
Циклов внутри может быть несколько, их порядок такой же, как у вложенных for:
corpus = ["я люблю python", "python любит меня"]tokens = [tok for sent in corpus for tok in sent.split()] # «для каждого предложения, для каждого слова в нём»print(tokens) # → ['я', 'люблю', 'python', 'python', 'любит', 'меня']Генератор списка обычно не медленнее цикла с append, а часто немного быстрее (насколько — зависит от версии Python
и задачи). Но главное — читаемость. Если выражение не помещается в строку или в нём больше двух for, пишите обычный цикл.
Мини-задача: транспонируйте матрицу [[1, 2, 3], [4, 5, 6]] (список строк таблицы) одним генератором списка — строки должны стать столбцами.
matrix = [[1, 2, 3], [4, 5, 6]]print([[row[j] for row in matrix] for j in range(3)]) # → [[1, 4], [2, 5], [3, 6]]В NumPy это будет просто matrix.T (модуль 0.3), но полезно один раз сделать руками.
Ленивые генераторы
Заголовок раздела «Ленивые генераторы»Замените квадратные скобки круглыми, и получится генераторгенератор (ленивый итератор)EN generatorФункция с yield или выражение в круглых скобках; выдаёт значения по одному и не держит всё в памяти. Не путать с генератором списка. — объект, который
выдаёт значения по одному и по требованию, не держа их все в памяти. Названия похожи, но не путайте: генератор
списка сразу строит готовый список, а просто генератор вычисляет следующее значение, только когда его попросят.
sys.getsizeof показывает, сколько байт занимает сам объект:
import sysprint(sys.getsizeof([x * x for x in range(1_000_000)])) # → 8448728 (~8 МБ на один только список ссылок)print(sys.getsizeof((x * x for x in range(1_000_000)))) # → 208 (размер не зависит от длины)# точные числа — для Python 3.11, в других версиях они немного другиеprint(sum(x * x for x in range(1_000_000))) # → 333332833333500000Функция с yield вместо return тоже становится генератором. yield отдаёт одно значение и «замораживает» функцию
до следующей просьбы; следующий запрос продолжает работу с того же места. Так читают огромные датасеты кусками —
батчами:
def batches(items, batch_size): for i in range(0, len(items), batch_size): # i = 0, 3, 6 при длине 7 и batch_size = 3 yield items[i:i + batch_size] # срез: items[0:3], items[3:6], items[6:9]
for b in batches(list(range(7)), 3): print(b) # → [0, 1, 2], затем [3, 4, 5], затем [6]Встроенная функция next(gen) берёт у генератора следующее значение, а цикл for делает это сам. Генератор
одноразовый: после того как значения кончились, next() выбрасывает StopIteration, а повторный for ничего
не выдаст. В уроке 0.2.4 вы увидите, что открытый файл ведёт себя так же: его строки читаются лениво.
enumerate и zip
Заголовок раздела «enumerate и zip»enumerate даёт пары «номер, элемент» — не нужно заводить счётчик вручную:
for i, tok in enumerate(["<bos>", "я", "люблю"]): print(i, tok) # → 0 <bos>, 1 я, 2 люблюzip идёт по нескольким последовательностям параллельно и останавливается на самой короткой. На нём держатся две
ключевые для LLM конструкции: пары соседних токенов для алгоритма BPEBPE, кодирование пар байтовEN Byte Pair Encoding (BPE)Алгоритм токенизации — многократно сливает самую частую пару соседних токенов в новый токен. Изначально (Gage, 1994) — алгоритм сжатия данных. (он ищет самые частые пары
и склеивает их в новый токен) и пары «вход → цель» для обучения языковой модели (она видит токен и учится угадывать
следующий):
toks = ["к", "о", "т", "о", "к"]print(list(zip(toks, toks[1:])))# → [('к', 'о'), ('о', 'т'), ('т', 'о'), ('о', 'к')] пары соседей — основа BPE (урок 4.2.2)
for x, y in zip(toks[:-1], toks[1:]): print(f"{x} -> {y}", end="; ")# → к -> о; о -> т; т -> о; о -> к; вход → цель: так учат языковую модель (урок 4.1.1)Если длины обязаны совпадать, передайте strict=True (Python 3.10+): вместо тихой обрезки получите ValueError.
Из двух списков легко сделать словарь: dict(zip(keys, values)). А zip(*pairs) «расстёгивает» список пар обратно
в два кортежа.
flowchart LR
A["«Мама мыла раму»"] --> B["split + lower<br/>генератор списка"]
B --> C["['мама', 'мыла', 'раму']"]
C --> D["zip(toks, toks[1:])"]
D --> E["пары соседей<br/>('мама', 'мыла') …"]
C --> F["enumerate + dict comprehension"]
F --> G["stoi: {'мама': 0, …}"]lambda и функции высшего порядка
Заголовок раздела «lambda и функции высшего порядка»Функция высшего порядкафункция высшего порядкаEN higher-order functionФункция, которая принимает или возвращает другие функции. Примеры — sorted с key, map, filter. принимает или возвращает другую функцию. Самая
частая в ML-коде — sorted. Без дополнительных аргументов она сортирует по возрастанию, а аргумент key принимает
функцию, которая для каждого элемента возвращает «ключ сравнения»: сортировка идёт по ключам.
words = ["трансформер", "кот", "автоград"]print(sorted(words)) # → ['автоград', 'кот', 'трансформер'] по алфавитуprint(sorted(words, key=len)) # → ['кот', 'автоград', 'трансформер'] по длинеprint(sorted([3, 1, 2], reverse=True)) # → [3, 2, 1] по убываниюЗдесь key=len — встроенная функция len, переданная без скобок: мы отдаём саму функцию, а sorted вызовет её
для каждого слова. Если нужной функции нет, её удобно записать прямо на месте как лямбда-функциюлямбда-функцияEN lambda (anonymous function):
выражение lambda аргументы: результат. Запись lambda w: w[-1] — то же самое, что
def f(w): return w[-1], только без имени.
На словаре частот:
counts = {"кот": 3, "и": 5, "пёс": 3, "ёж": 1, "дом": 3}
print(sorted(counts, key=counts.get, reverse=True)) # слова по убыванию частоты# → ['и', 'кот', 'пёс', 'дом', 'ёж']print(sorted(counts.items(), key=lambda kv: (-kv[1], kv[0])))# → [('и', 5), ('дом', 3), ('кот', 3), ('пёс', 3), ('ёж', 1)]print(max(counts, key=counts.get)) # → и (слово с наибольшей частотой)Последняя строка ищет не самую большую частоту, а слово, у которого она самая большая. В математике и ML такую операцию называют argmax — «аргумент максимума» (подробно — в уроке 1.0.3). Именно так модель выбирает самый вероятный следующий токен.
sorted(counts) перебирает ключи словаря, а key=counts.get для каждого слова возвращает его частоту. Во втором
примере сортируются пары (слово, частота) из counts.items(): в лямбде kv[0] — слово, kv[1] — частота, и ключ —
кортеж (-частота, слово). Кортежи сравниваются поэлементно: сначала -частота (минус превращает сортировку
по возрастанию в убывание частоты), при равенстве — само слово. Так вы получаете детерминированный порядок, что важно для воспроизводимостивоспроизводимостьEN reproducibilityВозможность получить тот же результат, повторно запустив тот же код на тех же данных.:
словарь токенизатора не должен зависеть от случайностей.
Ещё три вещи, которые нужно знать про сортировку:
sorted(x)возвращает новый список, аx.sort()сортирует на месте и возвращаетNone. Кодx = x.sort()превращает ваш список вNone.- Сортировка в Python устойчивая: элементы с равным ключом сохраняют исходный порядок. Это видно в первом примере: «кот», «пёс», «дом» идут в порядке вставки в словарь.
- Строки сравниваются по номерам символов в Юникоде (
ordиз урока 0.0.2). Поэтому заглавные идут раньше строчных (ord("Я")= 1071,ord("а")= 1072), а «ё» (номер 1105) — после «я» (номер 1103):sorted(["ёж", "еж", "яма", "Яблоко"])даёт['Яблоко', 'еж', 'яма', 'ёж']. Для «человеческого» порядка нуженkey, напримерlambda w: w.lower().replace("ё", "е").
Функция может и возвращать функцию. Внутренняя функция «запоминает» переменные внешней, такая конструкция называется замыканиемзамыканиеEN closureВнутренняя функция, которая запоминает переменные внешней функции и после её завершения. Так устроены планировщики learning rate.:
def make_scheduler(base_lr, warmup): def lr_at(step): return base_lr * min(1.0, (step + 1) / warmup) return lr_at
sched = make_scheduler(1e-3, 10)print(sched(0), sched(4), sched(100)) # → 0.0001 0.0005 0.001Именно так устроены планировщики в PyTorch: LambdaLR(optimizer, lr_lambda=...) принимает функцию «шаг → множитель».
В Python есть и классические функции высшего порядка map(f, xs) (применить f к каждому элементу) и filter(f, xs)
(оставить элементы, для которых f истинна), но генератор списка почти всегда читается лучше: вместо
list(filter(lambda w: len(w) > 2, words)) пишите [w for w in words if len(w) > 2].
Рекурсия
Заголовок раздела «Рекурсия»РекурсиярекурсияEN recursion — функция, которая вызывает саму себя на меньшей части задачи. Нужны два элемента: база (когда остановиться) и шаг (как свести задачу к меньшей). Классический пример — факториал (читается «эн факториал»): произведение всех целых чисел от 1 до , . Например, . Заметьте: — факториал выражается через факториал меньшего числа, . По договорённости — это и будет база:
def factorial(n): if n == 0: # база: 0! = 1, дальше не идём return 1 return n * factorial(n - 1) # шаг: свели задачу к меньшему n
print(factorial(5)) # → 120Вызов factorial(5) ждёт результата factorial(4), тот — factorial(3), и так до factorial(0), который сразу
возвращает 1. Потом ответы «всплывают» обратно: . Факториал проще посчитать циклом,
но рекурсия незаменима для вложенных структур произвольной глубины — списков в списках, словарей в словарях:
def flatten(nested): out = [] for item in nested: if isinstance(item, list): # isinstance(x, тип) — является ли x объектом этого типа out.extend(flatten(item)) # шаг: развернуть вложенный список той же функцией else: out.append(item) # база: обычный элемент return out
print(flatten([1, [2, [3, [4]]], 5])) # → [1, 2, 3, 4, 5]Конфиги моделей — как раз такие деревья. В PyTorch веса модели хранятся в плоском словаре с ключами через точку
(вида layers.0.attention.q_proj.weight), а получается такой ключ рекурсивным обходом вложенных модулей. Свёртку
вложенного словаря в плоский вы напишете в практике.
Мини-задача: напишите depth(x) — глубину вложенности списка. depth(7) == 0, depth([1, [2, [3]]]) == 3.
def depth(x): if not isinstance(x, list): return 0 return 1 + max((depth(i) for i in x), default=0)default=0 нужен для пустого списка: max от пустой последовательности иначе выбросит ValueError.
Практика
Заголовок раздела «Практика»Пять функций — по одной на каждый инструмент урока: генератор словаря с условием, sorted с ключом-кортежем,
zip для пар токенов, функция, возвращающая функцию, и рекурсия по вложенному конфигу.
В примере к compose встречается запись str.strip — это метод strip, взятый у самого типа строк, как обычная функция:
str.strip(" кот ") делает то же, что " кот ".strip(). Так методы удобно передавать в другие функции.
def prune_vocab(counts, min_count):
"""Оставить только слова, встретившиеся не меньше min_count раз (одной строкой — dict comprehension).
prune_vocab({"кот": 3, "ёж": 1, "дом": 2}, 2) -> {"кот": 3, "дом": 2}
"""
# TODO: генератор словаря с условием — {ключ: значение for ключ, значение in counts.items() if ...}
# Исходный словарь не меняйте: генератор и так строит новый.
...
def top_k(counts, k):
"""k самых частых слов списком пар (слово, частота).
Сортировка: по частоте по убыванию, при равной частоте — по алфавиту.
top_k({"кот": 3, "и": 5, "пёс": 3, "дом": 3}, 3) -> [("и", 5), ("дом", 3), ("кот", 3)]
"""
# TODO: 1) отсортируйте пары counts.items() функцией sorted с key=lambda kv: (...);
# kv — пара (слово, частота): kv[0] — слово, kv[1] — частота;
# ключ-кортеж (-частота, слово) даёт «по убыванию частоты, при равенстве — по алфавиту»;
# 2) возьмите первые k элементов срезом [:k] и верните их.
...
def bigrams(tokens):
"""Список пар соседних токенов (как в алгоритме BPE).
bigrams(["к", "о", "т"]) -> [("к", "о"), ("о", "т")]
"""
# TODO: zip(tokens, tokens[1:]) идёт парами соседей; оберните в list(...), чтобы получить список.
...
def compose(*funcs):
"""Вернуть НОВУЮ функцию, которая применяет funcs слева направо.
clean = compose(str.strip, str.lower)
clean(" Привет ") -> "привет"
(str.strip — метод strip, взятый у типа str: str.strip(" a ") == "a")
compose() без аргументов возвращает функцию, которая отдаёт аргумент без изменений.
"""
# TODO: план —
# def composed(x): # внутренняя функция: получает одно значение
# ... # по очереди примените к x каждую функцию из funcs: x = f(x)
# return x
# return composed # вернуть саму функцию — БЕЗ скобок
...
def flatten_config(cfg, prefix=""):
"""Вложенный словарь -> плоский с ключами через точку (так устроены имена весов в PyTorch).
flatten_config({"model": {"n_layer": 8, "attn": {"n_head": 4}}, "lr": 0.001})
-> {"model.n_layer": 8, "model.attn.n_head": 4, "lr": 0.001}
Значения, которые не являются словарями (числа, строки, списки), копируются как есть.
"""
# TODO: план —
# 1) flat = {} — сюда собираем результат;
# 2) цикл for key, value in cfg.items():
# 3) полный ключ: f"{prefix}.{key}", а если prefix пустой — просто key;
# 4) value — словарь (isinstance(value, dict))? Тогда вызовите flatten_config(value, полный_ключ)
# и добавьте его ответ в flat методом flat.update(...). Иначе: flat[полный_ключ] = value;
# 5) после цикла return flat.
...
# Проверьте себя кнопкой «Запустить»:
print(prune_vocab({"кот": 3, "ёж": 1, "дом": 2}, 2))
print(top_k({"кот": 3, "и": 5, "пёс": 3, "дом": 3}, 3))
print(bigrams(["к", "о", "т"]))
clean = compose(str.strip, str.lower)
print(clean(" Привет ") if callable(clean) else "compose пока не возвращает функцию")
print(flatten_config({"model": {"n_layer": 8, "attn": {"n_head": 4}}, "lr": 0.001}))
Проверь себя
Заголовок раздела «Проверь себя»- Значения по умолчанию вычисляются один раз: изменяемые объекты в них не кладите, используйте
None. *argsи**kwargsсобирают лишние аргументы в кортеж и словарь; в вызове*и**распаковывают. На этом построены конфиги вродеMiniMindConfig.- Генераторы списков, словарей и множеств заменяют
map/filter; круглые скобки дают ленивый генератор. enumerateнумерует,zip(toks, toks[1:])даёт пары соседей,sorted(..., key=lambda kv: (-kv[1], kv[0]))— детерминированный топ.- Рекурсия — естественный способ обойти вложенные структуры; помните про лимит глубины около 1000.
Дальше: урок 0.2.3 — классы и магические методы. Вы научите свой объект складываться через + и умножаться через *, и это
ровно то, что понадобится для собственного автограда в модуле 3.2.