Предупреждение
Последний раз данная статья обновлялась 05.05.2022, информация может быть устаревшей.
Список вопросов к Python собеседованию
Декоратор (Decorator) — структурный шаблон проектирования, предназначенный для динамического подключения дополнительного поведения к объекту. Шаблон Декоратор предоставляет гибкую альтернативу практике создания подклассов с целью расширения функциональности.
Декоратор — это функция, которая позволяет обернуть другую функцию для расширения её функциональности без непосредственного изменения её кода.
Чтобы лучше пониманять работу декораторов нужно помнить тот факт, что:
- в Python всё является объектами;
- Функции — это объекты первого класса;
- следовательно, язык поддерживает функции высших порядков.
Объектами первого класса в контексте конкретного языка программирования называются элементы, с которыми можно делать всё то же, что и с любым другим объектом: передавать как параметр, возвращать из функции и присваивать переменной.
Функции высших порядков — это такие функции, которые могут принимать в качестве аргументов и возвращать другие функции.
Это означает, что мы можем:
- сохранять функции в переменные;
- передавать их в качестве аргументов;
- возвращать из других функций;
- определить одну функцию внутри другой (вложенные функции).
По сути, это и позволяет реализовать декоратор:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| def decorator(func):
print("Inside decorator")
def wrapper(): # вложенная функция (обертка)
print("Inside wrapper - Befor func exec")
res = func()
print("Inside wrapper - After func exec")
return res
print("Inside decorator 2")
return wrapper # возвращаем функцию (не вызывая)
def outer_func():
print("Inside outer_func")
# Передаем функцию как параметр, результат присваиваем переменной
wrapped_func = decorator(outer_func)
# вызываем обернутую функцию первый раз
wrapped_func()
# вызываем обернутую функцию второй раз
wrapped_func()
|
Результат запуска:
1
2
3
4
5
6
7
8
9
10
11
| # первый вызов функции wrapped_func()
Inside decorator - Befor wrapper def
Inside decorator - After wrapper def
Inside wrapper - Befor func exec
Inside outer_func
Inside wrapper - After func exec
# Второй вызов функции wrapped_func()
Inside wrapper - Befor func exec
Inside outer_func
Inside wrapper - After func exec
|
Выражение @decorator
- это синтаксический сахар, короткая запись для outer_func = decorator(outer_func)
.
1
2
3
4
5
6
7
8
9
10
| def decorator(func):
def wrapper():
func()
return wrapper
@decorator
def outer_func():
pass
outer_func()
|
Еще немного о декораторах:
- декораторы можно вкладывать друг в друга (при этом порядок декорирования важен);
- декораторы можно использовать с другими методами (например, «магическими»);
- декораторы могут принимать в качестве аргументов не только функции.
Условные недостатки декораторов:
- несколько замедляют вызов функции
- если функция декорирована — это не отменить (существуют трюки, позволяющие создать декоратор, который можно отсоединить от функции, но это плохая практика)
- оборачивают функции, что может затруднить отладку (лечится использованием
functools.wraps
).
Области применения декораторов:
- когда нужно избежать повторений при использовании похожих методов;
- могут быть использованы для расширения возможностей функций из сторонних библиотек (код которых мы не можем изменять);
- в Django декораторы используются для управления кешированием, контроля за правами доступа и определения обработчиков адресов;
- в Twisted — для создания поддельных асинхронных inline-вызовов.
Допустим, теперь декорируемая функция outer_func()
может принимать произвольное количество аргументов.
Чтобы наш декоратор работал корректно необходимо использовать *args
и **kwargs
(распаковка аргументов) во внутренней функции wrapper()
, а так же передавать произвольное число позиционных и ключевых аргументов функции func()
, которую декоратор получает в качестве аргумента:
1
2
3
4
5
6
7
8
9
10
11
| def decorator(func):
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
return res
return wrapper
@decorator
def outer_func(a, b, name: str):
pass
outer_func(7, 8, name="Tom")
|
Теперь декоратор @decorator
будет работать как для функций, которые вообще не принимают аргументы, так и для функций которые принимают произвольное количество аргументов.
Из-за того, что декоратор возвращает не первоначальную функцию, а функцию обертку wrapper
- теряется строка документации (docstring) основной функции, доступ к которой можно получить с помощью метода __doc__
.
functools.wraps
— декоратор
Функция wraps
из модуля functools
копирует всю информацию об оборачиваемой функции (имя, модуля, docstrings и т.п.) в функцию-обёртку.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| from functools import wraps
def decorator(func):
'''Декоратор'''
@wraps(func)
def wrapper():
'''Функция wrapper'''
func()
return wrapper
@decorator
def outer_func():
'''Оборачиваемая функция'''
print('функция wrapped')
print(outer_func.__name__)
print(outer_func.__doc__)
|
Чтобы передать параметр в сам декоратор нужно добавить еще один слой абстракции, то есть — еще одну функцию-обертку.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| from functools import wraps
def benchmark(type: str = "sec", iters: int = 1): # новый уровень абстракции
import time
def decorator(func): # сам декоратор
@wraps(func)
def wrapper(*args, **kwargs): # функция обертка
start_time = time.time()
for _ in range(iters):
result = func(*args, **kwargs)
end_time = time.time()
exec_time = end_time - start_time
if type == "ms":
print(f"Exec time of {func.__name__} x{iters} is {exec_time*1000} ms")
else:
print(f"Exec time of {func.__name__} x{iters} is {exec_time} s")
return result
return wrapper
return decorator
@benchmark(type="ms", iters=100)
def outer_func(a, b, name: str):
"""Outer_func docstring"""
print('функция wrapped')
|
Функция benchmark()
не является декоратором. Это обычная функция, которая принимает аргументы type
и iters
, а затем возвращает декоратор. В свою очередь, он декорирует функцию outer_func()
. Поэтому мы использовали не выражение @benchmark
, а @benchmark(type="ms", iters=100)
— круглые скобки означают, что функция вызывается, после чего возвращает сам декоратор.
Декоратором могут быть не только функции, но и любые вызываемые объекты. Экземпляры класса с методом __call__
тоже можно вызывать, поэтому классы можно использовать в качестве декораторов.
Для функций с параметрами *args
и **kwargs
нужно передать в метод __call__
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class Decorator:
def __init__(self, func):
print('Класс Decorator метод __init__')
self.func = func
def __call__(self, *args, **kwargs):
print('перед вызовом класса...', self.func.__name__)
self.func(*args, **kwargs)
print('после вызова класса')
@Decorator
def outer_func(a, b, name: str):
print('функция wrapped')
outer_func(7, 8, "Bob")
|
Для передачи аргументов в класс-декоратор эти аргументы получает инициализатор __init__
. Метод __call__
будет получать декорируемую функцию и возвращать функцию-обертку, которая, по сути, будет выполнять эту декорируемую функцию. Функция-обертка wrapper
получает *args
и **kwargs
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| from functools import wraps
class Decorator:
def __init__(self, output: str = "log"):
print('Класс Decorator метод __init__')
self.output = output
def __call__(self, func):
print('Класс Decorator метод __call__')
@wraps(func)
def wrapper(*args, **kwargs):
print('перед вызовом func...', func.__name__)
result = func(*args, **kwargs)
if self.output == "file":
print("Save to file")
else:
print("Console output")
print('после вызова func')
return result
return wrapper
@Decorator(output="file")
def outer_func(a, b, name: str):
"""Outer_func docstring"""
print('функция wrapped', a, b, name)
outer_func(7, 8, "Bob")
|
Функции и методы в Python — это практически одно и то же. Отличие в том, что методы всегда принимают первым параметром self
(ссылку на объект). Следовательно, мы легко можем написать декоратор для метода:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| def method_decorator(method):
def wrapper(self):
result = method(self)
return result.upper() if self.power > 150 else result
return wrapper
class Car:
def __init__(self, power):
self.power = power
@method_decorator
def wroom_wroom(self)
return "wroom-wroom..."
lada = Car(87)
bmw = Car(300)
print(lada.wroom_wroom()) # wroom-wroom...
print(bmw.wroom_wroom()) # WROOM-WROOM...
|
Декоратор можно использовать для декорирования класса. Отличие лишь в том, что декоратор получает класс, а не функцию.
Singleton — это класс с одним экземпляром. Его можно сохранить как атрибут функции-обертки и вернуть при запросе.
1
2
3
4
5
6
7
8
9
10
11
12
| def singleton(cls):
'''Класс Singleton (один экземпляр)'''
def wrapper(*args, **kwargs):
if not wrapper.instance:
wrapper.instance = cls(*args, **kwargs)
return wrapper.instance
wrapper.instance = None
return wrapper
@singleton
class SomeClass:
pass
|
@classmethod
@staticmethod
@property
Подробнее в статье ООП в Python.
@contextlib.contextmanager
@functools.lru_cache
@abc.abstractmethod
Подробнее тут.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| import functools
def convert(func=None, convert_to=None):
"""Этот код конвертирует единицы измерения из одного типа в другой."""
if func is None:
return functools.partial(convert, convert_to=convert_to)
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Conversion unit: {convert_to}")
val = func(*args, **kwargs)
# Добавим правила для преобразования
if convert_to is None:
return val
elif convert_to == "km":
return val / 1000
elif convert_to == "mile":
return val * 0.000621371
elif convert_to == "cm":
return val * 100
elif convert_to == "mm":
return val * 1000
else:
raise ValueError("Conversion unit is not supported.") # этот тип единиц не поддерживается
return wrapper
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| import time
def exec_time(type: str = "sec", iters: int = 1):
def exec_time_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""Docstring wrapper"""
start_time = time.time()
for _ in range(iters):
result = func(*args, **kwargs)
end_time = time.time()
if type == "ms":
print(f"Execution time of {func.__name__} x{iters} is {(end_time - start_time)*1000} ms")
else:
print(f"Execution time of {func.__name__} x{iters} is {end_time - start_time} s")
return result
return wrapper
return exec_time_decorator
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| def counter(func):
count = 0
@wraps(func)
def wrapper(*args, **kwargs):
"""Docstring wrapper"""
nonlocal count
count += 1
res = func(*args, **kwargs)
print(f"{func.__name__} была вызвана: {count}x")
return res
return wrapper
|
Источники