5.6. Decorate Function¶
Decorator must return reference 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.6.1. 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.6.2. Use Case - 0x01¶
Check if file exists, before executing function:
>>> 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
5.6.3. Use Case - 0x02¶
Timeit
>>> from time import time
>>>
>>>
>>> def timeit(func):
... def wrapper(*args, **kwargs):
... start = time()
... result = func(*args, **kwargs)
... end = time()
... duration = end - start
... print(f'Duration: {duration}')
... 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
5.6.4. Use Case - 0x03¶
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
5.6.5. Use Case - 0x04¶
Deprecated
>>> import warnings
>>>
>>>
>>> def deprecated(func):
... def wrapper(*args, **kwargs):
... name = func.__name__
... file = func.__code__.co_filename
... line = func.__code__.co_firstlineno + 1
... message = f'Call to deprecated function `{name}` in {file} at line {line}'
... warnings.warn(message, DeprecationWarning)
... return func(*args, **kwargs)
... return wrapper
>>>
>>>
>>> @deprecated
... def add(a, b):
... return a + b
>>>
>>>
>>> add(1, 2)
/home/python/myscript.py:11: DeprecationWarning: Call to deprecated function `add` in /home/python/myscript.py at line 19
5.6.6. Use Case - 0x05¶
Stacked Decorators
>>> from datetime import datetime
>>> import logging
>>>
>>> logging.basicConfig(
... level='DEBUG',
... format='{asctime}, "{levelname}", "{message}"',
... datefmt='"%Y-%m-%d", "%H:%M:%S"',
... 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.6.7. Use Case - 0x06¶
Global Scope Cache
Recap information about factorial (n!
):
5! = 5 * 4!
4! = 4 * 3!
3! = 3 * 2!
2! = 2 * 1!
1! = 1 * 0!
0! = 1
n! = n * (n-1)! # 1 for n=0
>>> def factorial(n):
... if n == 0:
... return 1
... else:
... return n * factorial(n-1)
5.6.8. Use Case - 0x07¶
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}
5.6.9. Use Case - 0x08¶
Local Scope Cache
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
5.6.10. Use Case - 0x09¶
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.6.11. Use Case - 0x0A¶
Database Cache
>>> 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'},
... 'bjohanssen': {'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'}
5.6.12. Use Case - 0x0B¶
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.6.13. Assignments¶
"""
* Assignment: Decorator Function Check
* Complexity: easy
* Lines of code: 3 lines
* Time: 5 min
English:
1. Create decorator `check`
2. Decorator calls function, only when `echo.disabled` is `False`
3. Note that decorators overwrite reference and in `wrapper`
you must check if `wrapper.disabled` is `False`
4. Else raise an exception `PermissionError`
5. Run doctests - all must succeed
Polish:
1. Stwórz dekorator `check`
2. Dekorator wywołuje funkcję, tylko gdy `echo.disabled` jest `False`
3. Zwróć uwagę, że dekoratory nadpisują referencje i we `wrapper`
musisz sprawdzić czy `wrapper.disabled` jest `False`
4. W przeciwnym przypadku podnieś wyjątek `PermissionError`
5. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isfunction
>>> assert isfunction(check), \
'Create check() function'
>>> assert isfunction(check(lambda: ...)), \
'check() should take function as an argument'
>>> @check
... def echo(text):
... print(text)
>>> assert isfunction(echo), \
'Decorator check() should return a function'
>>> echo.disabled = False
>>> echo('hello')
hello
>>> echo.disabled = True
>>> echo('hello')
Traceback (most recent call last):
PermissionError: Function is disabled
>>> assert hasattr(echo, 'disabled')
"""
# type: Callable[[Callable], Callable]
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. Modify decorator `check_astronauts`
2. To answer if person is an astronaut check field:
a. `is_astronaut` in `crew: list[dict]`
3. Decorator will call function, only if all crew members are astronauts
4. If any member is not an astronaut raise `PermissionError` and print
his first name and last name
5. Run doctests - all must succeed
Polish:
1. Zmodufikuj dekorator `check_astronauts`
2. Aby odpowiedzieć czy osoba jest astronautą sprawdź pole:
a. `is_astronaut` in `crew: list[dict]`
3. Dekorator wywoła funkcję, tylko gdy wszyscy załoganci są astronautami
4. Jeżeli, jakikolwiek członek nie jest astronautą, podnieś wyjątek
`PermissionError` i wypisz jego imię i nazwisko
5. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isfunction
>>> assert isfunction(check_astronauts), \
'Create check_astronauts() function'
>>> assert isfunction(check_astronauts(lambda: ...)), \
'check_astronauts() should take function as an argument'
>>> @check_astronauts
... def launch(crew):
... crew = ', '.join(astro['name'] for astro in crew)
... return f'Launching: {crew}'
>>> launch(CREW_PRIMARY)
'Launching: Pan Twardowski, Mark Watney, Melissa Lewis'
>>> launch(CREW_BACKUP)
Traceback (most recent call last):
PermissionError: Alex Vogel is not an astronaut
"""
CREW_PRIMARY = [
{'is_astronaut': True, 'name': 'Pan 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'}]
# type: Callable[[Callable], Callable]
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. Create decorator `@cache`
2. 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 value
3. Using `timeit` compare execution time (it might take around 30 seconds)
4. Last three tests (prints) are only infomation about execution time
to see it, remove comment '# doctest: +SKIP' this renders
test failures, but in return you'll get information about execution time
5. Run doctests - all must succeed (beside three prints)
Polish:
1. Stwórz dekorator `@cache`
2. Decorator ma sprawdzać przed uruchomieniem funkcji, czy dla danego
argumentu wynik został już wcześniej obliczony:
a. jeżeli tak, zwróć dane z `_cache`
b. jeżeli nie, oblicz, zaktualizuj `_cache` i zwróć wartość
3. Używając `timeit` porównaj czas wykonywania (może trwać około 30 sekund)
4. Ostatnie trzy testy (printy) to tylko informacja o czasie wykonywania
aby ją zobaczyć, usuń komentarz '# doctest: +SKIP' to spowoduje,
że testy przestaną przechodzić, ale w zamian wyświetlą czas wykonywania
5. Uruchom doctesty - wszystkie muszą się powieść (poza trzema printami)
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from timeit import timeit
>>> from inspect import isfunction
>>> sys.setrecursionlimit(5000)
>>> assert type(_cache) is dict, \
'Cache storage should be a dict'
>>> assert len(_cache) == 0, \
'Cache storage should be empty'
>>> assert isfunction(cache), \
'Create cache() function'
>>> assert isfunction(cache(lambda: ...)), \
'cache() should take function as an argument'
>>> @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)
>>> cached = timeit( # doctest: +SKIP
... stmt='fn1(500); fn1(400); fn1(450); fn1(350)',
... globals=globals(),
... number=10_000)
>>> uncached = timeit( # doctest: +SKIP
... stmt='fn2(500); fn2(400); fn2(450); fn2(350)',
... globals=globals(),
... number=10_000)
>>> ratio = uncached / cached # doctest: +SKIP
>>> print(f'With Cache: {cached:.4f} seconds') # doctest: +SKIP
>>> print(f'No Cache time: {uncached:.3f} seconds') # doctest: +SKIP
>>> print(f'Cached solution is {ratio:.1f} times faster') # doctest: +SKIP
TODO: Make tests faster
"""
_cache = {}
# type: Callable[[Callable], Callable]
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. Absolute path is when `path` starts with `current_directory`
2. Create function decorator `abspath`
3. If `path` is relative, then `abspath` will convert it to absolute
4. If `path` is absolute, then `abspath` will not modify it
5. Note: if you are using Windows operating system,
then one doctest (with absolute path) can fail
6. Run doctests - all must succeed
Polish:
1. Ścieżka bezwzględna jest gdy `path` zaczyna się od `current_directory`
2. Stwórz funkcję dekorator `abspath`
3. Jeżeli `path` jest względne, to `abspath` zamieni ją na bezwzględną
4. Jeżeli `path` jest bezwzględna, to `abspath` nie będzie jej modyfikował
5. Uwaga: jeżeli korzystasz z systemu operacyjnego Windows,
to jeden z doctestów (ścieżki bezwzględnej) może nie przejść pomyślnie
6. Uruchom doctesty - wszystkie muszą się powieść
Hints:
* `Path(filename).absolute()`
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isfunction
>>> assert isfunction(abspath), \
'Create abspath() function'
>>> assert isfunction(abspath(lambda: ...)), \
'abspath() should take function as an argument'
>>> @abspath
... def display(path):
... return str(path)
>>> current_dir = str(Path().cwd())
>>> display('iris.csv').startswith(current_dir)
True
>>> display('iris.csv').endswith('iris.csv')
True
>>> display('/home/python/iris.csv')
'/home/python/iris.csv'
TODO: Windows Path().absolute()
TODO: Test if function was called
"""
from pathlib import Path
# type: Callable[[Callable], Callable]
def abspath():
...
"""
* Assignment: Decorator Function Numeric
* Complexity: easy
* Lines of code: 4 lines
* Time: 5 min
English:
1. Modify decorator `numeric`
2. Decorator must check arguments `a` and `b` types
3. If type `a` or `b` are not `int` or `float`
raise exception `TypeError`
4. Run doctests - all must succeed
Polish:
1. Zmodyfikuj dekorator `numeric`
2. Dekorator ma sprawdzać typy argumentów `a` oraz `b`
3. Jeżeli typ `a` lub `b` nie jest `int` lub `float`
to podnieś wyjątek `TypeError`
4. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isfunction
>>> assert isfunction(numeric), \
'Create numeric() function'
>>> assert isfunction(numeric(lambda: ...)), \
'numeric() should take function as an argument'
>>> @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
>>> add(True, 0)
Traceback (most recent call last):
TypeError: Argument "a" must be int or float
>>> add(0, True)
Traceback (most recent call last):
TypeError: Argument "b" must be int or float
"""
# type: Callable[[Callable], Callable]
def numeric(func):
def wrapper(a, b):
return func(a, b)
return wrapper
"""
* Assignment: Decorator Function TypeCheck
* Complexity: hard
* Lines of code: 15 lines
* Time: 21 min
English:
1. Modify decorator `typecheck`
2. Decorator checks types of all arguments (`*args` oraz `**kwargs`)
3. Decorator checks return type
4. When received type is not expected raise `TypeError` with:
a. argument name
b. actual type
c. expected type
5. Run doctests - all must succeed
Polish:
1. Zmodyfikuj dekorator `typecheck`
2. Dekorator sprawdza typy wszystkich argumentów (`*args` oraz `**kwargs`)
3. Dekorator sprawdza typ zwracany
4. Gdy otrzymany typ nie jest równy oczekiwanemu podnieś `TypeError` z:
a. nazwa argumentu
b. aktualny typ
c. oczekiwany typ
5. Uruchom doctesty - wszystkie muszą się powieść
Hints:
* https://docs.python.org/3/howto/annotations.html
* `inspect.get_annotations()`
* `function.__code__.co_varnames`
* `dict(zip(...))`
* `dict.items()`
* `dict1 | dict2` - merging dicts
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isfunction
>>> assert isfunction(typecheck), \
'Create typecheck() function'
>>> assert isfunction(typecheck(lambda: ...)), \
'typecheck() should take function as an argument'
>>> @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
>>> @typecheck
... def echo(a: str, b: int, c: float = 0.0) -> bool:
... return str(a * b)
>>>
>>> echo('one', 1, 1.1)
Traceback (most recent call last):
TypeError: "return" is <class 'str'>, but <class 'bool'> was expected
"""
from inspect import get_annotations
# type: Callable[[Callable], Callable]
def typecheck(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper