Статьи

СУХОЙ код Python с декораторами

Декораторы — одна из самых приятных возможностей 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 был компоноваемым по умолчанию, он должен:

  1. Принять функцию в качестве аргумента
  2. Вернуть другую функцию

Мы 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. Обычно веб-приложения имеют два типа страниц:

  1. страницы, которые вы можете просматривать без проверки подлинности (главная страница, целевая страница, запись в блоге, логин, регистрация)
  2. страницы, которые необходимо пройти для просмотра (настройки профиля, входящие, панель инструментов)

Если вы попытаетесь просмотреть страницу последнего типа, вы, как правило, будете перенаправлены на страницу входа. Вот как это реализовать в 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 , и не стесняйтесь задавать любые вопросы и предоставить свой ценный отзыв, используя канал ниже.

Ну, это то, что касается декораторов. Счастливого украшения!