Декораторы в 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, которая:
- Принимает функцию в качестве аргумента
- Создает ее расширенную версию
- Возвращает расширенную функцию
Вот как это выглядит в коде:
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)».
