📖 Теория: повторение уроков 64–66

⚡ Кратко

Декораторы — это функции, которые оборачивают другие функции. Обёртка должна уметь принимать любые аргументы, вызывать оригинал и возвращать его результат. @functools.wraps сохраняет метаданные, а фабрики позволяют передавать параметры.

В ООП класс выступает шаблоном, а объект — конкретным экземпляром. Метод __init__ инициализирует поля через self. Методы класса всегда принимают self первым параметром.

  • Декораторы применяются снизу вверх.
  • Без @functools.wraps теряются __name__ и __doc__.
  • self — ссылка на текущий объект, через неё сохраняются поля.

Декораторы

Декоратор — это функция, которая принимает другую функцию, добавляет поведение и возвращает новую функцию. Внутри используется вложенная функция-обёртка, поэтому декораторы строятся на замыканиях.

Где применяются декораторы

  • Измерение времени выполнения.
  • Логирование вызовов функций.
  • Ограничение частоты вызовов (throttling).
  • Проверка и валидация входных данных.
  • Ограничение доступа.
  • Автоматическое повторение выполнения при ошибке (retry).
  • Кеширование результатов.
  • Автоматическое изменение данных.

Аргументы декорируемой функции

Чтобы декоратор работал с функциями, принимающими любые аргументы, обёртка использует *args и **kwargs и передаёт их в оригинал.

import logging

logging.basicConfig(
    filename="functions.log",
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s",
    encoding="utf-8"
)

def log_decorator(func):
    def wrapper(*args, **kwargs):
        logging.info(f"Функция {func.__name__} вызвана с аргументами: {args}, {kwargs}")
        result = func(*args, **kwargs)
        logging.info(f"Функция {func.__name__} вернула: {result}")
        return result
    return wrapper


@log_decorator
def add(a, b):
    return a + b


@log_decorator
def say_hello():
    print("Привет!")


print(add(3, 5))
say_hello()

Возвращаемое значение

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

def upper_decorator(func):
    def wrapper(*args, **kwargs):
        print("Выполняем функцию, но ничего не возвращаем")
        func(*args, **kwargs).upper()
    return wrapper


@upper_decorator
def get_text():
    return "hello"


result = get_text()
print("Результат:", result)  # None

И правильный вариант с возвратом:

def upper_decorator(func):
    def wrapper(*args, **kwargs):
        print("Выполняем функцию и возвращаем результат")
        return func(*args, **kwargs).upper()
    return wrapper


@upper_decorator
def get_text():
    return "hello"


result = get_text()
print("Результат:", result)  # HELLO

Сохранение метаданных через functools.wraps

При создании декоратора оригинальная функция теряет своё имя и документацию, потому что заменяется вложенной функцией wrapper. Декоратор functools.wraps помогает сохранить метаданные.

def simple_decorator(func):
    def wrapper(*args, **kwargs):
        """Вложенная функция wrapper"""
        print("\nДекорированная функция: ")
        print(f"Оригинальное имя функции: {func.__name__}")
        print(f"Оригинальная документация: {func.__doc__}")
        return func(*args, **kwargs)
    return wrapper


@simple_decorator
def example_function():
    """Это оригинальная функция."""
    print("Привет!")


print("Имя декорированной функции:", example_function.__name__)
print("Документация декорированной функции:", example_function.__doc__)
example_function()

Решение с functools.wraps:

import functools


def simple_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("\nДекорированная функция")
        return func(*args, **kwargs)
    return wrapper


@simple_decorator
def example_function():
    """Это оригинальная функция."""
    print("Привет!")


print("Имя декорированной функции:", example_function.__name__)
print("Документация декорированной функции:", example_function.__doc__)
example_function()

Декоратор-фабрика

Чтобы декоратор принимал аргументы, создаётся функция-декоратор-фабрика, которая возвращает сам декоратор.

def message_decorator(message):
    def decorator(func):
        def wrapper():
            print(message)
            return func()
        return wrapper
    return decorator


@message_decorator("Начинаем выполнение")
def analise_data():
    print("Данные проанализированы")


@message_decorator("Загрузка данных...")
def load_data():
    print("Данные загружены")


analise_data()
print()
load_data()

Пример retry с параметром attempts:

import time


def retry(attempts):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"Попытка {i+1} не удалась: {e}")
                    time.sleep(5)
            print("Все попытки исчерпаны.")
        return wrapper
    return decorator


@retry(3)
def get_data(filename):
    """Читает данные из файла"""
    with open(filename, "r", encoding="utf-8") as file:
        return file.read()


# Для запуска создайте рядом файл data.txt
data = get_data("data.txt")
if data:
    print("Содержимое файла:")
    print(data)

Несколько декораторов

Декораторы выполняются снизу вверх. Важно учитывать порядок, так как один декоратор может изменить результат перед следующим.

def border_decorator(func):
    def wrapper():
        print("*" * 100)
        func()
        print("*" * 100)
    return wrapper


def repeat_decorator(func):
    def wrapper():
        for _ in range(3):
            func()
    return wrapper


@border_decorator
@repeat_decorator
def print_line():
    print("-" * 100)


print_line()

Основы ООП

Объектно-ориентированное программирование (ООП) — это подход, в котором основное внимание уделяется объектам и их взаимодействию. ООП описывает сущности реального мира в виде объектов, объединяющих данные (поля) и поведение (методы).

Класс и объект

  • Класс — шаблон (чертёж), по которому создаются объекты. Описывает, какие данные будут у объектов и какие методы можно выполнять.
  • Объект — конкретный экземпляр класса. У него есть своё состояние и возможность выполнять действия.
class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author


book1 = Book("1984", "George Orwell")
book2 = Book("Brave New World", "Aldous Huxley")
print(book1.title, book1.author)
print(book2.title, book2.author)

ООП vs функциональный подход

Объектно-ориентированный подходФункциональный подход
Основан на объектах и классахОснован на функциях как строительных блоках
Объекты хранят состояние и имеют поведениеДанные и поведение разделены
Программа — система взаимодействующих объектовЧасто используются чистые функции
Удобен для моделирования реальных сущностейХорош для математических расчётов и анализа данных
Хорошо подходит для сложных расширяемых программХорошо подходит для обработки потоков информации

В Python можно использовать оба подхода, они часто дополняют друг друга.

Преимущества ООП

  • Упрощает моделирование реальных сущностей.
  • Позволяет структурировать сложные программы.
  • Облегчает расширение и сопровождение.
  • Повышает переиспользуемость и читаемость кода.

Создание класса и правила имён

# Пустой класс без полей и методов
class ClassName:
    pass
  • Имя класса пишется с заглавной буквы.
  • Несколько слов — CamelCase.
  • Не стоит использовать имена встроенных типов: list, str, object и т.д.

Магический метод __init__

__init__ — это инициализатор, который автоматически вызывается при создании объекта и устанавливает его начальное состояние.

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

Важно: конструктором объекта считается __new__, который создаёт объект, а __init__ инициализирует уже созданный объект.

Особенности __init__

  • Вызывается автоматически при создании объекта.
  • Называется строго __init__.
  • Обязан принимать первым аргументом self.
  • Через self задаются поля объекта.
  • Может отсутствовать, если класс не имеет состояния.

Поля, методы и атрибуты

  • Поля (свойства) — переменные, хранящие состояние объекта. Задаются через self в __init__.
  • Методы — функции внутри класса, описывающие поведение объекта.
  • Атрибут — общее название любой части объекта, доступной через точку: object.something.

Параметр self

self — обязательный первый параметр методов класса, ссылка на текущий объект. self не является зарезервированным словом, но принято использовать именно его.

class Book:
    def __init__(self, book_title, book_author):
        self.title = book_title
        self.author = book_author

    def get_description(self):
        return f"{self.title} — {self.author}"

    def show_info(self):
        print(self.get_description())

    def make_author_upper(self):
        self.author = self.author.upper()


book = Book("1984", "George Orwell")
book.show_info()
book.make_author_upper()
book.show_info()

Без self переменные title и author были бы локальными переменными метода и исчезли бы после его завершения.

Создание объекта и доступ к полям

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author


book = Book("1984", "George Orwell")
print(book.title)
print(book.author)

book.title = "Animal Farm"
print(book.title)

Каждый объект класса — независимый экземпляр. Изменение в одном объекте не влияет на другие.

Проверить по документации: подробнее о дескрипторах, слотах и современных подходах к классам — в разделе Data model.