Objektmängder

Detta är en äldre version av boken som nu heter Think Python. Du kanske föredrar att läsa en nyare version.

Hur man tänker som en datavetare

Kapitel 15

15.1 Komposition

Inom nu har du sett flera exempel på komposition.Ett av de första exemplen var att använda ett metodanrop som en del av ett uttryck. Ett annat exempel är den inbäddade strukturen på uttalanden; du kan placera ett if-uttalande inom en while-slinga, inom ett annat if-uttalande och så vidare.

När du har sett det här mönstret och lärt dig om listor och objekt borde du inte bli förvånad över att lära dig att du kan skapa listor av objekt. Du kan också skapa objekt som innehåller listor (somattribut), du kan skapa listor som innehåller listor, du kan skapa objekt som innehåller objekt och så vidare.

I det här och nästa kapitel kommer vi att titta på några exempel på dessa kombinationer, med kortobjekt som exempel.

15.2 Kortobjekt

Om du inte är bekant med vanliga spelkort är det ett bra tillfälle att skaffa en kortlek, annars kanske det här kapitlet inte blir så meningsfullt. 52 kort finns i en kortlek och vart och ett av dem tillhör en av fyra färger och en av tretton klasser. Färgerna är spader, hjärter, ruter och klöver (i fallande ordning i bridge). Rankarna är ess, 2, 3, 4, 5, 6, 7, 8, 9, 10, knekt, dam och kung. Beroende på vilket spel du spelar kan esset vara högre än kung eller lägre än 2.

Om vi vill definiera ett nytt objekt för att representera ett spelkort är det uppenbart vilka attribut som ska användas: rank ochsuit. Det är inte lika uppenbart vilken typ av attribut som skall vara. En möjlighet är att använda strängar som innehåller ord som ”spader” för färg och ”dam” för rang. Ett problem med detta genomförande är att det inte skulle vara lätt att jämföra korten för att se vilket kort som har högre rang eller färg.

Ett alternativ är att använda heltal för att koda rang och färg.Med ”koda” menar vi inte det som en del människor tror, dvs. att kryptera eller översätta till en hemlig kod. Vad en datavetare menar med ”koda” är ”att definiera en koppling mellan en sifferföljd och de objekt som jag vill representera”. Till exempel:

Spader -> 3
Hjärta -> 2
Diamanter -> 1
Klubbar 0

Ett uppenbart drag i denna avbildning är att färgerna avbildas till heltal i ordning, så vi kan jämföra färger genom att jämföra heltal. Kartläggningen för rankor är ganska uppenbar; var och en av de numeriska rankorna kartlägger motsvarande heltal, och för de öppna korten är kartläggningen ganska tydlig:

Jack -> 11
Drottning -> 12
Kung

.> 13

Anledningen till att vi använder matematisk notation för dessa mappningar är att de inte är en del av Pythonprogrammet. De är en del av programmets utformning, men de förekommer aldrig uttryckligen i koden. Klassdefinitionen för typen Card ser ut så här:

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

Som vanligt tillhandahåller vi en initialiseringsmetod som tar en valfriparameter för varje attribut. Standardvärdet för suit är0, vilket representerar klöver.

För att skapa ett kort åberopar vi Card-konstruktören med det önskade kortets suit och rank.

threeOfClubs = Card(3, 1)

I nästa avsnitt ska vi ta reda på vilket kort vi just skapat.

15.3 Klassattribut och metoden __str__

För att skriva ut Card-objekt på ett sätt som folk lätt kan läsa vill vi mappa heltalskoderna till ord. Ett naturligt sätt att göra det är med listor av strängar. Vi tilldelar dessa listor till klassattribut högst upp i klassdefinitionen:

klass Card:
suitList =
rankList =
#init metod utelämnad
def __str__(self):
return (self.rankList + ” of ” +
self.suitList)

Ett klassattribut definieras utanför alla metoder och kan nås från alla metoder i klassen.

Inuti __str__ kan vi använda suitList och rankListför att mappa de numeriska värdena för suit och rank till strängar.Till exempel betyder uttrycket self.suitList ”använd attributet suit från objektet self som ett index till klassattributet som heter suitList, och välj lämplig sträng.”

Anledningen till ”narf” i det första elementet i rankList är för att fungera som en platshållare för det nollte elementet i thelist, som aldrig ska användas. De enda giltiga rangerna är 1 till 13. Detta slösade element är inte helt nödvändigt. Vi kunde ha börjat med 0 som vanligt, men det är mindre förvirrande att koda 2 som 2, 3 som 3 och så vidare.

Med de metoder vi har hittills kan vi skapa och skriva ut kort:

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

Klassens attribut, som t.ex. suitList, delas av alla Cardobjects. Fördelen med detta är att vi kan använda vilket Cardobject som helst för att komma åt klassens attribut:

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

Nackdelen är att om vi ändrar ett klassattribut påverkar det varje instans av klassen. Om vi till exempel bestämmer oss för att ”Jack of Diamonds” egentligen borde heta ”Jack of Swirly Whales” kan vi göra så här:

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

Problemet är att alla diamanter bara blevSwirly Whales:

>>>> print card2
3 of Swirly Whales

Det är vanligtvis ingen bra idé att ändra klassattribut.

15.4 Jämförelse av kort

För primitiva typer finns det villkorliga operatörer (<, >, ==, etc.)som jämförvärden och bestämmer när ett är större än, mindre än eller lika med ett annat. För användardefinierade typer kan vi åsidosätta beteendet hos de inbyggda operatörerna genom att tillhandahålla en metod som heter__cmp__. Enligt konvention har __cmp__ två parametrar, self och other, och returnerar1 om det första objektet är större, -1 om det andra objektet är större och 0 om de är lika stora som varandra.

Vissa typer är helt ordnade, vilket innebär att du kan jämföra två element och avgöra vilket som är störst. Till exempel är heltal och flyttal fullständigt ordnade. Vissa mängder är oordnade, vilket innebär att det inte finns något meningsfullt sätt att säga att ett element är större än ett annat. Frukterna är till exempel oordnade, vilket är anledningen till att man inte kan jämföra äpplen och apelsiner.

Mängden spelkort är delvis ordnad, vilket innebär att man ibland kan jämföra kort och ibland inte. Du vet till exempel att klöver 3 är högre än klöver 2 och att diamant 3 är högre än klöver 3. Men vilket är bättre, klöver 3 eller ruter 2? Den ena har en högre rang, men den andra har en högre färg.

För att göra korten jämförbara måste du bestämma dig för vad som är viktigast, rang eller färg. För att vara ärlig är valet godtyckligt. För att kunna välja säger vi att färgen är viktigare, eftersom en ny kortlek kommer sorterad med alla klöver tillsammans, följt av alla ruter och så vidare.

Med detta bestämt kan vi skriva __cmp__:

def __cmp__(self, other):
# Kontrollera färgerna
if self.suit > other.suit: return 1
if self.suit < other.suit: return -1
# Färgerna är desamma… kontrollera rangerna
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 denna ordning är ess lägre än tvåor (2s).

Som en övning, ändra __cmp__ så att ess rankas högre än kungar.

15.5 Däck

När vi nu har objekt för att representera kort är nästa logiska steg att definiera en klass för att representera ett däck. Naturligtvis består ett Deck av kort, så varje Deck-objekt kommer att innehålla en lista med kort som attribut.

Nedan följer en klassdefinition för Deck-klassen. Initialiseringsmetoden skapar attributet kort och genererar standarduppsättningen av femtiotvå kort:

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

Det enklaste sättet att fylla på kortleken är med en nästlad loop. Den yttre slingan räknar upp färgerna från 0 till 3. Den inre slingan räknar upp rankarna från 1 till 13. Eftersom den yttre slingan itererar fyra gånger och den inre slingan itererar tretton gånger är det totala antalet gånger som kroppen utförs femtiotvå (tretton gånger fyra). Varje iteration skapar en ny instans av Card med den aktuella färgen och rank och lägger till det kortet till listan cards.

Metoden append fungerar på listor men naturligtvis inte på tupler.

15.6 Utskrift av kortleken

Som vanligt när vi definierar en ny typ av objekt vill vi ha en metod som skriver ut innehållet i ett objekt.För att skriva ut en Deck går vi igenom listan och skriver ut varje Card:

class Deck:

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

Här, och från och med nu, används ellipsen (…) anger att vi har utelämnat de andra metoderna i klassen.

Som ett alternativ till printDeck skulle vi kunna skriva en __str__-metod för Deck-klassen. Fördelen med __str__ är att den är mer flexibel. Istället för att bara skriva ut innehållet i objektet genererar den en strängrepresentation som andra delar av programmet kan manipulera före utskrift, eller lagra för senare användning.

Här är en version av __str__ som returnerar en strängrepresentation av en Deck.För att lägga till lite piffighet arrangerar den korten i en kaskad där varje kort är indraget ett mellanslag mer än det föregående kortet:

class Deck:

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

Det här exemplet visar flera funktioner. För det första använder vi i som en loopvariabel och ett index i listan med kort i stället för att gå igenom self.cards och tilldela varje kort till en variabel.

För det andra använder vi strängmultiplikationsoperatorn för att indela varje kort med ett mellanslag mer än det sista. Uttrycket” ”*i ger ett antal mellanslag som är lika med det aktuella värdet av i.

För det tredje använder vi funktionen str i stället för att använda kommandot print för att skriva ut korten. Att överlämna ett objekt som argument tillstr är likvärdigt med att åberopa metoden __str__ på objektet.

Till sist använder vi variabeln s som en ackumulator.Initialt är s den tomma strängen. Varje gång genom slingan genereras en ny sträng som sammanfogas med det gamla värdet av sto för att få det nya värdet. När slingan avslutas innehåller s den fullständiga strängrepresentationen av däcket, som ser ut så här:

>>>> deck = Deck()
>>>> print deck
Klubbens ess
2 klöver
3 klöver
4 klöver
5 klöver
6 klöver Klöver
7 klöver
8 klöver
9 klöver
10 klöver
Klubbens knekt
Klubbens dam
Klubbens kung
Ruter ess

Och så vidare. Även om resultatet visas på 52 rader är det en lång sträng som innehåller nya rader.

15.7 Blandning av kortleken

Om en kortlek är perfekt blandad är det lika troligt att alla kort förekommer var som helst i kortleken, och det är lika troligt att alla platser i kortleken innehåller alla kort.

För att blanda kortleken kommer vi att använda funktionen randrange från slumpmodulen. Med två heltalsargument, a och b, väljer randrange ett slumpmässigt heltal inom intervallet a <= x < b. Eftersom den övre gränsen är strikt mindre än b kan vi använda längden på en lista som andra argument, och vi är garanterade att få ett lagligt index.Det här uttrycket väljer till exempel index för ett slumpmässigt kort i en kortlek:

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

Ett enkelt sätt att blanda kortleken är att gå igenom korten och byta ut varje kort mot ett slumpmässigt valt kort. Det är möjligt att kortet byts med sig självt, men det är okej. Faktum är att om vi uteslöt den möjligheten skulle ordningen på korten vara mindre än helt slumpmässig:

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

Istället för att anta att det finns femtiotvå kort i kortleken, tar vi reda på listans faktiska längd och lagrar den i nCards.

För varje kort i kortleken väljer vi ett slumpmässigt kort bland de kort som ännu inte har blandats. Sedan byter vi ut det aktuella kortet (i) mot det valda kortet (j). För att byta ut korten använder vi en tupeltilldelning, som i avsnitt 9.2:

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

Som en övning, skriv om den här raden av kod utan att använda en sekvenstilldelning.

15.8 Ta bort och dela ut kort

En annan metod som skulle vara användbar för Deck-klassen är removeCard, som tar ett kort som argument, tar bort det och återger True om kortet fanns i kortleken och False i annat fall:

class Deck:

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

Operatorn in returnerar true om den första operanden finns i thesecond, som måste vara en lista eller en tupel. Om den första operanden är ettobjekt använder Python objektets __cmp__-metod för att avgörakvaliteten med objekt i listan. Eftersom __cmp__ i klassenCard kontrollerar djup jämlikhet, kontrollerar metoden removeCard djup jämlikhet.

För att dela ut kort vill vi ta bort och returnera det översta kortet.Listmetoden pop ger ett bekvämt sätt att göra det:

class Deck:

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

Pop tar i själva verket bort det sista kortet i listan, så vi delar ut från botten av kortleken.

En ytterligare operation som vi sannolikt kommer att vilja ha är den boolska funktionenisEmpty, som returnerar sant om kortleken inte innehåller några kort:

class Deck:

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

15.9 Ordlista

kodning Att representera en uppsättning värden med hjälp av en annan uppsättning värden genom att konstruera en mappning mellan dem. klassattribut En variabel som är definierad i en klassdefinition men utanför en metod. Klassattribut är tillgängliga från alla metoder i klassen och delas av alla instanser av klassen. ackumulator En variabel som används i en slinga för att ackumulera en serie värden, t.ex. genom att sammanfoga dem till en sträng eller addera dem till en löpande summa.

Detta är en äldre version av den bok som nu kallas Think Python. Du kanske föredrar att läsa en nyare version.

Hur man tänker som en datavetare

Lämna ett svar

Din e-postadress kommer inte publiceras.