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!
Uppskattad arbetstid: 4 timmar.
Redovisning: Ingen obligatorisk redovisning.
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#
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. 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.
Vi börjar med att deklarera en tärningsklass och skapa två tärningsobjekt:
Kod |
Utskrift |
---|---|
class Dice:
pass
d1 = Dice()
d2 = Dice()
print(d1)
print(d2)
|
<__main__.Dice object at 0x10099d048>
<__main__.Dice object at 0x10099d0f0>
|
Kommentarer:
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. Vi måste ändå ha en indenterad rad, vilket man kan åstadkomma med nyckelordet
pass
.pass
är ett sätt att ha en kodrad som inte gör någonting.Uttrycket
Dice()
skapar ett tärningsobjekt och returnerar en referens till det (jämför hur vi skapadeTurtle
-objekt).Ä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
).
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 |
Utskrift |
---|---|
class Dice:
def __init__(self, sides):
self.sides = sides
self.value = random.randint(1, self.sides)
d1 = Dice(6)
d2 = Dice(12)
print('d1: ', d1.sides, d1.value)
print('d2: ', d2.sides, d2.value)
|
d1: 6 1
d2: 12 10
|
Kommentarer:
Eftersom klassen nu har ett innehåll tar vi bort
pass
.Initieringsmetoden (vanligen kallad ”konstruktor”) måste heta
__init__
och haself
som första parameter. Därefter kan man lägga till de parametrar man vill.Observera indenteringen!
Tilldelningssatserna
self.namn = värde
skapar egenskaper med angivet namn och värde.
Egenskaperna kallas också attribut eller instansvariabler.
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.
Metoden __str__
#
I utskrifterna i ovanstående exempel har vi explicit fått hämta värden
på de olika egenskaperna. I detta fall är det ju bara två stycken, men
om det blir fler vore det trevligt att ha ett standardsätt för att skapa
en sträng från ett objekt. Fördelen blir att man då exempelvis kan
skriva print(d1)
och få objektet utskrivet på ett snyggt sätt.
För detta används en annan ”magisk” metod: __str__
Kod |
Utskrift |
---|---|
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}'
d1 = Dice(6)
d2 = Dice(12)
print('d1: ', d1)
print('d2: ', d2)
|
d1: Sidor: 6, värde: 6
d2: Sidor: 12, värde: 11
|
Metoden, som definierar hur objektet ska uttryckas som en sträng, ska
bara ha parametern self
. Som nämndes ovan måste man skriva self.attributnamn
för att komma åt attributen. Strängens innehåll bestämmer man själv.
Egna metoder#
Ovanstående metoder hade föreskrivna namn och betydelser. Man kan också
lägga till egna metoder. I detta fall vore det bra med en metod roll
för att slå tärningen, d.v.s. ge den ett nytt slumpmässigt värde:
Kod |
Utskrift |
---|---|
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 roll(self):
self.value = random.randint(1, self.sides)
d1 = Dice(6)
d2 = Dice(12)
for _ in range(5):
d1.roll()
d2.roll()
print(f'{d1.value:2d}, {d2.value:2d}')
|
6, 10
5, 12
5, 7
6, 3
3, 10
|
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. En klass för en uppsättning pokertärningar kan då göras så här:
Kod |
Utskrift |
---|---|
class PokerDice:
def __init__(self):
self.dice_list = []
for _ in range(5):
self.dice_list.append(Dice(6))
def __str__(self):
return str(sorted([d.value for d in self.dice_list]))
def roll(self):
for d in self.dice_list:
d.roll() # Använder rollmetoden i Dice
print('Pokertärningar:')
pd = PokerDice()
for _ 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]
|
Kommentarer:
Initieringsmetoden skapar en lista med 5 tärningsobjekt med 6 sidor.
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
.
Övningar#
Ibland kan man vilja ha ett annat antal än 5. Modifiera koden så att man kan ange hur många tärningar man vill ha. Endast init-metoden behöver ändras:
Svar
def __init__(self, number_of_dice): self.dice_list = [] for _ in range(number_of_dice): self.dice_list.append(Dice(6))
Skriv en metod som returnerar antalet tärningar!
Svar
def number_of_dice(self): return len(self.dice_list)
Metoden
__str__
utnyttjar en listbyggare för att skapa resultatet. Skriv om metoden utan att använda listbyggare.Svar
res = [] for d in self.dice_list: res.append(d.value) return str(sorted(res))
Skriv
__init__
-metoden med hjälp av en listbyggare.Svar
def __init__(self, number_of_dice): self.dice_list = [Dice(6) for _ in range(number_of_dice)]
Skriv en klass
Rectangle
. 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 area samt en metod som ritar rektangeln med hjälpturtle
-paketet.Svar
Vi låter rektangelns position vara det nedre vänstra hörnets position.
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) # Example usage r = Rectangle(200, 100, 0, 0) print(r) r.draw()