decorators python uitleg met tutorial en voorbeelden

Met decorators kun je functies door andere functies uit laten voeren. Dit is bijvoorbeeld handig als je de uitvoertijd van verschillende functies wilt meten. Het gebruik van decorators oogt op het eerste gezicht redelijk complex. In deze tutorial geven we een eenvoudige uitleg en laten we stap voor stap zien hoe je decorators toepast.

Aan het eind ben je in staat decorators te gebruiken en kun je zelf een timer toevoegen in je code.

Functies en functies in functies

Python is een object georiënteerde programmeertaal. Hierdoor bestaat vrijwel alles uit objecten. Zo is ook een functie een object. Functies kunnen argumenten hebben. Onderstaande functie heeft één argument: 'arg'.

Deze tutorial kun je meedoen als je Python installeert. Er hoeven geen speciale packages geïnstalleerd te worden. We werken in Jupyter Notebook gedurende deze tutorial, maar je kunt ook een andere applicatie kiezen.

In [1]:
def function(arg):
    print(arg)

Doordat alles in Python uit objecten bestaat kan een argument in een functie zelf ook weer een functie zijn. Hierdoor kan binnen een functie een andere functie uitgevoerd worden. Dit kun je zien in het volgende voorbeeld. Hier maken we een functie met erbinnen een andere functie.

De buitenste functie heeft één argument: 'message'. De binnenste functie print de inhoudt van argument 'message'. Vervolgens geeft de buitenste functie de binnenste functie terug. Dit maakt mogelijk dat we het resultaat kunnen bewaren in een variabele. Regel 'hello_func = outer_function('Hey')' bewaart alles wat in de binnenste functie uitgevoerd wordt, met argument 'Hey', in variabele 'hello_func’. Hierdoor is 'hello_func' nu als functie aan te roepen, en krijgen we het resultaat te zien.

In [2]:
def outer_function(message):
    def inner_function():
        print(message)
    return inner_function

hello_func = outer_function('Hey')

hello_func()
Hey

Functies gebruiken op deze manier kan vreemd lijken. Het geeft echter handige mogelijkheden. Je kunt de binnenste functie aanpassen. De binnenste functie doet iets met het argument uit de buitenste functie. Hier kun je zowel voor als na code aan toevoegen. In het volgende voorbeeld wordt er een tekst voor en na geprint.

In [3]:
def outer_function(message):
    def inner_function():
        print('Inner function start')
        print(message)
        print('Inner function stop')
    return inner_function

hello_func = outer_function('Hey')

hello_func()
Inner function start
Hey
Inner function stop
Wil jij leren hoe je Python inzet voor data analyses? Schrijf je in voor een van onze Python & data science trainingen.


Decorators gebruiken

Een functie kan een andere functie als argument krijgen. Dit zien we in het volgende voorbeeld. In de buitenste functie verwachten we nu een argument 'function'. De binnenste functie print eerst een stuk tekst. Vervolgens voert de binnenste functie de functie 'function' uit. Tot slot print de binnenste functie weer een stuk tekst.

We maken een nieuwe functie: 'say_hello()'. Deze functie print het woord 'Hey'. Als we nu de buitenste functie uitvoeren met als argument de nieuwe functie 'say_hello', gebeurt het volgende:

  • Functie 'outer_function' wordt uitgevoerd met als argument functie 'say_hello'
  • 'outer_function' geeft 'inner_function' terug
  • 'inner_function' print tekst 'Inner function start'
  • 'inner_function' voert functie 'say_hello' uit, tekst 'Hey' wordt geprint
  • 'inner_function' print tekst 'Inner function stop'
In [4]:
def outer_function(function):
    def inner_function():
        print('Inner function start')
        result = function()
        print('Inner function stop')
        return result
    return inner_function

def say_hello():
    print('Hey')

hello_func = outer_function(say_hello)

hello_func()
Inner function start
Hey
Inner function stop

We hebben nu gezien dat we hiermee voor en na het uitvoeren van een functie logica uit kunnen voeren. Denk hier bijvoorbeeld aan dat je zo kunt timen wanneer een functie start en stopt. Dit kan handig zijn voor data science vraagstukken waar data scientists met veel data werken.

De regel kan 'hello_func = outer_function(say_hello)' kan vervangen worden door een decorator. Een decorator begint altijd met een @ teken. Vervolgens benoem je de naam van de functie die je wilt gebruiken. Onderstaand toont hetzelfde resultaat als het vorige voorbeeld. We maken nu alleen gebruik van decorator '@outer_function'.

In [5]:
def outer_function(function):   
    def inner_function():
        print('Inner function start')
        result = function()
        print('Inner function stop')
        return result
    return inner_function

@outer_function
def say_hello():
    print('Hello')

say_hello()
Inner function start
Hello
Inner function stop

Omdat je meerder functies als decorators kunt gebruiken moeten ze een specifieke naam hebben. De binnenste functie wordt vaak 'wrapper' genoemd. Je kunt hier trouwens iedere naam voor kiezen.

In onderstaand voorbeeld maken we een timer. We noemen de buitenste functie 'timer'. De binnenste noemen we 'wrapper'. Deze functie bewaart de starttijd. Vervolgens wordt de specifieke functie uitgevoerd. Hierna wordt de eindtijd bepaald. Tot slot wordt de naam van de specifieke functie geprint, samen met de uitvoertijd. We maken hier op 2 plaatsen gebruik van '*args, **kwargs'. Dit staat voor arguments en key word arguments. Door dit toe te voegen kan de wrapper omgaan met meerdere argumenten. Wanneer je dit niet toe zou voegen krijg je een error. Probeer maar eens.

We maken van deze timer gebruik met functie 'say_hello'. Deze functie wacht eerst een seconde. Vervolgens wordt een bericht geprint.

In [6]:
import time

def timer(func):   
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f'Timer: function {func.__name__} completed in {round(end_time - start_time, 1)} sec.')
        return result
    return wrapper

@timer
def say_hello(msg):
    time.sleep(1)
    print(msg)
    
say_hello('Hi')
Hi
Timer: function say_hello completed in 1.0 sec.

Met 'say_hello('Hi')' wordt dus gebruik gemaakt van de timer. Hierdoor zien we dat het bericht wordt geprint. Ook wordt de uitvoertijd geprint. Hier kun je al zien dat dit handig is. De timer is nu herbruikbaar. Dit zien we ook in het volgende voorbeeld. In dit voorbeeld voegen we een nieuwe functie toe: 'say_full_name()'. Op beide functies passen we de decorator timer toe.

In [7]:
import time

def timer(func):   
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f'Timer: function {func.__name__} completed in {round(end_time - start_time, 1)} sec.')
        return result
    return wrapper

@timer
def say_hello(msg):
    time.sleep(1)
    print(msg)
    
@timer
def say_full_name(first_name, last_name):
    print(f'{first_name} {last_name}')

say_hello('Hi')
say_full_name('John', 'Parker')
Hi
Timer: function say_hello completed in 1.0 sec.
John Parker
Timer: function say_full_name completed in 0.0 sec.

We zien nu dat de timer voor beide functies te gebruiken is. Je ziet ook dat de nieuwe functie 2 argumenten heeft. De functie 'say_hello()' heeft slechts 1 argument. Doordat we gebruik maken van '*args, **kwargs' maakt dit niet uit.

Toepassen van meerdere decorators

Je hebt nu gezien hoe je gebruik maakt van een decorator. Nu kan het ook zijn dat je meerdere decorators tegelijk toe wilt passen. We gaan kijken hoe dit kan. In onderstaand voorbeeld voegen we een nieuwe functie toe: 'logger()'. Het doel van deze functie is het printen van tekst voor en nadat de functie wordt uitgevoerd. Ook printen we de eventuele arguments en key word arguments.

Zowel de logger als timer passen we toe op de eerder gemaakt functies.

In [8]:
import time

def logger(func):   
    def wrapper(*args, **kwargs):
        print(f'Logger: starts for function {func.__name__}')
        result = func(*args, **kwargs)
        print(f'Logger: function {func.__name__} ran with args: {args}, and kwargs: {kwargs}.')
        return result
    return wrapper


def timer(func):   
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f'Timer: function {func.__name__} completed in {round(end_time - start_time, 1)} sec.')
        return result
    return wrapper

@logger
@timer
def say_hello(msg):
    time.sleep(1)
    print(msg)

@logger    
@timer
def say_full_name(first_name, last_name):
    print(f'{first_name} {last_name}')

say_hello('Hi')
say_full_name('John', 'Parker')
Logger: starts for function wrapper
Hi
Timer: function say_hello completed in 1.0 sec.
Logger: function wrapper ran with args: ('Hi',), and kwargs: {}.
Logger: starts for function wrapper
John Parker
Timer: function say_full_name completed in 0.0 sec.
Logger: function wrapper ran with args: ('John', 'Parker'), and kwargs: {}.

We krijgen geen errors. Het Python script kan dus uitgevoerd worden. Alleen als we goed naar het resultaat kijken zien we het volgende. De naam van de functie die de timer uitvoert is 'wrapper'. Dit is niet netjes. Hier zouden we de naam van de werkelijk uit te voeren functie willen zien.

Dit is gemakkelijk te veranderen. Hiervoor moet je het volgende importeren: 'from functools import wraps'. Hierna zet je '@wraps(func)' voor de binnenste functies. Hierin is 'func' de naam van de uit te voeren functie. We passen dit toe in onderstaand voorbeeld.

In [9]:
import time
from functools import wraps

def logger(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f'Logger: starts for function {func.__name__}')
        result = func(*args, **kwargs)
        print(f'Logger: function {func.__name__} ran with args: {args}, and kwargs: {kwargs}.')
        return result
    return wrapper


def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f'Timer: function {func.__name__} completed in {round(end_time - start_time, 1)} sec.')
        return result
    return wrapper

@logger
@timer
def say_hello(msg):
    time.sleep(1)
    print(msg)

@logger    
@timer
def say_full_name(first_name, last_name):
    print(f'{first_name} {last_name}')

say_hello('Hi')
say_full_name('John', 'Parker')
Logger: starts for function say_hello
Hi
Timer: function say_hello completed in 1.0 sec.
Logger: function say_hello ran with args: ('Hi',), and kwargs: {}.
Logger: starts for function say_full_name
John Parker
Timer: function say_full_name completed in 0.0 sec.
Logger: function say_full_name ran with args: ('John', 'Parker'), and kwargs: {}.

Wanneer je nu naar het resultaat kijkt zie je dat overal de juiste functienamen getoond worden.

Wat je moet onthouden

Met decorators kun je functies gemakkelijk door andere functies uit laten voeren. Je werkt hierbij met een buitenste- en binnenste functie. De binnenste functie wordt vaak 'wrapper' genoemd. Het geeft je de mogelijkheid om code voor en na een functie uit te voeren. Dit op een manier die je kunt hergebruiken. Dat komt omdat je met functies werkt. Een decorator plaats ja altijd voor een functie. Je gebruikt het @ teken, gevolgd door de functienaam.

Veelgebruikte voorbeelden van decorators zijn timers en loggers. Hier hebben we decorators toegepast op functies. Het is vergelijkbaar toe te passen binnen classes.

Wil je nog veel meer leren over Python en de mogelijkheden voor data analyse? Schrijf je dan in voor onze Python cursus voor data science, onze machine learning training, of voor onze data science opleiding en leer met vertrouwen te programmeren en analyseren in Python. Nadat je een van onze trainingen hebt gevolgd kun je zelfstandig verder aan de slag. Je kunt ook altijd even contact opnemen als je een vraag hebt.

Download één van onze opleidingsbrochures voor meer informatie

 

by: