Klassen — data en gedrag samen
`class`, `__init__` en `self`, methodes, een printbare vorm met `__repr__`, gelijkheid met `__eq__`, klasseniveau tegen instantieniveau, en wanneer een klasse niet gewenst is.
Hoe wil je leren?
Het probleem dat we oplossen
Tot nu toe hebben we de verschillende feiten over één ding in een dictionary bewaard.
order = {"name": "pen", "price": 15.0, "quantity": 3}
print(order["nmae"])KeyError: 'nmae'De fout werd gevangen, wat al iets is — maar pas wanneer die regel draait. Verborgen in een voorwaarde, kan het pas over een maand tevoorschijn komen.
En het probleem zit niet alleen in de spelling. Elke key kan in een dictionary komen, dus nergens staat wat een orderregel moet bevatten. De functie die het optelt staat ergens anders, en niemand controleert of de dictionary alle velden heeft voordat hij hem aanroept. De data zit op één plek, wat ermee kan op een andere, en wat bij wat hoort bestaat alleen in je hoofd.
class OrderLine:
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
def total(self):
return round(self.price * self.quantity, 2)
line = OrderLine("pen", 15.0, 3)
print(line.name)
print(line.total())pen
45.0Een klasse is een vorm: hij bepaalt wat een ding van deze soort bevat, en wat ermee kan.
Aan het einde van dit hoofdstuk kun je
- Een
classschrijven, en uitleggen wat__init__enselfdoen - Methodes schrijven, en uitleggen hoe ze verschillen van gewone functies
- Een object een printbare vorm geven met
__repr__ - Uitleggen wat een klasse-attribuut scheidt van een instantie-attribuut — en een bekende valkuil vermijden
- Twee objecten vergelijkbaar maken met
__eq__ - Zeggen wanneer een klasse niet gewenst is
Wat je eerst nodig hebt: JSON en CSV.
Wat self eigenlijk is
self is geen tovenaarswoord — het is de eerste parameter, en wat erin landet is het object waarop de methode werd aangeroepen.
line.total() schrijven betekent "voer OrderLine zijn total-methode uit, en geef hem line als self". Daarom geeft self.price binnenin die specifieke regel zijn prijs.
Vergeet self en de melding is verwarrend:
class OrderLine:
def __init__(self, name):
self.name = name
def shout():
return "hello"
line = OrderLine("pen")
print(line.shout())TypeError: OrderLine.shout() takes 0 positional arguments but 1 was given"1 was given" — maar de haakjes waren leeg. Wat gegeven werd is line zelf, stilletjes, en shout liet geen ruimte om het te ontvangen.
__init__ is ook gewoon een methode, met een speciale naam: Python roept hem net na het maken van het object aan. Binnenin zet self.name = name de attributen.
Een typfout geeft een duidelijke fout
class OrderLine:
def __init__(self, name):
self.name = name
line = OrderLine("pen")
print(line.nmae)AttributeError: 'OrderLine' object has no attribute 'nmae'Eerlijk gezegd: een klasse vangt deze fout niet vroeger op. Een AttributeError, net als een KeyError, verschijnt pas wanneer de regel draait. Verborgen in een voorwaarde, kan ook deze pas over een maand tevoorschijn komen.
Waar zit de winst dan? Op drie plekken.
- De fout vertelt meer. De melding noemt de klasse —
'OrderLine' object— dus je ziet op wat voor ding de verkeerde naam werd opgezocht. - Wat er moet zitten staat op één plek.
__init__lezen laat zien welke attributen eenOrderLineheeft. Een dictionary heeft zo'n lijst niet. - Je editor kan helpen. Een editor leest de klasse en suggereert attributenamen na
line., en veel editors strepen een onbekende naam al onder — voordat de code ooit draait. Met dictionary-keys kan een editor dat meestal niet.
Onthoud ook de limiet: een typfout wordt niet bij het schrijven gevangen. Net als order["nmae"] = 5 stilletjes een nieuwe key maakt, maakt line.nmae = 5 stilletjes een nieuw attribuut.
En je kunt bekijken wat erin zit:
class OrderLine:
def __init__(self, name):
self.name = name
line = OrderLine("pen")
print(type(line))
print(isinstance(line, OrderLine))
print(line.__dict__)<class '__main__.OrderLine'>
True
{'name': 'pen'}__dict__ laat zien dat er echt een dictionary in het object zit. De klasse heeft de dictionary niet verwijderd — hij gaf hem een naam en een vorm.
__repr__ — wat printen laat zien
Zonder een __repr__:
class OrderLine:
def __init__(self, name):
self.name = name
print(OrderLine("pen"))<__main__.OrderLine object at 0x79b769f03830>Het getal aan het einde is een geheugenadres, en het verschilt bij elke run — op je scherm zie je een ander getal. Bij debuggen is het van geen nut.
class OrderLine:
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return f"OrderLine(name={self.name!r}, price={self.price!r})"
line = OrderLine("pen", 15.0)
print(line)
print([line, line])OrderLine(name='pen', price=15.0)
[OrderLine(name='pen', price=15.0), OrderLine(name='pen', price=15.0)]De tweede regel is de echte winst: het werkt ook in een lijst. Wanneer Python een lijst print, roept hij elk item zijn __repr__ aan, niet zijn __str__. Daarom is van de twee __repr__ de eerste die je schrijft.
De conventie is om een string terug te geven die eruitziet als de code die het object bouwde. !r zetten haalt aanhalingstekens om de strings binnenin — om dezelfde reden als in hoofdstuk 24.
Gelijkheid — de standaard is niet wat je wilt
class OrderLine:
def __init__(self, name):
self.name = name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a == a)False
TrueDe twee objecten bevatten hetzelfde, en toch geeft == False. Want de standaardgelijkheid is identiteit — == gedraagt zich net als is, en vraagt "is dit precies hetzelfde object?".
Om dit te veranderen, moet je zeggen wat gelijk is:
class OrderLine:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a is b)True
FalseNu zegt == "gelijk" en is "dezelfde" — het onderscheid uit hoofdstuk 21, nu in een eigen type.
Let op: __eq__ schrijven laat het object standaard onhashbaar, dus het kan niet in een set of dictionary-key. Het volgende hoofdstuk's dataclass doet beide in één regel.
Klasseniveau en instantieniveau
class Basket:
TAX_RATE = 0.15
items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)
print(a.items is b.items)['pen', 'bag']
['pen', 'bag']
TrueTwee aparte manden, maar slechts één lijst.
Herken je het? Dit is de veranderlijke standaard-valkuil uit hoofdstuk 21, op een nieuwe plek. De items = [] net onder de class-regel draait één keer, wanneer de klasse gemaakt wordt — niet één keer per object. Dus de lijst hoort bij de klasse, en elke instantie deelt hem.
De oplossing is van hetzelfde type: wat bij elk object hoort, bouw je in __init__.
class Basket:
TAX_RATE = 0.15
def __init__(self):
self.items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)
print(a.TAX_RATE, b.TAX_RATE)['pen']
['bag']
0.15 0.15TAX_RATE bleef op klasseniveau, en terecht — het is hetzelfde voor elke mand, en we willen het niet veranderen. Maar let op dat Python je niet tegenhoudt om het te veranderen: Basket.TAX_RATE = 0.2 geeft geen fout. De alles-hoofdletters naam is slechts een conventie die de lezer zegt "behandel dit als een constante, verander het niet". De regel in één zin: constanten op klasseniveau; alles wat verandert in __init__.
Wanneer je geen klasse schrijft
Zodra je klassen geleerd hebt, is het verleidelijk om alles een klasse te maken. Een paar tekens dat de klasse niet nodig is:
De klasse heeft alleen een __init__ en één methode. Dat is een functie, in twee gesplitst zonder reden.
Geen methodes, alleen data. Het volgende hoofdstuk's dataclass is precies daarvoor gemaakt, in veel minder regels.
Het object verandert nooit en houdt één ding. Een tuple, of een platte waarde, is genoeg.
Een klasse is het juiste gereedschap waar data en gedrag echt bij elkaar horen: data die verandert, en een paar operaties die altijd op die data draaien.
Een compleet voorbeeld
"""An order built from two small classes."""
TAX_RATE = 0.15
class OrderLine:
"""One item on an order: what it is, what it costs, how many."""
def __init__(self, name, price, quantity):
if quantity < 1:
raise ValueError(f"quantity must be at least 1: {quantity}")
self.name = name
self.price = price
self.quantity = quantity
def total(self):
"""Price for this line, tax included."""
return round(self.price * self.quantity * (1 + TAX_RATE), 2)
def __repr__(self):
return f"OrderLine({self.name!r}, {self.price!r}, {self.quantity!r})"
class Order:
"""A named collection of lines, and the questions you ask about it."""
def __init__(self, customer):
self.customer = customer
self.lines = []
def add(self, name, price, quantity):
self.lines.append(OrderLine(name, price, quantity))
return self
def total(self):
return round(sum(line.total() for line in self.lines), 2)
def largest(self):
"""The most expensive line, or None for an empty order."""
if not self.lines:
return None
return max(self.lines, key=lambda line: line.total())
def __repr__(self):
return f"Order({self.customer!r}, {len(self.lines)} lines)"
def main():
order = Order("rafi")
order.add("pen", 15.0, 3)
order.add("bag", 850.0, 1)
order.add("ink", 120.0, 2)
for line in order.lines:
print(f"{line.name:<6} {line.total():>9.2f}")
print(f"{'total':<6} {order.total():>9.2f}")
print()
print(order)
print("largest:", order.largest())
try:
order.add("clip", 5.0, 0)
except ValueError as err:
print("rejected:", err)
if __name__ == "__main__":
main()pen 51.75
bag 977.50
ink 276.00
total 1305.25
Order('rafi', 3 lines)
largest: OrderLine('bag', 850.0, 1)
rejected: quantity must be at least 1: 0Vier dingen om op te letten.
__init__ valideert. Met quantity < 1 wordt het object nooit aangemaakt. Dit is het nuttigste wat een klasse biedt: een OrderLine hebben betekent dat de quantity minstens één is — nergens waar het gebruikt wordt hoeft nog gecontroleerd te worden. Een dictionary kan die garantie niet geven.
Order weet niet hoe een regel geprijsd wordt. Hij roept line.total() aan en dat is alles. Hoe btw toegepast wordt is OrderLine zijn zaak, en het veranderen verandert één plek.
Beide klassen hebben een __repr__, en largest() zijn uitvoer laat zien waarom het ertoe doet. Zonder zou die laatste regel een geheugenadres zijn geweest, wat je niets vertelt.
add geeft zichzelf terug (return self). Dat maakt order.add(...).add(...) mogelijk. Het wordt hier niet gebruikt, bewust — de optie hebben en het overal gebruiken zijn twee dingen.
Wanneer het breekt
TypeError: ... takes 0 positional arguments but 1 was given De methode mist self. Voeg self toe als eerste parameter.
AttributeError: 'X' object has no attribute 'y' Een typfout, of self.y was nooit gezet in __init__. print(obj.__dict__) laat zien wat er echt zit.
TypeError: X() takes no arguments Er is geen __init__, of de naam is verkeerd gespeld (__init__, niet __int__, twee underscores aan beide kanten).
Elk object deelt één lijst De lijst staat op klasseniveau. Verplaats hem naar binnen __init__.
Twee objecten houden hetzelfde en == geeft False De standaardgelijkheid is identiteit. Schrijf __eq__ — of gebruik het volgende hoofdstuk's dataclass.
Na __eq__ schrijven gaat het niet in een set __eq__ schrijven haalt de standaardhash weg. dataclass(frozen=True) geeft beide.
Printen geeft <__main__.X object at 0x...> Er is geen __repr__.
Stap 4 van 6 — Voorspellen
Test je begrip
shout() wordt zonder argumenten aangeroepen. Wat gebeurt er?
class OrderLine:
def __init__(self, name):
self.name = name
def shout():
return "hello"
line = OrderLine("pen")
print(line.shout())- AEen `TypeError` — hij neemt één, en er werden nul gegeven
- BEen `TypeError` — hij neemt nul, en er werd één gegeven
- CEen `AttributeError`
- D`hello` wordt geprint
Twee aparte manden, elk één item. Wat wordt geprint?
class Basket:
items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)- A['pen', 'bag'] []
- B['pen'] ['pen', 'bag']
- C['pen'] ['bag']
- D['pen', 'bag'] ['pen', 'bag']
De twee objecten houden dezelfde naam. Wat wordt geprint?
class OrderLine:
def __init__(self, name):
self.name = name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a == a)- AFalse True
- BEen `TypeError`
- CFalse False
- DTrue True
Voor antwoorden heb je een account nodig
Log in om je antwoorden te controleren
De vragen staan hierboven, en ze in je hoofd uitwerken is waar het om draait. Log in om de antwoorden, de uitleg en de hints in drie niveaus te zien.
Jouw beurt
Schrijf twee klassen.
Book — gebouwd uit title, author en pages. Het werpt een ValueError als pages onder één is. Geef het een __repr__, en een __eq__ die twee boeken als gelijk beschouwt als titel en auteur overeenkomen.
Shelf — verzamelt boeken, en biedt add(book), total_pages(), longest(), en by_author(name) die een lijst teruggeeft.
Dan vijf experimenten:
- Verplaats
books = []uit__init__en onder declass-regel inShelf. Maak twee aparte planken, voeg een boek toe aan de ene, print dan de andere. Wat zie je? - Verwijder de
__repr__en print een lijst met boeken. Hoe veel moeilijker is debuggen? - Verwijder
__eq__en vergelijk twee boeken met dezelfde titel en auteur. - Haal
selfweg uit één methode en roep hem aan. Hoeveel argumenten geeft de melding aan dat er zijn gegeven? - Maak een
Booken schrijfbook.pagse = 100(opzettelijk verkeerd gespeld). Is er een fout? Danprint(book.__dict__)— wat zie je?
Die laatste is belangrijk omdat hij de limiet van een klasse laat zien. Een verkeerde naam geeft een fout bij het lezen, niet bij het schrijven — een nieuw attribuut zetten is altijd toegestaan. Het volgende hoofdstuk laat zien dat type hints en een type checker die kloof ook kunnen sluiten.
Stap 6 van 6
Uitdaging — de hoofdstukquiz
Tien vragen van makkelijk naar moeilijk. De laatste zijn met opzet lastig.
Log in om de quiz te maken