6.11. Decorate Function

  • Decorator must return reference to wrapper

  • wrapper is a closure function

  • wrapper name is a convention, but you can name it anyhow

  • wrapper gets arguments passed to function

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