9.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()
9.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'
9.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
9.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
9.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
9.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
9.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"
9.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)
9.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}
9.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
9.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}
9.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'}
9.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')
9.6.13. Assignments¶
"""
* Assignment: Decorator Function Check
* Complexity: easy
* Lines of code: 3 lines
* Time: 3 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 Staff
* Complexity: easy
* Lines of code: 3 lines
* Time: 8 min
English:
1. Modify decorator `can_login`
2. To answer if person is staff check field:
`is_staff` in `users: list[dict]`
3. Decorator will call decorated function, only if all users are staff
4. If user is not a staff:
raise `PermissionError` with message `USERNAME is not a staff`,
where USERNAME is user's username
5. Run doctests - all must succeed
Polish:
1. Zmodyfikuj dekorator `can_login`
2. Aby odpowiedzieć czy osoba jest staffem sprawdź pole:
`is_staff` in `users: list[dict]`
3. Dekorator wywoła dekorowaną funkcję, tylko gdy każdy członek jest staff
4. Jeżeli użytkownik nie jest staffem:
podnieś `PermissionError` z komunikatem `USERNAME is not a staff`,
gdzie USERNAME to username użytkownika
5. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isfunction
>>> assert isfunction(can_login), \
'Create can_login() function'
>>> assert isfunction(can_login(lambda: ...)), \
'can_login() should take function as an argument'
>>> @can_login
... def login(users):
... users = ', '.join(user['username'] for user in users)
... return f'Logging-in: {users}'
>>> login(group1)
'Logging-in: mwatney, mlewis, rmartinez'
>>> login(group2)
Traceback (most recent call last):
PermissionError: avogel is not a staff
"""
group1 = [
{'is_staff': True, 'username': 'mwatney'},
{'is_staff': True, 'username': 'mlewis'},
{'is_staff': True, 'username': 'rmartinez'},
]
group2 = [
{'is_staff': False, 'username': 'avogel'},
{'is_staff': True, 'username': 'bjohanssen'},
{'is_staff': True, 'username': 'cbeck'},
]
# type: Callable[[Callable], Callable]
def can_login(func):
def wrapper(users):
return func(users)
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