Примеры использования декораторов в Python
Полезные функции и методы
Начну с полезных функций и методов, которые будут использованы дальше, в примерах декораторов.
func.__name__
- получить имя функции с помощью специальной переменной__name__
. Аналогично можно узнать имя класса -cls.__name__
-
getattr(cls, 'attribute_name', None)
- проверить наличие атрибута (метод, переменная класса или экземпляра класса). Возвращает атрибут(объект).None
в конце игнорирует ошибки, если атрибута нет.Например, проверим, есть ли у класса
cls
методdispatch
:dispatch = getattr(cls, 'dispatch', None)
-
hasattr(some_object, "__iter__")
- проверить, есть ли у объекта атрибут (переменная или метод):In [15]: hasattr(some_object, "__iter__") False
getattr
возвращает сам объект, аhasattr
возвращаетTrue
илиFalse
. -
setattr(cls, 'attribute_name', attribute)
- присвоить атрибут классуcls
. Еслиattribute
- функция, то так можно присвоить метод классуcls
. А так можно декорировать этот метод -setattr(cls, 'attribute_name', verbose(attribute))
, гдеverbose
- декоратор.Функция - это объект. С помощью замыкания и
setattr
можно добавить атрибуты:def add_mark(**kwargs): def decorator(func): for key, value in kwargs.items(): setattr(func, key, value) return func return decorator @add_mark(test=True, ordered=True) def test_function(a, b): return a + b In [73]: test_function.ordered Out[73]: True In [74]: test_function.test Out[74]: True
-
hasattr(object, '__call__')
илиcallable(object)
- проверить, является ли объект вызываемым (например, методом класса). Еще можно сравнить тип объекта с уже существующим -isinstance(object, type(self.__init__))
callable_attributes = {k:v for k, v in cls.__dict__.items() if callable(v)}
- получить словарь с вызываемыми объектами класса cls с помощью генератора словаря -
IPAddress.__dict__.items()
- получить атрибуты классаIPAddress
In [14]: IPAddress.__dict__.items() Out[14]: dict_items([('__module__', '__main__'), ('__init__', <function IPAddress.__init__ at 0x7f04c76abdc0>), ('__repr__', <function IPAddress.__repr__ at 0x7f04c76ab670>), ('__eq__', <function IPAddress.__eq__ at 0x7f04c76e1f70>), ('__lt__', <function IPAddress.__lt__ at 0x7f04c76e15e0>), ('__dict__', <attribute '__dict__' of 'IPAddress' objects>), ('__weakref__', <attribute '__weakref__' of 'IPAddress' objects>), ('__doc__', None), ('__hash__', None), ('__getattribute__', <function decorator.<locals>._newgetattr at 0x7f04c76ab820>)])
или тоже самое другим способом:
In [16]: vars(IPAddress).items() Out[16]: dict_items([('__module__', '__main__'), ('__init__', <function IPAddress.__init__ at 0x7f04c76abdc0>), ('__repr__', <function IPAddress.__repr__ at 0x7f04c76ab670>), ('__eq__', <function IPAddress.__eq__ at 0x7f04c76e1f70>), ('__lt__', <function IPAddress.__lt__ at 0x7f04c76e15e0>), ('__dict__', <attribute '__dict__' of 'IPAddress' objects>), ('__weakref__', <attribute '__weakref__' of 'IPAddress' objects>), ('__doc__', None), ('__hash__', None), ('__getattribute__', <function decorator.<locals>._newgetattr at 0x7f04c76ab820>)])
Справедливо и для экземпляров класса:
vars(object).items()
А так можно получить атрибуты класса, зная только экземпляр:
In [15]: vars(obj.__class__) Out[15]: mappingproxy({'__module__': '__main__', '__init__': <function __main__.IPAddress.__init__(self, ip)>, '__repr__': <function __main__.IPAddress.__repr__(self)>, '__eq__': <function __main__.IPAddress.__eq__(self, other)>, '__lt__': <function __main__.IPAddress.__lt__(self, other)>, '__dict__': <attribute '__dict__' of 'IPAddress' objects>, '__weakref__': <attribute '__weakref__' of 'IPAddress' objects>, '__doc__': None, '__hash__': None, '__gt__': <function functools._gt_from_lt(self, other, NotImplemented=NotImplemented)>, '__le__': <function functools._le_from_lt(self, other, NotImplemented=NotImplemented)>, '__ge__': <function functools._ge_from_lt(self, other, NotImplemented=NotImplemented)>})
Пример:
In [17]: import ipaddress ...: from functools import total_ordering ...: ...: ...: def total_order(cls): ...: return total_ordering(cls) ...: ...: ...: @total_order ...: class IPAddress: ...: def __init__(self, ip): ...: self._ip = int(ipaddress.ip_address(ip)) ...: ...: def __repr__(self): ...: return f"IPAddress('{self._ip}')" ...: ...: def __eq__(self, other): ...: return self._ip == other._ip ...: ...: def __lt__(self, other): ...: return self._ip < other._ip ...: In [18]: obj = IPAddress("10.1.1.1") In [19]: obj.__dict__ Out[19]: {'_ip': 167837953} In [20]: obj.__class__.__dict__ Out[20]: mappingproxy({'__module__': '__main__', '__init__': <function __main__.IPAddress.__init__(self, ip)>, '__repr__': <function __main__.IPAddress.__repr__(self)>, '__eq__': <function __main__.IPAddress.__eq__(self, other)>, '__lt__': <function __main__.IPAddress.__lt__(self, other)>, '__dict__': <attribute '__dict__' of 'IPAddress' objects>, '__weakref__': <attribute '__weakref__' of 'IPAddress' objects>, '__doc__': None, '__hash__': None, '__gt__': <function functools._gt_from_lt(self, other, NotImplemented=NotImplemented)>, '__le__': <function functools._le_from_lt(self, other, NotImplemented=NotImplemented)>, '__ge__': <function functools._ge_from_lt(self, other, NotImplemented=NotImplemented)>}) In [21]: vars(obj) Out[21]: {'_ip': 167837953} In [22]: vars(obj.__class__) Out[22]: mappingproxy({'__module__': '__main__', '__init__': <function __main__.IPAddress.__init__(self, ip)>, '__repr__': <function __main__.IPAddress.__repr__(self)>, '__eq__': <function __main__.IPAddress.__eq__(self, other)>, '__lt__': <function __main__.IPAddress.__lt__(self, other)>, '__dict__': <attribute '__dict__' of 'IPAddress' objects>, '__weakref__': <attribute '__weakref__' of 'IPAddress' objects>, '__doc__': None, '__hash__': None, '__gt__': <function functools._gt_from_lt(self, other, NotImplemented=NotImplemented)>, '__le__': <function functools._le_from_lt(self, other, NotImplemented=NotImplemented)>, '__ge__': <function functools._ge_from_lt(self, other, NotImplemented=NotImplemented)>})
-
frozenset
- получитьkey
,value
из словаря. Например:In [27]: d = {'a': 123, ...: 'b': 1234, ...: } In [30]: frozenset(d.items()) Out[30]: frozenset({('a', 123), ('b', 1234)}) In [33]: k,v = frozenset(d.items()) In [34]: k Out[34]: ('a', 123) In [35]: v Out[35]: ('b', 1234)
Примеры декораторов
Пример 1
Если необходимо обработать аргументы целевой функции (в примере ниже - это upper
) декоратором (это - verbose
), следует использовать конструкцию args, kwargs
во внутренней функции декоратора(это - wrapper
). Например:
def verbose(func):
def wrapper(*args, **kwargs):
print(f'Вызываю функцию {func.__name__}')
return func(*args, **kwargs)
return wrapper
@verbose
def upper(string):
return string.upper()
upper('line')
Вызываю функцию upper
'LINE'
Если аргументов целевой функции недостаточно, используем декоратор с аргументами(в примере ниже - это restrict_args_type
). Тогда в замыкание добавляется еще один уровень вложенности. Например:
def restrict_args_type(required_type):
def decorator(func):
@wraps(func)
def wrapper(*args):
if not all(isinstance(arg, required_type) for arg in args):
raise ValueError(f'Все аргументы должны быть {required_type.__name__}')
return func(*args)
return wrapper
return decorator
@restrict_args_type(str)
def to_upper(*args):
result = [s.upper() for s in args]
return result
to_upper('a', 'a')
['A', 'A']
Для работы с аргументами декоратора есть удобный декоратор из модуля functools. Можно делать так:
from functools import partial
def info(func, arg1, arg2):
print('Decorator arg1 = ' + str(arg1))
print('Decorator arg2 = ' + str(arg2))
def wrapper(*args, **kwargs):
print('Function {} args: {} kwargs: {}'.format(function.__name__, str(args), str(kwargs)))
return function(*args, **kwargs)
return wrapper
decorator_with_arguments = partial(info, arg1=3, arg2='Py')
@decorator_with_arguments
def doubler(number):
return number * 2
print(doubler(5))
Пример 2
Если не нужно обрабатывать аргументы целевой функции (в примере ниже - upper
), можно обойтись декоратором без внутренней функции.
Например, очевидное-невероятное, бесполезный декоратор, не делающий ничего(или, например, возвращающий имя функции с помощью func.__name__
):
In [1]: def decorator(func):
...: return func
...:
In [2]: @decorator
...: def upper(string):
...: return string.upper()
...:
In [3]: upper('line')
Out[3]: 'LINE'
Для примера выше можно использовать, хотя бы, декоратор с аргументами, чтобы как-то применить их в нашей целевой функции (upper
). Например:
def add_mark(**kwargs):
def decorator(func):
for key, value in kwargs.items():
setattr(func, key, value)
return func
return decorator
@add_mark(test=True, ordered=True)
def test_function(a, b):
return a + b
In [73]: test_function.ordered
Out[73]: True
In [74]: test_function.test
Out[74]: True
Или такой пример (декоратор не влияет на аргументы целевой функции, но аргумент декоратора формирует словарь):
url_function_map = {}
def register(route):
def decorator(func):
url_function_map[route] = func
return func
return decorator
@register('/')
def func(a,b):
return a+b
@register('/scripts')
def func2(a,b):
return a+b
In [3]: url_function_map
Out[3]: {'/': <function __main__.func>, '/scripts': <function __main__.func2>}
Пример 3
Внутренняя функция декоратора не обязана возвращать func(*args, **kwargs)
. Такая запись вызывает функцию.
Иногда необходимо и достаточно вызвать функцию внутри самого декоратора и вернуть полученный результат. Например:
from netmiko import (ConnectHandler, NetMikoAuthenticationException,
NetMikoTimeoutException)
def retry(times):
def decorator(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
if not result:
for i in range(times):
result = func(*args, **kwargs)
if result:
return result
return result
# если вернуть func(*args, **kwargs), то функция
# выполнится повторно (первый вызов result = func(*args, **kwargs)
return wrapper
return decorator
device_params = {
'device_type': 'cisco_ios',
'ip': '192.168.100.1',
'username': 'cisco',
'password': 'cisco',
'secret': 'cisco'
}
@retry(times = 3)
def send_show_command(device, show_command):
print('Подключаюсь к', device['ip'])
try:
with ConnectHandler(**device) as ssh:
ssh.enable()
result = ssh.send_command(show_command)
return result
except (NetMikoAuthenticationException, NetMikoTimeoutException):
return None
if __name__ == "__main__":
output = send_show_command(device_params, 'sh clock')
Как правило, декоратор выполняет какое-то действие перед вызовом целевой функции через return func(*args, **kwargs)
. Если нам нужно, например, с помощью декоратора, посчитать время целевой (декорируемой) функции, тогда нужно вызвать эту функцию в самом декораторе и вернуть результат. Например:
from netmiko import ConnectHandler
from datetime import datetime
import time
def timecode(func):
start_time = datetime.now()
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
finish_time = datetime.now() - start_time
print(f"Функция выполнялась: {finish_time}")
return result
device_params = {
'device_type': 'cisco_ios',
'ip': '192.168.100.1',
'username': 'cisco',
'password': 'cisco',
'secret': 'cisco'
}
@timecode
def send_show_command(params, command):
with ConnectHandler(**params) as ssh:
ssh.enable()
result = ssh.send_command(command)
return result
if __name__ == "__main__":
print(send_show_command(device_params, 'sh clock'))
Пример 4. Декораторы классов
Если необходимо декорировать каждый вызываемый метод класса (или не каждый, а конкретные), то можно сделать так - тык
Немного про используемые в примере функции:
-
if callable(value) and name not in ('__repr__', '__str__')
- функцияcallable(value)
возвращаетTrue
илиFalse
в зависимости от того, является ли объектvalue
вызываемым, т.е. методом класса. Альтернативой функцииcallable
является такая конструкция -if hasattr(object, '__call__')
. С помощьюname not in ('__repr__', '__str__')
мы исключаем из декорируемых методов__repr__
и__str__
. -
setattr(cls, name, verbose(value))
- возвращаем уже существующий метод, но декорированный декораторомverbose
. При этом, если нужно передать декоратору аргументы, тогда выглядеть будет так -setattr(cls, name, verbose(arg1, arg2)(value))
Хороший пример, выполняющий тоже самое (декорирование вызываемых атрибутов класса), что и пример по ссылке выше:
если где-то есть декоратор функции track_exceptions_decorator
, тогда:
def track_exception(cls):
# Get all callable attributes of the class
callable_attributes = {k:v for k, v in cls.__dict__.items()
if callable(v)}
# Decorate each callable attribute of to the input class
for name, func in callable_attributes.items():
decorated = track_exceptions_decorator(func)
setattr(cls, name, decorated)
return cls
@track_exceptions
class A:
def f1(self):
print('1')
def f2(self):
print('2')
Альтернативным способом декорировать методы класса может быть использование __getattribute__
в классе-декораторе.
Специальный метод __getattribute__
вызывается каждый раз, когда мы обращаемся к любому атрибуту класса(переменной экземпляра класса, методу).
Например так вызывается метод:
cls.__getattribute__(object, '__ge__')
или
object.__getattribute__('__ge__')
Не самый удачный пример такого класса-декоратора (полностью заменяет декорируемый класс) - тык
Создать новые методы в классе с помощью декораторов можно, например, так
def create_init(cls):
...
def create_repr(cls):
...
def create_dataclass(cls):
print('создаем dataclass')
cls.__init__ = create_init(cls)
cls.__repr__ = create_repr(cls)
return cls
@create_dataclass
class Book:
pass
Пример - тык
Пример 5. Модуль functools
Определенно стоит изучить возможности модуля functools
. В нем набор готовых декораторов.
Документация на модуль - тык.
Например, вы можете реализовать в своем классе один из методов __lt__()
, __le__()
, __gt__()
, __ge__()
, добавить __eq__
, а все недостающие методы за вас реализует декоратор total_ordering
:
import ipaddress
from functools import total_ordering
def total_order(cls):
return total_ordering(cls)
@total_order
class IPAddress:
def __init__(self, ip):
self._ip = int(ipaddress.ip_address(ip))
def __repr__(self):
return f"IPAddress('{self._ip}')"
def __eq__(self, other):
return self._ip == other._ip
def __lt__(self, other):
return self._ip < other._ip
Тоже самое, но вручную:
import ipaddress
def __le__(self, other):
return list(vars(self).values()) <= list(vars(other).values())
def __ge__(self, other):
return list(vars(self).values()) >= list(vars(other).values())
def total_order(cls):
if not "__eq__" or not "__lt__" in vars(cls).keys():
raise ValueError
magic_methods = {"__le__": __le__,
"__ge__": __ge__,
}
for k, v in magic_methods.items():
if k not in vars(cls).keys():
setattr(cls, k, v)
return cls
@total_order
class IPAddress:
def __init__(self, ip):
self._ip = int(ipaddress.ip_address(ip))
def __repr__(self):
return f"IPAddress('{self._ip}')"
def __eq__(self, other):
return self._ip == other._ip
def __lt__(self, other):
return self._ip < other._ip
Т.к. при использовании декораторов, информация исходной функции заменяется внутренней функцией декоратора в том же модуле functools
есть декоратор wraps
, который исправляет ситуацию. Теперь не нужно вручную заполнять специальные переменные __name__
, __doc__
, __module__
, например, так:
def decorator(func):
def decorated_func(*args, **kwargs):
...
return func(*args, **kwargs)
decorated_func.__name__ = func.__name__
return decorated_func
Пример 6. Класс как декоратор
Функцию можно декорировать классом. Этот способ является альтернативой декораторам с аргументами.
Если в классе реализован специальный метод __call__
, то экземпляр такого класса будет вызываемым (как функция) и называться функтором.
Таким образом, в __call__
мы можем передавать декорируемую функцию, например:
from functools import wraps
class Repeater:
def __init__(self, n):
self.n = n
def __call__(self, f):
@wraps(f)
def wrapper(*args, **kwargs):
for _ in range(self.n):
f(*args, **kwargs)
return wrapper
@Repeater(3)
def foo():
print('foo')
foo()
# foo
# foo
# foo