3.2. Function Decorator with Functions

3.2.1. Syntax

  • decorator is a decorator name

  • function is a function name

  • args arbitrary number of positional arguments

  • kwargs arbitrary number of keyword arguments

Syntax:
@decorator
def my_function(*args, **kwargs):
    pass
Is equivalent to:
my_function = decorator(my_function)

3.2.2. Definition

  • function is a pointer to function which is being decorated

  • By calling function(*args, **kwargs) you actually run original (wrapped) function with it's original arguments

  • Decorator must return pointer to wrapper

  • wrapper is a closure function

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

  • wrapper gets arguments passed to function

def decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

3.2.3. Usage

def decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

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


my_function('hello')
# hello

3.2.4. Examples

3.2.4.1. File exists

import os


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


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

3.2.4.2. Debug

def debug(func):
    def wrapper(*args, **kwargs):
        print(f'Calling "{func.__name__}()", args: {args}, kwargs: {kwargs}')
        result = func(*args, **kwargs)
        print(f'Result is {result}')
        return result
    return wrapper


@debug
def add_numbers(a, b):
    return a + b


add_numbers(1, 2)
# Calling "add_numbers()", args: (1, 2), kwargs: {}
# Result is 3

add_numbers(1, b=2)
# Calling "add_numbers()", args: (1,), kwargs: {'b': 2}
# Result is 3

add_numbers(a=1, b=2)
# Calling "add_numbers()", args: (), kwargs: {'a': 1, 'b': 2}
# Result is 3

3.2.4.3. Cache

CACHE = {}

def cache(func):
    def wrapper(n):
        if n not in CACHE:
            CACHE[n] = func(n)
        return CACHE[n]
    return wrapper


@cache
def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n - 1)


factorial(5)
# 120

print(CACHE)
# {0: 1, 1: 1, 2: 2, 3: 6, 4: 24, 5: 120}
def cache(func):
    _cache = {}
    def wrapper(n):
        if n not in _cache:
            _cache[n] = func(n)
        return _cache[n]
    return wrapper


@cache
def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n - 1)


factorial(5)
# 120

3.2.4.4. Memoize

def cache(func):
    def wrapper(n):
        cache = getattr(wrapper, '__cache__', {})
        if n not in cache:
            print(f'"n={n}" Not in cache. Calculating...')
            cache[n] = func(n)
            setattr(wrapper, '__cache__', cache)
        else:
            print(f'"n={n}" Found in cache. Fetching...')
        return cache[n]
    return wrapper


@cache
def factorial(n: int) -> int:
    if n == 0:
        return 1
    else:
        return n * factorial(n-1)


print(factorial(3))
# "n=3" Not in cache. Calculating...
# "n=2" Not in cache. Calculating...
# "n=1" Not in cache. Calculating...
# "n=0" Not in cache. Calculating...
# 6

print(factorial.__cache__)
# {3: 6}

print(factorial(5))
# "n=5" Not in cache. Calculating...
# "n=4" Not in cache. Calculating...
# "n=3" Found in cache. Fetching...
# 120

print(factorial.__cache__)
# {3: 6, 4: 24, 5: 120}

print(factorial(6))
# "n=6" Not in cache. Calculating...
# "n=5" Found in cache. Fetching...
# 720

print(factorial.__cache__)
# {3: 6, 4: 24, 5: 120, 6: 720}

print(factorial(4))
# "n=4" Found in cache. Fetching...
# 24

print(factorial.__cache__)
# {3: 6, 4: 24, 5: 120, 6: 720}

3.2.4.5. Flask URL Routing

Listing 3.60. 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': 'Jan', 'last_name': 'Twardowski'}
    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)

3.2.4.6. Django Login Required

  • Decorator checks whether user is_authenticated.

  • If not, user will be redirected to login page.

from django.shortcuts import render


def edit_profile(request):
    if not request.user.is_authenticated:
        return render(request, 'templates/login_error.html')
    else:
        return render(request, 'templates/edit-profile.html')


def delete_profile(request):
    if not request.user.is_authenticated:
        return render(request, 'templates/login_error.html')
    else:
        return render(request, 'templates/delete-profile.html')
from django.shortcuts import render
from django.contrib.auth.decorators import login_required


@login_required
def edit_profile(request):
    return render(request, 'templates/edit-profile.html')


@login_required
def delete_profile(request):
    return render(request, 'templates/delete-profile.html')

3.2.5. Assignments

3.2.5.1. Memoization

English
  1. Use data from "Input" section (see below)

  2. Create function factorial_cache(n: int) -> int

  3. Create CACHE: Dict[int, int] with computation results from function

    • key: function argument

    • value: computation result

  4. Create decorator @cache

  5. Decorator must check before running function, if for given argument the computation was already done:

    • if yes, return from CACHE

    • if not, calculate new result, update cache and return computed value

  6. Using timeit

Polish
  1. Użyj kodu z sekcji "Input" (patrz poniżej)

  2. Stwórz funkcję factorial_cache(n: int) -> int

  3. Stwórz CACHE: Dict[int, int] z wynikami wyliczenia funkcji

    • klucz: argument funkcji

    • wartość: wynik obliczeń

  4. Stwórz dekorator @cache

  5. Decorator ma sprawdzać przed uruchomieniem funkcji, czy dla danego argumenu 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ść

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

Input
import sys
from timeit import timeit

sys.setrecursionlimit(5000)


def factorial_nocache(n: int) -> int:
    if n == 0:
        return 1
    else:
        return n * factorial_nocache(n-1)

duration_cache = timeit(
    stmt='factorial_cache(500); factorial_cache(400); factorial_cache(450); factorial_cache(350)',
    globals=globals(),
    number=10000,
)

duration_nocache = timeit(
    stmt='factorial_nocache(500); factorial_nocache(400); factorial_nocache(450); factorial_nocache(350)',
    globals=globals(),
    number=10000
)

print(f'factorial_cache time: {round(duration_cache, 4)} seconds')
print(f'factorial_nocache time: {round(duration_nocache, 3)} seconds')
print(f'Cached solution is {round(duration_nocache / duration_cache, 1)} times faster')

3.2.5.2. Type Checking Decorator

English

Todo

English translation

Polish
  1. Użyj danych z sekcji "Input" (patrz poniżej)

  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.

Input
def function(a: str, b: int) -> bool:
    return bool(a * b)

print(function.__annotations__)
# {'a': <class 'str'>, 'return': <class 'bool'>, 'b': <class 'int'>}