6.9. Decorator Class

  • MyDecorator is a decorator name

  • myfunction is a function name

  • func is a reference to function which is being decorated

Definition:

>>> class MyDecorator:
...     def __init__(self, func):
...         self._func = func
...
...     def __call__(self, *args, **kwargs):
...         return self._func(*args, **kwargs)

Usage:

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

6.9.1. Example

>>> class Run:
...     def __init__(self, func):
...         self._func = func
...
...     def __call__(self, *args, **kwargs):
...         return self._func(*args, **kwargs)
>>>
>>>
>>> @Run
... def hello(name):
...     return f'My name... {name}'
>>>
>>>
>>> hello('José Jiménez')
'My name... José Jiménez'

6.9.2. Use Case - 1

  • Login Check

>>> class User:
...     def __init__(self):
...         self.is_authenticated = False
...
...     def login(self, username, password):
...         self.is_authenticated = True
>>>
>>>
>>> class LoginCheck:
...     def __init__(self, func):
...         self._func = func
...
...     def __call__(self, *args, **kwargs):
...         if mark.is_authenticated:
...             return self._func(*args, **kwargs)
...         else:
...             print('Permission Denied')
>>>
>>>
>>> @LoginCheck
... def edit_profile():
...     print('Editing profile...')
>>>
>>>
>>> mark = User()
>>>
>>> edit_profile()
Permission Denied
>>>
>>> mark.login('mwatney', 'Ares3')
>>> edit_profile()
Editing profile...

6.9.3. Use Case - 2

  • Cache Args

>>> class Cache(dict):
...     def __init__(self, func):
...         self._func = func
...
...     def __call__(self, *args):
...         return self[args]
...
...     def __missing__(self, key):
...         self[key] = self._func(*key)
...         return self[key]
>>>
>>>
>>> @Cache
... def myfunction(a, b):
...     return a * b
>>>
>>>
>>> myfunction(2, 4)  # Computed
8
>>> myfunction('hi', 3)  # Computed
'hihihi'
>>> myfunction('ha', 3)  # Computed
'hahaha'
>>>
>>> myfunction('ha', 3)  # Fetched from cache
'hahaha'
>>> myfunction('hi', 3)  # Fetched from cache
'hihihi'
>>> myfunction(2, 4)  # Fetched from cache
8
>>> myfunction(4, 2)  # Computed
8
>>>
>>> myfunction  
{(2, 4): 8,
 ('hi', 3): 'hihihi',
 ('ha', 3): 'hahaha',
 (4, 2): 8}

6.9.4. Use Case - 3

  • Cache Kwargs

>>> class Cache(dict):
...     _func: callable
...     _args: tuple
...     _kwargs: dict
...
...     def __init__(self, func):
...         self._func = func
...
...     def __call__(self, *args, **kwargs):
...         self._args = args
...         self._kwargs = kwargs
...         key = hash(args + tuple(kwargs.values()))
...         return self[key]
...
...     def __missing__(self, key):
...         self[key] = self._func(*self._args, **self._kwargs)
...         return self[key]
>>>
>>>
>>> @Cache
... def myfunction(a, b):
...     return a * b
>>>
>>>
>>> myfunction(1, 2)
2
>>> myfunction(2, 1)
2
>>> myfunction(6, 1)
6
>>> myfunction(6, 7)
42
>>> myfunction(9, 7)
63
>>>
>>> myfunction  
{-3550055125485641917: 2,
 6794810172467074373: 2,
 8062003079928221385: 6,
 1461316589696902609: 42,
 -4120545409808486724: 63}

6.9.5. 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 Class Syntax
# - Difficulty: easy
# - Lines: 5
# - Minutes: 5

# %% English
# 1. Create decorator class `MyDecorator`
# 2. `MyDecorator` should have `__init__` which takes function as an argument
# 3. `MyDecorator` should have `__call__` with parameters: `*args` and `**kwargs`
# 4. `__call__` should call original function with original parameters, and return its value
# 5. Run doctests - all must succeed

# %% Polish
# 1. Stwórz dekorator klasę `MyDecorator`
# 2. `MyDecorator` powinien mieć `__init__`, który przyjmuje funkcję jako argument
# 3. `MyDecorator` powinien mieć `__call__` z parameterami: `*args` i `**kwargs`
# 4. `__call__` powinien wywoływać oryginalną funkcję oryginalnymi parametrami i zwracać jej wartość
# 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 isclass

>>> assert isclass(MyDecorator), \
'MyDecorator should be a decorator class'

>>> assert MyDecorator(lambda: ...), \
'MyDecorator should take function as an argument'

>>> assert isinstance(MyDecorator(lambda: ...), MyDecorator), \
'MyDecorator() should return an object which is an instance of MyDecorator'

>>> @MyDecorator
... def echo(text):
...     return text

>>> echo('hello')
'hello'
"""

# type: type
class MyDecorator:
    ...


# %% 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 Class Abspath
# - Difficulty: easy
# - Lines: 6
# - Minutes: 3

# %% English
# 1. Absolute path is when `path` starts with `current_directory`
# 2. Create class 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 klasę 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 = Path(path).absolute()`

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

>>> from inspect import isclass

>>> assert isclass(Abspath), \
'Abspath should be a decorator class'

>>> assert Abspath(lambda: ...), \
'Abspath should take function as an argument'

>>> assert isinstance(Abspath(lambda: ...), Abspath), \
'Abspath() should return an object which is an instance of Abspath'

>>> @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')  # Should pass regardless your OS
'/home/python/iris.csv'

TODO: Windows Path().absolute()
TODO: Test if function was called
"""

from pathlib import Path


def abspath(func):
    def wrapper(filepath):
        abspath = Path(filepath).absolute()
        return func(abspath)

    return wrapper


# type: type
class 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 Class TypeCheck
# - Difficulty: medium
# - Lines: 8
# - Minutes: 5

# %% English
# 1. Refactor decorator `decorator` to 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. Przerób dekorator `decorator` na klasę `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 isclass

>>> assert isclass(TypeCheck), \
'TypeCheck should be a decorator class'

>>> assert TypeCheck(lambda: ...), \
'TypeCheck should take function as an argument'

>>> assert isinstance(TypeCheck(lambda: ...), TypeCheck), \
'TypeCheck() should return an object which is an instance of TypeCheck'

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


def typecheck(func):
    annotations = get_annotations(func)

    def merge(args, kwargs) -> dict:
        args = dict(zip(annotations, args))
        return args | kwargs

    def check(argname, argvalue):
        argtype = type(argvalue)
        expected = annotations[argname]
        if argtype is not expected:
            raise TypeError(f'"{argname}" is {argtype}, '
                            f'but {expected} was expected')

    def wrapper(*args, **kwargs):
        arguments = merge(args, kwargs).items()
        for argname, argvalue in arguments:
            check(argname, argvalue)
        result = func(*args, **kwargs)
        check('return', result)
        return result

    return wrapper

# Refactor typecheck into class
# type: type
class TypeCheck:
    ...