• en
  • Language: ru
  • Documentation version: latest

7. Декораторы

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

Сначала давайте обсудим, как написать свой собственный декоратор.

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

7.1. Все в Python является объектом:

Прежде всего, давайте разберемся, что такое функции в Python:

def hi(name="yasoob"):
    return "hi " + name

print(hi())
# output: 'hi yasoob'

# We can even assign a function to a variable like
greet = hi
# We are not using parentheses here because we are not calling the function hi
# instead we are just putting it into the greet variable. Let's try to run this

print(greet())
# output: 'hi yasoob'

# Let's see what happens if we delete the old hi function!
del hi
print(hi())
#outputs: NameError

print(greet())
#outputs: 'hi yasoob'

7.2. Определение функций внутри функций:

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

def hi(name="yasoob"):
    print("now you are inside the hi() function")

    def greet():
        return "now you are in the greet() function"

    def welcome():
        return "now you are in the welcome() function"

    print(greet())
    print(welcome())
    print("now you are back in the hi() function")

hi()
#output:now you are inside the hi() function
#       now you are in the greet() function
#       now you are in the welcome() function
#       now you are back in the hi() function

# This shows that whenever you call hi(), greet() and welcome()
# are also called. However the greet() and welcome() functions
# are not available outside the hi() function e.g:

greet()
#outputs: NameError: name 'greet' is not defined

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

7.3. Возврат функций изнутри функций:

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

def hi(name="yasoob"):
    def greet():
        return "now you are in the greet() function"

    def welcome():
        return "now you are in the welcome() function"

    if name == "yasoob":
        return greet
    else:
        return welcome

a = hi()
print(a)
#outputs: <function greet at 0x7f2143c01500>

#This clearly shows that `a` now points to the greet() function in hi()
#Now try this

print(a())
#outputs: now you are in the greet() function

Просто посмотрите на код еще раз. В пункте if/else мы возвращаем greet и welcome, а не greet() и welcome(). Почему так? Потому что, когда вы ставите пару круглых скобок после функции, функция выполняется, а если вы не ставите скобки после функции, то ее можно передавать и присваивать другим переменным без ее выполнения. Вы поняли? Позвольте мне объяснить это немного подробнее. Когда мы пишем a = hi(), выполняется hi() и, поскольку по умолчанию имя yasoob, возвращается функция greet. Если мы изменим оператор на a = hi(name = "ali"), то будет возвращена функция welcome. Мы также можем сделать print hi()(), который выведет теперь вы находитесь в функции greet().

7.4. Передача функции в качестве аргумента другой функции:

def hi():
    return "hi yasoob!"

def doSomethingBeforeHi(func):
    print("I am doing some boring work before executing hi()")
    print(func())

doSomethingBeforeHi(hi)
#outputs:I am doing some boring work before executing hi()
#        hi yasoob!

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

7.5. Написание вашего первого декоратора:

В последнем примере мы действительно создали декоратор! Давайте изменим предыдущий декоратор и сделаем немного более удобную программу:

def a_new_decorator(a_func):

    def wrapTheFunction():
        print("I am doing some boring work before executing a_func()")

        a_func()

        print("I am doing some boring work after executing a_func()")

    return wrapTheFunction

def a_function_requiring_decoration():
    print("I am the function which needs some decoration to remove my foul smell")

a_function_requiring_decoration()
#outputs: "I am the function which needs some decoration to remove my foul smell"

a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)
#now a_function_requiring_decoration is wrapped by wrapTheFunction()

a_function_requiring_decoration()
#outputs:I am doing some boring work before executing a_func()
#        I am the function which needs some decoration to remove my foul smell
#        I am doing some boring work after executing a_func()

Получилось ли у вас? Мы просто применили ранее изученные принципы. Именно это и делают декораторы в Python! Они оборачивают функцию и изменяют ее поведение тем или иным образом. Теперь вам, возможно, интересно, почему мы нигде не использовали символ @ в нашем коде? Это просто короткий способ создания декорированной функции. Вот как мы могли бы выполнить предыдущий пример кода с использованием @.

@a_new_decorator
def a_function_requiring_decoration():
    """Hey you! Decorate me!"""
    print("I am the function which needs some decoration to "
          "remove my foul smell")

a_function_requiring_decoration()
#outputs: I am doing some boring work before executing a_func()
#         I am the function which needs some decoration to remove my foul smell
#         I am doing some boring work after executing a_func()

#the @a_new_decorator is just a short way of saying:
a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)

Надеюсь, теперь у вас есть базовое понимание того, как декораторы работают в Python. Теперь в нашем коде есть одна проблема. Если мы выполним:

print(a_function_requiring_decoration.__name__)
# Output: wrapTheFunction

Это не то, что мы ожидали! Ее название «a_функция_требующая_декорирования». Наша функция была заменена функцией wrapTheFunction. Она переопределила имя и docstring нашей функции. К счастью, Python предоставляет нам простую функцию для решения этой проблемы, и это functools.wraps. Давайте изменим наш предыдущий пример, чтобы использовать functools.wraps:

from functools import wraps

def a_new_decorator(a_func):
    @wraps(a_func)
    def wrapTheFunction():
        print("I am doing some boring work before executing a_func()")
        a_func()
        print("I am doing some boring work after executing a_func()")
    return wrapTheFunction

@a_new_decorator
def a_function_requiring_decoration():
    """Hey yo! Decorate me!"""
    print("I am the function which needs some decoration to "
          "remove my foul smell")

print(a_function_requiring_decoration.__name__)
# Output: a_function_requiring_decoration

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

Блюпринт:

from functools import wraps
def decorator_name(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if not can_run:
            return "Function will not run"
        return f(*args, **kwargs)
    return decorated

@decorator_name
def func():
    return("Function is running")

can_run = True
print(func())
# Output: Function is running

can_run = False
print(func())
# Output: Function will not run

Примечание: @wraps принимает декорируемую функцию и добавляет функциональность копирования имени функции, doc-строки, списка аргументов и т.д.. Это позволяет нам получить доступ к свойствам предварительно декорированной функции в декораторе.

7.5.1. Примеры использования:

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

7.5.2. Авторизация

Декораторы могут помочь проверить, авторизован ли кто-то на использование конечной точки в веб-приложении. Они широко используются в веб-фреймворке Flask и Django. Вот пример использования аутентификации на основе декораторов:

Пример :

from functools import wraps

def requires_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization
        if not auth or not check_auth(auth.username, auth.password):
            authenticate()
        return f(*args, **kwargs)
    return decorated

7.5.3. Ведение журнала

Ведение журнала - еще одна область, где декораторы сияют. Вот пример:

from functools import wraps

def logit(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

@logit
def addition_func(x):
   """Do some math."""
   return x + x


result = addition_func(4)
# Output: addition_func was called

Уверен, вы уже думаете о том, как ловко использовать декоративные элементы.

7.6. Декораторы с аргументами

Если подумать, разве @wraps не является декоратором? Но он принимает аргумент, как это делает любая обычная функция. Так почему мы не можем сделать и это?

Это происходит потому, что когда вы используете синтаксис @my_decorator, вы применяете функцию-обертку с одной функцией в качестве параметра. Помните, что все в Python является объектом, и это включает в себя функции! Помня об этом, мы можем написать функцию, которая возвращает функцию-обертку.

7.6.1. Вложение декоратора в функцию

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

from functools import wraps

def logit(logfile='out.log'):
    def logging_decorator(func):
        @wraps(func)
        def wrapped_function(*args, **kwargs):
            log_string = func.__name__ + " was called"
            print(log_string)
            # Open the logfile and append
            with open(logfile, 'a') as opened_file:
                # Now we log to the specified logfile
                opened_file.write(log_string + '\n')
            return func(*args, **kwargs)
        return wrapped_function
    return logging_decorator

@logit()
def myfunc1():
    pass

myfunc1()
# Output: myfunc1 was called
# A file called out.log now exists, with the above string

@logit(logfile='func2.log')
def myfunc2():
    pass

myfunc2()
# Output: myfunc2 was called
# A file called func2.log now exists, with the above string

7.6.2. Занятия для декораторов

Теперь у нас есть наш декоратор logit в production, но когда некоторые части нашего приложения считаются критическими, отказ может быть чем-то, что требует более немедленного внимания. Допустим, иногда вы хотите просто вести журнал в файл. В других случаях вы хотите отправить письмо по электронной почте, чтобы проблема была доведена до вашего сведения, и при этом сохранить журнал для собственных записей. Это случай для использования наследования, но до сих пор мы видели только использование функций для создания декораторов.

К счастью, классы также можно использовать для создания декораторов. Итак, давайте перестроим logit как класс, а не как функцию.

class logit(object):

    _logfile = 'out.log'

    def __init__(self, func):
        self.func = func

    def __call__(self, *args):
        log_string = self.func.__name__ + " was called"
        print(log_string)
        # Open the logfile and append
        with open(self._logfile, 'a') as opened_file:
            # Now we log to the specified logfile
            opened_file.write(log_string + '\n')
        # Now, send a notification
        self.notify()

        # return base func
        return self.func(*args)



    def notify(self):
        # logit only logs, no more
        pass

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

logit._logfile = 'out2.log' # if change log file
@logit
def myfunc1():
    pass

myfunc1()
# Output: myfunc1 was called

Теперь давайте подкласс logit, чтобы добавить функциональность электронной почты (хотя эта тема здесь рассматриваться не будет).

class email_logit(logit):
    '''
    A logit implementation for sending emails to admins
    when the function is called.
    '''
    def __init__(self, email='admin@myproject.com', *args, **kwargs):
        self.email = email
        super(email_logit, self).__init__(*args, **kwargs)

    def notify(self):
        # Send an email to self.email
        # Will not be implemented here
        pass

Отсюда @email_logit работает так же, как @logit, но в дополнение к регистрации отправляет письмо администратору.