4.5. Class Decorator with Functions

4.5.1. Rationale

  • MyDecorator is a decorator name

  • myfunction is a function name

Syntax:
@MyDecorator
def myfunction(*args, **kwargs):
    ...
Is equivalent to:
myfunction = MyDecorator(myfunction)

4.5.2. Syntax

  • cls is a pointer to class which is being decorated (MyClass in this case)

  • Wrapper is a closure class

  • Wrapper name is a convention, but you can name it anyhow

  • Wrapper can inherit from MyClass

  • Decorator must return pointer to Wrapper

Listing 4.69. Definition
class MyDecorator:
    def __init__(self, func):
        self._func = func

    def __call__(self, *args, **kwargs):
        return self._func(*args, **kwargs)
Listing 4.70. Decoration
@MyDecorator
def myfunction():
    ...
Listing 4.71. Usage
myfunction()

4.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'

4.5.4. Use Cases

Listing 4.72. 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...
Listing 4.73. 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,
# }

4.5.5. Assignments

4.5.5.1. Decorator Class Abspath

  • Assignment name: Decorator Class Abspath

  • Last update: 2020-10-01

  • Complexity level: easy

  • Lines of code to write: 10 lines

  • Estimated time of completion: 13 min

  • Solution: solution/decorator_cls_abspath.py

English
  1. Use data from "Input" 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 "Output" section (see below)

Polish
  1. Użyj danych z sekcji "Input" (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ą "Output" (patrz poniżej)

Input
@Abspath
def display(path):
    return str(path)
Output
>>> from pathlib import Path
>>> cwd = str(Path().cwd())
>>> display('iris.csv').startswith(cwd)
True
>>> display('iris.csv').endswith('iris.csv')
True
>>> display('/home/python/iris.csv')
'/home/python/iris.csv'
Hints
  • from pathlib import Path

  • current_directory = Path.cwd()

  • path = Path(current_directory, filename)

4.5.5.2. Decorator Class Type Check

  • Assignment name: Decorator Class Type Check

  • Last update: 2020-10-01

  • Complexity level: medium

  • Lines of code to write: 15 lines

  • Estimated time of completion: 21 min

  • Solution: solution/decorator_cls_typecheck.py

English
  1. Use data from "Input" 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:

    • argument name

    • actual type

    • expected type

  6. Compare result with "Output" section (see below)

Polish
  1. Użyj danych z sekcji "Input" (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:

    • nazwa argumentu

    • aktualny typ

    • oczekiwany typ

  6. Porównaj wyniki z sekcją "Output" (patrz poniżej)

Input
@TypeCheck
def echo(a: str, b: int, c: float = 0.0) -> bool:
    return bool(a * b)
Output
>>> 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
Hints
echo.__annotations__
# {'a': <class 'str'>, 'b': <class 'int'>, 'c': <class 'float'>, 'return': <class 'bool'>}