6.7. Decorator Function

  • func is a reference to function which is being decorated

  • args arbitrary number of positional arguments

  • kwargs arbitrary number of keyword arguments

  • By calling func(*args, **kwargs) you actually run original (wrapped) function with it's original arguments

Definition:

>>> def mydecorator(func):
...     def wrapper(*args, **kwargs):
...         return func(*args, **kwargs)
...     return wrapper

Usage:

>>> @mydecorator
... def say_hello():
...     return 'hello'
>>>
>>>
>>> say_hello()
'hello'

6.7.1. 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 Function Check
# - Difficulty: easy
# - Lines: 3
# - Minutes: 3

# %% 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
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> 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


# %% 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 Function Staff
# - Difficulty: easy
# - Lines: 3
# - Minutes: 8

# %% 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
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> 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


# %% 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 Function Memoization
# - Difficulty: easy
# - Lines: 3
# - Minutes: 8

# %% English
# 1. Create decorator `@cache`
# 2. Decorator must check before running function, if for given argument
#    the computation was already done:
#    - if yes, return from `_cache`
#    - if not, calculate new result, update cache and return value
# 3. Check doctest output to see how much faster is cached version
# 4. 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:
#    - jeżeli tak, zwróć dane z `_cache`
#    - jeżeli nie, oblicz, zaktualizuj `_cache` i zwróć wartość
# 3. Sprawdź output doctestów, aby zobaczyć jak szybka jest wersja z cache
# 4. Uruchom doctesty - wszystkie muszą się powieść (poza trzema printami)

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from timeit import timeit
>>> from inspect import isfunction
>>> import logging
>>> 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)

>>> def run():
...     cached = timeit(
...         stmt='fn1(500); fn1(400); fn1(450); fn1(350)',
...         globals=globals(),
...         number=1000)
...     uncached = timeit(
...         stmt='fn2(500); fn2(400); fn2(450); fn2(350)',
...         globals=globals(),
...         number=1000)
...     ratio = uncached / cached
...     logging.warning(f'Uncached: {uncached:.4f} seconds')
...     logging.warning(f'Cached:   {cached:.4f} seconds')
...     logging.warning(f'Faster:   {ratio:.1f} times')
>>>
>>> _ = run()
"""

_cache = {}


# type: Callable[[Callable], Callable]
def cache(func):
    def wrapper(n):
        return func(n)

    return wrapper


# %% 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 Function Abspath
# - Difficulty: easy
# - Lines: 5
# - Minutes: 5

# %% 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
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> 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():
    ...


# %% 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 Function Numeric
# - Difficulty: easy
# - Lines: 4
# - Minutes: 5

# %% 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
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> 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


# %% 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 Function TypeCheck
# - Difficulty: hard
# - Lines: 15
# - Minutes: 21

# %% 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:
#    - argument name
#    - actual type
#    - 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:
#    - nazwa argumentu
#    - aktualny typ
#    - 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
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> 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