Понимаем декораторы в Python’e, шаг за шагом. Шаг 1

в 19:19, , рубрики: decorator, decorators, python, step-by-step, декораторы, разработка

Понимаем декораторы в Pythone, шаг за шагом. Шаг 1

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

Итак, что же такое «декоратор»?

Впереди достаточно длинная статья, так что, если кто-то спешит — вот пример того, как работают декораторы:

def makebold(fn):
    def wrapped():
        return "<b>" + fn() + "</b>"
    return wrapped
 
def makeitalic(fn):
    def wrapped():
        return "<i>" + fn() + "</i>"
    return wrapped
 
@makebold
@makeitalic
def hello():
    return "hello habr"
 
print hello() ## выведет <b><i>hello habr</i></b>


Те же из вас, кто готов потратить немного времени, приглашаются прочесть длииииный пост.

Функции в Python'e являются объектами

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

def shout(word="да"):
    return word.capitalize()+"!"
 
print shout()
# выведет: 'Да!'
 
# Так как функция - это объект, вы можете назначить её переменнной,
# как и любой другой объект
scream = shout
 
# Заметьте, что мы не используем скобок: мы НЕ вызываем функцию "shout",
# мы помещаем её в переменную "scream". Это означает, что теперь мы
# можем вызывать "shout" через "scream":
 
print scream()
# выведет: 'Да!'

# Более того, это значит, что мы можем удалить "shout", и функция всё ещё
# будет доступна через переменную "scream"
 
del shout
try:
    print shout()
except NameError, e:
    print e
    #выведет: "name 'shout' is not defined"
 
print scream()
# выведет: 'Да!'

Запомним этот факт, скоро мы к нему вернёмся, но кроме того, стоит понимать, что функция в Python'e может быть определена… внутри другой функции!

def talk():
    # Внутри определения функции "talk" мы можем определить другую...
    def whisper(word="да"):
        return word.lower()+"...";
 
    # ... и сразу же её использовать!
    print whisper()

# Теперь, КАЖДЫЙ РАЗ при вызове "talk", внутри неё определяется а затем
# и вызывается функция "whisper".
talk()
# выведет: "да..."
 
# Но вне функции "talk" НЕ существует никакой функции "whisper":
try:
    print whisper()
except NameError, e:
    print e
    #выведет : "name 'whisper' is not defined"

Ссылки на функции

Ну что, вы всё ещё здесь?:)

Теперь мы знаем, что функции являются полноправными объектами, а значит:

  • могут быть записаны в переменную;
  • могут быть определены одна внутри другой.

Что ж, а это значит, что функция может вернуть другую функцию!
Давайте посмотрим:

def getTalk(type="shout"):
 
    # Мы определяем функции прямо здесь
    def shout(word="да"):
        return word.capitalize()+"!"
 
    def whisper(word="да") :
        return word.lower()+"...";
 
    # Затем возвращаем необходимую
    if type == "shout":
        # Заметьте, что мы НЕ используем "()", нам нужно не вызвать функцию,
        # а вернуть объект функции
        return shout
    else:
        return whisper
 
# Как использовать это непонятное нечто?
 
# Возьмём функцию и запишем её в переменную
talk = getTalk()
 
# Как мы можем видеть, "talk" теперь - объект "function":
print talk
# выведет: <function shout at 0xb7ea817c>
 
# Который можно вызывать, как и функцию, определённую "обычным образом":
print talk()
 
# Если нам захочется - можно вызвать её напрямую из возвращаемого значения:
print getTalk("whisper")()
# выведет: да...

Подождите, раз мы можем возвращать функцию, значит, мы можем и передавать её другой функции, как параметр:

def doSomethingBefore(func):
    print "Я делаю что-то ещё, перед тем как вызвать функцию, которую ты мне передал"
    print func()
 
doSomethingBefore(scream)
#выведет:
#Я делаю что-то ещё, перед тем как вызвать функцию, которую ты мне передал
#Да!

Ну что, теперь у нас есть все необходимые знания для того, чтобы понять, как работают декораторы.
Как вы могли догадаться, декораторы — это, по сути, просто своеобразные «обёртки», которые дают нам возможность делать что-либо до и после того, что сделает декорируемая функция, не изменяя её.

Создадим свой декоратор «вручную»

# Декоратор - это функция, ожидающая ДРУГУЮ функцию в качестве параметра
def my_shiny_new_decorator(a_function_to_decorate):
    # Внутри себя декоратор определяет функцию-"обёртку".
    # Она будет (что бы вы думали?..) обёрнута вокруг декорируемой,
    # получая возможность исполнять произвольный код до и после неё.

    def the_wrapper_around_the_original_function():
        # Поместим здесь код, который мы хотим запускать ДО вызова
        # оригинальной функции
        print "Я - код, который отработает до вызова функции"
 
        # ВЫЗОВЕМ саму декорируемую функцию
        a_function_to_decorate()

        # А здесь поместим код, который мы хотим запускать ПОСЛЕ вызова
        # оригинальной функции
        print "А я - код, срабатывающий после"

    # На данный момент функция "a_function_to_decorate" НЕ ВЫЗЫВАЛАСЬ НИ РАЗУ

    # Теперь, вернём функцию-обёртку, которая содержит в себе
    # декорируемую функцию, и код, который необходимо выполнить до и после.
    # Всё просто!
    return the_wrapper_around_the_original_function

# Представим теперь, что у нас есть функция, которую мы не планируем больше трогать.
def a_stand_alone_function():
    print "Я простая одинокая функция, ты ведь не посмеешь меня изменять?.."
 
a_stand_alone_function()
# выведет: Я простая одинокая функция, ты ведь не посмеешь меня изменять?..
 
# Однако, чтобы изменить её поведение, мы можем декорировать её, то есть
# Просто передать декоратору, который обернет исходную функцию в любой код,
# который нам потребуется, и вернёт новую, готовую к использованию функцию:
 
a_stand_alone_function_decorated = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function_decorated()
#выведет:
#Я - код, который отработает до вызова функции
#Я простая одинокая функция, ты ведь не посмеешь меня изменять?..
#А я - код, срабатывающий после

Наверное, теперь мы бы хотели, чтобы каждый раз, во время вызова a_stand_alone_function, вместо неё вызывалась a_stand_alone_function_decorated. Нет ничего проще, просто перезапишем a_stand_alone_function функцией, которую нам вернул my_shiny_new_decorator:

a_stand_alone_function = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function()
#выведет:
#Я - код, который отработает до вызова функции
#Я простая одинокая функция, ты ведь не посмеешь меня изменять?..
#А я - код, срабатывающий после

Вы ведь уже догадались, что это ровно тоже самое, что делают @декораторы.:)

Разрушаем ореол таинственности вокруг декораторов

Вот так можно было записать предыдущий пример, используя синтаксис декораторов:

@my_shiny_new_decorator
def another_stand_alone_function():
    print "Оставь меня в покое"
 
another_stand_alone_function()
#выведет:
#Я - код, который отработает до вызова функции
#Оставь меня в покое
#А я - код, срабатывающий после

Да, всё действительно так просто! @decorator — просто синтаксический сахар для конструкций вида:

another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)

Декораторы — это просто pythonic-реализация паттерна проектирования «Декоратор». В Python включены некоторые классические паттерны проектирования, такие как рассматриваемые в этой статье декораторы, или привычные любому пайтонисту итераторы.

Конечно, можно вкладывать декораторы друг в друга, например так:

def bread(func):
    def wrapper():
        print "</------>"
        func()
        print "<______/>"
    return wrapper
 
def ingredients(func):
    def wrapper():
        print "#помидоры#"
        func()
        print "~салат~"
    return wrapper
 
def sandwich(food="--ветчина--"):
    print food
 
sandwich()
#outputs: --ветчина--
sandwich = bread(ingredients(sandwich))
sandwich()
#outputs:
# </------>
# #помидоры#
# --ветчина--
# ~салат~
# <______/>

И используя синтаксис декораторов:

@bread
@ingredients
def sandwich(food="--ветчина--"):
    print food
 
sandwich()
#outputs:
# </------>
# #помидоры#
# --ветчина--
# ~салат~
# <______/>

Следует помнить о том, что порядок декорирования ВАЖЕН:

@ingredients
@bread
def sandwich(food="--ветчина--"):
    print food
 
sandwich()
#outputs:
# #помидоры#
#</------>
# --ветчина--
#<______/>
# ~салат~

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

Содержание:

Прим. переводчика:
Спасибо за внимание. Буду благодарен любым замечаниям по переводу и оформлению, постараюсь учесть их все в второй части перевода.

Автор: Utter_step

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js