6.11. 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()
6.11.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'
6.11.2. Use Case - 1
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
6.11.3. Use Case - 2
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
6.11.4. Use Case - 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
6.11.5. Use Case - 4
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
6.11.6. Use Case - 5
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"
6.11.7. Use Case - 6
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)
6.11.8. Use Case - 7
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}
6.11.9. Use Case - 8
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
6.11.10. Use Case - 9
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}
6.11.11. Use Case - 10
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'}
6.11.12. Use Case - 11
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')
6.11.13. Assignments
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% About
# - Name: Decorator Syntax About
# - Difficulty: easy
# - Lines: 5
# - Minutes: 3
# %% 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. Run doctests - all must succeed
# %% 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. Uruchom doctesty - wszystkie muszą się powieść
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> from inspect import isfunction
>>> assert isfunction(mydecorator), \
'Create mydecorator() function'
>>> assert isfunction(mydecorator(lambda: ...)), \
'mydecorator() should take function as an argument'
>>> @mydecorator
... def echo(text):
... return text
>>> echo('hello')
'hello'
"""
# type: Callable[[Callable], Callable]
def mydecorator():
pass
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% About
# - Name: Decorator Syntax Disable
# - Difficulty: easy
# - Lines: 1
# - Minutes: 3
# %% English
# 1. Modify decorator `disable`
# 2. Decorator raises `PermissionError` and does not execute function
# 3. Run doctests - all must succeed
# %% Polish
# 1. Zmodyfikuj dekorator `disable`
# 2. Dekorator podnosi `PermissionError` i nie wywołuje funkcji
# 3. Uruchom doctesty - wszystkie muszą się powieść
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> from inspect import isfunction
>>> assert isfunction(disable), \
'Create disable() function'
>>> assert isfunction(disable(lambda: ...)), \
'disable() should take function as an argument'
>>> @disable
... def echo(text):
... print(text)
>>> echo('hello')
Traceback (most recent call last):
PermissionError: Function is disabled
"""
# type: Callable[[Callable], Callable]
def disable(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper