Slotproject: een uitleenbibliotheek voor de buurt · oefening
Lever te late uitleningen op
Er is nog één bewerking over. Het contract beschrijft die anders dan de andere twee:
overdue_loans(on_day)is een iterable of generator
available_items() en active_loans() geven allebei een lijst terug. Deze hoeft dat niet te doen. Het is de moeite waard om dat verschil te begrijpen in plaats van alleen aan de eis te voldoen.
Wat een generator verandert
Hoofdstuk 11 bouwde dit idee op. Een generatorfunctie wordt niet uitgevoerd wanneer je haar aanroept. Ze geeft iets terug dat steeds een stukje wordt uitgevoerd, telkens wanneer je om de volgende waarde vraagt.
def overdue_loans(self, on_day):
for loan in self._loans.values():
if loan.is_overdue(on_day):
yield loan
Eén yield maakt van de hele methode een generatorfunctie. Er wordt nergens een lijst gemaakt en op het moment dat de methode terugkeert, is er nog niets onderzocht.
Voor een rapport over te late uitleningen is dat op een andere manier relevant dan voor de andere twee. Meestal is het antwoord leeg en wil een aanroeper alleen de eerste paar resultaten:
for loan in library.overdue_loans(today):
print(f"{loan.item_id} was due on day {loan.due_day}")
break
Met een lijst wordt elke uitlening onderzocht voordat de eerste regel wordt afgedrukt. Met een generator worden uitleningen onderzocht totdat de eerste te late is gevonden. Als er geen te laat is, onderzoekt de generator nog steeds elke actieve uitlening, maar bouwt hij nooit een resultatenlijst.
Waar de regel staat
Let op de voorwaarde:
if loan.is_overdue(on_day):
Die is dus niet if on_day > loan.due_day. In les 4 zette je die vergelijking op Loan. Hier levert dat iets op: de keuze tussen > en >= staat op precies één plek. Als je haar hier had uitgeschreven, waren er nu twee plekken die fout konden zijn. Uiteindelijk zouden ze op verschillende manieren fout gaan.
De keerzijde
Een generator over self._loans werkt op de actuele collectie. Hij leest de dictionary terwijl hij verdergaat. Een aanroeper die tijdens het doorlopen een item terugbrengt, krijgt daardoor een RuntimeError over de veranderde grootte van de dictionary.
Dat is een wezenlijk verschil met active_loans(), dat een kopie maakt. Het is geen reden om generators te vermijden, maar wel om te weten welke vorm je teruggeeft: list(library.overdue_loans(today)) maakt een momentopname wanneer je de generator verbruikt voordat je de bibliotheek wijzigt. Die keuze is aan de aanroeper; jij hoeft die niet op te leggen.
Waarom kun je active_loans() doorlopen terwijl items worden teruggebracht, maar overdue_loans() niet?
Vier bestanden, en waarom deze vier
Na deze les is het project af. Het is dus de moeite waard om naar de uiteindelijke vorm te kijken.
models.py bevat de begrippen: waar het programma over gaat. Het importeert niets uit de rest van het project.
errors.py bevat wat de bibliotheek weigert. Het importeert helemaal niets.
library.py bevat het gedrag en importeert beide.
main.py is het programma en importeert wat het nodig heeft om te werken.
Elke pijl wijst dezelfde kant op, vanuit het programma naar de begrippen, en geen enkele wijst terug. Dat is de vorm waarvoor hoofdstuk 10 pleitte. De controle is eenvoudig: je kunt models.py begrijpen, testen en hergebruiken zonder een van de andere bestanden. Dat zou niet gelden voor een indeling waarin het domein de bibliotheek kent die het bevat.
Opdracht
Implementeer overdue_loans in library.py, de laatste bewerking uit het contract.
overdue_loans(on_day) levert elke actieve uitlening op die op die dag te laat is, één voor één. Gebruik yield in plaats van een lijst te bouwen: een rapport over te late uitleningen is meestal leeg en een aanroeper wil vaak alleen de eerste paar.
Vraag elke uitlening of die te laat is in plaats van hier dagen te vergelijken. Loan.is_overdue beheert die beslissing al en die hoort op één plek te blijven.
Je hoeft verder niets in library.py aan te passen. Dit is de laatste wijziging aan het bestand.