31.10.2019       Выпуск 306 (28.10.2019 - 03.11.2019)       Статьи

What You Probably Don't Know About Python Decorators

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

Читать>>




Экспериментальная функция:

Ниже вы видите текст статьи по ссылке. По нему можно быстро понять ссылка достойна прочтения или нет

Просим обратить внимание, что текст по ссылке и здесь может не совпадать.

if you have ever been interviewed for a developer position, you heard this question:

What is a python decorator?
A decorator is a special kind of function that either takes a function and returns a function, or takes a class and returns a class.

Well, nope.

A decorator is a special kind of function
that either takes a function and returns a function
  • it can return anything
  • it can also take a method
  • and in some cases it can take anything
or takes a class and returns a class.

Let's dive into python decorators and find out what they can do. I will not cover the basics, like decorator with parameters, I will give some strange examples to illustrate useful cases.

Official definition


def foo(self):
    #perform method operation
foo = decorator(foo)


# Can be replaced with this 
@decorator
def foo(self):
    #perform method operation

and this -

class Foo:
  pass
Foo = decorator(Foo)

# Can be replaced with this 

@decorator
class Foo:
  pass

Looks pretty simple and a lot of people underestimate the power of decorators. The trick is there are no restrictions for the

decorator

function.

Most of the examples in the internet will show you something like this -

def print_decorator(func):
    def wrapper(*args, **kwargs):
        print('function', func.__name__, 'called with args - ', args, 'and kwargs - ', kwargs)
        result = func(*args, **kwargs)
        print('function', func.__name__, 'returns', result)
        return result
    return wrapper


@print_decorator
def mul(a, b):
    return a * b

mul(2, 2)
### function mul called with args -  (2, 2) and kwargs -  {}
### function mul returns 4
### 4
mul(3, b=5)
### function mul called with args -  (3,) and kwargs -  {'b': 5}
### function mul returns 15
### 15
### 

However you can easily do this -

@type
def func(): # func will be func = type(func) -> <class 'function'>
    return 42

print(func)
### <class 'function'>


@print
def func2(): # print doesn't return anything, so func == None
    return 42

### <function func2 at 0x7fd9c4788950>
print(func2)
### None
### 

Yes, i just used build-ins

type

and

print

as decorators. It is useless, and you will never do such nonsense, but it is possible.

Essentially anything that accept class, method or function can be a decorator.

Decorator as design pattern

Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

If you want to know more online from free sources, you can

check this out

In short, a decorator pattern wraps an object and dynamically adds some functionality to it. This is one of the roots of the confusion surrounding decorators in python, because they have the same name, can be used to implement the same idea, but can do a lot more.

Decorator features in python

Let's look at some characteristics of decorators in Python

Decorator is the syntax feature, that can be applied to

The decorator can do anything with a decorated object, for example

  • add new behaviour (classical decorator pattern from GoF book)
  • replace decorated object with something else
  • change interface of decorated class or function (can be used as implementation of Adapter pattern)
  • cache function calls (for example functools.lru_cache)
  • create relation with other objects in system (e.g. route('/path/') decorator in flask)
  • do nothing

Any callable can be used as decorator, including functions, methods,

functors

SHOW ME THE CODE!

Let's go over few examples

Replace decorated object with something else

class FunctionTracker:
    def __init__(self, func):
        self.func = func
        self.stats = []

    def __call__(self, *args, **kwargs):
        try:
            result = self.func(*args, **kwargs)
        except Exception as e:
            self.stats.append((args, kwargs, e))
            raise e
        else:
            self.stats.append((args, kwargs, result))
            return result

    @classmethod
    def track_function(cls, func):
        return cls(func)


@FunctionTracker.track_function
def func(x, y):
    return x/y

func(4, 2)
### 2.0
func(x=5, y=2)
### 2.5
func(3, 0)
### Traceback (most recent call last):
###   File "<input>", line 1, in <module>
###     func(3, 0)
###   File "<input>", line 11, in __call__
###     raise e
###   File "<input>", line 8, in __call__
###     result = self.func(*args, **kwargs)
###   File "<input>", line 3, in func
###     return x/y
### ZeroDivisionError: division by zero

func.stats
### [((4, 2), {}, 2.0), ((), {'x': 5, 'y': 2}, 2.5), ((3, 0), {}, ZeroDivisionError('division by zero',))]
func
### <__main__.FunctionTracker object at 0x7fd35978a668>

Be careful with this example, as accumulating data in this way can lead to memory leaks.

Notice how the original

func

was replaced by an instance of

FunctionTracker

, which can be used in the same way as the original function, thanks to the

__call__

method.

Create relation with other objects in system

import json
import base64
import urllib.parse


BASE64_ENCODED_JSON = 'BASE64_ENCODED_JSON'
URLENCODED = 'URLENCODED'


class DataEncoder:
    def __init__(self):
        self._registry = {} # Mapping for algorithm_name and implementation

    def register_format(self, data_format):
        def _register_encoder(encoder_function):
            self._registry[data_format] = encoder_function
            return encoder_function
        return _register_encoder

    def encode(self, data_format, data):
        return self._registry[data_format](data)


encoder = DataEncoder()


@encoder.register_format(BASE64_ENCODED_JSON)
def base64_encoded_json(data):
    return base64.b64encode(
        json.dumps(data).encode('utf-8')
    )

# Notice that `encoder.register_format` can be used as regular function
encoder.register_format(URLENCODED)(urllib.parse.urlencode)
### <function urlencode at 0x7fe9422a86a8>


encoder._registry
### {'URLENCODED': <function urlencode at 0x7fe9422a86a8>, 'BASE64_ENCODED_JSON': <function base64_encoded_json at 0x7fe9406e0620>}
data = {'greetings': 'Hello there', 'answer': 'General Kenobi'}
encoder.encode(BASE64_ENCODED_JSON, data)
### b'eyJncmVldGluZ3MiOiAiSGVsbG8gdGhlcmUiLCAiYW5zd2VyIjogIkdlbmVyYWwgS2Vub2JpIn0='
encoder.encode(URLENCODED, data)
### 'greetings=Hello+there&answer=General+Kenobi'
### 

In this example, the function

base64_encoded_json

and

urllib.parse.urlencode

are connected using decorator syntax as implementations for the

DataEncoder

class.

Flask uses a similar approach for registering functions as views via

route('/path/')

decorator; Django has

@admin.register

for registering admin classes.

It can also be used as an alternative to inheritance, in the case of

DataEncoder

I can create a subclass of

DataEncoder

with new encoding implementation instead, which isn't a best solution.

Silly example

Several decorators can be applied to one function. That means, that this

equals this

Which means, that

dec2

can accept

ANYTHING

, that

dec1

can return.

Disclaimer: There will be weird looking code here. It is here only because I can. I would never recommend anyone use this.

def dict_from_func(func):
    return {func.__name__: func}


operations = {}

@operations.update
@dict_from_func
def mul(a, b):
    return a * b


@operations.update
@dict_from_func
def add(a, b):
    return a / b


print(mul)
### None
print(operations)
### {'mul': <function mul at 0x7fdaf17bbae8>, 'add': <function add at 0x7fdaf16a2510>}
operations['mul'](2, 5)
### 10

Here I use the 

dict.update

 method as a decorator, even if it is not intended for this. This is possible because 

dict_from_func

 returns a dict, and 

dict.update

 takes a dict as an argument.

As a result, I have all the decorated functions compiled in the operations dictionary.

As a side effect - all functions are replaced with 

None

, because

dict.update

does not return any value.

Essentially this -

@operations.update
@dict_from_func
def add(a, b):
    return a / b

equals this

def add(a, b):
    return a / b

add = operations.update(dict_from_func(add))

You can try to win a bet with your colleagues using this example.

In conclusion

Decorators is amazing Python feature . You can use them for a variety of purposes. It's not just a “function or class that takes a function or class and returns a function or class”.






Разместим вашу рекламу

Пиши: mail@pythondigest.ru

Нашли опечатку?

Выделите фрагмент и отправьте нажатием Ctrl+Enter.

Система Orphus