Lektion 9: Introduktion till klasser
Moment: | Klasser |
Begrepp som introduceras: | Klasser, __init__- och __str__-metoder |
Arbetssätt: | Arbeta gärna tillsammans med någon, men skriv egen kod. Diskutera med varandra!
Försök svara på de frågor som har givna svar innan du tittar på svaret. Fråga handledarna om det är något svar du inte förstår! Tips: Använd debuggern |
Uppskattad arbetstid: | Schemalagd handledningstid: 4 timmar. Utöver det räkna med eget arbete med lika många timmar. |
Redovisning: | Ingen obligatorisk redovisning. |
Backup: | När du arbetar på egen dator måste du se till att det löpande görs backup på de filer (py-filer) som du skapar under kursens gång. Om du har filerna enbart lagrade på din dator finns risk att allt försvinner vid ett datorhaveri eller vid stöld. |
Klasser
I alla tidigare lektioner har vi talat om objekt av olika slag (t.ex. turtle-objekt, list-objekt, sträng-objekt, tupel-objekt). I denna lektion ska vi beskriva hur man definierar egna klasser av objekt.
Objekt kan ha både egenskaper (data) och metoder. Ett turtle-objekt, till exempel, har bl.a. egenskaperna position, riktning, storlek och färg samt metoder, d.v.s. en sorts funktioner, som hämtar information om eller påverkar ett speciellt objekt (forward, left, ...).
En klass kan sägas vara en ritning eller beskrivning av vilka data som objekten ska ha och vilka metoder som ska finnas.
Exempel 1: Tärningar, definiera en klass Dice
Antag att vi vill skapa ett tärningsobjekt. En tärning har många egenskaper t ex färg, storlek, material, antal sidor och värde (dvs sidan som visas uppåt). Vilka egenskaper man väljer att representera beror naturligtvis på vad man ska ha objekten till. Här tänker vi att vi ska ha tärningsobjekten i någon form av spelsammanhang och väljer då egenskaperna antal sidor och aktuellt värde.
Vi vill också kunna skapa flera olika tärningar med olika antal sidor, inte bara sexsidiga.
Vi börjar med att deklarera en tärningsklass, skapa två tärningsobjekt och skriva ut dem.
Koden sparar vi i en fil dice.py
Kod i filen dice.py | Utskrift |
---|---|
# Definition av klassen Dice
class Dice:
pass # Gör inget
# Testar klassen Dice
d1 = Dice() # Skapar Dice-objektet d1
d2 = Dice() # Skapar Dice-objektet d2
print(d1) # Skriver ut objekten
print(d2)
|
<__main__.Dice object at 0x10099d048>
<__main__.Dice object at 0x10099d0f0>
|
-
Klasser definieras med ordet
class
följt av det namn vi vill ha på klassen samt ett kolon. - Klassnamn ska börja på stor bokstav.
-
Efter den raden följer en eller flera indenterade rader med klassens innehåll.
I detta fall har vi inte lagt till något från början. Klassen innehåller inget egentligen, bara en sats
pass
. Vi måste ändå ha en indenterad rad för vilket man använder nyckelordetpass
till. Satsenpass
gör ingenting. -
Uttrycket
Dice()
skapar ett tärningsobjekt och returnerar en referens till det (jämför hur vi skapadeTurtle
-objekt,t = turtle.Turtle()
, därturtle.
anger namnet på modulen där klassen finns). -
Även om utskriften kanske verkar kryptisk så antyder den i alla fall att vi har två olika
tärningsobjekt — de har ju olika adresser (koden efter
at
, 0x10099d048 resp 0x10099d0f0)
Metoden __init__
, den s.k. konstruktorn som skapar objektet
Vi har ännu inte lagt in några egenskaper som antal sidor och värden.
För att göra detta använder man en speciell ("magisk") metod med namnet__init__
.
I detta fall passar följande bra:
Kod i filen dice.py | Utskrift |
---|---|
import random
# Definition av klassen Dice
class Dice:
# Metoden init, skapar en tärning m sides sidor
def __init__(self, sides):
self.sides = sides
# Slumpat ett heltal 1-self.sides
self.value = random.randint(1, self.sides)
# Testa klassen Dice
d1 = Dice(6) # Skapa en 6 sidig tärning
d2 = Dice(12) # Skapa en 12-sidig tärning
print(d1) # Skriver ut objekten
print(d2)
|
<__main__.Dice object at 0x03776118>
<__main__.Dice object at 0x03776178>
|
-
Eftersom klassen nu har ett innehåll (de röda satserna) tar vi bort satsen
pass
. -
Initieringsmetoden (ibland kallad "konstruktor") måste heta
__init__
och haself
som första parameter. Därefter kan man lägga till de parametrar man vill. - Notera att det är dubbla understrykningstecken
__
före och efterinit
. - Observera indenteringen av koden i metoden
-
Tilldelningssatserna
self.namn = värde
skapar egenskaper med angivet namn och värde.
Egenskaperna kallas också attribut eller instansvariabler.
Vi ser att instansvariablernaself.sides
ochself.value
skapas.
Konstruktorn lägger alltså in två egenskaper varav värdet till den ena ges av en parameter och det andra av slumpfunktionen. - Observera att
sides
ochself.sides
är två olika saker. Den första är en parameter, den andra ett attribut.
Attributen lagras i objektet och existerar så länge objektet existerar. Parametern, däremot, finns bara med det namnet medan initieringsmetoden körs, precis som alla andra parametrar till funktioner och metoder.
Genom att spara parameterns värde i ett attribut finns värdet alltid tillgängligt i objektet.
Det hade gått lika bra att använda något annat namn på parametern. -
När man skapar objekten (t ex
Dice(6)
) anger man bara de argument som man själv lagt till. Parameternself
läggs till automatiskt. - Utskriften är dock lika kryptisk som tidigare.
Metoden __str__
Vi skall nu skriva en metod som gör att utskriften av objekten ( dvs print(d1)
och print(d2)
) blir mer informativ.
Det vi önskar är en metoden som gör en lämplig textrepresentation av objektet. Det vore trevligt att ha ett standardsätt för att
skapa en sträng från ett objekt, därför namnet str. Fördelen blir att man då exempelvis kan skriva print(d1)
och få objektet utskrivet på
ett snyggt sätt.
För detta använder man en annan "magisk" metod: __str__
.
Notera att det är dubbla understrykningstecken __
före och efter str
.
Kod i filen dice.py | Utskrift |
---|---|
import random
# Definition av klassen Dice
class Dice:
# Metoden init
def __init__(self, sides):
self.sides = sides
self.value = random.randint(1, self.sides)
# Metoden str, ger en textrepresentation av objektet
def __str__(self):
return f'Sidor: {self.sides:2d}, värde: {self.value:2d}'
# Testa klassen Dice
d1 = Dice(6)
d2 = Dice(12)
print(d1) # Metoden __str__ anropas
print(d2) # Metoden __str__ anropas
|
d1: Sidor: 6, värde: 6
d2: Sidor: 12, värde: 11
|
self
.
Metoden bygger en sträng där värdena för self.sides
och self.value
ingår, dvs det som kännetecknar tärningen.
Som nämndes ovan måste man använda self.
för att komma åt attributen.
Strängens innehåll bestämmer man själv.
Det som händer vid satsen print(d1)
är att metoden ___str__
anropas för objektet d1
.
Den metoden returnerar en sträng där attributen finns med för just objektet d1
och denna sträng skriver funktionen print
ut.
Egna metoder
Ovanstående metoder hade föreskrivna namn och betydelser. Man kan också lägga till egna metoder. Exvis metoder som returnerar tärningens respektive attribut (värde på instansvariabel). Vi behöver därmed två metoder och döper dem till:getValue
, som returnerar värdet avself.value
getSides
, som returnerar värdet avself.sides
Kod i filen dice.py | Utskrift |
---|---|
import random
class Dice:
def __init__(self, sides):
self.sides = sides
self.value = random.randint(1, self.sides)
def __str__(self):
return f'Sidor: {self.sides:2d}, värde: {self.value:2d}'
# Returnerar value
def getValue(self):
return self.value
# Returnerar sides
def getSides(self):
return self.sides
# Test av klassen Dice
d1 = Dice(6) # Skapa en 6 sidig tärning
d2 = Dice(12) # Skapa en 12-sidig tärning
print('d1 värde=', d1.getValue())
print('d2 värde=', d2.getValue())
print('d1 sidor=', d1.getSides())
print('d2 sidor=', d2.getSides())
|
d1 värde= 5
d2 värde= 12
d1 sidor= 6
d2 sidor= 12
|
get
-metoderna hade vi fått samma utskrift med följande:d1.value
kommer vi åt d1
-objektets instansvariabel value
.
Vi har nu tre metoder för att på olika sätt ta reda på värdet av objektet. När tärningen skapas får den ett slumpat värde, men därefter går värdet ej att ändra.
Vi behöver en metod roll
för att slå tärningen,
d.v.s. ge den ett nytt slumpmässigt värde:
Kod i filen dice.py | Utskrift |
---|---|
import random
class Dice:
def __init__(self, sides):
self.sides = sides
self.value = random.randint(1, self.sides)
def __str__(self):
return f'Sidor: {self.sides:2d}, värde: {self.value:2d}'
def getValue(self):
return self.value
def getSides(self):
return self.sides
# Slår tärningen
def roll(self):
self.value = random.randint(1, self.sides)
# Test av klassen Dice
d1 = Dice(6)
d2 = Dice(12)
for i in range(5):
d1.roll()
d2.roll()
print(d1.getValue(), ' ', d2.getValue())
|
2 5
6 12
4 7
4 5
2 3
|
getValue
och getSides
är accessors. roll
är mutator.
Om man redan har en klass kan man sedan i sin tur använda den i en annan klass, som i följande exempel.
Exempel 2: Pokertärningar
När man spelar tärningspoker brukar man använda 5 tärningar som man slår på en gång. För att representera en uppsättning pokertärningar kan vi använda en lista med 5 tärningar. Nedan finns en klass för en uppsättning pokertärningar. Skriv koden i en ny filPokerDice.py
.
Tips: Kommentera bort den kod i filen Dice.py
som testar klassen Dice
, annars kommer den att köras när du testar
klassen pokerDice
Kod i filen PokerDice.py | Utskrift |
---|---|
import dice # Klassen PokerDice måste känna till klassen Dice
class PokerDice:
def __init__(self):
self.dice_list = []
for i in range(5):
self.dice_list.append(dice.Dice(6))
def __str__(self):
# Använder metoden getValue i Dice
return str(sorted([d.getValue() for d in self.dice_list]))
def roll(self):
for d in self.dice_list:
d.roll() # Använder rollmetoden i Dice
# Test av klassen PokerDice
print('Pokertärningar:')
pd = PokerDice()
for i in range(10):
pd.roll()
print(pd)
|
Pokertärningar:
[1, 3, 3, 3, 4]
[1, 1, 1, 2, 5]
[2, 4, 5, 5, 6]
[1, 1, 1, 3, 3]
[2, 2, 2, 5, 6]
[1, 3, 4, 6, 6]
[1, 2, 3, 4, 6]
[1, 3, 4, 6, 6]
[1, 2, 3, 5, 5]
[1, 1, 3, 4, 6]
|
-
Initieringsmetoden skapar en lista med 5 tärningsobjekt med 6 sidor.
dice.Dice(6)
betyder att klassen Dice finns i python-filendice
. -
Metoden
__str__
gör en lista med de olika tärningarnas värden, sorterar den och returnerar den i sträng-form.
Observera hur vi använder listbyggare för att skapa returvärdet. -
Metoden
roll
utnyttjarroll
-metoden i klassenDice
.
Det finns alltså två stroll
-metoder, en för Dice-objekt och en för RollDice-objekt.
Övningar
-
Ibland kan man vilja ha ett annat antal än 5. Modifiera koden i klassen PokerDice så att man kan ange hur många tärningar man vill ha.
Testa att det fungerar.
Endast init-metoden behöver ändras:
def __init__(self, number_of_dice): self.dice_list = [] for i in range(number_of_dice): self.dice_list.append(dice.Dice(6))
-
Skriv en metod i klassen PokerDice som returnerar antalet tärningar! Testa att det fungerar.
def number_of_dice(self): return len(self.dice_list)
-
Metoden
__str__
i klassen PokerDice utnyttjar en listbyggare för att skapa resultatet. Skriv den utan att använda denna facilitet. Testa att det fungerar.res =[] for d in self.dice_list: res.append(d.value) return str(sorted(res))
-
Skriv
__init__
-metoden i klassen PokerDice med hjälp av en listbyggare. Testa att det fungerar.def __init__(self): self.dice_list = [dice.Dice(6) for i in range(number_of_dice)]
-
Skriv en klass
Rectangle
i en ny filrektangel.py
. En rektangel ska ha en bredd, en höjd och en position i x-y-planet. Förse klassen med standardmetoderna__init__
och__str__
. Skriv också en metod som returnerar rektangelns yta samt en metod som ritar rektangeln med hjälpTurtle
-klassen. Testa klassen.import turtle class Rectangle: def __init__(self, width, height, xpos, ypos): self.width = width self.height = height self.xpos = xpos self.ypos = ypos def __str__(self): return f'Rectangle({self.width}, {self.height}, ' + \ f'{self.xpos}, {self.ypos})' def area(self): return self.width*self.height def draw(self): t = turtle.Turtle() t.penup() t.goto(self.xpos, self.ypos) t.pendown() for x in range(2): t.forward(self.width) t.left(90) t.forward(self.height) t.left(90) r = Rectangle(200, 100, 0, 0) print(r) r.draw()
Rektangelns position har tolkats som det nedre vänstra hörnet.
Fråga
Hur många timmar har du arbetat med denna lektion?
Gå till nästa lektion eller gå tillbaka