Декораторы — одна из самых приятных возможностей Python, но для начинающего программиста на Python они могут показаться волшебством. Цель этой статьи — понять механизм, лежащий в основе декораторов Python.
Вот что вы узнаете:
- что такое Python-декораторы и для чего они хороши
- как определить наши собственные декораторы
- примеры реальных декораторов и как они работают
- как написать лучший код, используя декораторы
Вступление
Если вы еще не видели (или, возможно, не знали, что имеете дело с ним), декораторы выглядят так:
1
2
3
|
@decorator
def function_to_decorate():
pass
|
Вы обычно сталкиваетесь с ними выше определения функции, и они имеют префикс @
. Декораторы особенно хороши для хранения вашего кода СУХИМ (не повторяйте себя) , и они делают это, одновременно улучшая читабельность вашего кода.
Все еще нечетко? Не надо, поскольку декораторы — это просто функции Python. Это верно! Вы уже знаете, как его создать. На самом деле, фундаментальным принципом декораторов является составление функций. Давайте возьмем пример:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
def x_plus_2(x):
return x + 2
print(x_plus_2(2)) # 2 + 2 == 4
def x_squared(x):
return x * x
print(x_squared(3)) # 3 ^ 2 == 9
# Let’s compose the two functions for x=2
print(x_squared(x_plus_2(2))) # (2 + 2) ^ 2 == 16
print(x_squared(x_plus_2(3))) # (3 + 2) ^ 2 == 25
print(x_squared(x_plus_2(4))) # (4 + 2) ^ 2 == 36
|
Что если мы хотим создать другую функцию, x_plus_2_squared
? Попытка составить функции была бы бесполезна:
1
|
x_squared(x_plus_2) # TypeError: unsupported operand type(s) for *: ‘function’ and ‘function’
|
Вы не можете составлять функции таким способом, потому что обе функции принимают числа в качестве аргументов. Тем не менее, это будет работать:
1
2
3
4
5
6
|
# Let’s now create a proper function composition without actually applying the function
x_plus_2_squared = lambda x: x_squared(x_plus_2(x))
print(x_plus_2_squared(2)) # (2 + 2) ^ 2 == 16
print(x_plus_2_squared(3)) # (3 + 2) ^ 2 == 25
print(x_plus_2_squared(4)) # (4 + 2) ^ 2 == 36
|
Давайте переопределим, как работает x_squared
. Если мы хотим, чтобы x_squared
был компоноваемым по умолчанию, он должен:
- Принять функцию в качестве аргумента
- Вернуть другую функцию
Мы x_squared
версию x_squared
просто в squared
.
1
2
3
4
5
6
|
def squared(func):
return lambda x: func(x) * func(x)
print(squared(x_plus_2)(2)) # (2 + 2) ^ 2 == 16
print(squared(x_plus_2)(3)) # (3 + 2) ^ 2 == 25
print(squared(x_plus_2)(4)) # (4 + 2) ^ 2 == 36
|
Теперь, когда мы определили функцию squared
в squared
таким образом, чтобы ее можно было компоновать, мы можем использовать ее с любой другой функцией. Вот некоторые примеры:
1
2
3
4
5
6
7
8
|
def x_plus_3(x):
return x + 3
def x_times_2(x):
return x * 2
print(squared(x_plus_3)(2)) # (2 + 3) ^ 2 == 25
print(squared(x_times_2)(2)) # (2 * 2) ^ 2 == 16
|
Можно сказать, что squared
украшает функции x_plus_2
, x_plus_3
и x_times_2
. Мы очень близки к достижению стандартного обозначения декоратора. Проверь это:
1
2
|
x_plus_2 = squared(x_plus_2) # We decorated x_plus_2 with squared
print(x_plus_2(2)) # x_plus_2 now returns the decorated squared result: (2 + 2) ^ 2
|
Это оно! x_plus_2
— это правильная функция, оформленная на Python. Вот где обозначение @
вступает в силу:
01
02
03
04
05
06
07
08
09
10
|
def x_plus_2(x):
return x + 2
x_plus_2 = squared(x_plus_2)
# ^ This is completely equivalent with:
@squared
def x_plus_2(x):
return x + 2
|
На самом деле, нотация @
является формой синтаксического сахара. Давайте попробуем это:
01
02
03
04
05
06
07
08
09
10
11
12
|
@squared
def x_times_3(x):
return 3 * x
print(x_times_3(2)) # (3 * 2) ^ 2 = 36.
# It might be a bit confusing, but by decorating it with squared, x_times_3 became in fact (3 * x) * (3 * x)
@squared
def x_minus_1(x):
return x — 1
print(x_minus_1(3)) # (3 — 1) ^ 2 = 4
|
Если squared
— это первый декоратор, который вы когда-либо написали, сильно похлопайте себя по спине. Вы поняли одну из самых сложных концепций в Python. Попутно вы узнали еще одну фундаментальную особенность языков функционального программирования: составление функций .
Создай свой собственный декоратор
Декоратор — это функция, которая принимает функцию в качестве аргумента и возвращает другую функцию. При этом общий шаблон для определения декоратора:
1
2
3
|
def decorator(function_to_decorate):
# …
return decorated_function
|
Если вы не знали, вы можете определить функции внутри функций. В большинстве случаев decorated_function
decor_ будет определена внутри decorator
.
1
2
3
4
|
def decorator(function_to_decorate):
def decorated_function(*args, **kwargs):
# … Since we decorate `function_to_decorate`, we should use it somewhere inside here
return decorated_function
|
Давайте посмотрим на более практичный пример:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import pytz
from datetime import datetime
def to_utc(function_to_decorate):
def decorated_function():
# Get the result of function_to_decorate and transform the result to UTC
return function_to_decorate().astimezone(pytz.utc)
return decorated_function
@to_utc
def package_pickup_time():
«»» This can come from a database or from an API «»»
tz = pytz.timezone(‘US/Pacific’)
return tz.localize(datetime(2017, 8, 2, 12, 30, 0, 0))
@to_utc
def package_delivery_time():
«»» This can come from a database or from an API «»»
tz = pytz.timezone(‘US/Eastern’)
return tz.localize(datetime(2017, 8, 2, 12, 30, 0, 0)) # What a coincidence, same time different timezone!
print(«PICKUP: «, package_pickup_time()) # ‘2017-08-02 19:30:00+00:00’
print(«DELIVERY: «, package_delivery_time()) # ‘2017-08-02 16:30:00+00:00’
|
Сладкий! Теперь вы можете быть уверены, что все в вашем приложении стандартизировано для часового пояса UTC.
Практический пример
Другой действительно популярный и классический вариант использования для декораторов — это кэширование результата функции:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import time
def cached(function_to_decorate):
_cache = {} # Where we keep the results
def decorated_function(*args):
start_time = time.time()
print(‘_cache:’, _cache)
if args not in _cache:
_cache[args] = function_to_decorate(*args) # Perform the computation and store it in cache
print(‘Compute time: %ss’ % round(time.time() — start_time, 2))
return _cache[args]
return decorated_function
@cached
def complex_computation(x, y):
print(‘Processing …’)
time.sleep(2)
return x + y
print(complex_computation(1, 2)) # 3, Performing the expensive operation
print(complex_computation(1, 2)) # 3, SKIP performing the expensive operation
print(complex_computation(4, 5)) # 9, Performing the expensive operation
print(complex_computation(4, 5)) # 9, SKIP performing the expensive operation
print(complex_computation(1, 2)) # 3, SKIP performing the expensive operation
|
Если вы посмотрите на код поверхностно, вы можете возразить. Декоратор не может быть использован повторно! Если мы украсим другую функцию (скажем, another_complex_computation
) и вызовем ее с теми же параметрами, то мы получим кэшированные результаты из complex_computation function
. Этого не произойдет. Декоратор многоразовый, и вот почему:
1
2
3
4
5
6
7
8
9
|
@cached
def another_complex_computation(x, y):
print(‘Processing …’)
time.sleep(2)
return x * y
print(another_complex_computation(1, 2)) # 2, Performing the expensive operation
print(another_complex_computation(1, 2)) # 2, SKIP performing the expensive operation
print(another_complex_computation(1, 2)) # 2, SKIP performing the expensive operation
|
cached
функция вызывается один раз для каждой декорируемой функции, поэтому каждый раз создается _cache
переменная _cache
которая живет в этом контексте. Давайте проверим это:
1
2
|
print(complex_computation(10, 20)) # -> 30
print(another_complex_computation(10, 20)) # -> 200
|
Декораторы в дикой природе
Декоратор, который мы только что написали, как вы могли заметить, очень полезен. Это настолько полезно, что в стандартном модуле functools
уже существует более сложная и надежная версия. Это называется lru_cache
. LRU — это сокращение от Least недавно использовавшейся стратегии кэширования.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
from functools import lru_cache
@lru_cache()
def complex_computation(x, y):
print(‘Processing …’)
time.sleep(2)
return x + y
print(complex_computation(1, 2)) # Processing … 3
print(complex_computation(1, 2)) # 3
print(complex_computation(2, 3)) # Processing … 5
print(complex_computation(1, 2)) # 3
print(complex_computation(2, 3)) # 5
|
Одно из моих любимых применений декораторов — веб-фреймворк Flask. Он настолько хорош, что этот фрагмент кода — первое, что вы видите на веб-сайте Flask . Вот фрагмент:
01
02
03
04
05
06
07
08
09
10
|
from flask import Flask
app = Flask(__name__)
@app.route(«/»)
def hello():
return «Hello World!»
if __name__ == «__main__»:
app.run()
|
Декоратор app.route
назначает функцию hello
в качестве обработчика запроса для маршрута "/"
. Простота удивительна.
Еще одно аккуратное использование декораторов внутри Django. Обычно веб-приложения имеют два типа страниц:
- страницы, которые вы можете просматривать без проверки подлинности (главная страница, целевая страница, запись в блоге, логин, регистрация)
- страницы, которые необходимо пройти для просмотра (настройки профиля, входящие, панель инструментов)
Если вы попытаетесь просмотреть страницу последнего типа, вы, как правило, будете перенаправлены на страницу входа. Вот как это реализовать в Django:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
# Public Pages
def home(request):
return HttpResponse(«<b>Home</b>»)
def landing(request):
return HttpResponse(«<b>Landing</b>»)
# Authenticated Pages
@login_required(login_url=’/login’)
def dashboard(request):
return HttpResponse(«<b>Dashboard</b>»)
@login_required(login_url=’/login’)
def profile_settings(request):
return HttpResponse(«<b>Profile Settings</b>»)
|
Заметьте, как аккуратно закрытые представления помечаются как login_required
. Проходя через код, читателю очень ясно, какие страницы требуют от пользователя входа в систему, а какие нет.
Выводы
Я надеюсь, что вам было весело узнать о декораторах, потому что они представляют собой очень аккуратную особенность Python. Вот некоторые вещи, которые нужно запомнить:
- Правильное использование и проектирование декораторов может сделать ваш код лучше, чище и красивее.
- Использование декораторов может помочь вам высушить код — переместить идентичный код из внутренних функций в декораторы.
- Чем больше вы используете декораторов, тем лучше и сложнее их использовать.
Не забудьте проверить, что у нас есть в наличии для продажи и для изучения на Envato Market , и не стесняйтесь задавать любые вопросы и предоставить свой ценный отзыв, используя канал ниже.
Ну, это то, что касается декораторов. Счастливого украшения!