5. Decorator

5.1. Definition

5.1.1. Decorating functions

  • my_decorator is decorator name

  • func is a pointer to function which is being decorated (my_function in this case)

  • wrapper is a closure function

  • wrapper name is a convention, but you can name it anyhow

  • wrapper gets arguments passed to my_function

  • by calling func(*func_args, **func_kwargs) you actually run original (wrapped) function

  • decorator must return pointer to wrapper

def my_decorator(func):
    def wrapper(*func_args, **func_kwargs):
        return func(*func_args, **func_kwargs)
    return wrapper


@my_decorator
def my_function(x):
    print(x)


my_function('hello')
# hello

5.1.2. Decorating classes

  • my_decorator is decorator name

  • cls is a pointer to class which is being decorated (MyClass in this case)

  • Wrapper is a closure class

  • Wrapper name is a convention, but you can name it anyhow

  • Wrapper inherits from MyClass so it is similar

  • decorator must return pointer to Wrapper

def my_decorator(cls):
    class Wrapper(cls):
        my_value = 'some value'
    return Wrapper


@my_decorator
class MyClass:
    pass


print(MyClass.my_value)
# some value

5.2. Zastosowanie

  • Modify arguments

  • Modify returned value

  • Do things before call

  • Do things after call

  • Avoid calling

  • Modify global state (not a good idea)

  • Metadata

5.3. Example

5.3.1. File exists

import os


def if_file_exists(func):

    def check(filename):
        if os.path.exists(filename):
            return func(filename)
        else:
            print(f'File "{filename}" does not exists')

    return check


@if_file_exists
def print_file(filename):
    with open(filename) as file:
        content = file.read()
        print(content)


if __name__ == '__main__':
    print_file('/etc/passwd')
    print_file('/tmp/passwd')

5.3.2. Deprecated

import warnings
import functools


def deprecated(func):

    @functools.wraps(func)
    def wrapper(*args, **kwargs):

        warnings.warn(f"Call to deprecated function {func.__name__}.",
            category=DeprecationWarning,
            filename=func.func_code.co_filename,
            lineno=func.func_code.co_firstlineno + 1)

        return func(*args, **kwargs)
    return wrapper


@deprecated
def my_function():
    pass

5.3.3. Timeout

Listing 370. Decorator usage
import signal
from time import sleep


def timeout(function, seconds=2, error_message='Timeout'):

    def wrapper(*args, **kwargs):

        def handler(signum, frame):
            raise TimeoutError

        signal.signal(signal.SIGALRM, handler)
        signal.alarm(seconds)

        try:
            function(*args, **kwargs)
        except TimeoutError:
            print(error_message)
        finally:
            signal.alarm(0)

    return wrapper


@timeout
def connect(username, password, host='127.0.0.1', port='80'):
    print('Connecting...')
    sleep(5)
    print('Connected')


connect('admin', 'admin')

5.4. Class Decorators

class memoize(dict):
    def __init__(self, function):
        self.function = function

    def __call__(self, *args):
        return self[args]

    def __missing__(self, key):
        result = self[key] = self.function(*key)
        return result


@memoize
def foo(a, b):
    return a * b


foo(2, 4)       # 8
foo             # {(2, 4): 8}

foo('hi', 3)    # 'hihihi'
foo             # {(2, 4): 8, ('hi', 3): 'hihihi'}

5.5. functools

5.5.1. @functools.cached_property(func)

class DataSet:
    def __init__(self, sequence_of_numbers):
        self._data = sequence_of_numbers

    @cached_property
    def stdev(self):
        return statistics.stdev(self._data)

    @cached_property
    def variance(self):
        return statistics.variance(self._data)

5.5.2. LRU (least recently used) cache

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

>>> [fib(n) for n in range(16)]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]

>>> fib.cache_info()
CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)

5.5.3. memoize

from functools import wraps


def memoize(func):
    cache = getattr(func, '__cache__', {})

    @wraps(func)
    def wrapper(*func_args):
        if func_args in cache:
            return cache[func_args]
        else:
            result = func(*func_args)
            cache[func_args] = result
            setattr(func, '__cache__', cache)
            return result

    return wrapper


@memoize
def fibonacci(n):
    if n < 2: return n
    return fibonacci(n - 1) + fibonacci(n - 2)


print(fibonacci(25))

5.6. Przykład

5.6.1. Example 2

class LoginCheck:
    def __init__(self, func):
        self._func = func

    def __call__(self, *args):
        if is_authenticated():
            return self._func(*func_args)
        else:
            return on_error()


def is_authenticated():
    ...

def on_error():
    print('Sorry - this site private')


@LoginCheck
def display_members_page():
    print('This is the members page')

5.6.2. Example 3

import functools

def singleton(cls):
    ''' Use class as singleton. '''

    cls.__new_original__ = cls.__new__

    @functools.wraps(cls.__new__)
    def singleton_new(cls, *args, **kw):
        it =  cls.__dict__.get('__it__')
        if it is not None:
            return it

        cls.__it__ = it = cls.__new_original__(cls, *args, **kw)
        it.__init_original__(*args, **kw)
        return it

    cls.__new__ = singleton_new
    cls.__init_original__ = cls.__init__
    cls.__init__ = object.__init__

    return cls

#
# Sample use:
#

@singleton
class Foo:
    def __new__(cls):
        cls.x = 10
        return object.__new__(cls)

    def __init__(self):
        assert self.x == 10
        self.x = 15

assert Foo().x == 15
Foo().x = 20
assert Foo().x == 20

5.6.3. Use cases

Listing 371. Use case wykorzystania dekotatorów do poprawienia czytelności kodu Flask
from flask import json
from flask import Response
from flask import render_template
from flask import Flask

app = Flask(__name__)


@app.route('/summary')
def summary():
    data = {'first_name': 'José', 'last_name': 'Jiménez'}
    return Response(
        response=json.dumps(data),
        status=200,
        mimetype='application/json'
    )

@app.route('/post/<int:post_id>')
def show_post(post_id):
    post = ... # get post from Database by post_id
    return render_template('post.html', post=post)


@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
    return render_template('hello.html', name=name)
Listing 372. Use case wykorzystania dekotatorów do poprawienia czytelności kodu Django
from django.shortcuts import render
from django.contrib.auth.decorators import login_required


def edit_profile(request):
    """
    Function checks whether user is_authenticated
    If not, user will be redirected to login page
    """
    if not request.user.is_authenticated:
        return render(request, 'templates/login_error.html')
    else:
        return render(request, 'templates/edit-profile.html')


@login_required
def edit_profile(request):
    """
    Decorator checks whether user is_authenticated
    If not, user will be redirected to login page
    """
    return render(request, 'templates/edit-profile.html')

5.7. Assignments

5.7.1. Memoization

def factorial(n: int) -> int:
    if n == 0:
        return 1
    else:
        return n * factorial(n-1)
  1. Dla danego kodu funkcji factorial

  2. Stwórz dict o nazwie CACHE z wynikami wyliczenia funkcji

    • klucz: argument funkcji

    • wartość: wynik obliczeń

  3. Dodaj dekorator do funkcji factorial(n: int) z listingu poniżej

  4. Decorator ma sprawdzać przed uruchomieniem funkcji, sprawdź czy wynik został już wcześniej obliczony:

    • jeżeli tak, to zwraca dane z CACHE

    • jeżeli nie, to oblicza, aktualizuje CACHE, a następnie zwraca wartość

  5. Wykorzystując timeit porównaj prędkość działania z obliczaniem na bieżąco dla parametru 100

5.7.2. Prosty dekorator

  1. Program przechodzi przez pliki i katalogi wykorzystując os.walk.

  2. Stwórz funkcję, która wypisuje na ekranie nazwę pliku lub katalogu.

  3. Stwórz dekorator do funkcji, który przed wyświetleniem jej na ekranie podmieni ścieżkę na bezwzględną (path + filename).

5.7.3. Type Checking Decorator

Listing 373. Force type checking for function
def function(a: str, b: int) -> bool:
    return bool(a * b)

print(function.__annotations__)
# {'a': <class 'str'>, 'return': <class 'bool'>, 'b': <class 'int'>}
  1. Na podstawie kodu Listing 373.

  2. Stwórz dekorator check_types

  3. Dekorator ma sprawdzać typy danych, wszystkich parametrów wchodzących do funkcji

  4. Jeżeli, którykolwiek się nie zgadza, wyrzuć wyjątek TypeError

  5. Wyjątek ma wypisywać:

    • nazwę parametru, który ma nieprawidłowy typ,

    • listę dozwolonych typów.