Продвинутые конструкции языка: лямбды, замыкания, генераторы

Если вы уже освоили базовый синтаксис, циклы и условия, но код всё ещё кажется «тяжёлым» или избыточно длинным — вы стоите на пороге понимания одной из самых мощных сторон современных языков программирования. Лямбды, замыкания и генераторы — это не просто «красивые фичи» для собеседований. Это инструменты, которые позволяют писать код легче, масштабируемее и тестируемее.

В этой статье мы разберём не только «как это пишется», но и «зачем это нужно», «в каких случаях применять» и «какие ошибки чаще всего совершают новички». Используем примеры на Python и JavaScript — они наиболее популярны в индустрии и отлично демонстрируют концепции. Но принципы универсальны: они работают в Java, C#, Go и других языках с поддержкой функциональных паттернов.

Почему продвинутые конструкции важны для разработчика?

В начале пути разработчик часто пишет код, который решает задачу, но делает это «в лоб». Смотрите сами — типичный пример фильтрации списка:

# Python — наивный подход
result = []
for item in data:
    if item > 10:
        result.append(item * 2)

Такой код работает, но у него есть системные недостатки:

  1. Он занимает много места — четыре строки там, где можно обойтись одной.
  2. Требует явного управления циклом и временной переменной result.
  3. Сложнее читается при добавлении вложенных условий — глаз цепляется за технические детали, а не за смысл операции.

Продвинутые конструкции позволяют переписать это иначе:

# С использованием 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;
};

Где и зачем их использовать?

Лямбды идеально вписываются в три сценария:

  1. Однострочные операции — фильтрация, сортировка, преобразование данных.
  2. Временные обработчики — например, колбэки в GUI или веб-серверах, которые не нужно переиспользовать.
  3. Аргументы для функций высшего порядка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

Что здесь произошло:

  1. outer_function вызвана с x=5.
  2. Внутри создана inner_function, которая ссылается на x.
  3. outer_function возвращает inner_function как объект.
  4. После выхода из outer_function её локальные переменные обычно уничтожаются. Но x не уничтожается — он «замкнут» внутри inner_function и будет жить, пока живёт эта функция.

На практике это работает именно так, как вы подумали: замыкание — это способ дать функции приватную память.

Зачем нужны замыкания?

Замыкания решают четыре класса задач:

  1. Функции-фабрики — создание специализированных функций с заранее заданными параметрами.
  2. Приватные переменные — инкапсуляция данных без классов (актуально для JS до появления #private-полей).
  3. Имитация классов — когда вам нужно состояние и поведение, но классы избыточны.
  4. Асинхронный код с сохранением контекста — колбэки, которые помнят, с какими данными они были созданы.

Пример 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

Где и зачем использовать генераторы?

Генераторы — это правильный выбор в четырёх ситуациях:

  1. Обработка больших данных — чтение многогигабайтных файлов, потоков из API, логов.
  2. Потоковая обработка — когда данные приходят порциями и нужно реагировать на каждую порцию.
  3. Реализация итераторов — создание собственных перебираемых объектов.
  4. Снижение нагрузки на память — когда вы работаете с последовательностью, которая потенциально бесконечна или просто очень велика.

Пример 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 цикл сам обрабатывает завершение.

Вывод: как это применить в реальной работе?

Лямбды, замыкания и генераторы — это не синтаксический сахар и не эзотерика. Это инструменты, которые меняют способ мышления о коде. Они позволяют:

  1. Сократить код без потери читаемости — меньше шаблонного кода, больше смысла.
  2. Уменьшить потребление памяти при обработке больших данных — обработка потоком, а не загрузка всего в оперативку.
  3. Создавать гибкие функции-фабрики и приватные переменные — инкапсуляция без классов, когда классы избыточны.
  4. Писать более надёжный асинхронный код с сохранением контекста — колбэки, которые помнят свои данные.

Ваш следующий шаг:

  1. Возьмите любой свой рабочий цикл с фильтрацией и перепишите его через filter + лямбду или генераторное выражение. Сравните читаемость.
  2. Создайте функцию-фабрику с замыканием для кэширования результатов — начните с простого кэша для вычислений.
  3. Найдите в своём проекте место, где вы читаете файл или обрабатываете API с пагинацией, и перепишите его через генератор.

Эти конструкции станут вашим инструментом, когда вы перейдёте от написания кода, который «работает», к написанию кода, который «легко читать, масштабировать и тестировать». Это переход от джуниора, который решает задачу, к разработчику, который думает о сопровождаемости и элегантности решения.

Напоминание: Не бойтесь экспериментировать. Если код стал слишком сложным — вернитесь к обычной функции. Инструменты должны помогать, а не усложнять. Хороший код — это код, который ваши коллеги (и вы сами через полгода) прочитают и поймут за минуту, а не за час.