Декораторы в 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)”.