← Python/Interfaces en typing
Les 5 van 8
MODULE 08 · 05◷ 30 min

Een gedragscontract benoemen met Protocol

Na deze les kun je
  • Een echt Protocol met een methodedeclaratie maken.
  • Onafhankelijke concrete objecten via hetzelfde contract gebruiken.
  • Vormcontrole onderscheiden van expliciete resultaatvalidatie.

Een applicatie kan meerdere objecten gebruiken die allemaal tekst kunnen versturen. Zonder extra afspraak moet iedere lezer zelf ontdekken welke methode daarvoor nodig is. Een Protocol geeft dat verwachte gedrag een herkenbare naam.

Een gedragscontract benoemen

Een Protocol is een klasse uit typing waarmee je een structureel typecontract beschrijft. Structureel betekent hier dat de benodigde leden en hun bedoelde typen bepalen of een object past. Een concrete verzender hoeft niet expliciet van jouw Protocol te erven.

Je beschrijft bijvoorbeeld een methode met een berichtparameter en een retourannotatie. In een Protocol kan de body uit ... bestaan: de beschrijving levert dan geen werkende verzendimplementatie. Een echt object moet die bewerking zelf uitvoeren.

Een Protocol benoemt gedrag zonder verplichte overerving
from typing import Protocol

class Readable(Protocol):
    def read(self) -> str: ...

class Fixed:
    def read(self):
        return "18"

class Latest:
    def read(self):
        return "21"

def label(source: Readable) -> str:
    return "Meting: " + source.read()

print(label(Fixed()))
print(label(Latest()))
Verwachte uitvoer
Meting: 18
Meting: 21

Het voorbeeld gebruikt twee onafhankelijke klassen met een passende read()-methode. De functie ontvangt volgens haar annotatie een Readable, maar voert gewoon de echte methode van het ontvangen object uit. Het Protocol neemt dat lezen niet over.

De annotatie blijft een beschrijving

Net als in de eerdere lessen controleert een parameterannotatie niet automatisch iedere aanroep. De definitie van een Protocol verandert dat niet. Voor een echte controle door een statische typechecker zou je zo'n checker afzonderlijk moeten uitvoeren; dat doet deze les niet.

Wat we wel onderzoeken, zijn de echte Protocol-declaratie, de annotaties en het uitgevoerde objectgedrag. Een klasse met alleen dezelfde naam of een hardcoded resultaat voldoet niet aan die gezamenlijke opdracht. De aanroep moet nog steeds het ontvangen object gebruiken.

Een beperkte runtimecontrole toevoegen

Met runtime_checkable kun je een Protocol geschikt maken voor bepaalde isinstance-controles tijdens uitvoering. Je schrijft de decorator boven de Protocol-klasse. Een gewone Protocol zonder die decorator kun je niet zomaar voor zo'n controle gebruiken.

De controle kijkt of de vereiste leden aanwezig zijn. Een methode die expliciet op None is gezet, geldt daarbij als uitgeschakeld. De controle valideert geen volledige methodehandtekening en geen retourtypen. Een object met read() dat een getal teruggeeft, kan dus toch als passend worden herkend voor een Protocol dat tekst beschrijft.

Zelfs een niet-aanroepbaar lid zoals send = 7 kan die beperkte vormcontrole passeren. Het is aanwezig en niet None, maar daarmee kun je nog geen geldige methodeaanroep doen.

De runtimecontrole onderzoekt niet het hele typecontract
from typing import Protocol, runtime_checkable

@runtime_checkable
class Readable(Protocol):
    def read(self) -> str: ...

class WrongReturn:
    def read(self):
        return 7

class WrongArguments:
    def read(self, extra):
        return "tekst"

print(isinstance(object(), Readable))
print(isinstance(WrongReturn(), Readable))
print(WrongReturn().read())
print(isinstance(WrongArguments(), Readable))

class NotCallable:
    read = 7

print(isinstance(NotCallable(), Readable))
Verwachte uitvoer
False
True
7
True
True

Dat resultaat is geen reden om alle verdere aannames veilig te vinden. Een aanwezige methode kan nog verkeerd worden aangeroepen of een onbruikbare waarde opleveren. Gebruik de beperkte controle alleen voor de betekenis die zij werkelijk heeft.

Zelf gedrag bewaken waar dat nodig is

De opdrachten gebruiken een Sender-Protocol met send(message: str) -> int. notify beschrijft dat contract en geeft het echte antwoord van de verzender door. Hij verandert dat antwoord niet om de annotatie passend te laten lijken.

looks_like_sender heeft bewust een beperkte naam: deze functie gebruikt alleen de vormcontrole. Zij bewijst niet dat een bericht daadwerkelijk kan worden verstuurd. Een ontbrekende methode en een aanwezige maar verkeerd beschreven methode zijn twee verschillende situaties.

Voor send_count vraagt de opdracht daarnaast om een echte controle van het ontvangen resultaat. Alleen een concrete, niet-negatieve int is volgens dat contract een geldig aantal. De expliciete controle wijst een verkeerde waarde af; het Protocol zelf doet dat niet.

Veelgemaakte fout: Protocol verwarren met implementatie

Maak geen instantie van het Protocol om daarmee te versturen. Het Protocol beschrijft de interface; de ontvangen concrete verzender voert haar uit. Ook een testobject kan zo'n concrete verzender zijn, zolang het benodigde gedrag werkelijk aanwezig is.

Een isinstance-uitslag vervangen door een vaste True zou eveneens het doel missen. Dan maak je zelfs het beperkte onderscheid tussen een aanwezig en een ontbrekend lid niet meer. Bewaar daarom de aparte rollen van beschrijving, vormcontrole en echte resultaatvalidatie.

AAN DE SLAG 0/4
1

Een echt benoemd Protocol declareren

Vul Sender als echt runtime_checkable Protocol aan met send(self, message: str) -> int. De methode heeft als declaratiebody ...; concrete objecten leveren hun eigen implementatie. Bewaar de decorator, vereiste methode en beide annotaties.

2

Het benoemde contract bij een functie gebruiken

Maak notify(sender: Sender, message: str) -> int. Roep de echte sender.send(message) eenmaal aan en geef haar resultaat ongewijzigd terug. De annotatie voegt geen conversie of runtime-typecontrole toe; fouten van de methode lopen door.

3

Alleen de beperkte vormcontrole uitvoeren

Maak looks_like_sender(value: object) -> bool met isinstance(value, Sender). Die controle onderscheidt een ontbrekende methode van een aanwezig lid, maar weigert niet automatisch verkeerde methodeparameters of een verkeerd retourtype.

4

Een ontvangen resultaat werkelijk valideren

Maak send_count(sender: Sender, message: str) -> int. Roep send eenmaal aan. Geef een concrete niet-negatieve int terug; weiger andere resultaattypen, inclusief bool, met TypeError("verzender gaf geen int") en een negatieve int met ValueError("verzender gaf negatief aantal"). Laat fouten uit send ongewijzigd doorlopen.

Onthoud dit

  • Een Protocol benoemt een contract op basis van beschikbare leden.
  • Passende concrete objecten hoeven er niet expliciet van te erven.
  • runtime_checkable bewijst aanwezigheid van leden, geen volledige typecorrectheid.

Hierna gebruik je zo'n gedragsafspraak die al in Python zelf bestaat: het iteratieprotocol.

main.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.