4.8. Decorator with Arguments

4.8.1. Rationale

Decorator:
@mydecorator(a, b)
def myfunction(*args, **kwargs):
    pass
Is equivalent to:
myfunction = mydecorator(a, b)(myfunction)

4.8.2. Syntax

Listing 4.74. Definition
def mydecorator(a=1, b=2):
    def decorator(func):
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapper
    return decorator
Listing 4.75. Decoration
@mydecorator(a=0)
def myfunction():
    ...
Listing 4.76. Usage
myfunction()

4.8.3. Example

def run(lang='en'):
    def decorator(func):
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapper
    return decorator

@run(lang='en')
def hello(name):
    return f'My name... {name}'

hello('José Jiménez')
# 'My name... José Jiménez'

4.8.4. Use Cases

Listing 4.77. Deprecated
import warnings

def deprecated(removed_in_version=None):
    def decorator(func):
        def wrapper(*args, **kwargs):
            name = func.__name__
            file = func.__code__.co_filename
            line = func.__code__.co_firstlineno + 1
            message = f"Call to deprecated function {name} in {file} at line {line}"
            message += f'\nIt will be removed in {removed_in_version}'
            warnings.warn(message, DeprecationWarning)
            return func(*args, **kwargs)
        return wrapper
    return decorator

@deprecated(removed_in_version=2.0)
def myfunction():
    pass

myfunction()
# /home/python/myscript.py:11: DeprecationWarning: Call to deprecated function myfunction in /home/python/myscript.py at line 19
# It will be removed in 2.0
Listing 4.78. Timeout using signal(SIGALRM)
from signal import signal, alarm, SIGALRM
from time import sleep


def timeout(seconds=2.0, error_message='Timeout'):
    def on_timeout(signum, frame):
        raise TimeoutError

    def decorator(func):
        def wrapper(*args, **kwargs):
            signal(SIGALRM, on_timeout)
            alarm(int(seconds))
            try:
                func(*args, **kwargs)
            except TimeoutError:
                print(error_message)
            finally:
                alarm(0)
        return wrapper
    return decorator


@timeout(seconds=3.0, error_message='Sorry, timeout')
def countdown(n):
    for i in reversed(range(n)):
        print(i)
        sleep(1)
    print('countdown finished')

countdown(5)
# 4
# 3
# 2
# Sorry, timeout
Listing 4.79. Timeout using threading.Timer
from _thread import interrupt_main
from threading import Timer
from time import sleep


def timeout(seconds=3.0, error_message='Timeout'):
    def decorator(func):
        def wrapper(*args, **kwargs):
            timer = Timer(seconds, interrupt_main)
            timer.start()
            try:
                result = func(*args, **kwargs)
            except KeyboardInterrupt:
                raise TimeoutError(error_message)
            finally:
                timer.cancel()
            return result
        return wrapper
    return decorator


@timeout(seconds=3.0, error_message='Sorry, timeout')
def countdown(n):
    for i in reversed(range(n)):
        print(i)
        sleep(1)
    print('countdown finished')

countdown(5)
# 4
# 3
# 2
# Traceback (most recent call last):
#     ...
# TimeoutError: Timeout

4.8.5. Assignments

4.8.5.1. Decorator Arguments Astronauts

  • Assignment name: Decorator Arguments Astronauts

  • Last update: 2020-10-01

  • Complexity level: easy

  • Lines of code to write: 9 lines

  • Estimated time of completion: 13 min

  • Solution: solution/decorator_arguments_astronauts.py

English
  1. Use data from "Input" section (see below)

  2. Create decorator check_astronauts

  3. To answer if person is an astronaut check field is_astronaut in crew: list[dict]

  4. Decorator will call decorated function, only if all crew members has field with specified value

  5. Both field name and value are given as keyword arguments to decorator

  6. If any member is not an astronaut raise PermissionError and print his first name and last name

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

Polish
  1. Użyj kodu z sekcji "Input" (patrz poniżej)

  2. Stwórz dekorator check_astronauts

  3. Aby odpowiedzieć czy osoba jest astronautą sprawdź pole is_astronaut in crew: list[dict]

  4. Dekorator wywoła dekorowaną funkcję tylko wtedy, gdy każdy członek załogi ma pole o podanej wartości

  5. Zarówno nazwa pola jak i wartość są podawane jako argumenty nazwane do dekoratora

  6. Jeżeli, jakikolwiek członek nie jest astronautą, podnieś wyjątek PermissionError i wypisz jego imię i nazwisko

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

Output
>>> CREW_PRIMARY = [
...    {'is_astronaut': True, 'name': 'Jan Twardowski'},
...    {'is_astronaut': True, 'name': 'Mark Watney'},
...    {'is_astronaut': True, 'name': 'Melissa Lewis'}]

>>> CREW_BACKUP = [
...    {'is_astronaut': True, 'name': 'Melissa Lewis'},
...    {'is_astronaut': True, 'name': 'Mark Watney'},
...    {'is_astronaut': False, 'name': 'Alex Vogel'}]

>>> @check_astronauts(field='is_astronaut', value=True)
... def launch(crew):
...    crew = ', '.join(astro['name'] for astro in crew)
...    return f'Launching: {crew}'

>>> launch(CREW_PRIMARY)
'Launching: Jan Twardowski, Mark Watney, Melissa Lewis'

>>> launch(CREW_BACKUP)
Traceback (most recent call last):
    ...
PermissionError: Alex Vogel is not an astronaut

>>> @check_astronauts(field='name', value='Melissa Lewis')
... def launch(crew):
...    crew = ', '.join(astro['name'] for astro in crew)
...    return f'Launching: {crew}'

>>> launch(CREW_PRIMARY)
Traceback (most recent call last):
    ...
PermissionError: Jan Twardowski is not an astronaut

>>> launch(CREW_BACKUP)
Traceback (most recent call last):
    ...
PermissionError: Mark Watney is not an astronaut

4.8.5.2. Decorator Arguments Type Check

  • Assignment name: Decorator Arguments Type Check

  • Last update: 2020-10-01

  • Complexity level: medium

  • Lines of code to write: 20 lines

  • Estimated time of completion: 13 min

  • Solution: solution/decorator_arguments_typecheck.py

English
  1. Use data from "Input" section (see below)

  2. Create decorator function typecheck

  3. Decorator checks types of all arguments (*args oraz **kwargs)

  4. Decorator checks return type only if check_return is True

  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 funkcję typecheck

  3. Dekorator sprawdza typy wszystkich argumentów (*args oraz **kwargs)

  4. Dekorator sprawdza typ zwracany tylko gdy check_return jest True

  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(check_return=True)
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'>}