12.12. 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'
12.12.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'
12.12.2. Use Case - 0x01
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...
12.12.3. Use Case - 0x02
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}
12.12.4. Use Case - 0x03
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}
12.12.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. 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
>>> 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:
...
"""
* Assignment: Decorator Class Abspath
* Complexity: easy
* Lines of code: 6 lines
* Time: 5 min
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
>>> 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(path):
path = Path(path).absolute()
return func(path)
return wrapper
# type: type
class Abspath:
...
"""
* Assignment: Decorator Class TypeCheck
* Complexity: medium
* Lines of code: 8 lines
* Time: 8 min
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:
a. argument name
b. actual type
c. 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:
a. nazwa argumentu
b. aktualny typ
c. 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
>>> 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:
...