Een programma over bestanden verdelen · oefening
Packagemappen en circulaire imports
Drie modules staan naast main.py en op die schaal is een vlak project prima. Bij twintig helpt de map niet meer: models.py, attempts.py, report.py, report_html.py, scoring.py, scoring_rules.py, en nergens een zichtbare groepering.
Een package is een map met modules en het volgende niveau van hetzelfde idee. Een module groepeert samenhangende definities; een package groepeert samenhangende modules.
Een package maken
quizapp/
__init__.py
models.py
scoring.py
main.py
quizapp is een regulier package omdat de map __init__.py bevat. Python ondersteunt ook namespacepackages zonder dat bestand, maar reguliere packages zijn de expliciete, voorspelbare vorm die dit project gebruikt. Je bereikt de modules met een punt:
from quizapp.models import Question
from quizapp import scoring
De punt in quizapp.models is het scheidingsteken voor de map, zoals Python ‘binnen’ schrijft.
Waar __init__.py voor dient
Het markeert de map als een regulier package en wordt uitgevoerd wanneer het package voor het eerst wordt geïmporteerd. Meestal hoort het leeg te zijn en een leeg bestand is daar volkomen goed en heel gebruikelijk.
Het ene veelvoorkomende gebruik is bepalen wat het package aanbiedt:
# quizapp/__init__.py
from quizapp.models import Question, Quiz
Met die regel kunnen gebruikers van het package from quizapp import Question schrijven zonder te weten welke module erin de klasse bevat. Dat is een echt voordeel en een echte verplichting: je hebt die naam beloofd en de klasse tussen modules verplaatsen mag die niet stukmaken.
Begin met een lege __init__.py. Voeg iets toe als je daar een reden voor hebt.
Wat doet een lege __init__.py?
Absolute imports binnen je eigen project
Binnen quizapp/scoring.py schrijf je het bereiken van de naastgelegen models.py vanaf de bovenkant van het project:
from quizapp.models import Question
Niet from models import Question, dat buiten het package zou zoeken, en niet from .models import Question, de relatieve vorm. Relatieve imports werken en je komt ze tegen in andermans code; de absolute vorm is voor een project van deze grootte duidelijker, omdat de regel overal hetzelfde leest.
Het probleem dat dit creëert
Twee modules die elkaar importeren heet een circulaire import en dat is het importprobleem waar mensen steeds weer tegenaan lopen.
# quizapp/models.py
from quizapp.scoring import grade_letter # models needs scoring
# quizapp/scoring.py
from quizapp.models import Question # scoring needs models
Python begint models te importeren, bereikt regel één en gaat scoring importeren. scoring bereikt regel één en vraagt om models, dat al wordt geïmporteerd en dus half af is: de naam bestaat, maar Question is nog niet gedefinieerd. Het resultaat is een ImportError die klaagt over een gedeeltelijk geïnitialiseerde module, en het bericht wijst naar het bestand dat toevallig als tweede kwam.
Het verwarrende is dat beide bestanden afzonderlijk correct zijn. De fout zit in de vorm van de afhankelijkheid, niet in een van de regels. Het diagram noemt de linkerkant ‘Before: two directions’, vóór de reparatie twee richtingen, en de rechterkant ‘After: one direction’, daarna één richting. ‘Imports’ en ‘imports back’ betekenen importeert en importeert terug; ‘needs scoring’ en ‘needs Question’ geven aan wat elke module nodig heeft. Na de reparatie betekent ‘no scoring import’ geen import van scoring en ‘uses Question’ gebruikt Question. Onderaan staan ‘circular import’, circulaire import, en ‘one-way dependency’, afhankelijkheid in één richting.

Het herstellen
De cyclus vertelt je bijna altijd iets waars, dus goede oplossingen zijn ontwerpoplossingen:
Een van de twee richtingen is verkeerd. Dit is het gebruikelijke antwoord. Heeft models echt scoring nodig of werd er ergens een letterbeoordeling berekend waar die niet hoort? De import verwijderen die niet zou moeten bestaan herstelt de cyclus en verbetert het ontwerp.
Het gedeelde onderdeel heeft een eigen module nodig. Als beide echt iets nodig hebben, verplaats dat dan naar een derde module die beide importeren. De cyclus wordt een duidelijke structuur.
De import is alleen binnen een functie nodig. import van de bovenkant van het bestand verplaatsen naar de functie die die gebruikt stelt de import uit totdat beide modules zijn geladen. Dit werkt en verdient een kritische blik: het verbergt de afhankelijkheid voor iedereen die de bovenkant van het bestand leest en betekent meestal dat een van de eerste twee oplossingen beter paste.
De oefening bevat een echte cyclus en de eerste oplossing is van toepassing. De werkruimte biedt ook de volledige scoremodule van het package en een rapportmodule binnen het package voor de uiteindelijke samenvoeging; je enige taak hier is de richting tussen scoring en models herstellen.
Opdracht
Deze les biedt een package quizapp met een opzettelijke circulaire import. Submit maakt het defecte package zichtbaar, hoewel Run nog steeds de eerdere vlakke modules gebruikt.
quizapp/scoring.py importeert Question uit quizapp.models, wat correct is: scoring heeft het domein nodig. quizapp/models.py importeert grade_letter uit quizapp.scoring, wat de cirkel sluit, waardoor geen van beide modules het laden kan afronden.
Herstel dit door de import te verwijderen die niet hoort te bestaan. Vraag welke richting werkelijk nodig is: moet een Question iets over letterbeoordelingen weten?
Question.grade_for(response) heeft de verkeerde import binnengehaald. Die hoort helemaal niet op een vraag, dus verwijder de methode. Een vraag meldt of een antwoord juist is; bepalen welke letter bij een verhouding hoort is de taak van scoring, en scoring vraagt het al aan de vraag.
Laat al het andere in quizapp/models.py zoals het is. Question, Quiz en Attempt behouden het gedrag dat ze het hele hoofdstuk hadden: het package is het volledige domein, waaruit de volgende les importeert.
De gegeven quizapp/scoring.py bevat de volledige regels voor percentages en letterbeoordelingen plus is_pass. De gegeven quizapp/report.py gebruikt die packageregels. Bewerk in deze oefening geen van beide hulpbestanden.