Osa 9

Objekt och referenser

Varje värde i Python är ett objekt. Alla objekt som du skapar baserat på en klass som du själv har definierat fungerar exakt på samma sätt som alla "vanliga" Python-objekt. Objekt kan till exempel lagras i en lista:

from datetime import date

class SlutfordKurs:

    def __init__(self, kurs: str, studiepoang: int, slutforsdatum: date):
        self.kurs = kurs
        self.studiepoang = studiepoang
        self.slutforsdatum = slutforsdatum


if __name__ == "__main__":
    # Vi skapar några slutförda kurser och lägger dessa i en lista
    prestationer = []

    mat1 = SlutfordKurs("Matematik 1", 5, date(2020, 3, 11))
    prg1 = SlutfordKurs("Programmering 1", 6, date(2019, 12, 17))

    prestationer.append(mat1)
    prestationer.append(prg1)

    # Vi lägger några till rakt till listan
    prestationer.append(SlutfordKurs("Fysik 2", 4, date(2019, 11, 10)))
    prestationer.append(SlutfordKurs("Programmering 2", 5, date(2020, 5, 19)))

    # Vi går igenom alla slutförda kurser, skriver ut deras namn och räknar ihop den totala mängden studiepoäng
    studiepoang = 0
    for prestation in prestationer:
        print(prestation.kurs)
        studiepoang += prestation.studiepoang

    print("Studiepoäng totalt:", studiepoang)
Exempelutskrift

Matematik 1 Programmering 1 Fysik 2 Programmering 2 Studiepoäng totalt: 20

Loading
Loading

Du kanske minns att listor inte innehåller några objekt i sig själva. De innehåller referenser till objekt. Exakt samma objekt kan förekomma flera gånger i en och samma lista, och det kan refereras till flera gånger i listan eller utanför den. Låt oss ta en titt på ett exempel:

class Produkt:
    def __init__(self, namn: int, enhet: str):
        self.namn = namn
        self.enhet = enhet


if __name__ == "__main__":
    affarslista = []
    mjolk = Produkt("Mjölk", "liter")

    affarslista.append(mjolk)
    affarslista.append(mjolk)
    affarslista.append(Produkt("Gurka", "st"))
9 1 1

Om det finns mer än en referens till samma objekt spelar det ingen roll vilken av referenserna som används:

class Hund:
    def __init__(self, namn):
        self.namn = namn

    def __str__(self):
        return self.namn

hundar = []
molly = Hund("Molly")
hundar.append(molly)
hundar.append(molly)
hundar.append(Hund("Molly"))

print("Hundar i början:")
for hund in hundar:
    print(hund)

print("Hunden på index 0 får ett nytt namn:")
hundar[0].namn = "Rex"
for hund in hundar:
    print(hund)

print("Hunden på index 2 får ett nytt namn:")
hundar[2].namn = "Fifi"
for hund in hundar:
    print(hund)
Exempelutskrift

Koirat alussa: Molly Molly Molly Hunden på index 0 får ett nytt namn:: Rex Rex Molly Hunden på index 2 får ett nytt namn: Rex Rex Fifi

Referenserna på index 0 och 1 i listan hänvisar till samma objekt. Var och en av referenserna kan användas för att komma åt objektet. Referensen på index 2 hänvisar till ett annat objekt, men med till synes samma innehåll. Om innehållet i det senare objektet ändras påverkas inte det andra.

Operatorn is används för att kontrollera om de två referenserna hänvisar till exakt samma objekt, medan operatorn == talar om för dig om innehållet i objekten är detsamma. Följande exempel gör förhoppningsvis skillnaden tydlig:

lista1 = [1, 2, 3]
lista2 = [1, 2, 3]
lista3 = lista1

print(lista1 is lista2)
print(lista1 is lista3)
print(lista2 is lista3)

print()

print(lista1 == lista2)
print(lista1 == lista3)
print(lista2 == lista3)
Exempelutskrift

False True False

True True True

Alla Python-objekt kan också lagras i en ordlista eller någon annan datastruktur. Detta gäller även objekt som är av en klass som du själv har definierat.

class Studerande:
    def __init__(self, namn: str, sp: int):
        self.namn = namn
        self.sp = sp

if __name__ == "__main__":
    # Vi använder studerandenummer som nyckel och värdet som fås är ett objekt av typen Studerande
    studeranden = {}
    studeranden["12345"] = Studerande("Olle Studerande", 10)
    studeranden["54321"] = Studerande("Ove Studerande", 67)

Visualiseringsverktyget kan hjälpa dig att förstå exemplet ovan:

9 1 2

Self eller inget self?

Hittills har vi bara snuddat vid ytan när det gäller att använda parameternamnet self. Låt oss titta närmare på när det bör eller inte bör användas.

Nedan har vi en enkel klass som låter oss skapa ett ordförråd-objekt som innehåller några ord:

class Ordforrad:
    def __init__(self):
        self.ord = []

    def tillsatt_ord(self, ord: str):
        if not ord in self.ord:
            self.ord.append(ord)

    def utskrift(self):
        for ord in sorted(self.ord):
            print(ord)

ordforrad = Ordforrad()
ordforrad.tillsatt_ord("python")
ordforrad.tillsatt_ord("objekt")
ordforrad.tillsatt_ord("objekt-orienterad programmering")
ordforrad.tillsatt_ord("objekt")
ordforrad.tillsatt_ord("nörd")

ordforrad.utskrift()
Exempelutskrift

nörd objekt objekt-orienterad programmering python

Listan med ord lagras i ett attribut med namnet self.ord. I det här fallet är parameternamnet self obligatoriskt både i klassens konstruktormetod och i alla andra metoder som använder variabeln. Om self utelämnas kommer de olika metoderna inte att få tillgång till samma lista med ord.

Låt oss lägga till en ny metod i vår klassdefinition. Metoden langsta_ord(self) returnerar (ett av) de längsta orden i ordförrådet.

Följande är ett sätt att utföra denna uppgift, men vi kommer snart att se att det inte är ett särskilt bra sätt:

class Ordforrad:
    def __init__(self):
        self.ord = []

    # ...

    def langsta_ord(self):
        # vi definierar två hjälpvariabler
        self.langsta = ""
        self.langsta_langd = 0

        for ord in self.ord:
            if len(ord) > self.langsta_langd:
                self.langsta_langd = len(ord)
                self.langsta = ord

        return self.langsta

Den här metoden använder två hjälpvariabler som deklareras med parameternamnet self. Kom ihåg att namnen på variablerna inte spelar någon roll i funktionell mening, så dessa variabler kan också namnges mer förvirrande som till exempel hjalpare och hjalpare2. Koden börjar se lite kryptisk ut:

class Ordforrad:
    def __init__(self):
        self.ord = []

    # ...

    def langsta_ord(self):
        # vi definierar två hjälpvariabler
        self.hjalpare = ""
        self.hjalpare2 = 0

        for ord in self.ord:
            if len(ord) > self.hjalpare2:
                self.hjalpare2 = len(ord)
                self.hjalpare = ord

        return self.hjalpare

När en variabel deklareras med parameternamnet self blir den ett attribut till objektet. Detta innebär att variabeln kommer att existera så länge objektet existerar. Specifikt kommer variabeln att fortsätta existera även efter att metoden som deklarerar den har avslutat sin exekvering (engelska “Execution”). I exemplet ovan är detta helt onödigt, eftersom hjälpvariablerna endast är avsedda att användas inom metoden longest_word(self). Så att deklarera hjälpvariabler med parameternamnet self är inte en särskilt bra idé här.

Förutom att variabler kan existera efter sitt "utgångsdatum" kan användning av self för att skapa nya attribut där de inte är nödvändiga orsaka svåra buggar i din kod. Särskilt generiskt namngivna attribut som self.hjalpare, som sedan används i flera olika metoder, kan orsaka oväntade beteenden som är svåra att spåra.

Om t.ex. en hjälpvariabel deklareras som ett attribut och tilldelas ett ursprungligt värde i konstruktorn, men variabeln sedan används i ett orelaterat sammanhang i en annan metod, blir resultatet ofta oförutsägbart:

class Ordforrad:
    def __init__(self):
        self.ord = []
        # vi definierar hjälparvariabler
        self.hjalpare = ""
        self.hjalpare2 = ""
        self.hjalpare3 = ""
        self.hjalpare4 = ""

    # ...

    def langsta_ord(self):
        for ord in self.ord:
            # detta fungerar inte eftersom hjalpare2 har fel typ
            if len(ord) > self.hjalpare2:
                self.hjalpare2 = len(ord)
                self.hjalpare = ord

        return self.hjalpare

Man skulle kunna tro att detta skulle lösas genom att bara deklarera attributen där de används, utanför konstruktorn, men detta resulterar i en situation där de attribut som är tillgängliga via ett objekt är beroende av vilka metoder som har utförts. I föregående del såg vi att fördelen med att deklarera attribut i konstruktorn är att alla instanser av klassen då kommer att ha exakt samma attribut. Om så inte är fallet kan det lätt leda till fel om man använder olika instanser av klassen.

Sammanfattningsvis, om du behöver hjälpvariabler för användning inom en enda metod, är det korrekta sättet att göra det utan self. För att göra din kod lättare att förstå, använd också informativa variabelnamn:

class Ordforrad:
    def __init__(self):
        self.ord = []

    # ...

    def langsta_ord(self):
        # detta är det korrekta sättet att definiera
        # hjälpvariabler för användning i en enda metod
        langsta = ""
        langsta_langd = 0

        for ord in self.ord:
            if len(ord) > langsta_langd:
                langsta_langd = len(ord)
                langsta = ord

        return langsta

I implementeringen ovan är hjälpvariablerna endast tillgängliga när metoden utförs. De värden som lagras i dem kan inte orsaka komplikationer i andra delar av programmet.

Objekt som argument till funktioner

De objekt som skapas baserat på våra egna klasser är vanligtvis mutabla. Du kanske kommer ihåg att till exempel Python-listor är föränderliga: när de passeras som argument till funktioner kan deras innehåll ändras som ett resultat av exekveringen.

Låt oss titta på ett enkelt exempel där en funktion får en referens till ett objekt av typen Studerande som sitt argument. Funktionen ändrar sedan namnet på studenten. Både funktionen och huvudfunktionen som anropar den har åtkomst till samma objekt, så ändringen syns även i huvudfunktionen.

class Studerande:
    def __init__(self, namn: str, studerandenummer: str):
        self.namn = namn
        self.studerandenummer = studerandenummer

    def __str__(self):
        return f"{self.namn} ({self.studerandenummer})"

# observera att typledtråden använder namnet på klassen definierad ovan
def andra_namn(studerande: Studerande):
    studerande.namn = "Olle Studerande"

# skapa ett Studerande-objekt
olle = Studerande("Olle Elev", "12345")

print(olle)
andra_namn(olle)
print(olle)
Exempelutskrift

Olle Elev (12345) Olle Studerande (12345)

Det är också möjligt att skapa objekt inom funktioner. Om en funktion returnerar en referens till det nyskapade objektet är det också åtkomligt inom huvudfunktionen:

from random import randint, choice

class Studerande:
    def __init__(self, namn: str, studerandenummer: str):
        self.namn = namn
        self.studerandenummer = studerandenummer

    def __str__(self):
        return f"{self.namn} ({self.studerandenummer})"


# Denna funktion skapar och returnerar ett nytt Studerande-objekt.
# Den väljer slumpmässigt värden för namnet och studerandenumret.
def ny_studerande():
    fornamn = ["Atte","Peter","Minna","Maria"]
    efternamn = ["Virtanen", "Lahtinen", "Leinonen", "Pythonson"]

    # Slumpmässigt namn
    namn = choice(fornamn) + " " + choice(efternamn)

    # Slumpmässigt studerandenummer
    studerandenummer = str(randint(10000,99999))

    # Skapa och returnera ett Studerande-objekt
    return Studerande(namn, studerandenummer)

if __name__ == "__main__":
    # Kalla funktionen fem gånger och spara resultatet i en lista
    studeranden = []
    for i in range(5):
        studeranden.append(ny_studerande())

    # Skriv ut resultatet
    for studerande in studeranden:
        print(studerande)

Om du kör ovanstående kan det resultera i följande utskrift (OBS: eftersom slumpen är inblandad kommer resultaten sannolikt att bli annorlunda om du testar koden själv).

Exempelutskrift

Maria Lahtinen (36213) Atte Virtanen (11859) Maria Pythonen (77330) Atte Pythonson (86451) Minna Pythonson (86211)

Objekt som argument till metoder

På liknande sätt kan objekt fungera som argument till metoder. Låt oss ta en titt på ett exempel från en nöjespark:

class Person:
    def __init__(self, namn: str, langd: int):
        self.namn = namn
        self.langd = langd

class Akattraktion:
    def __init__(self, namn: str, langdgrans: int):
        self.besokare = 0
        self.namn = namn
        self.langdgrans = langdgrans

    def ta_ombord(self, person: Person):
        if person.langd >= self.langdgrans:
            self.besokare += 1
            print(f"{person.namn} kom ombord")
        else:
            print(f"{person.namn} var för kort :(")

    def __str__(self):
        return f"{self.namn} ({self.besokare} besökare)"

Attraktionen innehåller en metod motta_besökare, som tar ett objekt av typen Person som argument. Om besökaren är tillräckligt lång släpps denne ombord och antalet besökare ökas. Klasserna kan testas på följande sätt:

berg_och_dalbana = Akattraktion("Berg_och_dalbana", 120)
jakob = Person("Jakob", 172)
vilma = Person("Vilma", 105)

berg_och_dalbana.ta_ombord(jakob)
berg_och_dalbana.ta_ombord(vilma)

print(berg_och_dalbana)
Exempelutskrift

Jakob kom ombord Venla var för kort :( Berg_och_dalbana (1 besökare)

Loading
Loading

En instans av samma klass som argument till en metod

Nedan har vi ytterligare en version av klassen Person:

class Person:
    def __init__(self, namn: str, fodelsear: int):
        self.namn = namn
        self.fodelsear = fodelsear

Låt oss anta att vi vill skriva ett program som jämför åldern på objekt av typen Person. Vi kan skriva en separat funktion för detta ändamål:

def aldre_an(person1: Person, person2: Person):
    if person1.fodelsear < person2.fodelsear:
        return True
    else:
        return False

muhammad = Person("Muhammad ibn Musa al-Khwarizmi", 780)
pascal = Person("Blaise Pascal", 1623)
grace = Person("Grace Hopper", 1906)

if aldre_an(muhammad, pascal):
    print(f"{muhammad} är äldre än {pascal}")
else:
    print(f"{muhammad} är inte äldre än {pascal}")

if aldre_an(grace, pascal):
    print(f"{grace} är äldre än {pascal}")
else:
    print(f"{grace} är inte äldre än {pascal}")
Exempelutskrift

Muhammad ibn Musa al-Khwarizmi är äldre än Blaise Pascal Grace Hopper är inte äldre än Blaise Pascal

En av principerna för objektorienterad programmering är att all funktionalitet som hanterar objekt av en viss typ ska inkluderas i klassdefinitionen, som metoder. I stället för en funktion kan vi alltså skriva en metod som gör det möjligt att jämföra åldern på ett Person-objekt med ett annat Person-objekt:

class Person:
    def __init__(self, namn: str, fodelsear: int):
        self.namn = namn
        self.fodelsear = fodelsear

    # OBS! Typledtrådar måste vara inom citationstecken ifall parametern är av samma typ som klassen självt!
    def aldre_an(self, annat: "Person"):
        if self.fodelsear < annat.fodelsear:
            return True
        else:
            return False

Här kallas det objekt som metoden anropas på för self, medan det andra Person-objektet kallas för annat.

Kom ihåg att anrop av en metod skiljer sig från anrop av en funktion. En metod är kopplad till ett objekt med punktnotationen:

muhammad = Person("Muhammad ibn Musa al-Khwarizmi", 780)
pascal = Person("Blaise Pascal", 1623)
grace = Person("Grace Hopper", 1906)

if muhammad.aldre_an(pascal):
    print(f"{muhammad.namn} är äldre än {pascal.namn}")
else:
    print(f"{muhammad.namn} är inte äldre än {pascal.namn}")

if grace.aldre_an(pascal):
    print(f"{grace.namn} är äldre än {pascal.namn}")
else:
    print(f"{grace.namn} är inte äldre än {pascal.namn}")

Till vänster om punkten finns själva objektet, som kallas self i metoddefinitionen. Inom parentes står argumentet till metoden, vilket är det objekt som kallas annat.

Utskriften från programmet är exakt densamma som med funktionsimplementeringen ovan.

Till sist, en ganska kosmetisk punkt: if...else-strukturen i metoden aldre_an är i stort sett onödig. Värdet på det booleska uttrycket i villkoret är redan exakt samma sanningsvärde som returneras. Metoden kan alltså förenklas:

class Person:
    def __init__(self, namn: str, fodelsear: int):
        self.namn = namn
        self.fodelsear = fodelsear

    # OBS! Typledtrådar måste vara inom citationstecken ifall parametern är av samma typ som klassen självt!
    def aldre_an(self, annat: "Person"):
        return self.fodelsear < annat.fodelsear:

Liksom det framkommer av kommentarerna i exemplen ovan, så måste typledtråden omslutas av citattecken ifall parametern i en metoddefinition är av samma typ som klassen själv. Om citattecknen utelämnas uppstår ett fel, vilket du kommer att se om du försöker med följande:

class Person:
    # ...

    # Detta fungerar inte, Person måste vara innanför citationstecken
    def aldre_an(self, annat: Person):
        return self.fodelsear < annat.fodelsear:
Loading
Du har nått slutet av den här delen! Fortsätt till nästa del:

Se dina poäng genom att klicka på cirkeln nere till höger av sidan.