4.2. Function Decorator with Functions

4.2.1. Rationale

Syntax:
@mydecorator
def myfunction(*args, **kwargs):
    ...
Is equivalent to:
myfunction = mydecorator(myfunction)

4.2.2. Syntax

  • 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

Listing 4.46. Definition
def mydecorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper
Listing 4.47. Decoration
@mydecorator
def myfunction():
    ...
Listing 4.48. Usage
myfunction()

4.2.3. Example

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

@run
def hello(name):
    return f'My name... {name}'

hello('José Jiménez')
# 'My name... José Jiménez'

4.2.4. Use Cases

Listing 4.49. File exists
import os

def ifexists(func):
    def wrapper(file):
        if os.path.exists(file):
            return func(file)
        else:
            print(f'File {file} does not exist')
    return wrapper


@ifexists
def display(file):
    print(f'Printing file {file}')


display('/etc/passwd')
# Printing file /etc/passwd

display('/tmp/passwd')
# File /tmp/passwd does not exist
Listing 4.50. Timeit
from datetime import datetime


def timeit(func):
    def wrapper(*args, **kwargs):
        start = datetime.now()
        result = func(*args, **kwargs)
        end = datetime.now()
        print(f'Duration: {end-start}')
        return result
    return wrapper


@timeit
def add(a, b):
    return a + b


add(1, 2)
# Duration: 0:00:00.000006
# 3

add(1, b=2)
# Duration: 0:00:00.000007
# 3

add(a=1, b=2)
# Duration: 0:00:00.000008
# 3
Listing 4.51. Debug
def debug(func):
    def wrapper(*args, **kwargs):
        function = func.__name__
        print(f'Calling: {function=}, {args=}, {kwargs=}')
        result = func(*args, **kwargs)
        print(f'Result: {result}')
        return result
    return wrapper


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


add(1, 2)
# Calling: function='add', args=(1, 2), kwargs={}
# Result: 3
# 3

add(1, b=2)
# Calling: function='add', args=(1,), kwargs={'b': 2}
# Result: 3
# 3

add(a=1, b=2)
# Calling: function='add', args=(), kwargs={'a': 1, 'b': 2}
# Result: 3
# 3
Listing 4.52. Stacked decorators
from datetime import datetime
import logging

logging.basicConfig(
    level='DEBUG',
    datefmt='"%Y-%m-%d", "%H:%M:%S"',
    format='{asctime}, "{levelname}", "{message}"',
    style='{')

log = logging.getLogger(__name__)


def timeit(func):
    def wrapper(*args, **kwargs):
        start = datetime.now()
        result = func(*args, **kwargs)
        end = datetime.now()
        log.info(f'Duration: {end - start}')
        return result
    return wrapper


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


@timeit
@debug
def add(a, b):
    return a + b


add(1, 2)
# "1969-07-21", "02:56:15", "DEBUG", "Calling: function='add', args=(1, 2), kwargs={}"
# "1969-07-21", "02:56:15", "DEBUG", "Result: 3"
# "1969-07-21", "02:56:15", "INFO", "Duration: 0:00:00.000209"

add(1, b=2)
# "1969-07-21", "02:56:15", "DEBUG", "Calling: function='add', args=(1,), kwargs={'b': 2}"
# "1969-07-21", "02:56:15", "DEBUG", "Result: 3"
# "1969-07-21", "02:56:15", "INFO", "Duration: 0:00:00.000154"

add(a=1, b=2)
# "1969-07-21", "02:56:15", "DEBUG", "Calling: function='add', args=(), kwargs={'a': 1, 'b': 2}"
# "1969-07-21", "02:56:15", "DEBUG", "Result: 3"
# "1969-07-21", "02:56:15", "INFO", "Duration: 0:00:00.000083"

4.2.5. Scope

Listing 4.53. Recap information about factorial (n!)
"""
5! = 5 * 4!
4! = 4 * 3!
3! = 3 * 2!
2! = 2 * 1!
1! = 1 * 0!
0! = 1
"""

factorial(5)                                    # = 120
    return 5 * factorial(4)                     # 5 * 24 = 120
        return 4 * factorial(3)                 # 4 * 6 = 24
            return 3 * factorial(2)             # 3 * 2 = 6
                return 2 * factorial(1)         # 2 * 1 = 2
                    return 1 * factorial(0)     # 1 * 1 = 1
                        return 1                # 1
Listing 4.54. Cache with global scope
_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}
Listing 4.55. Cache with local scope
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
Listing 4.56. Cache with embedded scope
def cache(func):
    def wrapper(n):
        if n not in wrapper._cache:
            wrapper._cache[n] = func(n)
        return wrapper._cache[n]
    if not hasattr(wrapper, '_cache'):
        setattr(wrapper, '_cache', {})
    return wrapper


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


print(factorial(4))
# 24

print(factorial._cache)
# {0: 1,
#  1: 1,
#  2: 2,
#  3: 6,
#  4: 24}

print(factorial(6))
# 720

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

print(factorial(6))
# 720

print(factorial._cache)
# {0: 1,
#  1: 1,
#  2: 2,
#  3: 6,
#  4: 24,
#  5: 120,
#  6: 720}

print(factorial(3))
# 6

print(factorial._cache)
# {0: 1,
#  1: 1,
#  2: 2,
#  3: 6,
#  4: 24,
#  5: 120,
#  6: 720}

4.2.6. Examples

Listing 4.57. Flask URL Routing
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 = {'firstname': 'Jan', 'lastname': 'Twardowski'}
    return Response(
        response=json.dumps(data),
        status=200,
        mimetype='application/json')


@app.route('/post/<int:pk>')
def post(pk):
    post = ... # get post from Database by pk
    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 4.58. FastAPI URL routing
from typing import Optional
from fastapi import FastAPI

app = FastAPI()


@app.get('/')
async def index():
    return {'Hello': 'World'}


@app.get('/items/{pk}')
async def items(pk: int, q: Optional[str] = None):
    return {'pk': pk, 'q': q}
Listing 4.59. 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')

4.2.7. Assignments

4.2.7.1. Decorator Function Disable

  • Assignment name: Decorator Function Disable

  • Last update: 2020-10-13

  • Complexity level: easy

  • Lines of code to write: 5 lines

  • Estimated time of completion: 8 min

  • Solution: solution/decorator_func_disable.py

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

  2. Create decorator disable

  3. Decorator raises an exception PermissionError and does not execute function

  4. Compare result with "Output" section (see below)

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

  2. Stwórz dekorator disable

  3. Dekorator podnosi wyjątek PermissionError i nie wywołuje funkcji

  4. Porównaj wyniki z sekcją "Output" (patrz poniżej)

Input
@disable
def echo(text):
    print(text)
Output
>>> from inspect import isfunction
>>> assert isfunction(check)
>>> assert isfunction(check(lambda: None))
>>> assert isfunction(echo)

>>> echo('hello')
Traceback (most recent call last):
    ...
PermissionError: Function is disabled

4.2.7.2. Decorator Function Check

  • Assignment name: Decorator Function Check

  • Last update: 2020-10-13

  • Complexity level: easy

  • Lines of code to write: 5 lines

  • Estimated time of completion: 8 min

  • Solution: solution/decorator_func_check.py

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

  2. Create decorator check

  3. Decorator calls function, only when echo.disabled is False

  4. Note that decorators overwrite pointers and in wrapper you must check if wrapper.disabled is False

  5. Else raise an exception PermissionError

  6. Compare result with "Output" section (see below)

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

  2. Stwórz dekorator check

  3. Dekorator wywołuje funkcję, tylko gdy echo.disabled jest False

  4. Zwróć uwagę, że dekoratory nadpisują wskaźniki i we wrapper musisz sprawdzić czy wrapper.disabled jest False

  5. W przeciwnym przypadku podnieś wyjątek PermissionError

  6. Porównaj wyniki z sekcją "Output" (patrz poniżej)

Input
@check
def echo(text):
    print(text)
Output
>>> from inspect import isfunction
>>> assert isfunction(check)
>>> assert isfunction(check(lambda: None))
>>> assert isfunction(echo)
>>> assert hasattr(echo, 'disabled')

>>> echo.disabled = False
>>> echo('hello')
hello

>>> echo.disabled = True
>>> echo('hello')
Traceback (most recent call last):
    ...
PermissionError: Function is disabled

4.2.7.3. Decorator Function Astronauts

  • Assignment name: Decorator Function Astronauts

  • Last update: 2020-10-01

  • Complexity level: easy

  • Lines of code to write: 7 lines

  • Estimated time of completion: 8 min

  • Solution: solution/decorator_func_astronauts.py

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

  2. Create decorator check_astronauts

  3. To answer if person is an astronaut check field is_astronaut in crew: list[dict]

  4. Decorator will call decorated function, only if all crew members are astronauts

  5. If any member is not an astronaut raise PermissionError and print his first name and last name

  6. Compare result with "Output" section (see below)

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

  2. Stwórz dekorator check_astronauts

  3. Aby odpowiedzieć czy osoba jest astronautą sprawdź pole is_astronaut in crew: list[dict]

  4. Dekorator wywoła dekorowaną funkcję, tylko gdy wszyscy członkowie załogi są astronautami

  5. Jeżeli, jakikolwiek członek nie jest astronautą, podnieś wyjątek PermissionError i wypisz jego imię i nazwisko

  6. Porównaj wyniki z sekcją "Output" (patrz poniżej)

Input
CREW_PRIMARY = [
    {'is_astronaut': True, 'name': 'Jan Twardowski'},
    {'is_astronaut': True, 'name': 'Mark Watney'},
    {'is_astronaut': True, 'name': 'Melissa Lewis'}]

CREW_BACKUP = [
    {'is_astronaut': True, 'name': 'Melissa Lewis'},
    {'is_astronaut': True, 'name': 'Mark Watney'},
    {'is_astronaut': False, 'name': 'Alex Vogel'}]


@check_astronauts
def launch(crew):
    crew = ', '.join(astro['name'] for astro in crew)
    return f'Launching: {crew}'
Output
>>> from inspect import isfunction
>>> assert isfunction(check_astronauts)
>>> assert isfunction(check_astronauts(lambda: None))
>>> assert isfunction(launch)

>>> launch(CREW_PRIMARY)
'Launching: Jan Twardowski, Mark Watney, Melissa Lewis'

>>> launch(CREW_BACKUP)
Traceback (most recent call last):
    ...
PermissionError: Alex Vogel is not an astronaut

4.2.7.4. Decorator Function Memoization

  • Assignment name: Decorator Function Memoization

  • Last update: 2020-10-01

  • Complexity level: easy

  • Lines of code to write: 5 lines

  • Estimated time of completion: 13 min

  • Solution: solution/decorator_func_memoization.py

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

  2. Create decorator @cache

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

  4. Compare execution time using timeit

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

  2. Stwórz dekorator @cache

  3. Decorator ma sprawdzać przed uruchomieniem funkcji, czy dla danego argumentu 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 za pomocą timeit

Input
import sys
from timeit import timeit
sys.setrecursionlimit(5000)


def cache(func):
    _cache = {}
    raise NotImplementedError


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


def fn2(n):
    if n == 0:
        return 1
    else:
        return n * fn2(n-1)


duration_cache = timeit(stmt='fn1(500); fn1(400); fn1(450); fn1(350)', globals=globals(), number=100_000)
duration_nocache = timeit(stmt='fn2(500); fn2(400); fn2(450); fn2(350)', globals=globals(), number=100_000)
duration_ratio = duration_nocache / duration_cache

print(f'With Cache time: {duration_cache:.4f} seconds')
print(f'Without Cache time: {duration_nocache:.3f} seconds')
print(f'Cached solution is {duration_ratio:.1f} times faster')

4.2.7.5. Decorator Function Abspath

  • Assignment name: Decorator Function Abspath

  • Last update: 2020-10-01

  • Complexity level: easy

  • Lines of code to write: 7 lines

  • Estimated time of completion: 13 min

  • Solution: solution/decorator_func_abspath.py

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

  2. Absolute path is when path starts with current_directory

  3. Create function decorator abspath

  4. If path is relative, then abspath will convert it to absolute

  5. If path is absolute, then abspath will not modify it

  6. Compare result with "Output" section (see below)

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

  2. Ścieżka bezwzględna jest gdy path zaczyna się od current_directory

  3. Stwórz funkcję dekorator abspath

  4. Jeżeli path jest względne, to abspath zamieni ją na bezwzględną

  5. Jeżeli path jest bezwzględna, to abspath nie będzie jej modyfikował

  6. Porównaj wyniki z sekcją "Output" (patrz poniżej)

Input
@abspath
def display(path):
    return str(path)
Output
>>> from pathlib import Path
>>> cwd = str(Path().cwd())
>>> display('iris.csv').startswith(cwd)
True
>>> display('iris.csv').endswith('iris.csv')
True
>>> display('/home/python/iris.csv')
'/home/python/iris.csv'
Hints
  • from pathlib import Path

  • current_directory = Path.cwd()

  • path = Path(current_directory, filename)

4.2.7.6. Decorator Function Numeric

  • Assignment name: Decorator Function Numeric

  • Last update: 2020-10-13

  • Complexity level: easy

  • Lines of code to write: 8 lines

  • Estimated time of completion: 8 min

  • Solution: solution/decorator_func_numeric.py

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

  2. Create decorator function numeric

  3. Decorator checks types arguments a and b

  4. If type a or b are not int or float raise exception TypeError

  5. Compare result with "Output" section (see below)

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

  2. Stwórz dekorator funkcję numeric

  3. Dekorator sprawdza typy argumentów a oraz b

  4. Jeżeli typ a lub b nie jest int lub float to podnieś wyjątek TypeError

  5. Porównaj wyniki z sekcją "Output" (patrz poniżej)

Output
>>> from inspect import isfunction
>>> assert isfunction(numeric)
>>> assert isfunction(numeric(lambda: None))

>>> @numeric
... def add(a, b):
...     return a + b

>>> add(1, 1)
2
>>> add(1.5, 2.5)
4.0
>>> add(-1, 1.5)
0.5
>>> add('one', 1)
Traceback (most recent call last):
    ...
TypeError: Argument "a" must be int or float
>>> add(1, 'two')
Traceback (most recent call last):
    ...
TypeError: Argument "b" must be int or float

4.2.7.7. Decorator Function Type Check

  • Assignment name: Decorator Function Type Check

  • Last update: 2020-10-01

  • Complexity level: hard

  • Lines of code to write: 15 lines

  • Estimated time of completion: 21 min

  • Solution: solution/decorator_func_typecheck.py

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

  2. Create decorator function typecheck

  3. Decorator checks types of all arguments (*args oraz **kwargs)

  4. Decorator checks return type

  5. In case when received type is not expected throw an exception TypeError with:

    • argument name

    • actual type

    • expected type

  6. Compare result with "Output" section (see below)

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

  2. Stwórz dekorator funkcję typecheck

  3. Dekorator sprawdza typy wszystkich argumentów (*args oraz **kwargs)

  4. Dekorator sprawdza typ zwracany

  5. W przypadku gdy otrzymany typ nie jest równy oczekiwanemu wyrzuć wyjątek TypeError z:

    • nazwa argumentu

    • aktualny typ

    • oczekiwany typ

  6. Porównaj wyniki z sekcją "Output" (patrz poniżej)

Input
@typecheck
def echo(a: str, b: int, c: float = 0.0) -> bool:
    return bool(a * b)
Output
>>> echo('one', 1)
True
>>> echo('one', 1, 1.1)
True
>>> echo('one', b=1)
True
>>> echo('one', 1, c=1.1)
True
>>> echo('one', b=1, c=1.1)
True
>>> echo(a='one', b=1, c=1.1)
True
>>> echo(c=1.1, b=1, a='one')
True
>>> echo(b=1, c=1.1, a='one')
True
>>> echo('one', c=1.1, b=1)
True

>>> echo(1, 1)
Traceback (most recent call last):
...
TypeError: "a" is <class 'int'>, but <class 'str'> was expected

>>> echo('one', 'two')
Traceback (most recent call last):
...
TypeError: "b" is <class 'str'>, but <class 'int'> was expected

>>> echo('one', 1, 'two')
Traceback (most recent call last):
...
TypeError: "c" is <class 'str'>, but <class 'float'> was expected

>>> echo(b='one', a='two')
Traceback (most recent call last):
...
TypeError: "b" is <class 'str'>, but <class 'int'> was expected

>>> echo('one', c=1.1, b=1.1)
Traceback (most recent call last):
...
TypeError: "b" is <class 'float'>, but <class 'int'> was expected
Hints
echo.__annotations__
# {'a': <class 'str'>, 'b': <class 'int'>, 'c': <class 'float'>, 'return': <class 'bool'>}