Что такое декораторы в Python

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

Декорирование функции в Python имеет следующий синтаксис:

@guard_zero
def divide(x, y):
    return x / y

Здесь декоратор guard_zero обновляет поведение функции divide(), чтобы исключить деление на 0.

Как использовать декораторы в Python

Использование декораторов лучше всего показать на примере. Сначала создадим функцию, которая делит два числа:

def divide(x, y):
    return x / y

Наша функция позволяет делить на 0, что математически недопустимо. Эту проблему можно решить, добавив проверку с использованием блока if.

Однако есть и другой вариант – декораторы. Используя декоратор, вы не меняете реализацию функции. Вместо этого вы расширяете ее извне. Пока что польза от этого не очевидна. Мы вернемся к этому позже.

Давайте начнем с создания функции-декоратора guard_zero, которая:

  1. Принимает функцию в качестве аргумента
  2. Создает ее расширенную версию
  3. Возвращает расширенную функцию

Вот как это выглядит в коде:

def guard_zero(operate):
    def inner(x, y):
        if y == 0:
            print("Cannot divide by 0.")
            return
        return operate(x, y)
    return inner

В этом коде:

  • Аргумент operate – это функция для расширения
  • Функция inner – это расширенная версия функции operate. Она проверяет второй аргумент на равенство нулю перед вызовом функции operate
  • Наконец, возвращается функция inner. Она является расширенной версией функции operate, переданной в качестве аргумента.

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

divide = guard_zero(divide)

Мы успешно декорировали функцию divide.

Однако, когда речь идет о декораторах, есть более питоничный способ их использования. Вместо того чтобы передавать расширенный объект в качестве аргумента функции-декоратора, вы можете “пометить” функцию декоратором с помощью символа @:

@guard_zero
def divide(x, y):
    return x / y

Это более удобный способ применения декораторов в Python. Кроме того, синтаксически это выглядит красиво, и замысел понятен.

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

print(divide(5, 0))

print(divide(5, 2))

Вывод:

Cannot divide by 0.
None

2.5

(В выводе появляется None, потому что guard_zero возвращает None, когда y равен 0).

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

def guard_zero(operate):
    def inner(x, y):
        if y == 0:
            print("Cannot divide by 0.")
            return
        return operate(x, y)
    return inner

@guard_zero
def divide(x, y):
    return x / y
    
print(divide(5, 0)) # Выводит "Cannot divide by 0"

Теперь вы знаете, как использовать декоратор для расширения функции. Но когда это действительно полезно?

Когда использовать декораторы в Python

Зачем столько хлопот с декоратором? В предыдущем примере можно было создать проверку с помощью if и сэкономить 10 строк кода.

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

Представьте, что в вашем проекте есть множество похожих функций:

def checkUsername(name):
    if type(name) is str:
        print("Correct format.")
    else:
        print("Incorrect format.")
    print("Handling username completed.")

def checkName(name):
    if type(name) is str:
        print("Correct format.")
    else:
        print("Incorrect format.")
    print("Handling name completed.")

def checkLastName(name):
    if type(name) is str:
        print("Correct format.")
    else:
        print("Incorrect format.")
    print("Handling last name completed.")

Как видите, все эти функции содержат один и тот же оператор if-else для проверки ввода. Это вносит много ненужных повторений в код.

Давайте улучшим этот кусок кода, реализовав декоратор валидатора ввода. В этом декораторе мы полностью исключим повторяющиеся проверки if-else:

def string_guard(operate):
    def inner(name):
        if type(name) is str:
            print("Correct format.")
        else:
            print("Incorrect format.")
        operate(name)
    return inner

Этот декоратор:

  • Принимает функцию в качестве аргумента
  • Расширяет ее поведение для проверки, является ли инпут строкой
  • Возвращает расширенную функцию

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

@string_guard
def checkUsername(name):
    print("Handling username completed.")

@string_guard
def checkName(name):
    print("Handling name completed.")

@string_guard
def checkLastName(name):
    print("Handling last name completed.")

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

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

Встроенные декораторы Python

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

Декоратор @property в Python

Декорирование метода в классе с помощью @property позволяет вызывать метод подобно обращению к атрибуту:

weight.pounds() ---> weight.pounds

Давайте посмотрим, как он работает и когда его следует использовать.

Пример использования декоратора @property

Давайте создадим класс Mass, который хранит массу в килограммах и фунтах:

class Mass:
    def __init__(self, kilos):
        self.kilos = kilos
        self.pounds = kilos * 2.205

Вы можете использовать этот класс следующим образом:

mass = Mass(1000)

print(mass.kilos)
print(mass.pounds)

# Вывод:
# 1000
# 2205

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

mass.kilos = 1200
print(mass.pounds)

# Вывод:
# 2205

Изменение количества килограммов не повлияло на количество фунтов. Это произошло потому, что вы не обновили фунты. Конечно, было бы лучше, если бы свойство pounds обновлялось одновременно с kilos.

Чтобы исправить это, вы можете заменить атрибут pounds на метод pounds(). Этот метод вычисляет фунты на основе количества килограммов по требованию.

class Mass:
    def __init__(self, kilos):
        self.kilos = kilos
            
    def pounds(self):
        return self.kilos * 2.205

Теперь вы можете проверить его:

mass = Mass(100)
print(mass.pounds())

mass.kilos = 500
print(mass.pounds())

# Результат:
# 220.5
# 1102.5

Это работает просто замечательно.

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

Вы, конечно, можете пройтись по всему проекту и добавить скобки для каждого вызова mass.pounds.

Но есть и альтернативный вариант.

Используйте декоратор @property для расширения метода pounds(). Это превратит метод в геттер. Это означает, что он будет по-прежнему доступен подобно переменной, несмотря на то, что это метод. Другими словами, вам не нужно будет использовать круглые скобки при его вызове:

class Mass:
    def __init__(self, kilos):
        self.kilos = kilos
        
    @property
    def pounds(self):
        return self.kilos * 2.205

Например:

mass = Mass(100)
print(mass.pounds)

mass.kilos = 500
print(mass.pounds)

Таким образом, использование декоратора @property снижает риск того, что старый код будет аварийно завершен из-за изменений в синтаксисе.

Декоратор @classmethod в Python

Метод класса полезен, когда вам нужен метод, связанный с классом, но не специфичный для экземпляра. Чаще всего методы класса используются в качестве “второго инициализатора”.

Чтобы создать метод класса в Python, декорируйте метод внутри класса при помощи @classmethod.

Метод класса как второй инициализатор в Python

Допустим, у вас есть класс Weight:

class Weight:
    def __init__(self, kilos):
        self.kilos = kilos

Вы создаете экземпляры Weight следующим образом:

w = Weight(100)

Но что, если вы хотите создать вес из фунтов, а не из килограммов? В этом случае необходимо предварительно перевести килограммы в фунты:

pounds = 220.5
kilos = pounds / 2.205

w2 = Weight(kilos)

Но это плохая практика: при частом применении это вносит много ненужных повторений в код.

Что, если бы вы могли создать объект Weight непосредственно из фунтов с применением чего-то вроде weight.from_pounds(220.5)? Для этого можно написать второй инициализатор для класса Weight. Это возможно с помощью декоратора @classmethod:

class Weight:
    def __init__(self, kilos):
        self.kilos = kilos
    
    @classmethod
    def from_pounds(cls, pounds):
        kilos = pounds / 2.205
        return cls(kilos)

Давайте разберем, как работает этот код:

  • @classmethod превращает метод from_pounds() в метод класса. В этом случае он становится “вторым инициализатором”.
  • Первый аргумент cls является обязательным аргументом в методе класса. Он аналогичен self. cls представляет весь класс, а не только его экземпляр.
  • Второй аргумент pounds – это количество фунтов, которым вы инициализируете форму объекта Weight.
  • Внутри метода from_pounds фунты преобразуются в килограммы.
  • Затем последняя строка возвращает новый объект Weight, созданный из фунтов. (cls(kilos) эквивалентен Weight(kilos)).

Теперь можно создать объект Weight непосредственно из количества фунтов:

w = Weight.from_pounds(220.5)
print(w.kilos)

# Вывод:
# 100

Декоратор @staticmethod в Python

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

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

Чтобы создать статический метод в Python, декорируйте метод в классе декоратором @staticmethod. Например, добавим статический метод conversion_info в класс Weight:

class Weight:
    def __init__(self, kilos):
        self.kilos = kilos
    
    @classmethod
    def from_pounds(cls, pounds):
        kilos = pounds / 2.205
        return cls(kilos)
    
    @staticmethod
    def conversion_info():
        print("Kilos are converted to pounds by multiplying by 2.205.")

Вы можете вызвать этот метод непосредственно на классе Weight, без создания объекта Weight для его вызова.

Weight.conversion_info()

# Вывод:
# Kilos are converted to pounds by multiplying by 2.205.

Поскольку метод является статическим, вы также можете использовать его на объекте Weight.

Заключение

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

Например, вы можете реализовать декоратор guard_zero для предотвращения деления на 0. Затем вы можете расширить с его помощью функцию:

@guard_zero
def divide(x, y):
    return x / y

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

В Python есть полезные встроенные декораторы, такие как @property, @classmethod и @staticmethod. Они помогают сделать ваши классы более элегантными. Под капотом эти декораторы расширяют методы, передавая их в функцию декоратора, которая их обновляет, добавляя полезный функционал.

Спасибо за внимание!

Перевод статьи Artturi Jalli “Decorators in Python: A Complete Guide (with Examples)”.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *