19. Decorator

19.1. Example

Code Listing 19.3. 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')

19.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

19.2.1. Przykład zastosowania

  • Zagnieżdżone
  • wykonywane od góry
@permission_required(uid=0)
@modyfikuj_sciezke_w_zaleznosci_od_systemu_operacyjnego
@timeout(seconds=10, error_message='za dlugo')
def instaluj_oprogramowanie(sciezka, nazwa_oprogramowania, wersja_paczki):
    pass

19.3. Function Decorators

19.3.1. Decorator as function

def my_decorator(f):
    def wrapper(*args, **kwargs):
        return f(*args, **kwargs)
    return wrapper

@my_decorator
def func(x):
    return x

19.3.2. Decorator as class

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'}

19.4. Class Decorators

Code Listing 19.4. Class Decorator
def decorator(cls):
    class NewClass(cls):
        attr = 100

    return NewClass


@decorator
class X:
    pass


@decorator
class Y:
    pass


@decorator
class Z:
    pass


X.attr  # 100
Y.attr  # 100
Z.attr  # 100

19.4.1. @staticmethod

class Foo:
    def __init__(self, tekst='Jose'):
        self.tekst = tekst

    def hello(self):
        print(f'hello {self.tekst}')

    @staticmethod
    def ehlo():
        print('hello')


# intuicyjna implementacja
def staticmethod(method):
    def wrapper(*args, **kwargs):
        args = (x for x in args if not instanceof(arg, Foo))
        return method(*args, **kwargs)
    return wrapper

19.4.2. @classmethod

  • @classmehtod turns a normal method to a factory method.
  • first argument for @classmethod function must always be cls (class)
  • Factory methods, that are used to create an instance for a class using for example some sort of pre-processing.
  • Static methods calling static methods: if you split a static methods in several static methods, you shouldn’t hard-code the class name but use class methods
class Hero:

  @staticmethod
  def say_hello():
     print("Helllo...")

  @classmethod
  def say_class_hello(cls):
     if cls.__name__ == "HeroSon":
        print("Hi Kido")
     elif cls.__name__ == "HeroDaughter":
        print("Hi Princess")


class HeroSon(Hero):
  def say_son_hello(self):
     print("test hello")


class HeroDaughter(Hero):
  def say_daughter_hello(self):
     print("test hello daughter")


testson = HeroSon()
testson.say_class_hello()       # "Hi Kido"
testson.say_hello()             # "Helllo..."

testdaughter = HeroDaughter()
testdaughter.say_class_hello()  # "Hi Princess"
testdaughter.say_hello()        # "Helllo..."

19.5. functools

19.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)

19.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)

19.5.3. memoize

def memoize(function):
    from functools import wraps

    memo = {}

    @wraps(function)
    def wrapper(*args):
        if args in memo:
            return memo[args]
        else:
            rv = function(*args)
            memo[args] = rv
            return rv
    return wrapper


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

fibonacci(25)

19.6. Przykład

import os
import logging


def if_file_exists(function):

    def check(filename):
        if os.path.exists(filename):
            function(filename)
        else:
            logging.error('File "%(filename)s" does not exists, will not execute!', locals())

    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')

19.6.1. Case Study

Code Listing 19.5. Case Study 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)
Code Listing 19.6. Case Study 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, 'myapp/login_error.html')
    else:
        return render(request, 'myapp/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, 'myapp/edit_profile.html')

19.7. Assignments

19.7.1. Prosty dekorator

  • Program przechodzi przez pliki i katalogi wykorzystując os.walk.
  • Stwórz funkcję, która wypisuje na ekranie nazwę pliku lub katalogu.
  • Stwórz dekorator do funkcji, który przed wyświetleniem jej na ekranie podmieni ścieżkę na bezwzględną (path + filename).
About:
  • Filename: decorator_abspath.py
  • Lines of code to write: 10 lines
  • Estimated time of completion: 15 min

19.7.2. Memoization

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

    • klucz: argument funkcji
    • wartość: wynik obliczeń
  2. Zmodyfikuj funkcję factorial(n: int) z listingu poniżej

  3. 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ść
  4. Porównaj prędkość działania z obliczaniem na bieżąco dla parametru 500

About:
  • Filename: decorator_memoize.py
  • Lines of code to write: 5 lines
  • Estimated time of completion: 15 min
Hints: