Главная / Блог / Декораторы в Python

Декораторы в Python:
понятие, структура,
примеры использования

Декораторы в Python: понятие, структура, примеры использования

Smartiqa Article docker
  • Дата: 29 июня 2022
  • Автор: Михаил Макарик
Декораторы - один из паттернов программирования, очень активно применяемый в языке Python. Они позволяют динамически менять, расширять, дополнять логику и поведение функций, классов, методов. Такая задача в практике Python возникает чуть ли не ежедневно, а использование декораторов - удобно и наглядно. Не нужно менять исходный код, чтобы добавить новый функционал. Рассмотрим синтаксис, виды декораторов, случаи применения.

1. Области видимости и замыкания в Питоне

Прежде, чем перейти непосредственно к декораторам, важно понять специфику функций в языке Python, области видимости и замыкания.

Когда исполняется код, объявленные в нем переменные и объекты могут иметь сложную внутреннюю структуру. Питон понимает, как их обнаруживать, в каком порядке вызывать и возвращать. Существенный момент здесь - области видимости.

Например, вы в своем скрипте объявили переменную number, а где-то в другом месте этого файла вызывается функция, внутри которой есть своя переменная с таким же именем. Более того, никто не даст гарантию, что импортируемый модуль не содержит в себе аналогичное наименование. Такие ситуации не столь редки, как может показаться, следовательно, требуется ознакомиться с областями видимости в Python.

Всего их имеется 3 (от высшей к низшей):
- встроенная,
- на уровне модуля,
- локальная.

  1. Встроенная (built-in) область видимости отвечает за переменные и функции на уровне интерпретатора. В частности, в любом Python-проекте мы всегда можем обратиться к True, print, map и т.д. Эти объекты мы лично не объявляли, ниоткуда не импортировали, но спокойно используем. А все потому, что есть встроенная область видимости, которая уже «подтянула» данные сущности.
  2. Видимость на уровне модуля (или глобальная) связана с конкретным вызываемым скриптом. Здесь хранятся объекты, которые представлены в файле: переменные, функции, классы. После их объявления они готовы к использованию в программе.
  3. Локальная видимость представлена конкретными функциями, блоками кода. Их внутренние переменные не могут вызываться в других местах: произойдет ошибка.

В случае конфликтов объектов из разных областей видимости, Питон всегда отдает предпочтение более низким уровням, и по цепочке обращается выше, если переменная или функция не найдены в текущей области. Приведем примеры, чтобы понять представленные определения.
Пример – IDE

square_side = 5


def square_area(side):
    print(square_side)
    print(side ** 2)


square_area(4)
Результат выполнения

5
16
Итак, мы написали функцию для вычисления площади квадрата. Посмотрим, с какими областями видимости в ней пришлось столкнуться.
Во-первых, вызывается встроенная функция print(): нами она нигде не объявлялась, значит, берется из встроенной области видимости.
Во-вторых, переменная square_side не задана внутри функции square_area(), но ошибки нет. Если ее Питон не обнаружил локально, то идет на ступень выше - к глобальной области видимости.
Здесь она есть, а потому спокойно возвращается.
В-третьих, side - локальная переменная. Она доступна только внутри функции, но не за ее пределами.
Попробуем немного «поломать» код и посмотреть, с какими проблемами можем столкнуться.
Пример – IDE

square_side = 5


def square_area(side):
    print(square_side)
    print(side ** 2)
    square_side = 70


square_area(4)
Результат выполнения

UnboundLocalError: local variable 'square_side' referenced before assignment

Что-то пошло не по сценарию! Так как мы объявили другую переменную square_side внутри функции, то Python, в первую очередь, будет брать локальное значение. Но, допущена ошибка: мы сначала вызываем параметр, а лишь потом его объявляем. Если поместить переменную до «принта», то никаких ошибок не случится: вернется значение 70.

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

Еще один момент, требующий ознакомления перед изучением декораторов: замыкания (т.к. декораторы ими и являются, но не все замыкания - это декораторы).
Замыкание (closure) - объект, который включает в себя блок кода (например, функцию) и дополнительные переменные за его пределами.
Фактически, запоминается и используется переменная, которая не является частью локальной области видимости. Для использования замыканий важно соблюдение условий:
  • у нас должна быть дочерняя функция (внутри другой функции),
  • дочерняя функция должна обращаться к переменной, объявленной в вышестоящей функции,
  • функция-родитель возвращает наследуемую функцию.

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

Перейдем к коду, чтобы всё стало понятнее.
Пример – IDE

def closure_example():
    x = 11

    def inner():
        print(f'Переменная из замыкания: {x}')

    return inner


closure_example()()

Результат выполнения

Переменная из замыкания: 11

Итак, внутри функции closure_example() объявлена переменная х, которая затем вызывается в дочерней. Но ошибки нет! Мы возвращаем замыкание, которое запоминает значение х.

2. Декоратор: определение и схема объявления

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

Если коротко, то декоратор - это обёртка над функцией (или другим объектом), которая изменяет ее поведение. Это удобно, т.к. не нужно менять исходный код, который не обязательно будет вашим.

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

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

Напишем первый, самый простой декоратор для функции print_hi() и посмотрим на его структуру, а также варианты объявления.
Пример – IDE

def simple_decorator(func):
    def inner():
        print('Начало работы декоратора...')
        func()
        print('Декоратор отработал!')
    return inner


def print_hi():
    print('Привет, я - функция, которую задекорировали!')


print_hi = simple_decorator(print_hi)
print_hi()

Результат выполнения

Начало работы декоратора...
Привет, я - функция, которую задекорировали!
Декоратор отработал!
Так, функция print_hi() просто печатает в консоль некое сообщение. Созданный декоратор сообщает о том, что декорируемая функция была подвергнута модификациям (ведь теперь выводится еще 2 дополнительных сообщения).

Чтобы задекорировать объект, нужно передать его внутрь декоратора, а затем вызвать. В нашем случае, мы присвоили задекорированной функции то же имя (print_hi), потому что так принято в Питоне, хотя никто не запрещает давать любое другое имя. Такое применение декорирования не самое удобное, поэтому был придуман синтаксический сахар: @.

Осуществим ту же процедуру, что и выше, но уже более питонически.
Пример – IDE

def simple_decorator(func):
    def inner():
        print('Начало работы декоратора...')
        func()
        print('Декоратор отработал!')
    return inner


@simple_decorator
def print_hi():
    print('Привет, я - функция, которую задекорировали!')


print_hi()
Результат выполнения

Начало работы декоратора...
Привет, я - функция, которую задекорировали!
Декоратор отработал!
Код стал короче и более красивым.

3. Продвинутые декораторы: функции с аргументами, декораторы с параметрами, классы-декораторы

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

3.1. Декорирование функции с аргументами

Когда мы декорировали функцию print_hi(), то все выглядело очень просто, т.к. у нее не было параметров. А если они будут? Да и у каждой функции есть свой перечень аргументов (как по наименованию, так и по количеству). На помощь приходят args, kwargs.
Пример – IDE

def decorate_func_with_params(func):

    def inner(*args, **kwargs):
        print(f'Декорируем функцию с параметрами: {args}, {kwargs}')
        func(*args, **kwargs)
        print('Все прошло успешно!')
    return inner


@decorate_func_with_params
def adder(*nums):
    print(sum(nums))


# Тесты
adder(1)
adder(2, 7, 3)
adder(0, 33, 4, 10, 0)
Результат выполнения

Декорируем функцию с параметрами: (1,), {}
1
Все прошло успешно!
Декорируем функцию с параметрами: (2, 7, 3), {}
12
Все прошло успешно!
Декорируем функцию с параметрами: (0, 33, 4, 10, 0), {}
47
Все прошло успешно!

3.2. Декораторы с параметрами

В ряде случаев может потребоваться передать аргументы в сам декоратор, чтобы изменить его поведение. Для осуществления этой задумки обертку усложняют еще одним уровнем вложенности: во внешнюю передается декорируемая функция, а во внутренней происходит вызов этой функции, а также работа с аргументами декоратора. Для наглядности создадим декоратор, который в качестве параметра будет принимать количество вызовов декорируемой функции.
Пример – IDE

def repeater(num_of_repeats=1):
    def outer_decorator(func):
        def inner_decorator(*args, **kwargs):
            if num_of_repeats > 0:
                for _ in range(num_of_repeats):
                    print(func(*args, **kwargs))
            else:
                print(func(*args, **kwargs))
        return inner_decorator
    return outer_decorator


@repeater(3)
def print_text(message):
    return f'Вам сообщение: {message}'


print_text('Просыпайся!')
Результат выполнения

Вам сообщение: Просыпайся!
Вам сообщение: Просыпайся!
Вам сообщение: Просыпайся!

3.3. Класс как декоратор

В качестве декоратора не запрещено использовать классы. При помощи метода __call__() экземпляры классов можно делать вызываемыми, что позволяет декорировать функции.

Приведем пример класса-счетчика, который будет считать количество раз, которое вызывалась декорируемая функция.
Пример – IDE

class Numerator:
    def __init__(self, func):
        self.func = func
        self.counts = 0

    def __call__(self, *args, **kwargs):
        self.counts += 1
        print(self.counts)
        return self.func(*args, **kwargs)


@Numerator
def info_func(*args, **kwargs):
    return args, kwargs

# Тесты
print(info_func(2, 3, p=100))
print(info_func(q=10))
print(info_func())
Результат выполнения

1
((2, 3), {'p': 100})
2
((), {'q': 10})
3
((), {})

Что мы сделали: info_func() является экземпляром класса Numerator, поэтому счетчик при каждом новом вызове будет увеличиваться.

4. Вопросы и задачи

  1. Сколько способов объявления декораторов имеется в Питоне?
  2. Приведите несколько примеров встроенных в Python декораторов, опишите их функционал.
  3. Напишите декоратор, который будет вычислять время работы функции в миллисекундах, секундах или минутах.

5. Ответы

5.1.

Python предполагает 2 способа объявления декораторов:
- напрямую (например, function = decorator(function)),
- через синтаксический сахар (@decorator).
Первый способ удобнее в том случае, когда требуется переопределить имя, а второй - более наглядный и понятный.

5.2.

В стандартную библиотеку Питона встроено немало декораторов. Вот несколько примеров:
  • lru_cache (из модуля functools) - позволяет кешировать результат выполнения функции, чтобы при новом ее вызове не пересчитывать выражения (актуально для рекурсивных вызовов, когда кеширование способно на порядки ускорять вычисления). Можно передать параметры: максимальное количество хранимых в памяти элементов; деление по типам (отдельно будут храниться, например, числа и строки),
  • @staticmethod - декоратор для методов класса, превращающий их, по факту, в обычные функции, но расположенные внутри класса. Применяется в случаях, когда некая вспомогательная функция используется внутри класса и связана с ним, но отдельно ее выносить не хочется,
  • dataclass (из модуля dataclass) - упрощает создание классов, автоматически создает метод __init__() с созданными в них переменными. Приведем простой пример для лучшего понимания.
Пример – IDE

from dataclasses import dataclass


@dataclass
class Person:
    name: str
    age: int

    def person_description(self):
        return f'Меня зовут {self.name}, мне {self.age}.'


ivan = Person('Иван', 31)
print(ivan.person_description())

Результат выполнения

Меня зовут Иван, мне 31.

5.3.

Создадим функцию для декорирования cube_me() (чтобы ее время работы было разным, воспользуемся итерированием), которая будет вычислять кубы чисел, вплоть до заданного. Т.к. декоратор может возвращать время работы в разных единицах времени, он обзаведется параметром.
Решение – IDE

from time import perf_counter


def timer(unit='s'):
    time_mapping = {
        's': 1,
        'ms': 1000,
        'm': 1 / 60,
    }

    def outer(func):
        def inner(*args, **kwargs):
            start_time = perf_counter()
            func(*args, **kwargs)
            end_time = perf_counter()
            total_time = round(
                (end_time - start_time) * time_mapping.get(unit, 's'),
                2
            )
            print(total_time)
        return inner
    return outer


@timer('m')
def cube_me(max_number):
    for number in range(max_number + 1):
        number ** 3


cube_me(10_000_000)

Результат выполнения

0.05

29 ИЮНЯ / 2022
Как вам материал?

Читайте также