Если вы уже освоили базовый синтаксис, циклы и условия, но код всё ещё кажется «тяжёлым» или избыточно длинным — вы стоите на пороге понимания одной из самых мощных сторон современных языков программирования. Лямбды, замыкания и генераторы — это не просто «красивые фичи» для собеседований. Это инструменты, которые позволяют писать код легче, масштабируемее и тестируемее.
В этой статье мы разберём не только «как это пишется», но и «зачем это нужно», «в каких случаях применять» и «какие ошибки чаще всего совершают новички». Используем примеры на Python и JavaScript — они наиболее популярны в индустрии и отлично демонстрируют концепции. Но принципы универсальны: они работают в Java, C#, Go и других языках с поддержкой функциональных паттернов.
Почему продвинутые конструкции важны для разработчика?
В начале пути разработчик часто пишет код, который решает задачу, но делает это «в лоб». Смотрите сами — типичный пример фильтрации списка:
# Python — наивный подход
result = []
for item in data:
if item > 10:
result.append(item * 2)
Такой код работает, но у него есть системные недостатки:
- Он занимает много места — четыре строки там, где можно обойтись одной.
- Требует явного управления циклом и временной переменной
result. - Сложнее читается при добавлении вложенных условий — глаз цепляется за технические детали, а не за смысл операции.
Продвинутые конструкции позволяют переписать это иначе:
# С использованием map и filter
result = map(lambda x: x * 2, filter(lambda x: x > 10, data))
Или ещё лучше — через генераторное выражение:
# Генератор — самый лаконичный вариант
result = (x * 2 for x in data if x > 10)
Ключевое отличие: вы переносите фокус с процесса (как перебирать, какие временные переменные создавать) на смысл (что мы хотим получить). Это фундамент функционального программирования, которое становится стандартом в современной разработке. И поверьте: когда вы начнёте читать такой код через полгода — вы скажете себе спасибо.
1. Лямбды: функции без имени
Что такое лямбда?
Лямбда — это короткая функция, которая не имеет имени. В отличие от обычных функций, объявленных через def в Python или function в JS, лямбды создаются в месте использования и чаще всего выступают как аргументы для других функций. Их главная фишка — компактность и ситуативность: вы описываете поведение ровно там, где оно нужно, не засоряя пространство имён.
Синтаксис в Python:
# Лямбда принимает параметры и возвращает выражение
multiply = lambda x, y: x * y
result = multiply(3, 5) # 15
# Типичное использование — сразу в аргументе
sorted_users = sorted(users, key=lambda user: user.age)
Синтаксис в JavaScript:
// Стрелочная функция — аналог лямбды в JS
const multiply = (x, y) => x * y;
// Если параметр один — скобки можно опустить
const square = x => x * x;
// Многострочное тело требует фигурных скобок и return
const calc = (a, b) => {
const result = a * b;
return result;
};
Где и зачем их использовать?
Лямбды идеально вписываются в три сценария:
- Однострочные операции — фильтрация, сортировка, преобразование данных.
- Временные обработчики — например, колбэки в GUI или веб-серверах, которые не нужно переиспользовать.
- Аргументы для функций высшего порядка —
map,filter,reduce,sortи любых кастомных функций, ожидающих колбэк.
Пример 1: Сортировка с лямбдой
Представим, что нужно отсортировать список пользователей по возрасту. Без лямбды пришлось бы писать отдельную функцию:
# Без лямбды: отдельная функция в три строки
def get_age(user):
return user.age
sorted_users = sorted(users, key=get_age)
С лямбдой — всё в одной строке, и намерение сразу понятно:
# С лямбдой: компактно и читаемо
sorted_users = sorted(users, key=lambda user: user.age)
Лямбда здесь не просто экономит строки — она исключает ментальный прыжок: вам не нужно искать глазами объявление get_age, чтобы понять, по какому полю идёт сортировка.
Пример 2: Фильтрация в JS
Типичный сценарий для фронтенда — отфильтровать массив объектов по условию:
// Оставляем только активных пользователей
const activeUsers = users.filter(user => user.isActive);
// Комбинируем filter и map для цепочки преобразований
const emails = users
.filter(user => user.isActive)
.map(user => user.email);
Типовые ошибки при работе с лямбдами
1. Попытка сделать лямбду сложной.
Лямбда должна быть однострочной и выражать одно действие. Если вы ловите себя на том, что пишете вложенные условия, циклы или многострочную логику — это не лямбда, это обычная функция, которую вы зачем-то пытаетесь втиснуть в анонимную форму.
❌ Неправильно:
# Так не делают — лямбда с условием и побочным эффектом
process = lambda x: x * 2 if x > 10 else print(x)
✅ Правильно:
# Выносим логику в именованную функцию
def process(x):
if x > 10:
return x * 2
print(x)
result = map(process, data)
2. Использование лямбды для рекурсии.
Лямбды анонимны — у них нет имени для рекурсивного вызова. Теоретически можно извратиться через Y-комбинатор или присвоить лямбду переменной и вызывать её внутри себя, но на практике это никогда не нужно. Рекурсия = обычная функция, и точка.
3. Потеря контекста this в JS.
В JavaScript стрелочные функции не имеют собственного this — они заимствуют его из родительского контекста. Это частая ловушка при использовании лямбд внутри методов объекта:
const user = {
name: 'Алексей',
greet: () => {
// this здесь — это не user, а глобальный контекст
console.log(`Привет, ${this.name}`);
}
};
user.greet(); // Привет, undefined
Решение: использовать обычную функцию function() — только у неё this определяется вызывающим объектом. Либо сохранить this во внешнюю переменную, если вы работаете с вложенными колбэками.
Чек-лист: когда использовать лямбду
| Ситуация | Использовать лямбду? |
|---|---|
| Однострочное выражение | ✅ Да |
| Многоуровневые условия | ❌ Нет (обычная функция) |
| Рекурсия | ❌ Нет |
| Вложенные циклы | ❌ Нет |
| Аргумент для map/filter/sort | ✅ Да |
| Обработчик события (в JS) | ✅ Да (если нет this) |
2. Замыкания: функции, которые «запоминают» окружение
Что такое замыкание?
Замыкание — это функция, которая «захватывает» переменные из своего лексического окружения и сохраняет к ним доступ даже после того, как внешняя функция отработала. Формально: замыкание = функция + её окружение на момент создания.
В Python и JavaScript замыкания работают «из коробки» — вам не нужно специально их объявлять. Они возникают автоматически, когда вы возвращаете функцию из другой функции, и эта внутренняя функция использует переменные внешней.
Как это работает?
Разберём на Python — шаг за шагом:
def outer_function(x): # 1. Внешняя функция получает x
def inner_function(): # 2. Внутренняя функция
print(f"x = {x}") # 3. Использует x из внешнего скоупа
return inner_function # 4. Возвращаем саму функцию
closure = outer_function(5) # 5. outer отработала, но...
closure() # 6. inner всё ещё помнит x = 5
Что здесь произошло:
outer_functionвызвана сx=5.- Внутри создана
inner_function, которая ссылается наx. outer_functionвозвращаетinner_functionкак объект.- После выхода из
outer_functionеё локальные переменные обычно уничтожаются. Ноxне уничтожается — он «замкнут» внутриinner_functionи будет жить, пока живёт эта функция.
На практике это работает именно так, как вы подумали: замыкание — это способ дать функции приватную память.
Зачем нужны замыкания?
Замыкания решают четыре класса задач:
- Функции-фабрики — создание специализированных функций с заранее заданными параметрами.
- Приватные переменные — инкапсуляция данных без классов (актуально для JS до появления
#private-полей). - Имитация классов — когда вам нужно состояние и поведение, но классы избыточны.
- Асинхронный код с сохранением контекста — колбэки, которые помнят, с какими данными они были созданы.
Пример 1: Функция-фабрика (Python)
Типичный кейс: нужно создать несколько функций умножения с фиксированными коэффициентами:
def make_multiplier(factor):
"""Фабрика функций умножения на factor."""
def multiplier(number):
return number * factor
return multiplier
# Создаём специализированные функции
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10)) # 20
print(triple(10)) # 30
double и triple — это независимые замыкания, каждое из которых «помнит» свой factor. В промышленном коде такой паттерн часто используется для построения конвейеров обработки данных, где каждая функция отвечает за свой этап преобразования.
Пример 2: Приватные переменные в JS
Классический счётчик, где переменная count недоступна извне, но методы могут с ней работать:
function createCounter() {
let count = 0; // Приватная переменная
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count
};
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 2
console.log(counter.count); // undefined — переменная защищена
До появления #private-полей в классах (ES2020) это был основной способ инкапсуляции в JavaScript. Сейчас это всё ещё полезно, когда вам нужен лёгкий объект с состоянием без церемонии объявления класса.
Типовые ошибки с замыканиями
1. Путать замыкания с глобальными переменными.
Замыкание работает с локальным окружением функции, а не с глобальным скоупом. Глобальная переменная доступна везде и может быть изменена кем угодно; замыкание — это изолированная память конкретной функции.
2. Замыкание в цикле с var в старом JS.
До ES6 переменная var была функционально скоуплена, а не блочно. Это означало, что в цикле for все итерации разделяли одну и ту же переменную i. Замыкание запоминало ссылку на эту переменную, а не её значение на момент итерации:
❌ Ошибка (var — все колбэки видят финальное значение i):
for (var i = 0; i console.log(i), 100);
}
// Вывод: 3, 3, 3
✅ Решение (let создаёт новый i для каждой итерации):
for (let i = 0; i console.log(i), 100);
}
// Вывод: 0, 1, 2
3. Непонимание времени жизни замыкания.
Замыкание живёт ровно до тех пор, пока существует функция, которая на него ссылается. Если вы присвоили замыкание переменной, а потом эту переменную перезаписали или она вышла из скоупа — сборщик мусора освободит память. Проблемы возникают, когда разработчики забывают об этом и создают циклические ссылки, но в современных движках сборщики мусора достаточно умны, чтобы с этим справляться.
Замыкания vs Глобальные переменные
| Характеристика | Замыкание | Глобальная переменная |
|---|---|---|
| Доступность | Только внутри функции | Везде |
| Безопасность | Высокая (приватность) | Низкая (конфликты имён) |
| Управление памятью | Автоматическое (по ссылке) | Автоматическое |
| Применение | Фабрики, приватность, асинхронность | Конфигурация, кэш |
3. Генераторы: экономия памяти и потоковая обработка
Что такое генератор?
Генератор — это специальная функция, которая возвращает значения по одному, «на лету», вместо того чтобы собирать полный список и возвращать его целиком. Она работает как поток: выдаёт элемент — ставится на паузу, запоминает своё состояние — при следующем запросе продолжает с того же места.
В Python генераторы создаются с помощью yield, в JavaScript — через function* и yield.
Ключевое преимущество: генераторы не хранят все данные в памяти. Они вычисляют значение только когда вы его запрашиваете. На миллионе записей разница между списком в памяти и генератором — это разница между упавшим процессом и стабильно работающим приложением.
Синтаксис в Python
def simple_generator():
yield 1
yield 2
yield 3
gen = simple_generator()
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
# print(next(gen)) # StopIteration — генератор исчерпан
# Нормальный способ — итерация в цикле
for value in simple_generator():
print(value)
Синтаксис в JavaScript
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = simpleGenerator();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
console.log(gen.next().done); // true
Где и зачем использовать генераторы?
Генераторы — это правильный выбор в четырёх ситуациях:
- Обработка больших данных — чтение многогигабайтных файлов, потоков из API, логов.
- Потоковая обработка — когда данные приходят порциями и нужно реагировать на каждую порцию.
- Реализация итераторов — создание собственных перебираемых объектов.
- Снижение нагрузки на память — когда вы работаете с последовательностью, которая потенциально бесконечна или просто очень велика.
Пример 1: Генератор чисел (Python)
def even_numbers(limit):
"""Генерирует чётные числа от 0 до limit."""
for i in range(limit):
if i % 2 == 0:
yield i
# Обработка без создания списка
for num in even_numbers(1000000):
# Каждое число обрабатывается сразу и забывается
process(num)
Вместо создания списка из 500 000 элементов в памяти, генератор выдаёт числа по одному. Для limit=1000000 это не критично, а для чтения 20-гигабайтного лог-файла — разница между работающим кодом и MemoryError.
Пример 2: Чтение большого файла (Python)
Классический подход с readlines() загружает все строки в память. Если файл весит 10 ГБ — вы просто уроните процесс. Генераторный подход читает и обрабатывает построчно:
def read_large_file(file_path):
"""Читает файл построчно, не загружая его в память."""
with open(file_path, 'r') as file:
for line in file:
yield line.strip()
# Обрабатываем терабайтный лог без проблем
for line in read_large_file('huge_log.txt'):
if 'ERROR' in line:
report_error(line)
Здесь файловый объект уже является итератором, но обёртка в генератор даёт дополнительную гибкость — можно добавить фильтрацию, трансформацию или агрегацию прямо в генераторе.
Пример 3: Генератор с асинхронностью в JS
Генераторы в JavaScript часто используются как основа для асинхронных паттернов. В связке с async/await они позволяют обрабатывать потоки данных с паузами:
async function* asyncGenerator() {
const data = await fetchData();
for (const item of data) {
yield processItem(item);
}
}
// Асинхронная итерация
for await (const result of asyncGenerator()) {
console.log(result);
}
Типовые ошибки с генераторами
1. Попытка итерировать генератор несколько раз.
Генератор — это одноразовый объект. После первого полного прохода он исчерпан, и повторная итерация не даст ни одного значения:
gen = (x for x in range(3))
list(gen) # [0, 1, 2]
list(gen) # [] — пусто!
Решение: если данные нужны несколько раз — либо создавайте новый генератор при каждом использовании, либо материализуйте его в список, если размер позволяет.
2. Использование генератора в месте, где ожидается список.
Некоторые функции или API ожидают именно список (list), а генератор — это итератор. Передача генератора может привести к ошибке или, что хуже, к молчаливому исчерпанию:
def process_data(data):
# Функция ожидает список и проходит по нему дважды
data.sort()
return data[0]
gen = (x for x in range(100))
# process_data(gen) # AttributeError: у генератора нет sort
Решение: явно обернуть в list(gen), если список действительно нужен.
3. Непонимание разницы между yield и return.
yield приостанавливает функцию и возвращает значение, сохраняя состояние для продолжения. return завершает функцию полностью и передаёт управление вызывающему коду. В генераторе return вызывает StopIteration — используйте его только для финализации генератора.
Генераторы vs Списки
| Характеристика | Генератор | Список |
|---|---|---|
| Потребление памяти | Минимальное (поэлементно) | Высокое (все данные) |
| Время создания | Мгновенное | Пропорционально размеру |
| Повторная итерация | Нет (одноразовый) | Да |
| Индексация | Нет | Да (по индексу) |
| Применение | Большие данные, потоки | Маленькие данные, повторный доступ |
4. Практические паттерны: как объединить лямбды, замыкания и генераторы
В реальной разработке эти три концепции редко живут по отдельности. Чаще всего они объединяются в композиционные паттерны — гибкие конструкции, которые элегантно решают конкретную задачу. Давайте разберём три рабочих паттерна, которые вы сможете применить в своих проектах уже сегодня.
Паттерн 1: Фильтрация и преобразование с генераторами
Задача: обработать большой поток данных — отфильтровать и преобразовать. Классический подход с list comprehension создаст промежуточный список, а генератор + лямбда обработают всё на лету:
# Генератор с фильтрацией и преобразованием
data = range(1000000) # Миллион записей
# Обрабатываем без создания промежуточных списков
processed = (
x * 2
for x in data
if x % 3 == 0 # Лямбда-подобное условие прямо в выражении
)
for item in processed:
write_to_db(item)
Генератор экономит память, а встроенное условие if x % 3 == 0 работает как лямбда-фильтр без явного вызова filter(). В промышленном коде я часто использую такой подход для ETL-пайплайнов: читаем из источника → фильтруем → трансформируем → пишем в приёмник — и всё это с константным потреблением памяти.
Паттерн 2: Замыкание + генератор для кэширования
Иногда нужно кэшировать результаты дорогих вычислений. Замыкание хранит кэш, а генератор выдаёт значения по требованию:
def cached_fibonacci():
"""Генератор чисел Фибоначчи с кэшем в замыкании."""
cache = {0: 0, 1: 1}
def fib(n):
if n not in cache:
cache[n] = fib(n - 1) + fib(n - 2)
return cache[n]
n = 0
while True:
yield fib(n)
n += 1
fib_gen = cached_fibonacci()
print(next(fib_gen)) # 0
print(next(fib_gen)) # 1
print(next(fib_gen)) # 1
print(next(fib_gen)) # 2
print(next(fib_gen)) # 3
Здесь замыкание (функция fib) хранит cache в своём окружении, а внешний генератор обеспечивает бесконечную последовательность. Паттерн удобен для ленивых вычислений, когда не нужно заранее знать, сколько чисел Фибоначчи потребуется.
Паттерн 3: Итератор с фильтрацией и контекстом (JS)
В JavaScript генераторы отлично сочетаются с замыканиями для создания настраиваемых итераторов:
function* eventProcessor(events, filterFn) {
// filterFn — это замыкание или лямбда для фильтрации
for (const event of events) {
if (filterFn(event)) {
yield {
...event,
processedAt: Date.now()
};
}
}
}
// Использование с лямбдой
const events = getEventStream();
const filteredProcessor = eventProcessor(
events,
event => event.type === 'ERROR' // Лямбда-фильтр
);
for (const result of filteredProcessor) {
sendAlert(result);
}
Этот паттерн я часто применяю в микросервисной архитектуре: читаем события из очереди, фильтруем по типу, обогащаем метаданными и пробрасываем дальше — и всё это через генератор, который не блокирует event loop.
5. Типовые ошибки и как их избежать
Давайте соберём самые частые грабли, на которые наступают разработчики при работе с лямбдами, замыканиями и генераторами, и способы их обойти.
Ошибка 1: «Лямбда слишком сложная»
Проблема: Лямбда содержит несколько строк, вложенные условия или циклы. Разработчик пытается запихнуть в одну строку логику, которая требует трёх.
Решение: Правило простое: если код не умещается в одно читаемое выражение — используйте обычную именованную функцию. Это не поражение, это зрелость:
# ❌ НЕПРАВИЛЬНО: лямбда с побочным эффектом
process = lambda x: x.upper() if x.isalpha() else (x.strip(), print(x))
# ✅ ПРАВИЛЬНО: честная функция
def process(x):
if x.isalpha():
return x.upper()
else:
print(x)
return x.strip()
Ошибка 2: «Замыкание не работает в цикле»
Проблема: В JavaScript с var замыкание в цикле for не фиксирует значение переменной на момент итерации — оно запоминает ссылку на переменную, которая меняется.
Решение: Используйте let (ES6 и новее) — он создаёт блочный скоуп для каждой итерации:
// ✅ ПРАВИЛЬНО: let создаёт новый i для каждой итерации
for (let i = 0; i console.log(i), 100);
}
// Вывод: 0, 1, 2
Ошибка 3: «Генератор исчерпан»
Проблема: Попытка итерировать генератор во второй раз. После первого прохода генератор пуст, и повторный цикл просто не выполнится — без ошибок, что особенно коварно.
Решение: Явно создавайте новый генератор для каждого прохода. Если данные нужны многократно и размер позволяет — материализуйте в список:
# ❌ НЕПРАВИЛЬНО: переиспользование исчерпанного генератора
gen = (x for x in range(10))
len1 = sum(1 for _ in gen) # 10
len2 = sum(1 for _ in gen) # 0 — генератор пуст!
# ✅ ПРАВИЛЬНО: либо создаём заново, либо материализуем
data = list(x for x in range(10))
len1 = len(data)
len2 = len(data) # Всё работает
Ошибка 4: «Непонимание контекста this в JS»
Проблема: Стрелочная функция (лямбда) внутри метода объекта теряет доступ к this — она использует this родительского контекста, а не объекта, на котором вызван метод.
Решение: Используйте обычную function() для методов объекта или сохраняйте this в переменную self / that:
const user = {
name: 'Алексей',
// ✅ ПРАВИЛЬНО: обычная функция для метода
greet() {
console.log(`Привет, ${this.name}`);
}
};
6. Чек-лист: когда применять каждую конструкцию
| Конструкция | Когда использовать | Когда НЕ использовать |
|---|---|---|
| Лямбда | Однострочные операции, аргументы для map/filter/sort, обработчики событий без контекста | Сложные условия, рекурсия, многоуровневые циклы, методы объектов (в JS) |
| Замыкание | Фабрики функций, приватные переменные, асинхронный код с фиксацией контекста, инкапсуляция состояния | Когда нужен простой доступ к переменной из разных мест без чёткой границы владения |
| Генератор | Большие данные, потоковая обработка, бесконечные последовательности, поэлементная обработка без лишней памяти | Маленькие коллекции, многократный произвольный доступ, когда нужна индексация или сортировка |
7. FAQ: ответы на частые вопросы
Вопрос 1: Лямбда и замыкание — это одно и то же?
Ответ: Нет. Лямбда — это синтаксическая конструкция (функция без имени). Замыкание — это семантическая концепция (функция, которая сохраняет доступ к переменным из внешнего скоупа). Лямбда может быть замыканием, если она захватывает переменные из окружения, но эти термины описывают разные аспекты. Можно создать лямбду без замыкания (lambda x: x * 2 не захватывает внешние переменные) и замыкание без лямбды (именованная функция внутри другой функции).
Вопрос 2: Можно ли использовать генератор в цикле for?
Ответ: Да, и это основной способ работы с генераторами. Цикл for автоматически вызывает next() и обрабатывает StopIteration:
for item in (x for x in range(5)):
print(item)
# Просто работает.
Вопрос 3: Что делать, если генератор нужно использовать несколько раз?
Ответ: Генератор одноразовый по определению. Если данные нужны многократно — либо создавайте новый генератор для каждого прохода, либо материализуйте его в список (коллекцию). Выбор зависит от объёма данных: для сотни элементов — список, для миллионов — генератор и пересоздание при необходимости.
Вопрос 4: В чём разница между yield и return в Python?
Ответ: return завершает функцию и возвращает значение вызывающему коду. Состояние функции теряется. yield приостанавливает функцию, возвращает значение, но сохраняет состояние (все локальные переменные и точку выполнения). При следующем вызове next() выполнение продолжается с того же места. return внутри генератора вызывает StopIteration и завершает его.
Вопрос 5: Почему замыкания в JS иногда вызывают ошибки с this?
Ответ: Стрелочные функции в JS не имеют собственного this — они используют this из лексического окружения, в котором были созданы. Для методов объекта это окружение — глобальный скоуп (или модуль), где this равен undefined в строгом режиме. Решение: использовать обычные функции (function()) для методов или сохранять this в переменную, если нужен доступ в колбэке.
Вопрос 6: Можно ли использовать лямбду для рекурсии?
Ответ: Технически — да, через Y-комбинатор или присвоение лямбды переменной, но на практике — никогда этого не делайте. Рекурсивная лямбда — это нечитаемый код, который сложно отлаживать. Рекурсия = обычная именованная функция, точка. Собеседование вы этим не впечатлите, а коллеги будут плеваться.
Вопрос 7: Как проверить, что генератор исчерпан?
Ответ: В Python можно поймать StopIteration через try/except, но обычно проще создать новый генератор или проверить факт исчерпания через флаг при итерации. В JS генератор возвращает объект { value: undefined, done: true } — проверяйте поле done. Но на практике вы редко сталкиваетесь с этой задачей: в for...of цикл сам обрабатывает завершение.
Вывод: как это применить в реальной работе?
Лямбды, замыкания и генераторы — это не синтаксический сахар и не эзотерика. Это инструменты, которые меняют способ мышления о коде. Они позволяют:
- Сократить код без потери читаемости — меньше шаблонного кода, больше смысла.
- Уменьшить потребление памяти при обработке больших данных — обработка потоком, а не загрузка всего в оперативку.
- Создавать гибкие функции-фабрики и приватные переменные — инкапсуляция без классов, когда классы избыточны.
- Писать более надёжный асинхронный код с сохранением контекста — колбэки, которые помнят свои данные.
Ваш следующий шаг:
- Возьмите любой свой рабочий цикл с фильтрацией и перепишите его через
filter+ лямбду или генераторное выражение. Сравните читаемость. - Создайте функцию-фабрику с замыканием для кэширования результатов — начните с простого кэша для вычислений.
- Найдите в своём проекте место, где вы читаете файл или обрабатываете API с пагинацией, и перепишите его через генератор.
Эти конструкции станут вашим инструментом, когда вы перейдёте от написания кода, который «работает», к написанию кода, который «легко читать, масштабировать и тестировать». Это переход от джуниора, который решает задачу, к разработчику, который думает о сопровождаемости и элегантности решения.
Напоминание: Не бойтесь экспериментировать. Если код стал слишком сложным — вернитесь к обычной функции. Инструменты должны помогать, а не усложнять. Хороший код — это код, который ваши коллеги (и вы сами через полгода) прочитают и поймут за минуту, а не за час.
