Функции в Python — это мощный инструмент для создания читаемого и эффективного кода. Но помимо основ, существует ряд более продвинутых техник, которые позволяют сделать код более гибким и выразительным. В этой статье мы углубимся в несколько важных аспектов:

  1. Замыкания — когда функция "помнит" свои внешние переменные.
  2. Декораторы — для расширения функциональности функций.
  3. Генераторы — для работы с большими данными.

Для этого примера возьмём задачу, которая встречается в реальной разработке: обработка логов с динамической фильтрацией. Мы будем использовать замыкания, декораторы и генераторы, чтобы сделать систему фильтрации и обработки данных гибкой и удобной.

Chatgpt image 5 дек. 2025 г., 17 25 54

Задача: обработка логов с фильтрацией

Представьте, что у нас есть система для анализа логов. Логи содержат информацию о действиях пользователей в системе:

  • user — имя пользователя
  • action — действие
  • timestamp — временная метка
  • status — статус выполнения действия (например, success, failure)

Наша задача — создать систему, которая:

  1. Фильтрует логи по различным критериям (например, по пользователю или статусу).
  2. Позволяет динамически изменять фильтры.
  3. Легко расширяется для добавления новых фильтров.

Для этого примера мы будем использовать три продвинутые техники:

  1. Замыкания, чтобы динамически запоминать параметры фильтрации.
  2. Декораторы, чтобы добавлять функциональность для фильтрации.
  3. Генераторы, чтобы обрабатывать логи эффективно, не загружая память.

Стартовые данные

Вот как могут выглядеть логи:

logs = [
    {"user": "alice", "action": "login", "timestamp": "2025-12-01 10:00:00", "status": "success"},
    {"user": "bob", "action": "login", "timestamp": "2025-12-01 10:05:00", "status": "failure"},
    {"user": "alice", "action": "logout", "timestamp": "2025-12-01 10:10:00", "status": "success"},
    {"user": "charlie", "action": "login", "timestamp": "2025-12-01 10:15:00", "status": "success"},
    {"user": "bob", "action": "logout", "timestamp": "2025-12-01 10:20:00", "status": "failure"},
]

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

Шаг 1: Замыкания для динамической фильтрации

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

def filter_by_user(logs, user):
    def filter_logs():
        for log in logs:
            if log["user"] == user:
                yield log
    return filter_logs

Здесь:

  • filter_by_user — это функция, которая принимает список логов и пользователя.
  • Внутри неё мы создаём другую функцию filter_logs(), которая фильтрует логи по заданному пользователю. Мы используем yield, чтобы вернуть логи по одному, не загружая всё в память.

Теперь можно использовать этот фильтр:

alice_filter = filter_by_user(logs, "alice")
for log in alice_filter():
    print(log)

Результат:

{'user': 'alice', 'action': 'login', 'timestamp': '2025-12-01 10:00:00', 'status': 'success'}
{'user': 'alice', 'action': 'logout', 'timestamp': '2025-12-01 10:10:00', 'status': 'success'}

Что происходит:

  • Мы создали замыкание, которое запоминает, по какому пользователю мы фильтруем.
  • Логика фильтрации вынесена в отдельную функцию, которую можно вызвать несколько раз с разными пользователями.

Шаг 2: Декораторы для расширения функциональности

Теперь давайте добавим функциональность для фильтрации по статусу. Вместо того чтобы писать новый фильтр с нуля, мы используем декоратор, который будет добавлять фильтрацию по статусу к уже существующей фильтрации по пользователю.

Декоратор для фильтрации по статусу

def filter_by_status(filter_func):
    def wrapper(logs, *args, **kwargs):
        status = kwargs.get("status")
        if status:
            logs = [log for log in logs if log["status"] == status]
        return filter_func(logs, *args, **kwargs)
    return wrapper

Этот декоратор будет проверять, если в аргументах передан статус, и если он есть, фильтровать логи по статусу перед тем, как передать их в основную функцию фильтрации.

Декоратор для фильтрации по пользователю

📢 Подписывайтесь на наш Telegram-канал.

Там вы найдете анонсы обучающих статей и видео, готовый код для ваших проектов и увлекательные курсы. Ничего лишнего — только практика, вдохновение и развитие.

👉 https://t.me/codelab_channel

Теперь применим этот декоратор к функции фильтрации по пользователю:

@filter_by_status
def filter_by_user(logs, user):
    for log in logs:
        if log["user"] == user:
            yield log

Теперь при вызове фильтра можно указать, что мы хотим фильтровать по статусу:

alice_filter = filter_by_user(logs, "alice", status="success")
for log in alice_filter():
    print(log)

Результат:

{'user': 'alice', 'action': 'login', 'timestamp': '2025-12-01 10:00:00', 'status': 'success'}
{'user': 'alice', 'action': 'logout', 'timestamp': '2025-12-01 10:10:00', 'status': 'success'}

Теперь функция фильтрации по пользователю расширена возможностью фильтровать по статусу с помощью декоратора.

Декораторы — это синтаксический сахар для применения функций к другим функциям. Они позволяют расширять поведение функций, не изменяя их исходный код.

Шаг 3: Генераторы для обработки больших объёмов данных

Допустим, логи могут быть очень большими, и мы не можем держать их все в памяти. Для этого используем генераторы.

Мы уже использовали yield в функции filter_logs() для того, чтобы вернуть логи по одному, не занимая много памяти. Теперь давайте расширим это и используем генератор для обработки большого потока данных.

Генератор для обработки логов

def log_generator(logs):
    for log in logs:
        yield log

Теперь, чтобы фильтровать логи, мы можем просто обрабатывать их в цикле, не загружая весь список в память:

def process_logs(logs):
    for log in log_generator(logs):
        if log["status"] == "success":
            print(log)

Здесь log_generator будет поочередно возвращать элементы, и мы будем их обрабатывать по одному. Это особенно полезно, когда данные очень большие, и вы не хотите загружать их все в память сразу.

Итоговый код

Теперь у нас есть система фильтрации логов с использованием замыканий, декораторов и генераторов:

# Пример данных
logs = [
    {"user": "alice", "action": "login", "timestamp": "2025-12-01 10:00:00", "status": "success"},
    {"user": "bob", "action": "login", "timestamp": "2025-12-01 10:05:00", "status": "failure"},
    {"user": "alice", "action": "logout", "timestamp": "2025-12-01 10:10:00", "status": "success"},
    {"user": "charlie", "action": "login", "timestamp": "2025-12-01 10:15:00", "status": "success"},
    {"user": "bob", "action": "logout", "timestamp": "2025-12-01 10:20:00", "status": "failure"},
]

# Замыкание для фильтрации по пользователю
def filter_by_user(logs, user):
    def filter_logs():
        for log in logs:
            if log["user"] == user:
                yield log
    return filter_logs

# Декоратор для фильтрации по статусу
def filter_by_status(filter_func):
    def wrapper(logs, *args, **kwargs):
        status = kwargs.get("status")
        if status:
            logs = [log for log in logs if log["status"] == status]
        return filter_func(logs, *args, **kwargs)
    return wrapper

# Применяем декоратор
@filter_by_status
def filter_by_user(logs, user):
    for log in logs:
        if log["user"] == user:
            yield log

# Генератор для логов
def log_generator(logs):
    for log in logs:
        yield log

# Пример использования
alice_filter = filter_by_user(logs, "alice", status="success")
for log in alice_filter():
    print(log)

В примере выше есть конфликт имён функций — декоратор переопределяет функцию filter_by_user. В реальном коде стоит использовать разные имена.

Заключение

Мы разобрали три мощные концепции, которые делают работу с функциями ещё более удобной и гибкой:

  1. Замыкания: позволяют функции "помнить" параметры из внешней области видимости, создавая гибкие и динамичные фильтры.
  2. Декораторы: позволяют расширять функции, не меняя их исходного кода. Мы добавили фильтрацию по статусу.
  3. Генераторы: помогают работать с большими объёмами данных, обрабатывая их по одному элементу за раз, не загружая всё в память.

С помощью этих техник мы создали систему, которая легко фильтрует логи, расширяется новыми фильтрами и работает с большими данными без перегрузки памяти.

Практика на том же примере

  1. Добавьте новый фильтр по action (например, только login или logout).
  2. Создайте декоратор для проверки правильности формата временной метки.
  3. Добавьте новую фильтрацию, чтобы выводить только логи с определённым пользователем и статусом за определённый день (например, только для 2025-12-01).

Эти задачи помогут вам ещё глубже понять, как работает фильтрация и как использовать функции для обработки данных в реальных приложениях.