OOP Python – Methoden en inkapseling¶
Inkapseling (Engels: encapsulation) is één van de fundamenten van object-georiënteerd programmeren. Het wordt gebruikt om onbevoegden niet de gelegenheid te bieden de kenmerken van een object zomaar aan te passen. Als dat mogelijk moet zijn dan dienen zij toegang te krijgen tot de zogenaamde getters en setters, waarover zo dadelijk meer.
Aan het eind van deze tekst maken we oefening nummer 1.
zichtbaarheid
Python wijkt nadrukkelijk af van het idee van inkapseling zoals het bijvoorbeeld gedaan wordt bij Java. Python gebruikt niet de sleutel-woorden private of protected om de zichtbaarheid van een methode aan te geven.
Het voorbeeld Voorraad¶
Als voorbeeld hier een uitbreiding van onze webshop, waarbij het mogelijk is voorraad bij te bestellen, producten te verkopen en een geschiedenis van alle voorraad-mutaties bij te houden, inclusief tijdstip. Uiteraard is de opzet erg beperkt.
De code wordt in stapjes opgebouwd; we slaan deze code uiteindelijk op in het bestand voorraad.py.
Klasse en methoden¶
Stap 1: de klasse
class Voorraad:
Stap 2: het importeren van de huidige tijd
import datetime
Stap 3: een functie om het tijdstip van de mutatie vast te leggen.
@staticmethod
def _current_time():
now = datetime.datetime.now()
return f"{now: %Y-%m-%d %H:%M:%S}"
De functie _current_time() retourneert het tijdstip van de mutatie in het opgegeven formaat, tot op de seconde nauwkeurig. De @staticmethod decorator geeft aan dat dit een functie is die bij de klasse hoort maar niet afhankelijk is van een specifieke instantie.
De constructor¶
Stap 4: de constructor
def __init__(self, product_naam, aantal):
self._product_naam = product_naam
self.__aantal = aantal
self._mutatie_geschiedenis = []
print(f"Voorraad aangemaakt voor {self._product_naam}")
Bijbestellen en verkopen¶
Bij het aanmaken van een nieuwe voorraad wordt gevraagd om een productnaam en een beginhoeveelheid. De variabele _mutatie_geschiedenis is een lijst ([]) waarin alle mutaties worden vastgelegd.
Stap 5: de methode bijbestellen()
def bijbestellen(self, aantal):
if aantal > 0:
self.__aantal += aantal
self._mutatie_geschiedenis.append((Voorraad._current_time(), aantal))
self.toon_voorraad()
Indien het bij te bestellen aantal groter dan nul (0) is, wordt de voorraad aangepast en wordt de mutatie toegevoegd aan de lijst _mutatie_geschiedenis[] (zie je wat het datatype is van dat wat er aan de lijst wordt toegevoegd?). Ook de actuele voorraad wordt nu getoond.
Stap 6: de methode verkoop()
def verkoop(self, aantal):
if 0 < aantal <= self.__aantal:
self.__aantal -= aantal
self._mutatie_geschiedenis.append((Voorraad._current_time(), -aantal))
else:
print("Het aantal dient groter dan nul (0) en maximaal gelijk aan de voorraad te zijn")
self.toon_voorraad()
Er vindt een controle plaats of de voorraad toereikend is voor de verkoop. Zo niet, volgt er een passende mededeling. Zo ja, wordt de voorraad bijgewerkt en de mutatie weer in de lijst vastgelegd. Ook hier wordt de actuele voorraad getoond.
Stap 7: de methode toon_voorraad()
def toon_voorraad(self):
print(f"Voorraad {self._product_naam} bedraagt {self.__aantal}")
Mutatie-overzicht¶
Stap 8: het mutatie-overzicht
def toon_mutaties(self):
print(f"\nMutatie-geschiedenis voor {self._product_naam}:")
for datum, aantal in self._mutatie_geschiedenis:
if aantal > 0:
mutatie_type = "bijbesteld"
else:
mutatie_type = "verkocht"
aantal = abs(aantal)
print(f" {datum}: {aantal} {mutatie_type}")
In een lus wordt de volledige lijst van mutaties doorlopen en wordt de informatie naar het scherm geschreven.
De volledige code¶
De volledige klasse ziet er nu als volgt uit:
import datetime
class Voorraad:
"""Klasse voor het bijhouden van productvoorraad"""
@staticmethod
def _current_time():
now = datetime.datetime.now()
return f"{now: %Y-%m-%d %H:%M:%S}"
def __init__(self, product_naam, aantal):
self._product_naam = product_naam
self.__aantal = aantal
self._mutatie_geschiedenis = []
print(f"Voorraad aangemaakt voor {self._product_naam}")
def bijbestellen(self, aantal):
if aantal > 0:
self.__aantal += aantal
self._mutatie_geschiedenis.append((Voorraad._current_time(), aantal))
self.toon_voorraad()
def verkoop(self, aantal):
if 0 < aantal <= self.__aantal:
self.__aantal -= aantal
self._mutatie_geschiedenis.append((Voorraad._current_time(), -aantal))
else:
print("Het aantal dient groter dan nul (0) en maximaal gelijk aan de voorraad te zijn")
self.toon_voorraad()
def toon_voorraad(self):
print(f"Voorraad {self._product_naam} bedraagt {self.__aantal}")
def toon_mutaties(self):
print(f"\nMutatie-geschiedenis voor {self._product_naam}:")
for datum, aantal in self._mutatie_geschiedenis:
if aantal > 0:
mutatie_type = "bijbesteld"
else:
mutatie_type = "verkocht"
aantal = abs(aantal)
print(f" {datum}: {aantal} {mutatie_type}")
Testen¶
Nu is het tijd om de voorraad te testen!
laptop_voorraad = Voorraad("Laptop", 10)
laptop_voorraad.bijbestellen(5)
laptop_voorraad.verkoop(3)
laptop_voorraad.verkoop(7)
laptop_voorraad.bijbestellen(10)
laptop_voorraad.toon_mutaties()
Dit geeft de volgende uitvoer:
Voorraad aangemaakt voor Laptop
Voorraad Laptop bedraagt 15
Voorraad Laptop bedraagt 12
Voorraad Laptop bedraagt 5
Voorraad Laptop bedraagt 15
Mutatie-geschiedenis voor Laptop:
2026-01-31 14:23:15: 5 bijbesteld
2026-01-31 14:23:15: 3 verkocht
2026-01-31 14:23:15: 7 verkocht
2026-01-31 14:23:16: 10 bijbesteld
Private versus Protected attributen¶
Je ziet in de code dat we twee soorten underscores gebruiken:
- Enkele underscore (
_product_naam): Dit is een conventie in Python om aan te geven dat een attribuut "protected" is. Het is bedoeld voor intern gebruik maar is nog steeds toegankelijk van buitenaf. - Dubbele underscore (
__aantal): Dit triggert Python's name mangling mechanisme, waardoor het attribuut moeilijker direct toegankelijk wordt van buitenaf.
Laten we dit demonstreren:
laptop_voorraad = Voorraad("Laptop", 10)
# Dit werkt (protected met enkele underscore):
print(laptop_voorraad._product_naam) # Output: Laptop
# Dit werkt NIET (private met dubbele underscore):
print(laptop_voorraad.__aantal) # AttributeError!
Het tweede voorbeeld geeft een foutmelding:
AttributeError: 'Voorraad' object has no attribute '__aantal'
Python heeft het attribuut __aantal hernoemd naar _Voorraad__aantal (name mangling) om directe toegang te voorkomen. Dit is Python's manier om attributen meer "private" te maken, hoewel ze technisch gezien nog steeds toegankelijk zijn via laptop_voorraad._Voorraad__aantal.
Waarom inkapseling?¶
Inkapseling heeft verschillende voordelen:
- Controle: Je kunt validatie toevoegen (zoals in
verkoop()waar we checken of er genoeg voorraad is) - Flexibiliteit: Je kunt later de interne implementatie aanpassen zonder de interface te wijzigen
- Beveiliging: Je voorkomt dat gebruikers per ongeluk of opzettelijk ongeldige waarden instellen
In het volgende deel gaan we kijken naar getters en setters, en hoe we de @property decorator kunnen gebruiken voor elegantere toegang tot private attributen.
Samenvatting¶
In dit deel hebben we geleerd over:
- Inkapseling als OOP-principe
- Het verschil tussen
_(protected) en__(private) attributen - Het gebruik van
@staticmethodvoor klassemethoden - Het bijhouden van een geschiedenis met tijdstempels
- Het valideren van input in methoden
In het volgende deel gaan we kijken naar meerdere klassen die met elkaar samenwerken.
Maak nu oefening nummer 1.