5.2. Decorator Function with Func¶
5.2.1. Rationale¶
- Syntax:
@mydecorator def myfunction(*args, **kwargs): ...
- Is equivalent to:
myfunction = mydecorator(myfunction)
5.2.2. Syntax¶
Decorator must return pointer to
wrapper
wrapper
is a closure functionwrapper
name is a convention, but you can name it anyhowwrapper
gets arguments passed tofunction
Definition:
def mydecorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
Decoration:
@mydecorator
def myfunction():
...
Usage:
myfunction()
5.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'
5.2.4. Use Cases¶
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
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
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
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"
5.2.5. Scope¶
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
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)
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}
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
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)
# {0: 1,
# 1: 1,
# 2: 2,
# 3: 6,
# 4: 24,
# 5: 120,
# 6: 720}
print(factorial(6))
# 720
print(factorial(3))
# 6
print(factorial._cache)
# {0: 1,
# 1: 1,
# 2: 2,
# 3: 6,
# 4: 24,
# 5: 120,
# 6: 720}
5.2.6. Example¶
DATABASE = {
'mlewis': {'name': 'Melissa Lewis', 'email': 'melissa.lewis@nasa.gov'},
'mwatney': {'name': 'Mark Watney', 'email': 'mark.watney@nasa.gov'},
'avogel': {'name': 'Alex Vogel', 'email': 'alex.vogel@nasa.gov'},
'rmartinez': {'name': 'Rick Martinez', 'email': 'rick.martinez@nasa.gov'},
'bjohansen': {'name': 'Beth Johanssen', 'email': 'beth.johanssen@nasa.gov'},
'cbeck': {'name': 'Chris Beck', 'email': 'chris.beck@nasa.gov'},
}
_cache = {}
def cache(func):
def wrapper(username):
if username not in _cache:
_cache[username] = func(username)
return _cache[username]
return wrapper
@cache
def db_search(username):
return DATABASE[username]['name']
db_search('mwatney') # not in cache, searches database and updates cache with result
# 'Mark Watney'
db_search('mwatney') # found in cache and returns from it, no database search
# 'Mark Watney'
print(_cache)
# {'mwatney': 'Mark Watney'}
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)
FastAPI URL routing:
from typing import Optional
from fastapi import FastAPI
app = FastAPI()
@app.get('/')
async def index():
return {'message': 'Hello World'}
@app.get('/user/{pk}')
async def user(pk: int):
return {'pk': pk}
@app.get('/search')
async def items(q: Optional[str] = None):
return {'q': q}
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')
5.2.7. Assignments¶
"""
* Assignment: Decorator Function Syntax
* Complexity: easy
* Lines of code: 5 lines
* Time: 5 min
English:
1. Create decorator `mydecorator`
2. Decorator should have `wrapper` with `*args` and `**kwargs` parameters
3. Wrapper should call original function with it's original parameters,
and return its value
4. Decorator should return `wrapper` function
5. Compare result with "Tests" section (see below)
Polish:
1. Stwórz dekorator `mydecorator`
2. Dekorator powinien mieć `wrapper` z parametrami `*args` i `**kwargs`
3. Wrapper powinien wywoływać oryginalną funkcję z jej oryginalnymi
parametrami i zwracać jej wartość
4. Decorator powinien zwracać funckję `wrapper`
5. Porównaj wyniki z sekcją "Tests" (patrz poniżej)
Tests:
>>> from inspect import isfunction
>>> assert isfunction(mydecorator)
>>> assert isfunction(mydecorator(lambda: None))
>>> @mydecorator
... def echo(text):
... return text
>>> echo('hello')
'hello'
"""
"""
* Assignment: Decorator Function Disable
* Complexity: easy
* Lines of code: 1 lines
* Time: 5 min
English:
1. Use data from "Given" section (see below)
2. Modify decorator `disable`
3. Decorator raises an exception `PermissionError` and does not execute function
4. Compare result with "Tests" section (see below)
Polish:
1. Użyj kodu z sekcji "Given" (patrz poniżej)
2. Zmodyfikuj dekorator `disable`
3. Dekorator podnosi wyjątek `PermissionError` i nie wywołuje funkcji
4. Porównaj wyniki z sekcją "Tests" (patrz poniżej)
Tests:
>>> from inspect import isfunction
>>> assert isfunction(disable)
>>> assert isfunction(disable(lambda: None))
>>> @disable
... def echo(text):
... print(text)
>>> echo('hello')
Traceback (most recent call last):
PermissionError: Function is disabled
"""
# Given
def disable(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
"""
* Assignment: Decorator Function Check
* Complexity: easy
* Lines of code: 3 lines
* Time: 5 min
English:
1. Use data from "Given" 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 "Tests" section (see below)
Polish:
1. Użyj kodu z sekcji "Given" (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ą "Tests" (patrz poniżej)
Tests:
>>> @check
... def echo(text):
... print(text)
>>> from inspect import isfunction
>>> assert isfunction(check)
>>> assert isfunction(check(lambda: None))
>>> assert isfunction(echo)
>>> echo.disabled = False
>>> echo('hello')
hello
>>> echo.disabled = True
>>> echo('hello')
Traceback (most recent call last):
PermissionError: Function is disabled
>>> assert hasattr(echo, 'disabled')
"""
# Given
def check(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
"""
* Assignment: Decorator Function Astronauts
* Complexity: easy
* Lines of code: 3 lines
* Time: 8 min
English:
1. Use data from "Given" section (see below)
2. Modify decorator `check_astronauts`
3. To answer if person is an astronaut check field:
a. `is_astronaut` in `crew: list[dict]`
4. Decorator will call 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 "Tests" section (see below)
Polish:
1. Użyj kodu z sekcji "Given" (patrz poniżej)
2. Zmodufikuj dekorator `check_astronauts`
3. Aby odpowiedzieć czy osoba jest astronautą sprawdź pole:
a. `is_astronaut` in `crew: list[dict]`
4. Dekorator wywoła 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ą "Tests" (patrz poniżej)
Tests:
>>> from inspect import isfunction
>>> assert isfunction(check_astronauts)
>>> assert isfunction(check_astronauts(lambda: None))
>>> @check_astronauts
... def launch(crew):
... crew = ', '.join(astro['name'] for astro in crew)
... return f'Launching: {crew}'
>>> 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
"""
# Given
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'}]
def check_astronauts(func):
def wrapper(crew):
return func(crew)
return wrapper
"""
* Assignment: Decorator Function Memoization
* Complexity: easy
* Lines of code: 3 lines
* Time: 8 min
English:
1. Use data from "Given" section (see below)
2. Create decorator `@cache`
3. Decorator must check before running function, if for given argument
the computation was already done:
a. if yes, return from `_cache`
b. if not, calculate new result, update cache and return computed value
4. Compare execution time using `timeit` (it might take around 30 seconds)
5. Last three tests will fail, this is only infomation about execution time
Polish:
1. Użyj kodu z sekcji "Given" (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:
a. jeżeli tak, to zwraca dane z `_cache`
b. jeżeli nie, to oblicza, aktualizuje `_cache`, a następnie zwraca wartość
4. Porównaj prędkość działania za pomocą `timeit` (może to trwać około 30 sekund)
5. Ostatnie trzy testy nie przejdą, to tylko informacja o czasie wykonywania
Tests:
>>> from timeit import timeit
>>> from sys import setrecursionlimit
>>> setrecursionlimit(5000)
>>> @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=10_000)
>>> duration_nocache = timeit(stmt='fn2(500); fn2(400); fn2(450); fn2(350)',
... globals=globals(), number=10_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')
TODO: Make tests faster
"""
# Given
_cache = {}
def cache(func):
def wrapper(n):
return func(n)
return wrapper
""""
* Assignment: Decorator Function Abspath
* Complexity: easy
* Lines of code: 5 lines
* Time: 5 min
English:
1. Use data from "Given" 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 "Tests" section (see below)
Polish:
1. Użyj danych z sekcji "Given" (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ą "Tests" (patrz poniżej)
Hints:
* `path = Path(CURRENT_DIR, filename)`
Tests:
>>> @abspath
... def display(path):
... return str(path)
>>> display('iris.csv').startswith(str(CURRENT_DIR))
True
>>> display('iris.csv').endswith('iris.csv')
True
>>> display('/home/python/iris.csv')
'/home/python/iris.csv'
"""
# Given
from pathlib import Path
CURRENT_DIR = Path().cwd()
"""
* Assignment: Decorator Function Numeric
* Complexity: easy
* Lines of code: 4 lines
* Time: 5 min
English:
1. Use data from "Given" section (see below)
2. Modify decorator `numeric`
3. Decorator must check arguments `a` and `b` types
4. If type `a` or `b` are not `int` or `float` raise exception `TypeError`
5. Compare result with "Tests" section (see below)
Polish:
1. Użyj kodu z sekcji "Given" (patrz poniżej)
2. Zmodyfikuj dekorator `numeric`
3. Dekorator ma 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ą "Tests" (patrz poniżej)
Tests:
>>> 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
"""
# Given
def numeric(func):
def wrapper(a, b):
return func(a, b)
return wrapper
"""
* Assignment: Decorator Function Type Check
* Complexity: hard
* Lines of code: 15 lines
* Time: 21 min
English:
1. Use code from "Given" section (see below)
1. Modify decorator `typecheck`
2. Decorator checks types of all arguments (`*args` oraz `**kwargs`)
3. Decorator checks return type
4. In case when received type is not expected throw an exception `TypeError` with:
a. argument name
b. actual type
c. expected type
5. Compare result with "Tests" section (see below)
Polish:
1. Użyj kodu z sekcji "Given" (patrz poniżej)
1. Zmodyfikuj dekorator `typecheck`
2. Dekorator sprawdza typy wszystkich argumentów (`*args` oraz `**kwargs`)
3. Dekorator sprawdza typ zwracany
4. W przypadku gdy otrzymany typ nie jest równy oczekiwanemu wyrzuć wyjątek `TypeError` z:
a. nazwa argumentu
b. aktualny typ
c. oczekiwany typ
5. Porównaj wyniki z sekcją "Tests" (patrz poniżej)
Hints:
* `echo.__annotations__`
# {'a': <class 'str'>,
# 'b': <class 'int'>,
# 'c': <class 'float'>,
# 'return': <class 'bool'>}
s
Tests:
>>> @typecheck
... def echo(a: str, b: int, c: float = 0.0) -> bool:
... return bool(a * b)
>>> 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
"""
# Given
def typecheck(func):
def merge(*args, **kwargs):
"""Function merges *args, and **kwargs into single dict"""
args = dict(zip(func.__annotations__.keys(), args))
return kwargs | args # Python 3.9
# return {**args, **kwargs)} # Python 3.7, 3.8
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper