Achtergrond van de vraag
Decorators zijn een van de krachtigste tools van Python, waarmee het gedrag van functies of methoden kan worden gemodificeerd. Soms is het nodig om niet alleen een functie "te omhullen", maar om de decorator in te stellen met behulp van parameters (argumenten). Deze gevallen komen voor bij logging, tijdcontroles, toegangsbeperkingen, enz.
Probleem
Gewone decorators nemen alleen één functie aan die moet worden omhuld. Wanneer het nodig is om parameters aan de decorator zelf door te geven, wordt de syntaxis ingewikkelder, wat vaak leidt tot fouten, vooral bij geneste functies en het doorgeven van *args/**kwargs.
Oplossing
Een decorator met parameters wordt geïmplementeerd via een hogere-orde functie: eerst wordt de externe "decoratieve" functie aangeroepen met argumenten, die de decorator creëert en teruggeeft, en de decorator ontvangt de functie en retourneert de wrapper:
def herhaal(n): def decorator(func): def wrapper(*args, **kwargs): resultaat = None for _ in range(n): resultaat = func(*args, **kwargs) return resultaat return wrapper return decorator @herhaal(3) def groet(naam): print(f"Hallo, {naam}!") groet("Python") # Output: Hallo, Python! (3 keer)
Belangrijkste kenmerken:
Waarom kan een decorator met parameters niet op dezelfde manier worden geïmplementeerd als een gewone decorator?
Als je @decorator toepast, geeft Python de functie door als argument aan de decorator. Als je haakjes toevoegt (@decorator()), roept Python eerst de functie aan, en alleen het resultaat wordt geïnterpreteerd als een decorator.
def deco(func): # gewone decorator: @deco def deco_met_args(arg): # decorator met argument: @deco_met_args(arg)
Hoe verschillen decorators met argumenten fundamenteel van decorators zonder argumenten op het niveau van aanroep?
Decorators zonder argumenten ontvangen een functie als invoer, terwijl decorators met argumenten geen functie maar parameters ontvangen, en een decorator teruggeven.
Hoe pas je functools.wraps correct toe en waarom is dit belangrijk?
functools.wraps(func) in de wrapper behoudt de naam, documentatie-string en andere metadata van de originele functie; anders worden al deze gegevens vervangen in de wrapper, wat debugging en introspectie bemoeilijkt.
import functools def deco_met_args(arg): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator
Een decorator is geïmplementeerd zonder rekening te houden met parameters of met een onjuist aantal geneste functies:
def log(niveau): def wrapper(func): # fout - wrapper moet dieper zijn print(f"Log: {niveau}") func() # De functie wordt niet geretourneerd als een decorator return wrapper @log("INFO") def actie(): print("Werk!")
Voordelen:
Nadelen:
Gebruik van functools.wraps en correcte geneste functies:
import functools def timer(eenheden): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): import time start = time.time() resultaat = func(*args, **kwargs) eind = time.time() if eenheden == 'ms': duur = (eind - start) * 1000 else: duur = eind - start print(f"Duur: {duur:.4f} {eenheden}") return resultaat return wrapper return decorator @timer('ms') def op(): sum(range(1000)) op()
Voordelen:
Nadelen: