Skip to main content

Het gebruiken van classes vs functies in notebook IDE’s

Intro

Notebook IDE’s zijn gemaakt om snel waarde te leveren. Je laadt data, probeert een transformatie, maakt een plot en itereert door tot je ziet wat werkt. Die snelheid is precies waarom notebooks zo populair zijn bij data engineering, analytics en ML.

Notebooks zijn ontworpen rond vrijheid. Maar diezelfde vrijheid wordt problematisch zodra je notebook meer wordt dan een experiment. Dan ontstaat er vrijwel altijd een vorm van verborgen state: variabelen blijven in het geheugen staan en kunnen nog waarden bevatten van een vorige run. Daarnaast is execution order een stille boosdoener. Daar de valkuil: zodra een notebook langer leeft dan één werksessie, verandert “handig” snel in “fragiel”. Je krijgt vragen als: waar komt deze variabele vandaan? waarom werkt dit alleen als ik eerst cell X run? en hoe krijg ik dit in een pipeline zonder het te herschrijven?

Na verloop van tijd wordt het ook steeds lastiger om wijzigingen scherp te houden. Kleine variaties op dezelfde logica duiken op in meerdere cellen (“even snel aangepast”), en code reviews worden moeilijker omdat de echte verandering verspreid staat over het document.

Op dat moment komt de discussie op tafel: structureer je met functies (functioneel) of met classes (object-georiënteerd)? In notebooks is dat geen stijlkeuze, maar een keuze die direct raakt aan reproduceerbaarheid, testbaarheid en samenwerking. Hieronder krijg je een pragmatische beslisbril, inclusief trade-offs en valkuilen.

Het juiste model: notebooks zijn stateful; je code moet dat compenseren

Notebooks zijn in de kern een state machine. Cellen muteren de runtime en bouwen voort op wat er al in geheugen staat. Je hoeft dat niet weg te architecten, maar je wilt wel voorkomen dat state de uitkomst onvoorspelbaar maakt.

Je voelt vaak een spanning tussen twee doelen. Aan de ene kant wil je exploratie: snel itereren, veel print/plot, trial-and-error. Aan de andere kant wil je notebook die bestendig draait in productie. Een goed notebook-ontwerp probeert die twee niet te mengen in één grote blob, maar zorgt dat exploratie “bovenop” stabiele bouwblokken kan gebeuren. Je wilt structuur die exploratie faciliteert én die je later zonder drama kunt in productie kunnen zetten

(Visual suggestie: “Notebook state & afhankelijkheden” diagram — cellen → gedeelde runtime/state → outputs/artefacts.)

Waarom functies zo goed passen in notebooks

Functies sluiten bijna natuurlijk aan op notebooks omdat ze je dwingen om dataflow zichtbaar te maken. Zodra je een stap in een functie stopt, moet je nadenken over inputs en outputs. Dat is precies wat je nodig hebt om verborgen state te verminderen: de functie kan niet “per ongeluk” afhankelijk zijn van een variabele uit een andere cell, tenzij je die afhankelijkheid expliciet maakt.

Daarnaast helpen functies bij samenwerking. Kleine pythonic functies zijn makkelijker te reviewen, en ze vormen de meest pijnloze brug van notebook naar productie. Je kunt ze eerst lokaal in dezelfde notebook definiëren, en zodra ze stabiel worden verhuis je ze naar een module of package. Daarmee verandert je notebook vanzelf in wat het idealiter is: orchestratie, uitleg en resultaten—niet de complete codebase.

Als je twijfelt, begin met functies. Je koopt er direct herhaalbaarheid en team-readability mee, zonder vroegtijdig een ontwerp “vast” te zetten.

Notebook-valkuilen met functies

Ook met functies kun je in notebooks ongemerkt afhankelijk worden van de runtime. Dat gebeurt vooral wanneer functies eigenlijk nog steeds leunen op globale variabelen (“df staat wel ergens”), wanneer defaults niet deterministisch zijn (tijd, random seeds) of wanneer functies side effects hebben die niet zichtbaar zijn in de return values (bijvoorbeeld verspreide file writes).

De regel is simpel: als je je functie niet kunt draaien na een kernel restart zonder ‘setup-cellen’, dan is het geen echte functie maar een cell in vermomming.

De misvatting: classes maken het niet automatisch ‘professioneel’

In veel teams voelt objectoriëntatie als de professionele standaard. Het is daarom logisch dat de reflex ontstaat om een groeiende notebook te “redden” door er classes van te maken. Soms helpt dat, maar in notebooks maskeer je ermee net zo vaak het echte probleem.

Een class kan state achter self verbergen, maar lost execution-order issues niet op. Sterker: in een notebook kan een class juist uitnodigen om steeds meer “handige” dingen in self te stoppen—clients, tijdelijke dataframes, caches, feature flags—waardoor de hoeveelheid verborgen state toeneemt. En omdat notebooks interactief zijn, loop je ook tegen een typisch notebook-probleem aan: je runt een cell opnieuw, definieert de class opnieuw, maar bestaande objecten kunnen nog aan de oude definitie hangen. Dat levert bugs op die er in een normale codebase veel minder vaak zijn.

Notebook-valkuilen met classes

In notebooks kunnen classes extra verwarrend worden door de interactieve runtime. Het herdefiniëren van classes na een re-run is een klassieker: je denkt dat je nieuwe logica draait, maar sommige objecten leven nog in de oude wereld. Daarnaast groeit state gemakkelijk onzichtbaar in self, zeker als self de plek wordt waar je “even snel” iets parkeert. En zodra je objecten wilt serialiseren (bijvoorbeeld voor model artifacts of distributed runs), blijken clients en handlers in objecten ineens lastig. Debuggen kan tot slot stroever worden omdat stack traces naar cellen wijzen en state niet zichtbaar is zonder inspectie.

Classes verhogen in notebooks vaak de hidden complexity. Als je ze gebruikt, moet je juist strenger zijn: houd self klein, maak state expliciet, en verplaats classes bij voorkeur naar library-code buiten de notebook.

Praktisch detail: voor configuratie werkt een dataclass (of zelfs een simpele dict) vaak beter dan een “ConfigManager” class. Daarmee hou je data en gedrag gescheiden en voorkom je dat configuration een mini-framework wordt.

Classes: nuttig, maar alleen bij duidelijke redenen

De enige goede redenen om classes te gebruiken

Er zijn momenten waarop classes precies het juiste gereedschap zijn, maar dan moet de reden concreet zijn. Een class is bijvoorbeeld logisch wanneer je samenhangende state hebt met invariants: één configuratie, één set connecties/clients of een cache die consistent moet blijven. In zulke gevallen kan een object de grenzen bewaken en voorkom je dat overal losse parameters rondzwerven.

Classes zijn ook passend wanneer je een duidelijke lifecycle hebt die je als interface wilt aanbieden. Denk aan een ML-workflow waarin fit() gevolgd wordt door transform() of predict(), of aan resources die je expliciet wil openen, gebruiken en sluiten. Een derde reden is uitwisselbaarheid: als je meerdere implementaties achter één interface wilt hangen (bijvoorbeeld verschillende datasources of feature builders), dan is een OO-interface vaak helderder dan een set losse functies met veel if/else. Tot slot zijn er frameworks die objectoriëntatie verwachten (zoals scikit-learn-achtige patterns of plugin-systemen); dan is een class simpelweg de meest compatibele keuze.

Oftewel: classes zijn geen “structuurmiddel”, maar een modelmiddel. Ze helpen als er écht een object bestaat met gedrag en lifecycle. Als je class vooral een container is voor losse functies, zit je meestal verkeerd.

Een praktisch besliskader

Als je in een notebook twijfelt, helpt het om de vraag te herformuleren. Je kiest niet tussen “mooi” of “lelijk”, maar tussen twee soorten structuur: wil je stappen structureren of wil je gedrag modelleren?

Als je notebook vooral een pipeline is (load → transform → aggregate → export), dan is functionele decompositie bijna altijd de beste start. Je krijgt expliciete inputs/outputs, je kunt stap voor stap sanity checks toevoegen, en je kunt de logica later zonder veel gedoe verhuizen naar een module. Als je daarentegen echt objecten hebt met lifecycle (fit/transform/predict), of je hebt meerdere implementaties die je uitwisselbaar wil maken, dan wordt een class aantrekkelijk—maar liefst in library-code buiten het notebook.

(Visual suggestie: decision matrix functions vs classes vs hybride (functions + dataclass config) met criteria zoals lifecycle, multiple implementations, framework interface, testbaarheid.)

Conclusie

Het doel is niet om meteen perfect te ontwerpen, maar om structuur gecontroleerd te laten meegroeien met gebruik zonder dat je exploratie kapotmaakt. Functies brengen orde in dataflow; classes brengen orde in gedrag. In notebooks is dataflow meestal je grootste pijnpunt, dus functies winnen vaak als eerste stap. De volgende stap kan best een class zijn, maar maak niet zomaar die stap.

Meer nieuws

Kennismaken met Vitas?

De koffie staat voor je klaar.

Hebjij vragen voor ons? Neem dan contact op.

We staan klaar om al je vragen te beantwoorden.