Osa 9

Inkapsling

I objektorienterad programmering avser termen klient ett program som använder en klass eller instanser av en klass. En klass erbjuder klienten tjänster genom vilka klienten kan komma åt de objekt som skapats baserat på klassen. Målen här är att

  1. användningen av en klass och/eller objekt är så enkel som möjligt ur klientens synvinkel
  2. integriteten för varje objekt bevaras hela tiden

Ett objekts integritet innebär att objektets tillstånd alltid förblir acceptabelt. I praktiken innebär detta att värdena på objektets attribut alltid är acceptabla. Ett objekt som representerar ett datum ska till exempel aldrig ha 13 som värde för månaden, ett objekt som representerar en student ska aldrig ha ett negativt tal som värde för uppnådda studiepoäng och så vidare.

Låt oss ta en titt på en klass som heter Studerande:

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

    def tillagg_poang(self, studiepoang):
        if studiepoang > 0:
            self.studiepoang += studiepoang

Studerande objektet erbjuder sina klienter metoden tillagg_poang, som gör det möjligt för klienten att lägga till ett angivet antal studiepoäng till studentens totala antal. Metoden säkerställer att värdet som skickas som argument är över noll. Följande kod lägger till studiepoäng vid tre tillfällen:

oskar = Studerande("Oskar Studerande", "12345")
oskar.tillagg_poang(5)
oskar.tillagg_poang(5)
oskar.tillagg_poang(10)
print("Studiepoäng:", oskar.studiepoang)
Exempelutskrift

Studiepoäng: 20

Trots metoddefinitionen är det fortfarande möjligt att komma åt attributet studiepoang direkt. Detta kunde resultera i ett felaktigt tillstånd där objektets integritet går förlorad:

oskar = Studerande("Oskar Studerande", "12345")
oskar.studiepoang = -100
print("Studiepoäng:", oskar.studiepoang)
Exempelutskrift

Studiepoäng: -100

Inkapsling

Ett vanligt inslag i objektorienterade programmeringsspråk är att klasserna kan dölja sina attribut för eventuella kunder. Dolda attribut kallas vanligtvis privata. I Python uppnås denna sekretess genom att lägga till två understreck __ i början av attributnamnet:

class Bankkort:
    # Attributet nummer är gömt, attributet namn är åtkombart
    def __init__(self, nummer: str, namn: str):
        self.__nummer = nummer
        self.namn = namn

Ett privat attribut är inte direkt synligt för klienten. Försök att referera till det orsakar ett fel. I exemplet ovan är attributet namn lätt att komma åt och ändra:

kort = Bankkort("123456","Robert Rik")
print(kort.namn)
kort.namn = "Peter Pank"
print(kort.namn)
Exempelutskrift

Robert Rik Peter Pank

Ifall man provar få en utskrift av kortnumret så orsakar det däremot ett fel:

kort = Bankkort("123456","Robert Rik")
print(kort.__nummer)
Exempelutskrift

AttributeError: 'Bankkort' object has no attribute '__nummer'

Att dölja attribut från klienter kallas inkapsling. Som namnet antyder är attributet "slutet inne i en kapsel". Klienten erbjuds sedan ett lämpligt gränssnitt (engelska: interface) för att komma åt och bearbeta den data som finns lagrad i objektet.

Låt oss lägga till ett annat inkapslat attribut: saldot på kreditkortet. Den här gången lägger vi också till offentligt synliga metoder som gör det möjligt för klienten att komma åt och ändra saldot:

class Bankkort:
    def __init__(self, nummer: str, namn: str, saldo: float):
        self.__nummer = nummer
        self.namn = namn
        self.__saldo = saldo

    def tillsatt_pengar(self, mangd: float):
        if mangd > 0:
            self.__saldo += mangd

    def anvand_pengar(self, mangd: float):
        if mangd > 0 and mangd <= self.__saldo:
            self.__saldo -= mangd

    def hamta_saldo(self):
        return self.__saldo
kort = Bankkort("123456", "Robert Rik", 5000)
print(kort.hamta_saldo())
kort.tillsatt_pengar(100)
print(kort.hamta_saldo())
kort.anvand_pengar(500)
print(kort.hamta_saldo())
# Detta lyckas inte, eftersom saldot inte är tillräckligt
kort.anvand_pengar(10000)
print(kort.hamta_saldo())
Exempelutskrift

5000 5100 4600 4600

Saldot kan inte ändras direkt eftersom attributet är privat, men vi har inkluderat metoderna tillsatt_pengar och ta_ut_pengar för att ändra värdet. Metoden returnera_saldo returnerar det värde som lagrats i saldo. Metoderna innehåller några rudimentära kontroller för att bibehålla objektets integritet: till exempel kan kortet inte överdras.

Loading

En kort notis om privata attribut, Python och objektorienterad programmering

Det finns sätt att kringgå understryknings __-notationen för att dölja attribut, som du kan stöta på om du söker efter material online. Inget Python-attribut är verkligen privat, och det är avsiktligt från skaparna av Pythons. Å andra sidan förväntas en Python-programmerare i allmänhet respektera de riktlinjer för synlighet som anges i klasser, och det krävs en särskild ansträngning för att komma runt dessa. I andra objektorienterade programmeringsspråk, till exempel Java, är privata variabler ofta verkligen dolda, och det är bäst om du tänker på privata Python-variabler som sådana också.

Getter och sättare

I objektorienterad programmering kallas metoder som är avsedda för att komma åt och ändra attribut vanligtvis för getter och sättare (eng: setters). Inte alla Python-programmerare använder termerna "getter" och "sättare", men konceptet med egenskaper som beskrivs nedan är mycket liknande, vilket är varför vi kommer att använda den allmänt accepterade objektorienterade programmeringsterminologin här.

Ovan skapade vi några offentliga metoder för att komma åt privata attribut, men det finns ett enklare, "pythoniskt" sätt att komma åt attribut. Låt oss ta en titt på en enkel klass som heter Planbok med ett enda privat attribut pengar:

class Planbok:
    def __init__(self):
        self.__pengar = 0

Vi kan tillägga getter och sättar metoder för att komma åt det privata attributet genom att använda @property dekoratorn:

class Planbok:
    def __init__(self):
        self.__pengar = 0

    # Gettermetod
    @property
    def pengar(self):
        return self.__pengar

    # Sättarmetod
    @pengar.setter
    def pengar(self, pengar):
        if pengar >= 0:
            self.__pengar = pengar

Först definierar vi en getter-metod som returnerar den summa pengar som för närvarande finns i plånboken. Sedan definierar vi en sättar-metod som sätter ett nytt värde för pengar-attributet och samtidigt ser till att det nya värdet inte är negativt.

De nya metoderna kan användas på följande sätt:

planbok = Planbok()
print(planbok.pengar)

planbok.pengar = 50
print(planbok.pengar)

planbok.pengar = -30
print(planbok.pengar)
Exempelutskrift

0 50 50

För klienten är det ingen skillnad att använda dessa nya metoder jämfört med att direkt komma åt ett attribut. Parenteser är inte nödvändiga, utan det är helt acceptabelt att ange planbok.pengar = 50, som om vi helt enkelt tilldelar ett värde till en variabel. Syftet var faktiskt att dölja (dvs. kapsla in) den interna implementeringen av attributet och samtidigt erbjuda ett enkelt sätt att komma åt och ändra den data som lagras i objektet.

I det föregående exemplet finns dock ett litet problem: klienten meddelas inte om att det inte går att ange ett negativt värde för attributet pengar. När ett värde som anges är uppenbart felaktigt är det vanligtvis en bra idé att skapa ett undantag och på så sätt informera klienten. I det här fallet bör undantaget förmodligen vara av typen ValueError för att visa att det angivna värdet var oacceptabelt.

Här har vi en förbättrad version av klassen, tillsammans med lite kod för att testa den:

class Planbok:
    def __init__(self):
        self.__pengar = 0

    # Gettermetod
    @property
    def pengar(self):
        return self.__pengar

    # Sättarmetod
    @pengar.setter
    def pengar(self, pengar):
        if pengar >= 0:
            self.__pengar = pengar
        else:
            raise ValueError("Mängden får inte vara under 0")
planbok.pengar = -30
print(planbok.pengar)
Exempelutskrift

ValueError: Mängden får inte vara under 0

OBS: getter-metoden, dvs @property-dekoratorn, måste introduceras före sättar-metoden i koden, annars blir det fel när klassen exekveras. Detta beror på att @property-dekoratorn definierar namnet på det "attribut" som erbjuds till klienten. Sättar-metoden, som läggs till med .setter, lägger helt enkelt till en ny funktionalitet till den.

Loading

Följande exempel har en klass med två privata attribut, tillsammans med getter och sättare för båda. Prova programmet med olika värden som skickas som argument:

class Spelare:
    def __init__(self, namn: str, spelnummer: int):
        self.__namn = namn
        self.__spelnummer = spelnummer

    @property
    def namn(self):
        return self.__namn

    @namn.setter
    def namn(self, namn: str):
        if namn != "":
            self.__namn = namn
        else:
            raise ValueError("Namnet kan inte vara tomt")

    @property
    def spelnummer(self):
        return self.__spelnummer

    @spelnummer.setter
    def spelnummer(self, spelnummer: int):
        if spelnummer > 0:
            self.__spelnummer = spelnummer
        else:
            raise ValueError("Spelnumret måste vara ett positivt heltal")
spelare = Spelare("Fredrik Fotare", 10)
print(spelare.namn)
print(spelare.spelnummer)

spelare.namn = "Fia Futis"
spelare.spelnummer = 11
print(spelare.namn)
print(spelare.spelnummer)
Exempelutskrift

Fredrik Fotare 10 Fia Futis 11

Som avslutning på detta avsnitt ska vi titta på en klass som modellerar en enkel dagbok. Alla attribut är privata, men de hanteras genom olika gränssnitt: dagbokens ägare har getter- och sättar-metoder, men dagboksposterna behandlas med "traditionella" metoder. I det här fallet är det vettigt att neka klienten all tillgång till dagbokens interna datastruktur. Endast de offentliga metoderna är direkt synliga för klienten.

Inkapsling säkerställer också att den interna implementeringen av klassen kan ändras när som helst, förutsatt att det offentliga gränssnittet förblir intakt. Klienten behöver inte veta eller bry sig om huruvida den interna datastrukturen är baserad på listor, ordlistor eller något helt annat.

class Dagbok:
    def __init__(self, agare: str):
        self.__agare = agare
        self.__inlagg = []

    @property
    def agare(self):
        return self.__agare

    @agare.setter
    def agare(self, agare):
        if agare != "":
            self.__agare = agare
        else:
            raise ValueError("Ägaren kan inte vara tom")

    def tillsatt_inlagg(self, inlagg: str):
        self.__inlagg.append(inlagg)

    def skriv_ut(self):
        print("Totalt", len(self.__inlagg), "inlägg")
        for inlagg in self.__inlagg:
            print("- " + inlagg)
dagbok = Dagbok("Peter")
dagbok.tillsatt_inlagg("Idag åt jag gröt")
dagbok.tillsatt_inlagg("Idag lärde jag mig objekt-orienterad programmering")
dagbok.tillsatt_inlagg("Idag lade jag mig tidigt")
dagbok.skriv_ut()
Exempelutskrift

Totalt 3 inlägg

  • Idag åt jag gröt
  • Idag lärde jag mig objekt-orienterad programmering
  • Idag lade jag mig tidigt
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.