Статьи

Python 201: декораторы

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


Простая функция

Функция — это блок кода, который начинается с ключевого слова Python def, за которым следует фактическое имя функции. Функция может принимать ноль или более аргументов, аргументы ключевых слов или их комбинацию. Функция всегда что-то возвращает. Если вы не укажете, что должна возвращать функция, она вернет None . Вот очень простая функция, которая просто возвращает строку:

#----------------------------------------------------------------------
def a_function():
    """A pretty useless function"""
    return "1+1"
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    value = a_function()
    print(value)

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

#----------------------------------------------------------------------
def another_function(func):
    """
    A function that accepts another function
    """
    def other_func():
        val = "The result of %s is %s" % (func(),
                                          eval(func())
                                          )
        return val
    return other_func

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

#----------------------------------------------------------------------
def another_function(func):
    """
    A function that accepts another function
    """
 
    def other_func():
        val = "The result of %s is %s" % (func(),
                                          eval(func())
                                          )
        return val
    return other_func
 
#----------------------------------------------------------------------
def a_function():
    """A pretty useless function"""
    return "1+1"
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    value = a_function()
    print(value)
    decorator = another_function(a_function)
    print decorator()

Вот как работает декоратор. Мы создаем одну функцию, а затем передаем ее во вторую функцию. Вторая функция — это функция декоратора. Декоратор изменит или улучшит переданную ему функцию и вернет модификацию. Если вы запустите этот код, вы должны увидеть следующее как вывод в stdout:


1+1
The result of 1+1 is 2

Let’s change the code slightly to turn another_function into a decorator:

#----------------------------------------------------------------------
def another_function(func):
    """
    A function that accepts another function
    """
 
    def other_func():
        val = "The result of %s is %s" % (func(),
                                          eval(func())
                                          )
        return val
    return other_func
 
#----------------------------------------------------------------------
@another_function
def a_function():
    """A pretty useless function"""
    return "1+1"
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    value = a_function()
    print(value)

Вы заметите, что в Python декоратор начинается с символа @, за которым следует имя функции, которую мы будем использовать для «декорирования» нашего регулятора. Чтобы применить декоратор, вы просто помещаете его в строку перед определением функции. Теперь, когда мы вызываем a_function, она будет оформлена, и мы получим следующий результат:

Результат 1 + 1 равен 2

Давайте создадим декоратор, который на самом деле делает что-то полезное.


Создание декоратора регистрации

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

import logging
 
#----------------------------------------------------------------------
def log(func):
    """
    Log what function is called
    """
    def wrap_log(*args, **kwargs):
        name = func.__name__
        logger = logging.getLogger(name)
        logger.setLevel(logging.INFO)
 
        # add file handler
        fh = logging.FileHandler("%s.log" % name)
        fmt = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        formatter = logging.Formatter(fmt)
        fh.setFormatter(formatter)
        logger.addHandler(fh)
 
        logger.info("Running function: %s" % name)
        result = func(*args, **kwargs)
        logger.info("Result: %s" % result)
        return func
    return wrap_log
 
#----------------------------------------------------------------------
@log
def double_function(a):
    """
    Double the input parameter
    """
    return a*2
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    value = double_function(2)

Этот маленький скрипт имеет функцию журнала, которая принимает функцию в качестве единственного аргумента. Он создаст объект logger и имя файла журнала на основе имени функции. Затем функция log запишет, какая функция была вызвана и что функция вернула, если что-нибудь.


Встроенные декораторы

Python поставляется с несколькими встроенными декораторами. Большая тройка:

  • @classmethod
  • @staticmethod
  • @имущество

В различных частях стандартной библиотеки Python есть декораторы. Одним из примеров будет functools.wraps. Мы будем ограничивать нашу сферу тремя выше, хотя.

@classmethod и @staticmethod

Я никогда не использовал их сам, поэтому я провел немало исследований. @Classmethod декоратор может быть вызван с экземпляром класса или непосредственно сам классом в качестве первого аргумента. Согласно документации Python : он может быть вызван либо в классе (например, Cf ()), либо в экземпляре (например, C (). F ()). Экземпляр игнорируется за исключением его класса. Если метод класса вызывается для производного класса, объект производного класса передается как подразумеваемый первый аргумент. Основной случай использования декоратора @classmethod, который я обнаружил в своем исследовании, — это альтернативный конструктор или вспомогательный метод для инициализации.

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

Это может помочь увидеть пример кода того, как работают эти два декоратора:

########################################################################
class DecoratorTest(object):
    """
    Test regular method vs @classmethod vs @staticmethod
    """
 
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        pass
 
    #----------------------------------------------------------------------
    def doubler(self, x):
        """"""
        print("running doubler")
        return x*2
 
    #----------------------------------------------------------------------
    @classmethod
    def class_tripler(klass, x):
        """"""
        print("running tripler: %s" % klass)
        return x*3
 
    #----------------------------------------------------------------------
    @staticmethod
    def static_quad(x):
        """"""
        print("running quad")
        return x*4
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    decor = DecoratorTest()
    print(decor.doubler(5))
    print(decor.class_tripler(3))
    print(DecoratorTest.class_tripler(3))
    print(DecoratorTest.static_quad(2))
    print(decor.static_quad(3))
 
    print(decor.doubler)
    print(decor.class_tripler)
    print(decor.static_quad)

This example demonstrates that you can call a regular method and both decorated methods in the same way. You will notice that you can call both the @classmethod and the @staticmethod decorated functions directly with from the class or from an instance of the class. If you try to call a regular function with the class (i.e. DecoratorTest.doubler(2)) you will receive a TypeError. You will also note that the last print statement shows that decor.static_quad returns a regular function instead of a bound method.


Python’s Properties

I’ve written about the @property decorator once already this year, so I’ll just reprint a slight variation of that article here.

Python has a neat little concept called a property that can do several useful things. We will be looking into how to do the following:

  • Convert class methods into read-only attributes
  • Reimplement setters and getters into an attribute

One of the simplest ways to use a property is to use it as a decorator of a method. This allows you to turn a class method into a class attribute. I find this useful when I need to do some kind of combination of values. Others have found it useful for writing conversion methods that they want to have access to as methods. Let’s take a look at a simple example:

########################################################################
class Person(object):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self, first_name, last_name):
        """Constructor"""
        self.first_name = first_name
        self.last_name = last_name
 
    #----------------------------------------------------------------------
    @property
    def full_name(self):
        """
        Return the full name
        """
        return "%s %s" % (self.first_name, self.last_name)

In the code above, we create two class attributes or properties: self.first_name and self.last_name. Next we create a full_name method that has a @property decorator attached to it. This allows us to the following in an interpreter session:

>>> person = Person("Mike", "Driscoll")
>>> person.full_name
'Mike Driscoll'
>>> person.first_name
'Mike'
>>> person.full_name = "Jackalope"
Traceback (most recent call last):
  File "<string>", line 1, in <fragment>
AttributeError: can't set attribute

As you can see, because we turned the method into a property, we can access it using normal dot notation. However, if we try to set the property to something different, we will cause an AttributeError to be raised. The only way to change the full_name property is to do so indirectly:

>>> person.first_name = "Dan"
>>> person.full_name
'Dan Driscoll'

This is kind of limiting, so let’s look at another example where we can make a property that does allow us to set it.

Replacing Setters and Getters with a Python property

Let’s pretend that we have some legacy code that someone wrote who didn’t understand Python very well. If you’re like me, you’ve already seen this kind of code before:

from decimal import Decimal
 
########################################################################
class Fees(object):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        self._fee = None
 
    #----------------------------------------------------------------------
    def get_fee(self):
        """
        Return the current fee
        """
        return self._fee
 
    #----------------------------------------------------------------------
    def set_fee(self, value):
        """
        Set the fee
        """
        if isinstance(value, str):
            self._fee = Decimal(value)
        elif isinstance(value, Decimal):
            self._fee = value

To use this class, we have to use the setters and getters that are defined:

>>> f = Fees()
>>> f.set_fee("1")
>>> f.get_fee()
Decimal('1')

If you want to add the normal dot notation access of attributes to this code without breaking all the applications that depend on this piece of code, you can change it very simply by adding a property:

from decimal import Decimal
 
########################################################################
class Fees(object):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        self._fee = None
 
    #----------------------------------------------------------------------
    def get_fee(self):
        """
        Return the current fee
        """
        return self._fee
 
    #----------------------------------------------------------------------
    def set_fee(self, value):
        """
        Set the fee
        """
        if isinstance(value, str):
            self._fee = Decimal(value)
        elif isinstance(value, Decimal):
            self._fee = value
 
    fee = property(get_fee, set_fee)

We added one line to the end of this code. Now we can do stuff like this:

>>> f = Fees()
>>> f.set_fee("1")
>>> f.fee
Decimal('1')
>>> f.fee = "2"
>>> f.get_fee()
Decimal('2')

As you can see, when we use property in this manner, it allows the fee property to set and get the value itself without breaking the legacy code. Let’s rewrite this code using the property decorator and see if we can get it to allow setting.

from decimal import Decimal
 
########################################################################
class Fees(object):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        self._fee = None
 
    #----------------------------------------------------------------------
    @property
    def fee(self):
        """
        The fee property - the getter
        """
        return self._fee
 
    #----------------------------------------------------------------------
    @fee.setter
    def fee(self, value):
        """
        The setter of the fee property
        """
        if isinstance(value, str):
            self._fee = Decimal(value)
        elif isinstance(value, Decimal):
            self._fee = value
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    f = Fees()

The code above demonstrates how to create a “setter” for the fee property. You can do this by decorating a second method that is also called fee with a decorator called @fee.setter. The setter is invoked when you do something like this:

>>> f = Fees()
>>> f.fee = "1"

If you look at the signature for property, it has fget, fset, fdel and doc as “arguments”. You can create another decorated method using the same name to correspond to a delete function using @fee.deleter if you want to catch the del command against the attribute.


Wrapping Up

At this point you should know how to create your own decorators and how to use a few of Python’s built-in decorators. We looked at @classmethod, @property and @staticmethod. I would be curious to know how my readers use the built-in decorators and how they use their own custom decorators.