5.5. Decorator Class with Func¶
5.5.1. Rationale¶
MyDecorator
is a decorator namemyfunction
is a function name
- Syntax:
@MyDecorator def myfunction(*args, **kwargs): ...
- Is equivalent to:
myfunction = MyDecorator(myfunction)
5.5.2. Syntax¶
cls
is a pointer to class which is being decorated (MyClass
in this case)Wrapper
is a closure classWrapper
name is a convention, but you can name it anyhowWrapper
can inherit fromMyClass
Decorator must return pointer to
Wrapper
Definition:
class MyDecorator:
def __init__(self, func):
self._func = func
def __call__(self, *args, **kwargs):
return self._func(*args, **kwargs)
Decoration:
@MyDecorator
def myfunction():
...
Usage:
myfunction()
5.5.3. 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'
5.5.4. Use Cases¶
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 user.is_authenticated:
return self._func(*args, **kwargs)
else:
print('Permission Denied')
@LoginCheck
def edit_profile():
print('Editing profile...')
user = User()
edit_profile()
# Permission Denied
user.login('admin', 'MyVoiceIsMyPassword')
edit_profile()
# Editing profile...
Dict Cache:
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) # 8 # Computed
myfunction('hi', 3) # 'hihihi' # Computed
myfunction('ha', 3) # 'hahaha' # Computed
myfunction('ha', 3) # 'hahaha' # Fetched from cache
myfunction('hi', 3) # 'hihihi' # Fetched from cache
myfunction(2, 4) # 8 # Fetched from cache
myfunction(4, 2) # 8 # Computed
myfunction
# {
# (2, 4): 8,
# ('hi ', 3): 'hihihi',
# ('ha', 3): 'hahaha',
# (4, 2): 8,
# }
from pickle import dumps
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(dumps(args) + dumps(kwargs))
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
# {-5031589639694290544: 2,
# -7391056524300571861: 2,
# -2712444627064717062: 6,
# 7201789803359913928: 42,
# 8409437572158207229: 63}
5.5.5. Assignments¶
"""
* Assignment: Decorator Class Syntax
* Complexity: easy
* Lines of code: 5 lines
* Time: 5 min
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. Compare result with "Tests" section (see below)
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. Porównaj wyniki z sekcją "Tests" (patrz poniżej)
Tests:
>>> from inspect import isclass
>>> assert isclass(MyDecorator)
>>> assert isinstance(MyDecorator(lambda: None), MyDecorator)
>>> @MyDecorator
... def echo(text):
... return text
>>> echo('hello')
'hello'
"""
"""
* Assignment: Decorator Class Abspath
* Complexity: easy
* Lines of code: 10 lines
* Time: 13 min
English:
1. Use data from "Given" section (see below)
2. Absolute path is when `path` starts with `current_directory`
3. Create class decorator `Abspath`
4. If `path` is relative, then `Abspath` will convert it to absolute
5. If `path` is absolute, then `Abspath` will not modify it
6. Compare result with "Tests" section (see below)
Polish:
1. Użyj danych z sekcji "Given" (patrz poniżej)
2. Ścieżka bezwzględna jest gdy `path` zaczyna się od `current_directory`
3. Stwórz klasę dekorator `Abspath`
4. Jeżeli `path` jest względne, to `Abspath` zamieni ją na bezwzględną
5. Jeżeli `path` jest bezwzględna, to `Abspath` nie będzie jej modyfikował
6. Porównaj wyniki z sekcją "Tests" (patrz poniżej)
Hints:
* `path = Path(CURRENT_DIR, filename)`
Tests:
>>> @Abspath
... def display(path):
... return str(path)
>>> display('iris.csv').startswith(str(CURRENT_DIR))
True
>>> display('iris.csv').endswith('iris.csv')
True
>>> display('/home/python/iris.csv')
'/home/python/iris.csv'
"""
# Given
from pathlib import Path
CURRENT_DIR = Path().cwd()
"""
* Assignment: Decorator Class Type Check
* Complexity: medium
* Lines of code: 15 lines
* Time: 21 min
English:
1. Use data from "Given" section (see below)
2. Create decorator class `TypeCheck`
3. Decorator checks types of all arguments (`*args` oraz `**kwargs`)
4. Decorator checks return type
5. In case when received type is not expected throw an exception `TypeError` with:
a. argument name
b. actual type
c. expected type
6. Compare result with "Tests" section (see below)
Polish:
1. Użyj danych z sekcji "Given" (patrz poniżej)
2. Stwórz dekorator klasę `TypeCheck`
3. Dekorator sprawdza typy wszystkich argumentów (`*args` oraz `**kwargs`)
4. Dekorator sprawdza typ zwracany
5. W przypadku gdy otrzymany typ nie jest równy oczekiwanemu wyrzuć wyjątek `TypeError` z:
a. nazwa argumentu
b. aktualny typ
c. oczekiwany typ
6. Porównaj wyniki z sekcją "Tests" (patrz poniżej)
Hints:
* `echo.__annotations__`
Tests:
>>> @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
"""
# Given
def decorator(func):
def validate(argname, argval):
argtype = type(argval)
expected = func.__annotations__[argname]
if argtype is not expected:
raise TypeError(f'"{argname}" is {argtype}, but {expected} was expected')
def merge(*args, **kwargs):
args = dict(zip(func.__annotations__.keys(), args))
return kwargs | args # Python 3.9
# return {**args, **kwargs)} # Python 3.7, 3.8
def wrapper(*args, **kwargs):
for argname, argval in merge(*args, **kwargs).items():
validate(argname, argval)
result = func(*args, **kwargs)
validate('return', result)
return result
return wrapper