Декораторы в Python

  • 2 года назад

Функции в Python - объекты

Для понимания декораторов, сначала надо осознать, что все функции в Python это объекты. Что влечет за собой ряд последствий. Рассмотрим их на простом примере:

def shout(word="yes"):
    return word.capitalize()+"!"

print shout()
# outputs : 'Yes!'print

# так как это объект, вы можете назначить функцию переменной 
scream = shout

# Обратите внимание, мы не используем скобки: мы не вызываем функцию, а определяем занчение переменной scream 
print scream()
# outputs : 'Yes!'

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

print scream()
# outputs: 'Yes!'

Итак, запомните этот пример, мы к нему еще вернемся. Другая особенность функций в Python это то, что они могут быть объявлены внутри другой функции!

def talk():
    # Можете задавать функцию на ходу 
    def whisper(word="yes"):
        return word.lower()+"..."

    # и тут же её использовать!

print whisper()

talk()
# Вы вызываете talk, которая определяет whisper при каждом вызове. А потом whisper вызывает talk 
# outputs: 
# "yes..."

# whisper существует только в пределах talk 

try:
    print whisper()
except NameError, e:
    print e
#outputs : "name 'whisper' is not defined"*

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

Еще не запутались? А теперь самая интересная часть. Так как функции это ни что иное как объекты, то следовательно:

  • их можно назначать переменным
  • они могут быть объявлены внутри другой функции

То есть функция может вернуть другую функцию в качестве результата:

def getTalk(type="shout"):
    # Определяем функции на ходу
    def shout(word="yes"):
        return word.capitalize()+"!"

    def whisper(word="yes") :
        return word.lower()+"..."

    # Потом возвращаем одну из них
    if type == "shout":
    # Не используем скобки, так как мы не вызываем функцию, мы возвращаем объект функции
        return shout  
    else:
        return whisper

# Указываем функцию в качестве значения переменной
talk = getTalk()      

# talk - объект функции 
print talk
#outputs : <function shout at 0xb7ea817c>

# Объект вызывается при исполнении функции:
print talk()
#outputs : Yes!

# Можно его использовать напрямую:
print getTalk("whisper")()
#outputs : yes...

Стоп! Это еще не все! Если можно вернуть функцию, то и передать в качестве параметра её тоже можно.

def doSomethingBefore(func): 
    print "I do something before then I call the function you gave me"
    print func()

doSomethingBefore(scream)
#outputs: 
#I do something before then I call the function you gave me
#Yes!

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

Самодельные декораторы

Как сделать их вручную?

# Декоратор это функция, которая ожидает другую функцию в качестве параметра
def my_shiny_new_decorator(a_function_to_decorate):
    # Внутри декоратор определяет функцию на ходу
    # Эта функция окружит изначальную функцию
    # Так что код может быть исполнен как до, так и после неё
    def the_wrapper_around_the_original_function():
        # Сюда размещаем код для выполнения до оригинальной функции
        # вызов функции
        print "Before the function runs"

        # Здесь вызываем функцию со скобками
        a_function_to_decorate()

        # Здесь располагается код, который будет исполнен после оригинальной функции
        # вызов функции
        print "After the function runs"

    # Здесь, "a_function_to_decorate" не было запущена
    # Возвращаем только что созданную функцию.
    # Она содержит код для выполнения до и после оригинальной функции.
    return the_wrapper_around_the_original_function

# Представьте, что вы создали функцию и не хотите больше её изменять
def a_stand_alone_function():
  print "I am a stand alone function, don't you dare modify me"

a_stand_alone_function() 
#outputs: I am a stand alone function, don't you dare modify me

# Окружите её декоратором, чтобы изменить её работу.
# Просто передайте её декоратору, который динамически окружит её
# и вернет вместо неё тот код, который нужен вам
a_stand_alone_function_decorated = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function_decorated()
#outputs:
#Before the function runs
#I am a stand alone function, don't you dare modify me
#After the function runs

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

a_stand_alone_function = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function()
#outputs:
#До запуска функции
#I am a stand alone function, don't you dare modify me
#После запуска

# And guess what? That's EXACTLY what decorators do!

Разоблачение декораторов

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

@my_shiny_new_decorator
def another_stand_alone_function():
  print "Leave me alone"

another_stand_alone_function()  
#outputs:  
#Before the function runs
#Leave me alone
#After the function runs

Вот так все просто. @decorator - просто ссылка на:

another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)

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

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

def bread(func):
  def wrapper():
      print "</''''''\>"
      func()
      print "<\______/>"
  return wrapper

def ingredients(func):
  def wrapper():
      print "#tomatoes#"
      func()
      print "~salad~"
  return wrapper

def sandwich(food="--ham--"):
  print food

sandwich()
#outputs: --ham--
sandwich = bread(ingredients(sandwich))
sandwich()
#outputs:
#</''''''\>
# #tomatoes#
# --ham--
# ~salad~
#<\______/>

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

@bread
@ingredients
def sandwich(food="--ham--"):
  print food

sandwich()
#outputs:
#</''''''\>
# #tomatoes#
# --ham--
# ~salad~
#<\______/>

Порядок указания декораторов имеет значение:

@ingredients
@bread
def strange_sandwich(food="--ham--"):
  print food

strange_sandwich()
#outputs:
##tomatoes#
#</''''''\>
# --ham--
#<\______/>
# ~salad~

Наконец ответ на вопрос

В заключении вы видите ответ на сам вопрос:

def makebold(fn):
  # Функция которую возвращает декоратор
  def wrapper():
      return "<b>" + fn() + "</b>"
  return wrapper

# Декоратор для создания курсива
def makeitalic(fn):
  # Функция возвращаемая декоратором
  def wrapper():
      # Добавление кода до и после
      return "<i>" + fn() + "</i>"
  return wrapper

@makebold
@makeitalic
def say():
  return "hello"

print say() 
#outputs: <b><i>hello</i></b>

# А это аналог 
def say():
  return "hello"
say = makebold(makeitalic(say))

print say() 
#outputs: <b><i>hello</i></b>

В принципе на этом можно остановиться, но если желание ещё не пропало узнать новое о декораторах, то читаем дальше.

Передача параметров в функции окруженные декоратором

# Никакого волшебства, просто передаем параметр в декоратор:
def a_decorator_passing_arguments(function_to_decorate):
  def a_wrapper_accepting_arguments(arg1, arg2):
      print "I got args! Look:", arg1, arg2
      function_to_decorate(arg1, arg2)
  return a_wrapper_accepting_arguments

# Так как вы вызываете функцию, которую вернул декоратор, вы вызываете саму оболочку, а параметры будут переданы в окружаемую функцию
@a_decorator_passing_arguments
def print_full_name(first_name, last_name):
  print "My name is", first_name, last_name

print_full_name("Peter", "Venkman")
# outputs:
#I got args! Look: Peter Venkman
#My name is Peter Venkman

Применение декораторов к методам

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

def method_friendly_decorator(method_to_decorate):
    def wrapper(self, lie):
        lie = lie - 3
        return method_to_decorate(self, lie)
    return wrapper

class Lucy(object):

    def __init__(self):
        self.age = 32

    @method_friendly_decorator
    def sayYourAge(self, lie):
        print "I am %s, what did you think?" % (self.age + lie)

l = Lucy()
l.sayYourAge(-3)
#outputs: I am 26, what did you think?

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

def a_decorator_passing_arbitrary_arguments(function_to_decorate):
    # Оболочка принимает любые аргументы
    def a_wrapper_accepting_arbitrary_arguments(*args, **kwargs):
        print "Do I have args?:"
        print args
        print kwargs
        # Затем распаковываем аргументы (*args, **kwargs)
        function_to_decorate(*args, **kwargs)
    return a_wrapper_accepting_arbitrary_arguments

@a_decorator_passing_arbitrary_arguments
def function_with_no_argument():
    print "Python is cool, no argument here."

function_with_no_argument()
#outputs
#Проверяем наличие аргументов:
#()
#{}
#Аргументы отсутствуют.

@a_decorator_passing_arbitrary_arguments
def function_with_arguments(a, b, c):
    print a, b, c

function_with_arguments(1,2,3)
#outputs
#Проверяем наличие аргументов:
#(1, 2, 3)
#{}
# 1 2 3 

@a_decorator_passing_arbitrary_arguments
def function_with_named_arguments(a, b, c, platypus="Why not ?"):
    print "Do %s, %s and %s like platypus? %s" % (a, b, c, platypus)

function_with_named_arguments("Bill", "Linus", "Steve", platypus="Indeed!")
#outputs
#Проверяем наличие аргументов :
#('Bill', 'Linus', 'Steve')
#{'platypus': 'Indeed!'}

class Mary(object):

    def __init__(self):
        self.age = 31

    @a_decorator_passing_arbitrary_arguments
    def sayYourAge(self, lie=-3):
        print "I am %s, what did you think ?" % (self.age + lie)

m = Mary()
m.sayYourAge()
#outputs
# Do I have args?:
#(<__main__.Mary object at 0xb7d303ac>,)
#{}
#I am 28, what did you think?

Передача параметров декораторам

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

Перед тем как показать вам решение проблемы, давайте рассмотрим пример:

def my_decorator(func):
  print "I am a ordinary function"
  def wrapper():
      print "I am function returned by the decorator"
      func()
  return wrapper

# Вызывать можно и без "@"

def lazy_function():
  print "zzzzzzzz"

decorated_function = my_decorator(lazy_function)
#outputs: I am a ordinary function

#Вывод - "I am a ordinary function":
# calling a function. Nothing magic.

@my_decorator
def lazy_function():
  print "zzzzzzzz"

#outputs: I am a ordinary function

Ничего не изменилось. Происходит вызов my_decorator. Итак, когда вы указывете @my_decorator вы говорите Python вызвать функцию этого декоратора.

def decorator_maker():

  print "I make decorators! I am executed only once: when you make me create a decorator."

  def my_decorator(func):

      print "I am a decorator! I am executed only when you decorate a function."

      def wrapped():
          print ("I am the wrapper around the decorated function. "
                "I am called when you call the decorated function. "
                "As the wrapper, I return the RESULT of the decorated function.")
          return func()

      print "As the decorator, I return the wrapped function."

      return wrapped

  print "As a decorator maker, I return a decorator"
  return my_decorator

# Создадим новый декоратор
new_decorator = decorator_maker()       
#outputs:
#I make decorators! I am executed only once: when you make me create a decorator.
#As a decorator maker, I return a decorator

# Окружаем функцию декоратором
def decorated_function():
  print "I am the decorated function."

decorated_function = new_decorator(decorated_function)
#outputs:
#I am a decorator! I am executed only when you decorate a function.
#Возвращаем окруженную функцию

# Вызов функции:
decorated_function()
#outputs:
#I am the wrapper around the decorated function. I am called when you call the decorated function.
#As the wrapper, I return the RESULT of the decorated function.
#I am the decorated function.

Ничего удивительного. Проделаем все то же самое, но пропустим вспомогательные переменные:

def decorated_function():
    print "I am the decorated function."
decorated_function = decorator_maker()(decorated_function)
#outputs:
#I make decorators! I am executed only once: when you make me create a decorator.
#As a decorator maker, I return a decorator
#I am a decorator! I am executed only when you decorate a function.
#As the decorator, I return the wrapped function.

# Finally:
decorated_function()    
#outputs:
#I am the wrapper around the decorated function. I am called when you call the decorated function.
#As the wrapper, I return the RESULT of the decorated function.
#I am the decorated function.

Упростим решение:

@decorator_maker()
def decorated_function():
    print "I am the decorated function."
#outputs:
#I make decorators! I am executed only once: when you make me create a decorator.
#As a decorator maker, I return a decorator
#I am a decorator! I am executed only when you decorate a function.
#As the decorator, I return the wrapped function.

#Eventually: 
decorated_function()    
#outputs:
#I am the wrapper around the decorated function. I am called when you call the decorated function.
#As the wrapper, I return the RESULT of the decorated function.
#I am the decorated function.

Заметили? Мы используем вызов функции с синтаксисом @. Вернемся к декораторам с аргументами. Если мы применяем функции для генерации декораторов на ходу, то можем и передавать аргументы этой функции, не так ли?

def decorator_maker_with_arguments(decorator_arg1, decorator_arg2):

    print "I make decorators! And I accept arguments:", decorator_arg1, decorator_arg2

    def my_decorator(func):
        # Замыкания предоставляют нам возможность передавать аргументы.
        print "I am the decorator. Somehow you passed me arguments:", decorator_arg1, decorator_arg2

        # Не перепутайте параметры декоратора и функции
        def wrapped(function_arg1, function_arg2) :
            print ("I am the wrapper around the decorated function.\n"
            "I can access all the variables\n"
            "\t- from the decorator: {0} {1}\n"
            "\t- from the function call: {2} {3}\n"
            "Then I can pass them to the decorated function"
            .format(decorator_arg1, decorator_arg2,
            function_arg1, function_arg2))
            return func(function_arg1, function_arg2)

        return wrapped

    return my_decorator

@decorator_maker_with_arguments("Leonard", "Sheldon")
def decorated_function_with_arguments(function_arg1, function_arg2):
    print ("I am the decorated function and only knows about my arguments: {0}"
           " {1}".format(function_arg1, function_arg2))

decorated_function_with_arguments("Rajesh", "Howard")
#outputs:
#I make decorators! And I accept arguments: Leonard Sheldon
#I am the decorator. Somehow you passed me arguments: Leonard Sheldon
#I am the wrapper around the decorated function. 
#I can access all the variables 
#    - from the decorator: Leonard Sheldon 
#    - from the function call: Rajesh Howard 
#Then I can pass them to the decorated function
#I am the decorated function and only knows about my arguments: Rajesh Howard

Вот вам и декоратор с аргументами, которые можно задать при помощи переменных.

c1 = "Penny"
c2 = "Leslie"

@decorator_maker_with_arguments("Leonard", c1)
def decorated_function_with_arguments(function_arg1, function_arg2):
    print ("I am the decorated function and only knows about my arguments:"
           " {0} {1}".format(function_arg1, function_arg2))

decorated_function_with_arguments(c2, "Howard")
#outputs:
#I make decorators! And I accept arguments: Leonard Penny
#I am the decorator. Somehow you passed me arguments: Leonard Penny
#I am the wrapper around the decorated function. 
#I can access all the variables 
#    - from the decorator: Leonard Penny 
#    - from the function call: Leslie Howard 
#Then I can pass them to the decorated function
#I am the decorated function and only knows about my arguments: Leslie Howard

Как видите, любому декоратору можно передать параметры при таком подходе. Ничего не мешает использовать *args и **kwargs. Но помните, что декораторы вызываются только один раз. Только когда Python импортирует этот скрипт. То есть динамически установить значения этих переменных вы не сможете. Когда вы импортируете скрипт, функция уже использует декоратор, поэтому поменять что-то в последствии не получится.

Еще пример: декоратор декорирующий декоратор

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

def decorator_with_args(decorator_to_enhance):
  """ 
Эта функция должна применяться в качестве декоратора. Она должна декорировать другую функцию, которая в свою очередь тоже должна использоваться в качестве декоратора. Такой подход позволит любому декоратору принимать любое количество параметров
     """

    # Используем тот же трюк для передачи аргументов
    def decorator_maker(*args, **kwargs):

        # На ходу создаем декоратор, который принимает только функцию и скрывает переданные параметры от создателя.
         def decorator_wrapper(func):

            # Возвращаем результат оригинального декоратора, который, в конце концов, ни что иное, как обычная функция. Единственная загвоздка в том, что декоратор должен выглядеть следующим образом:                    return decorator_to_enhance(func, *args, **kwargs)

        return decorator_wrapper

    return decorator_maker

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

# Создаем функцию, которую будем использовать в качестве декоратора. 
# Не забудьте вид объявления декоратора "decorator(func, *args, **kwargs)"
@decorator_with_args 
def decorated_decorator(func, *args, **kwargs): 
    def wrapper(function_arg1, function_arg2):
        print "Decorated with", args, kwargs
        return func(function_arg1, function_arg2)
    return wrapper

@decorated_decorator(42, 404, 1024)
def decorated_function(function_arg1, function_arg2):
    print "Hello", function_arg1, function_arg2

decorated_function("Universe and", "everything")
#outputs:
#Decorated with (42, 404, 1024) {}
#Hello Universe and everything

# Whoooot!

Да-да, все это выглядит примерно следующим образом: "Чтобы понять рекурсию, сначала надо понять рекурсию". Но в итоге разве нет чувства, что вы освоили что-то новое и интересное?

Правильное применение декораторов

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

Python 2.5 и выше представляет решение последней проблемы. Он включает в себя модуль functools (functools.wraps), который копирует название, модуль и документацию каждой задекориованной функции. Самое интересное - functools.wraps и есть декоратор.

def foo():
    print "foo"

print foo.__name__
#outputs: foo

# С декоратором выглядет немного сумбурно    
def bar(func):
    def wrapper():
        print "bar"
        return func()
    return wrapper

@bar
def foo():
    print "foo"

print foo.__name__
#outputs: wrapper

# "functools" поможет
import functools

def bar(func):
    # "wrapper" окружает "func"
    @functools.wraps(func)
    def wrapper():
        print "bar"
        return func()
    return wrapper

@bar
def foo():
    print "foo"

print foo.__name__
#outputs: foo

Когда стоит применять декораторы?

Главный вопрос - а когда, собственно, применять декораторы? Конечно выглядят они отлично, но где же их использовать? Могу привести хоть 1000 примеров. Классический пример - добавление функционала для функции из сторонней библиотеки (которую вы не можете править). Вы можете применять декораторы для расширения нескольких функций, чтобы не переписывать их каждый раз. Это избавляет от излишнего кода:

def benchmark(func):
    """
    Декоратор выводящий время работы функции
    """
    import time
    def wrapper(*args, **kwargs):
        t = time.clock()
        res = func(*args, **kwargs)
        print func.__name__, time.clock()-t
        return res
    return wrapper

def logging(func):
    """
    Декоратор, сохраняющий данные об активности скрипта
    """
    def wrapper(*args, **kwargs):
        res = func(*args, **kwargs)
        print func.__name__, args, kwargs
        return res
    return wrapper

def counter(func):
    """
    Декоратор считающий и выводящий количество исполнений функции
    """
    def wrapper(*args, **kwargs):
        wrapper.count = wrapper.count + 1
        res = func(*args, **kwargs)
        print "{0} has been used: {1}x".format(func.__name__, wrapper.count)
        return res
    wrapper.count = 0
    return wrapper

@counter
@benchmark
@logging
def reverse_string(string):
    return str(reversed(string))

print reverse_string("Able was I ere I saw Elba")
print reverse_string("A man, a plan, a canoe, pasta, heros, rajahs, a coloratura, maps, snipe, percale, macaroni, a gag, a banana bag, a tan, a tag, a banana bag again (or a camel), a crepe, pins, Spam, a rut, a Rolo, cash, a jar, sore hats, a peon, a canal: Panama!")

#outputs:
#reverse_string ('Able was I ere I saw Elba',) {}
#wrapper 0.0
#wrapper has been used: 1x 
#ablE was I ere I saw elbA
#reverse_string ('A man, a plan, a canoe, pasta, heros, rajahs, a coloratura, maps, snipe, percale, macaroni, a gag, a banana bag, a tan, a tag, a banana bag again (or a camel), a crepe, pins, Spam, a rut, a Rolo, cash, a jar, sore hats, a peon, a canal: Panama!',) {}
#wrapper 0.0
#wrapper has been used: 2x
#!amanaP :lanac a ,noep a ,stah eros ,raj a ,hsac ,oloR a ,tur a ,mapS ,snip ,eperc a ,)lemac a ro( niaga gab ananab a ,gat a ,nat a ,gab ananab a ,gag a ,inoracam ,elacrep ,epins ,spam ,arutaroloc a ,shajar ,soreh ,atsap ,eonac a ,nalp a ,nam A

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

@counter
@benchmark
@logging
def get_random_futurama_quote():
    import httplib
    conn = httplib.HTTPConnection("slashdot.org:80")
    conn.request("HEAD", "/index.html")
    for key, value in conn.getresponse().getheaders():
        if key.startswith("x-b") or key.startswith("x-f"):
            return value
    return "No, I'm ... doesn't!"

print get_random_futurama_quote()
print get_random_futurama_quote()

#outputs:
#get_random_futurama_quote () {}
#wrapper 0.02
#wrapper has been used: 1x
#The laws of science be a harsh mistress.
#get_random_futurama_quote () {}
#wrapper 0.01
#wrapper has been used: 2x
#Curse you, merciful Poseidon!

Сам Python предлагает несколько декораторов: property, staticmethod и т.д. Django использует декораторы для кеширования и отслеживания прав на просмотр. Так что круг применения декораторов практически не ограничен.

Комментарии

0