Observera att denna sida är under utveckling!

Lektion 8: 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.

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>
Kommentarer:
  1. Klasser definieras med ordet class följt av det namn vi vill ha på klassen samt ett kolon.
  2. Klassnamn ska börja på stor bokstav.
  3. 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 nyckelordet pass till. Satsen pass gör ingenting.
  4. Uttrycket Dice() skapar ett tärningsobjekt och returnerar en referens till det (jämför hur vi skapade Turtle-objekt, t = turtle.Turtle(), där turtle. anger namnet på modulen där klassen finns).
  5. Ä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>
Kommentarer:
  1. Eftersom klassen nu har ett innehåll (de röda satserna) tar vi bort satsen pass.
  2. Initieringsmetoden (ibland kallad "konstruktor") måste heta __init__ och ha self som första parameter. Därefter kan man lägga till de parametrar man vill.
  3. Notera att det är dubbla understrykningstecken __ för och efter init.
  4. Observera indenteringen koden i metoden
  5. Tilldelningssatserna self.namn = värde skapar egenskaper med angivet namn och värde.
    Egenskaperna kallas också attribut eller instansvariabler.
    Vi ser att instansvariablerna self.sides och self.value skapas.
    Konstruktorn lägger alltså in två egenskaper varav värdet till den ena ges av en parameter och det andra av slumpfunktionen.
  6. Observera att sides och self.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.
  7. När man skapar objekten (t ex Dice(6)) anger man bara de argument som man själv lagt till. Parametern self läggs till automatiskt.
  8. 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
Metoden, som definierar hur objektet ska uttryckas som en sträng, ska bara ha parametern 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:
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
Alternativt istf de båda get-metoderna hade vi fått samma utskrift med följande:
# 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.value) # d1-objektets value print('d2 värde=', d2.value) # d2-objektets value print('d1 sidor=', d1.sides) # d1-objektets sides print('d2 sidor=', d2.sides) # d2-objektets sides
Med punktnotationen, ex.vis 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
Metoder som kan ändra ett objekt kallas mutators och metoder som man får veta saker om ett objekt kallas accessors. 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 fil PokerDice.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]
Kommentarer:
  1. Initieringsmetoden skapar en lista med 5 tärningsobjekt med 6 sidor.
    dice.Dice(6) betyder att klassen Dice finns i python-filen dice.
  2. 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.
  3. Metoden roll utnyttjar roll-metoden i klassen Dice.
    Det finns alltså två st roll-metoder, en för Dice-objekt och en för RollDice-objekt.

Övningar

  1. 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. Lösningsförslag
    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))
    
  2. Skriv en metod i klassen PokerDice som returnerar antalet tärningar! Testa att det fungerar. Lösningsförslag
        def number_of_dice(self):
            return len(self.dice_list)
    
  3. 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. Lösningsförslag
    res =[]
    for d in self.dice_list:
        res.append(d.value)
    return str(sorted(res))
    
  4. Skriv __init__-metoden i klassen PokerDice med hjälp av en listbyggare. Testa att det fungerar. Lösningsförslag
        def __init__(self):
            self.dice_list = [dice.Dice(6) for i in range(number_of_dice)]
    
  5. Skriv en klass Rectangle i en ny fil rektangel.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älp Turtle-klassen. Testa klassen. Lösningsförslag
    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