Osa 8

Metoder i egna klasser

Klasser som endast innehåller dataattribut skiljer sig inte så mycket från ordlistor. Nedan kan du se två sätt att modellera ett bankkonto, först med en klassdefinition och sedan med hjälp av en ordlista.

# Exempel 1: bankkoto med klassdefinition
class Bankkonto:

    def __init__(self, kontonummer: str, agare: str, saldo: float, arsranta: float):
        self.kontonummer = kontonummer
        self.agare = agare
        self.saldo = saldo
        self.arsranta = arsranta

peters_konto = Bankkonto("12345-678", "Peter Python", 1500.0, 0.015)
# Exempel2: bankkonto med ordlista
peters_konto = {"kontonummer": "12345-678", "agare": "Peter Python", "saldo": 1500.0, "arsranta": 0.0}

Med en ordlista är implementeringen mycket kortare och enklare. Med en klass är strukturen däremot mer "hårt bunden", så vi kan förvänta oss att alla Bankkonto-objekt är strukturellt lika. Dessutom är en klass också namngiven. Klassen Bankkonto refereras till när ett nytt bankkonto skapas, och objektets typ är Bankkonto, inte dict.

En annan stor fördel med klasser är att de förutom data även kan innehålla funktionalitet. En av de vägledande principerna för objektorienterad programmering är att ett objekt används för att komma åt både den data som är kopplad till ett objekt och funktionaliteten för att bearbeta denna data.

Metoder i klasser

En metod är ett underprogram eller en funktion som är knuten till en specifik klass. Vanligtvis påverkar en metod bara ett enda objekt. En metod definieras inom klassdefinitionen och den kan komma åt klassens dataattribut precis som vilken annan variabel som helst.

Låt oss fortsätta med klassen Bankkonto som introducerades ovan. Nedan har vi en ny metod som lägger till ränta på kontot:

class Bankkonto:

    def __init__(self, kontonummer: str, agare: str, saldo: float, arsranta: float):
        self.kontonummer = kontonummer
        self.agare = agare
        self.saldo = saldo
        self.arsranta = arsranta

    # Metoden lägger till den årliga räntat till saldot
    def tillsatt_ranta(self):
        self.saldo += self.saldo * self.arsranta


peters_konto = Bankkonto("12345-678", "Peter Python", 1500.0, 0.015)
peters_konto.tillsatt_ranta()
print(peters_konto.saldo)
Exempelutskrift

1522.5

Metoden tillsatt_ranta multiplicerar saldot på kontot med den årliga ränteprocenten och lägger sedan till resultatet till det aktuella saldot. Metoden verkar bara på det objekt som den anropas på.

Låt oss se hur detta fungerar när vi har skapat flera instanser av klassen:

# Klassen Bankkonto är definierat såsom i förra exemplet

peters_konto = Bankkonto("12345-678", "Peter Python", 1500.0, 0.015)
pernillas_konto = Bankkonto("99999-999", "Pernilla Pythonson", 1500.0, 0.05)
pers_konto = Bankkonto("1111-222", "Per Persson", 1500.0, 0.001)

# Vi tillsätter ränta till Peters och Pernillas konton, men inte Pers
peters_konto.tillsatt_ranta()
pernillas_konto.tillsatt_ranta()

# Vi skriver ut alla
print(peters_konto.saldo)
print(pernillas_konto.saldo)
print(pers_konto.saldo)
Exempelutskrift

1522.5 1575.0 1500.0

Som du kan se ovan läggs den årliga räntan endast till på de konton som metoden anropas på. Eftersom den årliga räntan är olika för Peters och Paulas konton, blir resultatet olika för dessa två konton. Saldot på Pernillas konto ändras inte, eftersom metoden tillsatt_ranta inte anropas på objektet pernillas_konto.

Inkapsling

Inom objektorienterad programmering dyker ordet klient upp då och då. Det används för att hänvisa till ett kodavsnitt som skapar ett objekt och använder den tjänst som tillges av dess metoder. När data som finns i ett objekt endast används genom de metoder som det tillges, garanteras objektets interna integritet. I praktiken innebär detta att t.ex. en Bankkonto-klass erbjuder metoder för att hantera saldo-attributet, så att saldot aldrig nås direkt av klienten. Dessa metoder kan sedan verifiera att saldot till exempel inte tillåts gå under noll.

Ett exempel på hur detta skulle fungera:

class Bankkonto:

    def __init__(self, kontonummer: str, agare: str, saldo: float, arsranta: float):
        self.kontonummer = kontonummer
        self.agare = agare
        self.saldo = saldo
        self.arsranta = arsranta

    # Metoden tillsätter den årliga räntat till saldot av kontot
    def tillsatt_ranta(self):
        self.saldo += self.saldo * self.arsranta

    # Den här metoden "tar ut" pengar från kontot
    # Metoden returnerar True ifall det lyckades, och False ifall det misslyckades
    def uttag(self, uttagssumma: float):
        if uttagssumma <= self.saldo:
            self.saldo -= uttagssumma
            return True

        return False

peters_konto = Bankkonto("12345-678", "Peter Python", 1500.0, 0.015)

if peters_konto.uttag(1000):
    print("Uttaget lyckades, kontots saldo är nu", peters_konto.saldo)
else:
    print("Uttaget lyckades inte, saldot var otillräckligt")

# Vi försöker på nytt
if peters_konto.uttag(1000):
    print("Uttaget lyckades, kontots saldo är nu", peters_konto.saldo)
else:
    print("Uttaget lyckades inte, saldot var otillräckligt")
Exempelutskrift

Uttaget lyckades, kontots saldo är nu 500.0 Uttaget lyckades inte, saldot var otillräckligt

Att bibehålla objektets interna integritet och erbjuda lämpliga metoder för att säkerställa detta kallas inkapsling. Tanken är att objektets inre arbete är dolt för klienten, men objektet erbjuder metoder som kan användas för att komma åt den data som lagras i objektet.

Att lägga till en metod innebär inte att attributet automatiskt döljs. Även om klassdefinitionen Bankkonto innehåller metoden uttag för att ta ut pengar, kan klientkoden fortfarande komma åt och ändra attributet saldo direkt:

peters_konto = Bankkonto("12345-678", "Peter Python", 1500.0, 0.015)

# Vi försöker lyfta 2000
if peters_konto.uttag(2000):
    print("Uttaget lyckades, kontots saldo är nu", peters_konto.saldo)
else:
    print("Uttaget lyckades inte, saldot var otillräckligt")

    # Vi "tvingar" ett uttag på 2000
    peters_konto.saldo -= 2000

print("Saldot är nu:", peters_konto.saldo)

Det är möjligt att dölja dataattributen från klientkoden, vilket kan bidra till att lösa detta problem. Vi återkommer till detta ämne i nästa del.

Loading

Som avslutning på detta avsnitt tittar vi på en klass som modellerar en spelares personliga bästa. Klassdefinitionen innehåller separata valideringsmetoder som kontrollerar att de argument som skickas är giltiga. Metoderna anropas redan i konstruktorn. Detta säkerställer att det skapade objektet är sunt internt.

from datetime import date

class PersonligtRekord:

    def __init__(self, spelare: str, dag: int, manad: int, ar: int, poang: int):
        # Standardvärden
        self.spelare = ""
        self.datum = date(1900, 1, 1)
        self.poang = 0

        if self.namn_ok(spelare):
            self.spelare = spelare

        if self.dtm_ok(dag, manad, ar):
            self.datum = date(ar, manad, dag)

        if self.poang_ok(poang):
            self.poang = poang

    # Hjälparmetoder som kollar att argumenten är giltiga
    def namn_ok(self, namn: str):
        return len(namn) >= 2 # Namnet ska vara minst två tecken

    def dtm_ok(self, dag, manad, ar):
        try:
            date(ar, manad, dag)
            return True
        except:
            # Ett undantag ifall datumet inte är giltigt
            return False

    def poang_ok(self, poang):
        return poang >= 0

if __name__ == "__main__":
    resultat1 = PersonligtRekord("Peter", 1, 11, 2020, 235)
    print(resultat1.poang)
    print(resultat1.spelare)
    print(resultat1.datum)

    # Datumet är inte giltigt
    resultat2 = PersonligtRekord("Pernilla", 4, 13, 2019, 4555)
    print(resultat2.poang)
    print(resultat2.spelare)
    print(resultat2.datum) # Skriver ut standardvärdet 1900-01-01
Exempelutskrift

235 Peter 2020-11-01 4555 Pernilla 1900-01-01

I exemplet ovan anropades även hjälpmetoderna via parameternamnet self när de användes i konstruktorn. Det är också möjligt att inkludera /statiska/ metoddefinitioner i klassdefinitioner. Dessa är metoder som kan anropas utan att det någonsin skapas en instans av klassen. Vi återkommer till detta i nästa del.

Parameternamnet self används endast när man hänvisar till /objektets egenskaper som en instans av klassen/. Det gäller både dataattributen och de metoder som är knutna till ett objekt. För att göra terminologin mer förvirrande kallas dataattributen och metoderna tillsammans ibland helt enkelt för objektets attribut, vilket är anledningen till att vi i detta material ofta har angett dataattribut när vi menar de variabler som definieras inom klassen. Det är här terminologin hos vissa Python-programmerare skiljer sig något från den terminologi som mer allmänt används inom objektorienterad programmering, där attribut vanligtvis bara hänvisar till dataattributen hos ett objekt.

Det är också möjligt att skapa lokala variabler inom metoddefinitioner utan att hänvisa till self. Detta bör du göra om det inte finns något behov av att komma åt variablerna utanför metoden. Lokala variabler inom metoder har inga speciella nyckelord; de används precis som alla vanliga variabler som du har stött på hittills. .

Så här skulle det till exempel fungera:

class Bonuskort:
    def __init__(self, namn: str, saldo: float):
        self.namn = namn
        self.saldo = saldo

    def tillsatt_bonus(self):
        # Nu är variabeln bonus en lokal variabel, inte ett
        # data attribut till objektet
        # Den kan inte nås genom ett objekt
        bonus = self.saldo * 0.25
        self.saldo += bonus

    def tillsatt_superbonus(self):
        # Variabeln superbonus är också en lokal variabel.
        # Vanligtvis är hjälpvariabler lokala variabler eftersom
        # det inte finns något behov av att komma åt dem från andra
        # metoder i klassen eller direkt via ett objekt.
        superbonus = self.saldo * 0.5
        self.saldo += superbonus

    def __str__(self):
        return f"Bonuskort(namn={self.namn}, saldo={self.saldo})"
Loading
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.