[Перевод] Декораторы, о которых вам не расскажут

e86a5d7f7427855f6270b0b035e9547d

От переводчика: мне понравился подход к объяснению декораторов, описанный в этой статье, а так как других вариантов перевода я не нашёл, я решил поделиться этим с аудиторией Хабра. Надеюсь что этот текст будет полезен как новичкам, так и опытным программистам.

Если вы программируете на языке Python, вы должны были слышать о декораторах, однако существует много людей, которые либо не знакомы с ними, либо, что еще хуже, знакомы с ними (использовали так или иначе), но так и не поняли их суть.

Если вы относитесь к последней категории, вы наверняка слышали: «Декораторы — это просто, это функции, которые принимают функции и возвращают другие функции!». Наверняка вы читали статьи в блогах о декораторах, которые добавляют что-то к результату функции или что-то выводят в консоль при ее вызове, или реализуют кэширование — как будто это настолько непреодолимые проблемы, что их можно решить только с помощью декораторов. Если вы пишете на Flask, вы наверняка использовали @app.route особо не задумываясь, что он на самом деле делает.

Короче говоря, вы знаете, что декораторы существуют, используете их, но не уверены точно для чего они нужны и почему они такие, какие есть. Практика приводит к совершенству, но бездумное следование инструкциям ведёт к апатии. Так человек, который совершенно не интересуется классикой, но которого учили игре на скрипке только как инструменте классической музыки, не станет виртуозом, не открыв для себя скрипку с других сторон.

Цель этого краткого руководства — развеять мифы, которые вы слышали о декораторах, и показать вам другие их стороны, о которых вы и не подозревали.

Обязательное напоминание

Закон штата требует, чтобы перед тем, как мы продолжим, я напомнил вам основные принципы работы декораторов.

Функции в Python

Первое, что вы должны понять: все функции в Python — это объекты первого класса, то есть такие же объекты, как и любые другие.

У них есть атрибуты:

def f():
    """Print something to standard out."""
    print('something')

print(dir(f))

#  ['__call__', '__class__', '__closure__', '__code__', '__defaults__',
#  '__delattr__', '__dict__', '__doc__', '__format__', '__get__',
#  '__getattribute__', '__globals__', '__hash__', '__init__', 
#  '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', 
#  '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 
#  'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc',
#  'func_globals', 'func_name']

Вы можете присваивать их в качестве значений переменных:

g = f
g()

# something

Можете использовать их в качестве аргументов других функций:

def func_name(function):
    return function.__name__

func_name(f)

# 'f'

Можете добавлять их внутрь структур данных:

function_collection = [f, g]
for function in function_collection:
    function()

# something
# something

Декоратор — это объект и вы можете делать с ним то что и с любым другим объектом.

Декораторы

Декораторы часто описывают как «функции которые принимают функции на вход и возвращают другие функции». Однако, в этом нет ни единого истинного слова. Что истинно, так это:

  • Декораторы применяются один раз, в момент создания функции

  • Аннотация функции x декоратором @d эквивалентна созданию функции x и затем немедленному переопределению x = d(x)

  • Декорирование функции последовательно двумя декораторами @d и @e эквивалентно: x = d(e(x)) после создания функции.

Второй из этих принципов продемонстрирован здесь:

def print_when_called(function):
    def new_function(*args, **kwargs):
        print("{} was called".format(function.__name__))
        return function(*args, **kwargs)
    return new_function

def one():
    return 1
one = print_when_called(one)

@print_when_called
def one_():
    return 1

[one(), one_(), one(), one_()]

# one was called
# one_ was called
# one was called
# one_ was called
# [1, 1, 1, 1]

Сразу же уточню: хотя я только что сказал, что декораторы применяются во время создания функции, сообщения в приведенном выше примере печатаются во время вызова функции. Это происходит потому, что print_when_called возвращает new_function, которая сама печатает сообщения перед вызовом one или one_. Посмотрите другой пример:

def print_when_applied(function):
    print("print_when_applied was applied to {}".format(function.__name__))
    return function

@print_when_applied
def never_called():
    import os
    os.system('rm -rf /')

# print_when_applied was applied to never_called

never_called, никогда не вызывается, но сообщение из декоратора print_when_applied все равно выводится.

А так можно продемонстрировать порядок применения декораторов, глядите внимательно на имена функций:

@print_when_applied
@print_when_called
def this_name_will_be_printed_when_called_but_not_at_definition_time():
    pass

this_name_will_be_printed_when_called_but_not_at_definition_time()

# print_when_applied was applied to new_function
# this_name_will_be_printed_when_called_but_not_at_definition_time was called

print_when_called возвращает функцию с именем new_function, и именно к ней обращается print_when_applied.

Мифы о декораторах

Чтобы по-настоящему понять потенциал декораторов, недостаточно узнать то, чего вы еще не знаете, надо забыть то, что вы уже знаете.

Миф первый: декоратор возвращает функцию

Ранее я утверждал, что применение декоратора d к функции x — это то же самое, что создать x, и потом сделать x = d(x).

Но кто сказал, что d должен возвращать функцию? На самом деле, func_name (которую я определил ранее) возвращает строку, но прекрасно работает как декоратор.

@func_name
def a_named_function():
    return

a_named_function

'a_named_function'

Почему это так важно? Это означает, что функции могут быть не просто вызываемыми объектами, а небольшими изолированными областями для выполнения… всего, что вы захотите. Например, если вы хотите обработать список, вы можете сделать следующее:

def process_list(list_):
    def decorator(function):
        return function(list_)
    return decorator

unprocessed_list = [0, 1, 2, 3]
special_var = "don't touch me please"

@process_list(unprocessed_list)
def processed_list(items):
    special_var = 1
    return [item for item in items if item > special_var]

(processed_list, special_var)

# ([2, 3], "don't touch me please")

processed_list — это список, а special_var остался неизменным благодаря правилам Python о масштабировании функций. Этот довольно тупой пример, есть гораздо более понятные способы сделать то же самое, но сам принцип очень полезен. Более привычно применить этот принцип можно следующим образом:

class FunctionHolder(object):
    def __init__(self, function):
        self.func = function
        self.called_count = 0
    def __call__(self, *args, **kwargs):
        try:
            return self.func(*args, **kwargs)
        finally:
            self.called_count += 1
            
def held(function):
    return FunctionHolder(function)

@held
def i_am_counted():
    pass

i_am_counted()
i_am_counted()
i_am_counted()
i_am_counted.called_count

# 3

Миф второй: декоратор — это функция

Ничто в x = d(x) не обязывает, чтобы d было функцией. d просто должно являться Callable (иметь метод __call__) Приведенный выше пример можно с тем же успехом записать так:

@FunctionHolder
def i_am_also_counted(val):
    print(val)
    
i_am_also_counted('a')
i_am_also_counted('b')
i_am_also_counted.called_count

# a
# b
# 2

Фактически, i_am_also_counted, который является экземпляром FunctionHolder, а не функцией, также может быть использован в качестве декоратора:

@i_am_also_counted
def about_to_be_printed():
    pass

i_am_also_counted.called_count

# 
# 3

Миф третий: декораторы принимают функции

Да, синтаксис Python подразумевает, что @-нотацию декораторов нельзя использовать где угодно, но это не значит, что в качестве аргументов можно использовать только функции. В следующем примере len, который работает с последовательностями (не функциями), используется в качестве декоратора:

@len
@func_name
def nineteen_characters():
    """are in this function's name"""
    pass

nineteen_characters

# 19

Фактически, вы можете применить какую угодно функцию в качестве декоратора:

mappings = {'correct': 'good', 'incorrect': 'bad'}

@list
@str.upper
@mappings.get
@func_name
def incorrect():
    pass

incorrect

# ['B', 'A', 'D']

Декоратор также можно применить к определению класса:

import re

def constructor(type_):
    def decorator(method):
        method.constructs_type = type_
        return method
    return decorator

def register_constructors(cls):
    for item_name in cls.__dict__:
        item = getattr(cls, item_name)
        if hasattr(item, 'constructs_type'):
            cls.constructors[item.constructs_type] = item
    return cls
            
@register_constructors
class IntStore(object):
    constructors = {}
    
    def __init__(self, value):
        self.value = value
        
    @classmethod
    @constructor(int)
    def from_int(cls, x):
        return cls(x)
    
    @classmethod
    @constructor(float)
    def from_float(cls, x):
        return cls(int(x))
    
    @classmethod
    @constructor(str)
    def from_string(cls, x):
        match = re.search(r'\d+', x)
        if match is None:
            return cls(0)
        return cls(int(match.group()))
    
    @classmethod
    def from_auto(cls, x):
        constructor = cls.constructors[type(x)]
        return constructor(x)
    
IntStore.from_auto('at the 11th hour').value == IntStore.from_auto(11.1).value

# True

Способы применения декораторов

Итак, теперь, когда мы убедились, что декораторы — это не «функции, которые принимают функции и возвращают функции», а »(вызываемые) объекты, которые принимают объекты и возвращают объекты» вопрос заключается не в том, что вы можете с ними вытворять, а в том, для чего они хороши.

Один из ответов: да, они все еще подходят для того, чтобы брать функции и оборачивать их в другие функции, менять их поведение и т.д. и т.п., это вполне себе нормальное их применение, есть миллион других статей по декораторам, которые покажут вам, как это сделать.

Но я хочу показать вам те способы применения, о которых эти статьи вам не расскажут. Вот вещи, где декораторы пригодились мне, но о которых другие люди говорят нечасто. Некоторые из них являются сокращенными версиями того, что я уже продемонстрировал (в той или иной степени) выше. Все это вещи, которые можно сделать и без использования декораторов — суть в том, что декораторы упрощают их, соответственно, вы будете гораздо чаще их делать, что важно для создания обслуживаемых систем.

Декораторы для аннотаций

Декораторы могут добавлять аннотации к функциям при их объявлении. Например, предположим, что мы хотим обозначить два типа функций: «красные» и «синие»:

def red(fn):
    fn.color = 'red'
    return fn

def blue(fn):
    fn.color = 'blue'
    return fn

@red
def combine(a, b):
    result = []
    result.extend(a)
    result.extend(b)
    return result

@blue
def unsafe_combine(a, b):
    a.extend(b)
    return a

@blue
def combine_and_save(a, b):
    result = a + b
    with open('combined', 'w') as f:
        f.write(repr(result))
    return result

Теперь во время выполнения каждой из этих функций у нас есть дополнительная информация, что позволяет нам принимать решения, которые мы не могли принять раньше:

def combine_using(fn, a, b):
    if hasattr(fn, 'color') and fn.color == 'blue':
        print("Sorry, only red functions allowed here!")
        return combine(a, b)  # fall back to default implementation
    return fn(a, b)

a = [1, 2]
b = [3, 4]
print(combine_using(unsafe_combine, a, b))
a

# Sorry, only red functions allowed here!
# [1, 2, 3, 4]
# [1, 2]

И хотя мы могли бы с равным успехом сделать так:

def combine(a, b):
    return a + b
combine.color = 'red'

использование декораторов имеет следующие преимущества:

  • они хорошо видны в коде

  • они имеют неотъемлемую связь с определением функции

  • они последовательны и надежны (сложнее сделать опечатку).

Если вы когда-нибудь использовали pytest, то @pytest.mark.parametrize, @pytest.mark.skip, @pytest.mark.[etc] делают именно это — просто устанавливают атрибуты вашей тестовой функции, (некоторые из которых) позже используются фреймворком для определения того, как именно тест должен быть запущен.

Декораторы для регистрации

Иногда мы хотим иметь централизованное средство для хранения ряда различных функций. Декораторы являются удобным средством для этого.

FUNCTION_REGISTRY = []

def registered(fn):
    FUNCTION_REGISTRY.append(fn)
    return fn

@registered
def step_1():
    print("Hello")
    
@registered
def step_2():
    print("world!")

Это дает нам возможность, например, пробежать по списку и выполнить все зарегистрированные функции:

def run_all():
    for function in FUNCTION_REGISTRY:
        function()
        
run_all()

# Hello
# world!

Опять же, мы могли бы сделать это и так:

def step_1():
    print("Hello")
    
def step_2():
    print("world!")
    
FUNCTION_REGISTRY = [step_1, step_2]

Однако в этом случае код функции step_1 не даст нам понять, была ли она зарегистрирована или нет. В дальнейшем если мы захотим разобраться как или почему она выполняется, мы сначала должны увидеть что она включена в FUNCTION_REGISTRY и потом увидеть что FUNCTION_REGISTRY используется в run_all. Таким образом если мы решим добавим step_3, нам нужно не забыть добавить ее в FUNCTION_REGISTRY. А декоратор @registered в коде step_1 и step_2 берет это на себя, да еще и напоминает визуально.

Декораторы для проверки

Обнаружение ошибок во время выполнения программы — это большая неприятность. Тем более, когда они появляются в конце очень долго работающего скрипта или программы, или в коде, работающем в продакшене. Поскольку декораторы оцениваются во время определения функции, мы можем использовать их для обеспечения гарантий «компиляции» сразу после импорта модуля.

Например, в своем коде вы хотите использовать другие языки или DSL в Python: регулярные выражения, SQL, XPath и т.д. Проблема в том, что это почти всегда будет представлено в виде обычных строк, а не кода, что означает, что вы не можете воспользоваться проверкой синтаксиса (хотя это и не обязательно так). Используя декоратор, мы можем, по крайней мере, получить предупреждение, когда строки в нашей функции имеют несовпадающие скобки — независимо от того, выполняется ли функция или когда она выполняется:

def brackets_balanced(s):
    brackets = {
        opening: closing
        for opening, closing in 
        '() {} []'.split()
    }
    closing = set(brackets.values())
    stack = []
    for char in s:
        if char not in closing:
            if char in brackets:
                stack.append(brackets[char])
            continue
        try:
            expected = stack.pop()
        except IndexError:
            return False
        if char != expected:
            return False
    return not stack

def ensure_brackets_balanced(fn):
    for const in fn.__code__.co_consts:
        if not isinstance(const, str) or brackets_balanced(const):
            continue
        print(
            "WARNING - {.__name__} contains unbalanced brackets: {}".format(
                fn, const
            )
        )
    return fn

@ensure_brackets_balanced
def get_root_div_paragraphs(xml_element):
    return xml_element.xpath("//div[not(ancestor::div]/p")

# WARNING - get_root_div_paragraphs contains unbalanced brackets: //div[not(ancestor::div]/p

Для проверки более продвинутых свойств структуры кода самой функции, вы можете выбрать инструмент, который может предоставить вам абстрактное синтаксическое дерево функции или написать свои собственные линтеры, используя что-то подобное.

Декораторы для диспетчеризации

Бывает удобно не решать явно, какие функции должны быть запущены при каких обстоятельствах, а просто указать необходимые правила для выполнения функции, а затем позволить компьютеру решить, какую функцию запустить, используя эту информацию. Декораторы — это чистый способ создания таких связок между входными условиями и стратегией обработки.

Например, рассмотрим приведенный выше пример IntStore. Декоратор constructor аннотирует каждый метод, декоратор класса register_constructors регистрирует каждый из них в классе, а метод from_auto использует эту информацию для диспетчеризации по типу входа.

Можно пойти еще дальше, создав набор стратегий с предварительными условиями, доступных для использования программой во время выполнения. Предоставление программе таких возможностей может придать ей надежность и гибкость, что очень желательно для некоторых приложений — например, для веб-скрейпинга, когда даже на одном сайте, структура документа, который вы разбираете, может меняться в зависимости от огромного количества факторов. Вот пример:

STRATEGIES = []

def precondition(cond):
    def decorator(fn):
        fn.precondition_met = lambda **kwargs: eval(cond, kwargs)
        STRATEGIES.append(fn)
        return fn
    return decorator

@precondition("s.startswith('The year is ')")
def parse_year_from_declaration(s):
    return int(s[-4:])

@precondition("any(substr.isdigit() for substr in s.split())")
def parse_year_from_word(s):
    for substr in s.split():
        try:
            return int(substr)
        except Exception:
            continue
            
@precondition("'-' in s")
def parse_year_from_iso(s):
    from dateutil import parser
    return parser.parse(s).year

def parse_year(s):
    for strategy in STRATEGIES:
        if strategy.precondition_met(s=s):
            return strategy(s)
        
parse_year("It's 2017 bro.")

# 2017

Декораторы для метапрограммирования

Метапрограммирование выходит за рамки этой статьи, поэтому я лишь кратко скажу, что испектирование и манипулирование Abstract Syntax Tree (AST) — это очень мощный инструмент. Тот же pytest активно использует метапрограммирование, чтобы, например, переписать assertions для получения более полезных сообщений об ошибках. astoptimizer — для ускорения работы ваших программ. patterns — для обеспечения удобного сопоставления шаблонов. Все эти приложения в некоторой степени зависят от того полезного факта, что тела функций проверяются только на синтаксическую корректность. В книге Metaprogramming Beyond Decency дается отличный вводный обзор декораторов для метапрограммирования.

Последнее замечание

Как все мы знаем: «с большой силой приходит большая ответственность». Декораторы — это мощное средство языка, и их нужно использовать аккуратно и с умом. Как я показал выше, они могут сделать код непредсказуемым, так как неизвестно что именно делает декорированная функция, и, вообще, до сих пор ли это функция. Декораторы меняют функцию навсегда, получить назад оригинальную, недекорированную версию невозможно, ведь декоратор это неотъемлемая часть определения функции. Используйте их осторожно.

Об авторе

© Habrahabr.ru