Sæt af objekter

Dette er en ældre version af bogen, der nu er kendt som Think Python. Du vil måske foretrække at læse en nyere version.

Hvordan man tænker som en datalog

Kapitel 15

15.1 Sammensætning

Igennem nu har du set flere eksempler på sammensætning. et af de første eksempler var at bruge en metodeinvokation som en del af et udtryk. Et andet eksempel er den indlejrede struktur af instruktioner; du kan placere en if-anvisning inden for en while-loop, inden for endnu en if-anvisning osv.

Når du har set dette mønster og har lært om lister og objekter, bør du ikke være overrasket over at lære, at du kan oprette lister af objekter. Du kan også oprette objekter, der indeholder lister (som attributter); du kan oprette lister, der indeholder lister; du kan oprette objekter, der indeholder objekter osv.

I dette og det næste kapitel vil vi se på nogle eksempler på disse kombinationer, med kortobjekter som eksempel.

15.2 Kortobjekter

Hvis du ikke er bekendt med almindelige spillekort, ville det nu være et godt tidspunkt at skaffe dig et spil, ellers giver dette kapitel måske ikke meget mening. 52 kort er der i et spil, som hver især tilhører en af fire kulører og en af tretten rækker. Farverne er spar, hjerter, ruder og kløver (i faldende rækkefølge i bridge). Rækkerne er es, 2, 3, 4, 5, 6, 7, 8, 9, 10, knægt, dronning og konge. Afhængigt af det spil, man spiller, kan esset have en højere rang end konge eller en lavere rang end 2.

Hvis vi ønsker at definere et nyt objekt til at repræsentere et spillekort, er det indlysende, hvilke attributter der skal være: rang og kulør. Det er ikke lige så indlysende, hvilken type attributterne skal være. En mulighed er at bruge strenge, der indeholder ord som “spar” for farver og “dronning” for rækker. Et problem med denne implementering er, at det ikke ville være let at sammenligne kortene for at se, hvilket kort der har en højere rang eller farve.

Et alternativ er at bruge hele tal til at kode rang og farve.Med “kode” mener vi ikke det, som nogle mennesker tror, nemlig at kryptere eller oversætte til en hemmelig kode. Det, som en datalog mener med “kode”, er “at definere en kortlægning mellem en talrække og de elementer, som jeg ønsker at repræsentere”. For eksempel:

Spader -> 3
Hjerter -> 2
Diamanter -> .> 1
Klubber -> 0

Et indlysende træk ved denne kortlægning er, at kulørerne kortlægges til hele tal i rækkefølge, så vi kan sammenligne kulører ved at sammenligne hele tal. Afbildningen forranker er ret indlysende; hver af de numeriske rækker svarer til det tilsvarende hele tal, og for billedkort er afbildningen ret indlysende:

Jack -> 11
Dronning -> 12
King

-> 13

Grunden til, at vi bruger matematisk notation for disse afbildninger, er, at de ikke er en del af Python-programmet. De er en del af programdesignet, men de optræder aldrig eksplicit i koden. Klassedefinitionen for typen Card ser således ud:

class Card:
def __init__(self, suit=0, rank=2):
self.suit = suit
self.rank = rank

Som sædvanlig leverer vi en initialiseringsmetode, der tager en valgfriparameter for hver attribut. Standardværdien for suit er0, som repræsenterer klør.

For at oprette et kort kalder vi Card-konstruktøren med det ønskede korts suit og rang.

threeOfClubs = Card(3, 1)

I næste afsnit skal vi finde ud af, hvilket kort vi lige har lavet.

15.3 Klasseattributter og __str__-metoden

For at udskrive Card-objekter på en måde, som folk let kan læse, ønsker vi at mappe de hele talkoder på ord. En naturlig måde at gøre det på er med lister af strenge. Vi tildeler disse lister til classattributes øverst i klassedefinitionen:

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

En klasseattribut er defineret uden for enhver metode, og den kan tilgås fra enhver af metoderne i klassen.

Inden for __str__ kan vi bruge suitList og rankListtil at mappe de numeriske værdier af suit og rank til strenge.For eksempel betyder udtrykket self.suitList “brug attributten suit fra objektet self som et indeks til klasseattributten suitList, og vælg den passende streng.”

Grunden til “narf” i det første element i rankList er at fungere som en pladsholder for det nul-te element i listen, som aldrig bør bruges. De eneste gyldige ranks er 1 til 13. Dette spildte element er ikke helt nødvendigt. Vi kunne have startet ved 0, som sædvanligt, men det er mindre forvirrende at kode 2 som 2, 3 som 3 osv.

Med de metoder, vi har indtil nu, kan vi oprette og udskrive kort:

>>>> card1 = Card(1, 11)
>>>>> print card1
Rudeknægt

Klasseattributter som suitList er fælles for alle Cardobjekter. Fordelen ved dette er, at vi kan bruge ethvert Cardobject til at få adgang til klasseattributterne:

>>>> card2 = Card(1, 3)
>>>>> print card2
3 of Diamonds
>>>>> print card2.suitList
Diamonds

Ulempen er, at hvis vi ændrer en klasseattribut, påvirker det alle instanser af klassen. Hvis vi f.eks. beslutter, at “Jack of Diamonds” i virkeligheden skal hedde “Jack of Swirly Whales”, kan vi gøre dette:

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

Problemet er, at alle ruder bare blev tilSwirly Whales:

>>>> print card2
3 af Swirly Whales

Det er normalt ikke en god idé at ændre klasseattributter.

15.4 Sammenligning af kort

For primitive typer er der betingede operatorer (<, >, == osv.), der sammenlignerværdier og bestemmer, hvornår en er større end, mindre end eller lig med en anden. For brugerdefinerede typer kan vi tilsidesætte opførslen af de indbyggede operatorer ved at stille en metode ved navn__cmp__ til rådighed. Ved konvention har __cmp__ to parametre, self og other, og returnerer1, hvis det første objekt er større, -1, hvis det andet objekt er større, og 0, hvis de er lig hinanden.

Nogle typer er fuldstændig ordnede, hvilket betyder, at du kan sammenligne to elementer og fortælle, hvilket der er størst. F.eks. er de hele talog flydepunktstallene fuldstændig ordnede. Nogle mængder er uordnede, hvilket betyder, at der ikke er nogen meningsfuld måde at sige, at et element er større end et andet. F.eks. er frugterne ikke ordnet, og derfor kan man ikke sammenligne æbler og appelsiner.

Mængden af spillekort er delvist ordnet, hvilket betyder, at man nogle gange kan sammenligne kort og andre gange ikke. For eksempel ved du, at klør 3 er højere end klør 2, og at diamanter 3 er højere end klør 3. Men hvad er bedst, klør 3 eller ruder 2? Den ene har en højere rang, men den anden har en højere kulør.

For at gøre kortene sammenlignelige er man nødt til at beslutte, hvad der er vigtigst, rang eller kulør. For at være ærlig, er valget vilkårligt. For at kunne vælge vil vi sige, at farven er vigtigere, fordi et nyt kortspil kommer sorteret med alle klør samlet, efterfulgt af alle ruder osv.

Med det besluttet kan vi skrive __cmp__:

def __cmp__(self, other):
# check farverne
if self.suit > other.suit: return 1
if self.suit < other.suit: return -1
# farverne er de samme… check ranks
if self.rank > other.rank: return 1
if self.rank < other.rank: return -1
# ranks are the same… it’s a tie
return 0

I denne rækkefølge vises esser lavere end toere (2’ere).

Som en øvelse skal du ændre __cmp__, så esser rangeres højere end konger.

15.5 Dæk

Nu da vi har objekter til at repræsentere kort, er det næste logiske skridt at definere en klasse til at repræsentere et dæk. Selvfølgelig består et deck af kort, så hvert Deck-objekt vil indeholde en liste over kort som en attribut.

Det følgende er en klassedefinition for Deck-klassen. Initialiseringsmetoden opretter attributten kort og genererer et standardsæt af tooghalvtreds kort:

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

Den nemmeste måde at udfylde spillet på er med en indlejret løkke. Den ydre løkke opregner farverne fra 0 til 3. Den indre løkke opregner rækkerne fra 1 til 13. Da den ydre sløjfe gentages fire gange, og den indre sløjfe gentages tretten gange, er det samlede antal gange, kroppen udføres, tooghalvtreds gange (tretten gange fire). Hver iteration skaber en ny instans af Card med den aktuelle farve og rang og føjer dette kort til kortlisten.

Append-metoden virker på lister, men naturligvis ikke på tupler.

15.6 Udskrivning af kortspillet

Som sædvanlig, når vi definerer en ny type objekt, ønsker vi en metode, der udskriver indholdet af et objekt.For at udskrive et Deck gennemgår vi listen og udskriver hvert kort:

class Deck:

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

Her, og fra nu af, er ellipsen (…) angiver, at vi haromgivet de andre metoder i klassen.

Som et alternativ til printDeck kunne vi skrive en __str__-metode til Deck-klassen. Fordelen ved __str__ er, at den er mere fleksibel. I stedet for blot at udskrive indholdet af objektet, genererer den en strengrepræsentation, som andre dele af programmet kan manipulere før udskrivning eller gemme til senere brug.

Her er en version af __str__, der returnerer en strengrepræsentation af et Deck.For at tilføje en smule pift, arrangerer den kortene i en kaskade, hvor hvert kort er indrykket en plads mere end det foregående kort:

class Deck:

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

Dette eksempel demonstrerer flere funktioner. For det første bruger vi i stedet for at gennemløbe self.cards og tildele hvert kort til en variabel i som en løkkevariabel og et indeks i listen over kort.

For det andet bruger vi operatoren stringmultiplikation til at indrykke hvert kort med en plads mere end det sidste. Udtrykket” “*i giver et antal mellemrum svarende til den aktuelle værdi af i.

For det tredje bruger vi str-funktionen i stedet for at bruge print-kommandoen til at udskrive kortene. At overdrage et objekt som argument tilstr svarer til at påkalde __str__-metoden på objektet.

Til sidst bruger vi variablen s som akkumulator.I første omgang er s den tomme streng. Hver gang gennem løkken genereres en ny streng, som sammenkædes med den gamle værdi af sto for at få den nye værdi. Når sløjfen slutter, indeholder s den fuldstændige stringrepræsentation af dækket, som ser således ud:

>>>> deck = Deck()
>>>>> print deck
Klørtrækker
2 af klør
3 af klør
4 af klør
5 af klør
6 af klør Klør
7 klør
8 klør
9 klør
9 klør
10 klør
klørknægt
klørdame
klørkonge
ruder ess

Og så videre. Selv om resultatet vises på 52 linjer, er det én lang streng, der indeholder newlines.

15.7 Blanding af bunken

Hvis en bunke er perfekt blandet, er der lige stor sandsynlighed for, at ethvert kort vises hvor som helst i bunken, og det er lige stor sandsynlighed for, at ethvert sted i bunken indeholder ethvert kort.

For at blande bunken vil vi bruge funktionen randrange fra random-modulet. Med to heltalsargumenter,a og b, vælger randrange et tilfældigt heltal i intervallet a <= x < b. Da den øvre grænse er strengt mindre end b, kan vi bruge længden af en liste som det andet argument, og vi er garanteret at få et lovligt indeks.Dette udtryk vælger f.eks. indekset for et tilfældigt kort i et spil:

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

En nem måde at blande spillet på er ved at gennemløbe kortene ogudbytte hvert kort med et tilfældigt valgt kort. Det er muligt, at kortet vil blive byttet med sig selv, men det er fint nok. Hvis vi udelukkede denne mulighed, ville rækkefølgen af kortene faktisk være mindre end helt tilfældig:

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

I stedet for at antage, at der er tooghalvtreds kort i kortspillet, får vi den faktiske længde af listen og gemmer den i nCards.

For hvert kort i kortspillet vælger vi et tilfældigt kort blandt de kort, der endnu ikke er blevet blandet. Derefter bytter vi det aktuelle kort (i) med det valgte kort (j). For at bytte kortene bruger vi en tupeltildeling, som i afsnit 9.2:

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

Som en øvelse skal du omskrive denne kodelinje uden at bruge en sekvenstildeling.

15.8 Fjernelse og uddeling af kort

En anden metode, der ville være nyttig for Deck-klassen, er removeCard, som tager et kort som argument, fjerner det og returnerer True, hvis kortet var i dækket, og False i modsat fald:

class Deck:

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

In-operatoren returnerer sandt, hvis den første operand er i dissecond, som skal være en liste eller en tupel. Hvis den første operand er etobjekt, bruger Python objektets __cmp__-metode til at bestemmekvaliteten med elementer i listen. Da __cmp__ iCard-klassen kontrollerer dyb lighed, kontrollerer removeCard-metoden dyb lighed.

For at dele kort ønsker vi at fjerne og returnere det øverste kort.Listemetoden pop giver en bekvem måde at gøre det på:

class Deck:

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

Pop fjerner faktisk det sidste kort i listen, så vi deler uden effekt fra bunden af kortspillet.

Endnu en operation, som vi sandsynligvis vil ønske, er den boolske funktionisEmpty, som returnerer sandt, hvis kortspillet ikke indeholder nogen kort:

class Deck:

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

15.9 Ordliste

encode At repræsentere et sæt værdier ved hjælp af et andet sæt værdier ved at konstruere en mapping mellem dem. class attribute En variabel, der er defineret inden for en klassedefinition, men uden for en metode. Klasseattributter er tilgængelige fra enhver metode i klassen og deles af alle instanser af klassen. akkumulator En variabel, der anvendes i en løkke til at akkumulere en række værdier, f.eks. ved at sammenkæde dem til en streng eller tilføje dem til en løbende sum.

Dette er en ældre version af den bog, der nu er kendt som Think Python. Du vil måske foretrække at læse en nyere version.

How to Think Like a Computer Scientist

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.