6.9. Decorator Class
MyDecorator
is a decorator namemyfunction
is a function namefunc
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:
...