Sets van objecten

Dit is een oudere versie van het boek dat nu bekend staat als Think Python. Misschien leest u liever een recentere versie.

How to Think Like a Computer Scientist

Hoofdstuk 15

15.1 Compositie

U hebt nu verschillende voorbeelden van compositie gezien.Een van de eerste voorbeelden was het gebruik van een methode-aanroep als onderdeel van een expressie. Een ander voorbeeld is de geneste structuur van verklaringen; je kunt een if-instructie binnen een while-lus plaatsen, binnen een andere if-instructie, enzovoort.

Na dit patroon gezien te hebben, en geleerd te hebben over lijsten en objecten, zou je niet verbaasd moeten zijn te leren dat je lijsten van objecten kunt maken. Je kunt ook objecten maken die lijsten bevatten (als attributen); je kunt lijsten maken die lijsten bevatten; je kunt objecten maken die objecten bevatten; enzovoort.

In dit hoofdstuk en het volgende zullen we enkele voorbeelden van deze combinaties bekijken, met Card-objecten als voorbeeld.

15.2 Kaartobjecten

Als u niet bekend bent met gewone speelkaarten, is dit een goed moment om een kaartspel te pakken te krijgen, anders heeft dit hoofdstuk misschien niet veel zin.Er zijn tweeënvijftig kaarten in een kaartspel, die elk behoren tot een van de vier kleuren en een van de dertien rangen. De kleuren zijn Schoppen, Harten, Ruiten, en Klaveren (in aflopende volgorde in bridge). De rangen zijn Aas, 2, 3, 4, 5, 6, 7, 8, 9, 10, Boer, Koningin en Koning. Afhankelijk van het spel dat je speelt, kan de rang van Aas hoger zijn dan Koning of lager dan 2.

Als we een nieuw object willen definiëren om een speelkaart weer te geven, is het duidelijk wat de attributen moeten zijn: de rang en deuit. Het is niet zo duidelijk van welk type de attributen moeten zijn. Een mogelijkheid is om strings te gebruiken met woorden als “schoppen” voor kleuren en “koningin” voor rangen. Een probleem met deze implementatie is dat het niet gemakkelijk zou zijn om kaarten te vergelijken om te zien welke een hogere rang of kleur heeft.

Een alternatief is om gehele getallen te gebruiken om de rangen en kleuren te coderen.Met “coderen” bedoelen we niet wat sommige mensen denken, namelijk coderen of vertalen in een geheime code. Wat een computerwetenschapper bedoelt met “coderen” is “een verband leggen tussen een reeks getallen en de dingen die ik wil voorstellen”. Bijvoorbeeld:

Schoppen -> 3
Harten -> 2
Diamanten -> 1
Clubs -> 0

Een voor de hand liggende eigenschap van deze mapping is dat de pakken in volgorde naar gehele getallen gaan, zodat we de pakken kunnen vergelijken door gehele getallen te vergelijken. De rangschikking is tamelijk duidelijk; elk van de numerieke rangen komt overeen met het overeenkomstige gehele getal, en voor de zichtkaarten:

Jack -> 11
Queen -> 12
King – > – > 13

De reden dat we wiskundige notatie gebruiken voor deze mappings is dat ze geen deel uitmaken van het Python programma. Ze maken deel uit van het programmaontwerp, maar ze verschijnen nooit expliciet in de code. De klasse-definitie voor het type Kaart ziet er als volgt uit:

klasse Kaart:
def __init__(self, suit=0, rank=2):
self.suit = suit
self.rank = rank

Zoals gebruikelijk voorzien we in een initialisatiemethode die voor elk attribuut een optionele parameter neemt. De standaardwaarde voor kleur is 0, wat Klaveren voorstelt.

Om een kaart te maken, roepen we de Card constructor op met de kleur en de rang van de kaart die we willen.

ThreeOfClubs = Card(3, 1)

In de volgende sectie gaan we uitzoeken welke kaart we zojuist hebben gemaakt.

15.3 Klasse-attributen en de methode __str__

Om Kaart-objecten zo af te drukken dat mensen ze gemakkelijk kunnen lezen, willen we de gehele getallen in woorden omzetten. Een natuurlijke manier om dat te doen is met lijsten van strings. We wijzen deze lijsten toe aan klasseattributen bovenaan de definitie van de klasse:

klasse Card:
suitList =
rankList =
#init method omitted
def __str__(self):
return (self.rankList + ” van ” +
self.suitList)

Een klasse-attribuut wordt gedefinieerd buiten een methode om, en het kan worden benaderd vanuit alle methoden in de klasse.

In __str__ kunnen we suitList en rankList gebruiken om de numerieke waarden van suit en rank in tekenreeksen om te zetten. De uitdrukking self.suitList betekent bijvoorbeeld “gebruik het attribuut suit van het object self als een index in het klasse-attribuut met de naam suitList, en selecteer de bijbehorende tekenreeks.”

De reden voor de “narf” in het eerste element in rankList is om te fungeren als een plaatshouder voor het nul-achtste element van de lijst, dat nooit moet worden gebruikt. De enige geldige rangen zijn 1 tot 13. Dit verspilde element is niet helemaal noodzakelijk. We hadden kunnen beginnen bij 0, zoals gebruikelijk, maar het is minder verwarrend om 2 te coderen als 2, 3 als 3, enzovoort.

Met de methoden die we tot nu toe hebben, kunnen we kaarten maken en afdrukken:

>>> card1 = Card(1, 11)
>>> print card1
Ruitenboer

Klasse-attributen zoals suitList worden gedeeld door alle Cardobjects. Het voordeel hiervan is dat we elk Cardobject kunnen gebruiken om toegang te krijgen tot de klasse-attributen:

>>> card2 = Card(1, 3)
>>> print card2
Ruiten3
>>> print card2.suitList
Diamanten

Het nadeel is dat als we een klasse-attribuut wijzigen, dit gevolgen heeft voor elke instantie van de klasse. Als we bijvoorbeeld besluiten dat “Ruitenboer” eigenlijk “Zwermwalvissenboer” zou moeten heten, kunnen we het volgende doen:

>>> kaart1.suitList = “Swirly Whales”
>>> print card1
Jack of Swirly Whales

Het probleem is dat alle ruiten gewoonSwirly Whales zijn geworden:

>> print card2
3 van Swirly Whales

Het is meestal geen goed idee om klasse-attributen te wijzigen.

15.4 Kaarten vergelijken

Voor primitieve types zijn er voorwaardelijke operatoren (<, >, ==, enz.) die waarden vergelijken en bepalen wanneer de ene groter is dan, kleiner is dan, of gelijk aan de andere. Voor door de gebruiker gedefinieerde typen kunnen we het gedrag van de ingebouwde operatoren opheffen door een methode met de naam__cmp__ op te geven. Volgens afspraak heeft __cmp__ twee parameters, self en other, en geeft1 terug als het eerste object groter is, -1 als het tweede object groter is, en 0 als ze gelijk aan elkaar zijn.

Sommige typen zijn volledig geordend, wat betekent dat je twee elementen kunt vergelijken en kunt zeggen welk element groter is. Bijvoorbeeld, de gehele getallen en de drijvende-komma getallen zijn volledig geordend. Sommige verzamelingen zijn niet geordend, wat betekent dat er geen zinvolle manier is om te zeggen dat een element groter is dan een ander. Bijvoorbeeld, de vruchten zijn niet geordend, en daarom kun je appels en sinaasappels niet met elkaar vergelijken.

De verzameling speelkaarten is gedeeltelijk geordend, wat betekent dat je kaarten soms wel en soms niet met elkaar kunt vergelijken. Je weet bijvoorbeeld dat Klaveren 3 hoger is dan Klaveren 2, en dat Ruiten 3 hoger is dan Klaveren 3. Maar wat is beter, de Klaver 3 of de Ruiten 2? De een heeft een hogere rang, maar de ander heeft een hogere kleur.

Om kaarten vergelijkbaar te maken, moet je beslissen wat belangrijker is, rang of kleur. Om eerlijk te zijn, de keuze is arbitrair. Omwille van de keuze zullen we zeggen dat de kleur belangrijker is, omdat een nieuw spel kaarten gesorteerd wordt geleverd met alle Klaveren bij elkaar, gevolgd door alle Ruiten, enzovoort.

Met dat besloten, kunnen we __cmp__ schrijven:

def __cmp__(self, other):
# controleer de kleuren
if self.suit > other.suit: return 1
if self.suit < other.suit: return -1
# kleuren zijn hetzelfde… controleer rangen
if self.rang > andere.rang: return 1
if self.rang < andere.rang: return -1
# rangen zijn gelijk… het is een gelijkspel
return 0

In deze volgorde staan Azen lager dan Deuces (2s).

Als oefening, pas __cmp__ aan zodat Azen hoger gerangschikt worden dan Koningen.

15.5 Dekken

Nu we objecten hebben om Kaarten te representeren, is de volgende logische stap het definiëren van een klasse om een Dek te representeren. Natuurlijk bestaat een deck uit kaarten, dus elk Deck object zal een lijst van kaarten als attribuut bevatten.

Het volgende is een klasse definitie voor de Deck klasse. Deinitialisatiemethode creëert het attribuut kaarten en genereert de standaard set van tweeënvijftig kaarten:

klasse Deck:
def __init__(self):
self.cards =
for suit in range(4):
for rank in range(1, 14):
self.cards.append(Card(suit, rank))

De eenvoudigste manier om het kaartspel te vullen is met een geneste lus. De buitenste lus telt de kleuren van 0 tot 3. De binnenste lus telt de rangen van 1 tot 13. Aangezien de buitenste lus vier keer itereert, en de binnenste lus dertien keer, is het totaal aantal keren dat het lichaam wordt uitgevoerd tweeënvijftig (dertien keer vier). Elke iteratie maakt een nieuwe instantie van Card met de huidige kleur en rang, en voegt die kaart toe aan de kaartenlijst.

De append methode werkt op lijsten, maar natuurlijk niet op tupels.

15.6 Het kaartspel afdrukken

Zoals gewoonlijk, wanneer we een nieuw type object definiëren, willen we een methode die de inhoud van een object afdrukt.Om een Deck af te drukken, gaan we door de lijst en drukken we elke Card af:

class Deck:

def printDeck(self):
for card in self.cards:
print card

Hier, en van nu af aan, geeft de ellips (…) aangegeven dat we de andere methoden in de klasse hebben weggelaten.

Als alternatief voor printDeck zouden we een __str__ methode kunnen schrijven voor de Deck klasse. Het voordeel van __str__ is dat het flexibeler is. In plaats van alleen de inhoud van het object af te drukken, genereert het een string-representatie die andere delen van het programma kunnen manipuleren voor het afdrukken, of opslaan voor later gebruik.

Hier volgt een versie van __str__ die een string-representatie van een Deck teruggeeft.Om een beetje pit toe te voegen, rangschikt het de kaarten in een cascade waar elke kaart een spatie meer ingesprongen is dan de vorige kaart:

class Deck:

def __str__(self):
s = “”
for i in range(len(self.cards)):
s = s + “”*i + str(self.cards) + “″n”
return s

Dit voorbeeld demonstreert verschillende functies. Ten eerste, in plaats van self.cards te doorlopen en elke kaart aan een variabele toe te wijzen, gebruiken we i als een lusvariabele en een index in de lijst met kaarten.

Ten tweede, we gebruiken de stringvermenigvuldigingsoperator om elke kaart met één spatie meer dan de laatste in te delen. De uitdrukking””*i levert een aantal spaties op dat gelijk is aan de huidige waarde van i.

Ten derde, in plaats van het commando print te gebruiken om de kaarten af te drukken, gebruiken we de functie str. Het doorgeven van een object als argument aanstr is gelijk aan het aanroepen van de methode __str__ op het object.

Ten slotte gebruiken we de variabele s als accumulator.Initieel is s de lege string. Elke keer dat de lus wordt doorlopen, wordt een nieuwe string gegenereerd en samengevoegd met de oude waarde van sto krijgt de nieuwe waarde. Wanneer de lus eindigt, bevat s de volledige string-representatie van het Deck, die er als volgt uitziet:

>>> deck = Deck()
>>> print deck
Aas van Klaveren
2 van Klaveren
3 van Klaveren
4 van Klaveren
5 van Klaveren
6 van Klaveren
7 van Klaveren
8 van Klaveren
9 van Klaveren
10 van Klaveren
Aas van Klaveren
Koningin van Klaveren
Aas van Ruiten

En zo verder. Hoewel het resultaat op 52 regels staat, is het één lange string die newlines bevat.

15.7 Het kaartspel schudden

Als een kaartspel perfect geschud is, dan heeft elke kaart evenveel kans om waar dan ook in het kaartspel voor te komen, en elke plaats in het kaartspel heeft evenveel kans om elke kaart te bevatten.

Om het kaartspel te schudden, gebruiken we de randrange-functie uit de random module. Met twee gehele argumenten, a en b, kiest randrange een willekeurig geheel getal in het bereik a <= x < b. Aangezien de bovengrens strikt kleiner is dan b, kunnen we de lengte van een lijst als tweede argument gebruiken, en we krijgen gegarandeerd een legale index.Deze uitdrukking kiest bijvoorbeeld de index van een willekeurige kaart in een kaartspel:

random.randrange(0, len(self.cards))

Een gemakkelijke manier om het kaartspel te schudden is door de kaarten te doorlopen en elke kaart te verwisselen met een willekeurig gekozen kaart. Het is mogelijk dat de kaart met zichzelf wordt verwisseld, maar dat is prima. In feite, als we die mogelijkheid zouden uitsluiten, zou de volgorde van de kaarten minder dan volledig willekeurig zijn:

class Deck:

def shuffle(self):
import random
nCards = len(self.cards)
for i in range(nCards):
j = random.randrange(i, nCards)
self.cards, self.cards = self.cards, self.cards

In plaats van aan te nemen dat er tweeënvijftig kaarten in het kaartspel zitten, krijgen we de werkelijke lengte van de lijst en slaan die op in nCards.

Voor elke kaart in het kaartspel kiezen we een willekeurige kaart uit de kaarten die nog niet geschud zijn. Dan verwisselen we de huidige kaart (i) met de gekozen kaart (j). Om de kaarten te verwisselen gebruiken we een tuple toewijzing, zoals in Paragraaf 9.2:

self.cards, self.cards = self.cards, self.cards

Herschrijf als oefening deze regel code zonder gebruik te maken van een sequence toewijzing.

15.8 Kaarten verwijderen en delen

Een andere methode die nuttig zou zijn voor de Deck klasse is removeCard, die een kaart als argument neemt, deze verwijdert, en True teruggeeft als de kaart in het deck zat en False anders:

class Deck:

def removeCard(self, card):
if card in self.cards:
self.cards.remove(card)
return True
else:
return False

De in operator retourneert true als de eerste operand in deze tweede staat, die een lijst of een tuple moet zijn. Indien de eerste operand een object is, gebruikt Python de __cmp__-methode van het object om de kwaliteit met de items in de lijst te bepalen. Omdat de __cmp__ in de klasseCard controleert op diepe gelijkheid, controleert de methode removeCard op diepe gelijkheid.

Om kaarten te delen, willen we de bovenste kaart verwijderen en teruggeven.De lijstmethode pop biedt een handige manier om dat te doen:

class Deck:

def popCard(self):
return self.cards.pop()

In feite verwijdert pop de laatste kaart in de lijst, dus we delen in feite vanaf de onderkant van het kaartspel.

Een andere bewerking die we waarschijnlijk zullen willen is de boolean functieisEmpty, die true teruggeeft als het kaartspel geen kaarten bevat:

class Deck:

def isEmpty(self):
return (len(self.cards) == 0)

15.9 Woordenlijst

coderen Het weergeven van een set waarden met behulp van een andere set waarden door er een mapping tussen te construeren. klasse-attribuut Een variabele die gedefinieerd is binnen een klasse-definitie maar buiten een methode. Klasse-attributen zijn toegankelijk vanuit elke methode in de klasse en worden gedeeld door alle instanties van de klasse. accumulator Een variabele die in een lus wordt gebruikt om een reeks waarden te accumuleren, bijvoorbeeld door ze samen te voegen tot een tekenreeks of ze op te tellen bij een lopende som.

Dit is een oudere versie van het boek dat nu bekend staat als Think Python. Misschien leest u liever een recentere versie.

How to Think Like a Computer Scientist

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.