← Python/Idiomatisch en performance
Les 4 van 8
MODULE 13 · 04◷ 30 min

Functies uitbreiden zonder hun contract kwijt te raken

Na deze les kun je
  • Een callable met een closure omwikkelen en argumenten doorgeven.
  • Naam en documentatie met functools.wraps behouden.
  • Waarden, aanroeptellingen en exceptionidentiteit bewaken.

Je wilt vastleggen hoe vaak een controlefunctie wordt aangeroepen, maar de functie zelf hoort alleen die controle te doen. Je kunt het extra gedrag daarom rond de bestaande functie zetten. Zo blijven de bedrijfsregel en het meten afzonderlijk leesbaar.

Een functie rond een functie

Een wrapper is een nieuwe functie die de oorspronkelijke functie aanroept. Een decorator ontvangt een callable en geeft gewoonlijk zo'n aangepaste callable terug. De notatie @decorate boven een definitie betekent dat de gedefinieerde functie door decorate(function) wordt vervangen.

Je hebt eerder functies als argument gebruikt. Een decorator voegt daar een geneste functie aan toe die de ontvangen callable onthoudt. Die combinatie van een functie en de bewaarde buitenste namen heet een closure.

*args verzamelt extra positionele argumenten in een tuple. **kwargs verzamelt keywordargumenten in een dictionary. Bij het aanroepen pakt fn(*args, **kwargs) ze weer uit, zodat de wrapper het oorspronkelijke aanroeppatroon behoudt.

Een wrapper gebruikt de echte functie
from functools import wraps

def cheerful(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs) + " :)"
    return wrapper

@cheerful
def greet(name, *, ending="!"):
    return name + ending

print(greet("Bo", ending="?"))
print(greet.__name__)
Verwachte uitvoer
Bo? :)
greet

De buitenste functie in het voorbeeld krijgt de oorspronkelijke groet en geeft een andere functie terug. Pas de laatste aanroep voert de groet uit. De wrapper voegt iets toe aan de retourwaarde; hij vervangt de groet niet door een vaste tekst.

De oorspronkelijke identiteit beschrijven

De nieuwe functie zou van zichzelf de naam en documentatie van de wrapper hebben. wraps uit functools kopieert geselecteerde metadata van de oorspronkelijke callable. De eigenschap __wrapped__ verwijst bovendien naar die callable.

Dat helpt bij documentatie, traceerbaarheid en introspectie. Het betekent niet dat beide functies hetzelfde object zijn: de wrapper blijft een nieuwe callable. Ook correcte metadata bewijst nog niet dat de wrapper waarden en fouten goed doorgeeft.

Bijhouden wat daadwerkelijk gebeurt

Een functieobject kan eigen attributen hebben. Een tellerwrapper kan daarom beginnen met wrapper.calls = 0 en bij iedere aanroep ophogen. Doe dat vóór de oorspronkelijke functie wordt uitgevoerd als ook een mislukte aanroep moet meetellen.

Een validatiewrapper heeft een andere afspraak: ongeldige invoer mag de oorspronkelijke functie juist niet bereiken. De volgorde van controleren en aanroepen bepaalt daar de betekenis. De oefening beperkt die invoer tot gewone getallen, zodat typecontrole niet van dit onderwerp afleidt.

Veelgemaakte fout: een fout wordt een uitkomst

Een algemene except die een bericht print en None teruggeeft, verandert het functiecontract. De aanroeper ziet dan geen fout meer en kan doorgaan met een ontbrekende uitkomst. Log daarom wat nodig is en geef de bestaande fout met kale raise door.

BaseException is de bovenste categorie voor fouten en afbreeksignalen; zowel Exception als KeyboardInterrupt vallen daaronder. KeyboardInterrupt valt niet onder Exception. Deze logger vangt BaseException alleen om te loggen en werpt hetzelfde object direct weer op; hij onderdrukt geen afbreekverzoek.

Het voorbeeld gebruikt een eigen StopSignal-subklasse zonder een echt proces te onderbreken. Alleen except Exception zou dit signaal missen. De buitenste proef vangt het signaal zelf weer op.

Een eigen afbreeksignaal observeren en direct doorgeven
events = []
class StopSignal(BaseException):
    pass
problem = StopSignal("stop")

def work():
    raise problem

def run_logged():
    events.append("start")
    try:
        return work()
    except BaseException:
        events.append("error")
        raise

try:
    run_logged()
except BaseException as caught:
    print(caught is problem)
print(events)
Verwachte uitvoer
True
['start', 'error']

Dit voorbeeld bewaart een verwijzing naar de oorspronkelijke fout en vergelijkt echte objectidentiteit. Een nieuw aangemaakte fout met dezelfde tekst zou die controle niet doorstaan. De gebeurtenissen vertellen ook of de fout- of succesroute werd genomen.

Gedrag en metadata samen bewaken

Alle oefenwrappers gebruiken wraps, geven argumenten door en behouden het resultaat. De voorvoegselwrapper verwacht een stringresultaat; de overige wrappers mogen ieder object teruggeven. Een ontvangen foutenobject mag niet worden vervangen of stil worden ingeslikt.

De logwrapper voegt alleen "start" en daarna "ok" of "error" toe aan de ontvangen lijst. Hij maakt die lijst niet leeg. Eerdere gebeurtenissen horen mogelijk bij een andere stap van hetzelfde programma.

AAN DE SLAG 0/4
1

Een echte retourwaarde uitbreiden

Vul with_prefix(fn, prefix) in: geef een wrapper die prefix vóór het stringresultaat van de echte fn zet. Geef alle argumenten en fouten door en gebruik wraps voor naam, documentatie en __wrapped__.

2

Ook mislukte aanroepen tellen

Vul count_calls(fn) in: geef een wrapper met calls=0 die vóór iedere aanroep ophoogt, ook bij een fout. Behoud argumenten, resultaat, oorspronkelijke fout en metadata.

3

Ongeldige invoer tegenhouden

Vul require_positive(fn) in voor een eerste numerieke parameter value: bij value<=0 geef je ValueError("waarde moet positief zijn") vóór fn wordt aangeroepen. Geef geldige argumenten/resultaat door en behoud metadata.

4

Succes en fout afzonderlijk loggen

Vul log_outcome(fn, events) in: append start vóór iedere aanroep, daarna ok bij succes of error bij iedere BaseException. Behoud eerdere events, argumenten, resultaatidentiteit, oorspronkelijke foutidentiteit en metadata.

Samenvatting

  • Een decorator maakt extra gedrag rond een bestaande callable mogelijk.
  • wraps bewaart metadata; je bewaakt zelf argumenten, resultaat en fouten.
  • De plaats van tellen, controleren en loggen bepaalt het contract.

Hierna maak je herbruikbare functievarianten met andere hulpmiddelen uit functools.

decorator_tools.py
UitvoerPython
›
›_

Hier komt je resultaat.

Voer je code uit en ontdek
wat je hebt gemaakt.

0/4 stappen
EVEN MEEKIJKEN

Bekijk hoe het kan.

Je mag de oplossing bekijken. Deze poging telt dan niet mee als afgeronde les. Met Reset kun je daarna opnieuw zelf oefenen.