Presentationer Slide 1/1

Kap 3.9

Generiska klasser och metoder

Generisk kod kan användas med flera datatyper utan att tappa tydlighet.

Mål

Efter lektionen ska du kunna...

  • förklara vad generisk kod betyder
  • använda TypeVar i enkla funktioner
  • skapa en enkel generisk klass med Generic
  • förstå när generisk kod gör koden tydligare

Idé

Samma logik, flera typer

Ibland gör koden exakt samma sak oavsett datatyp.

Då vill vi slippa skriva nästan samma funktion flera gånger.

Generiska typer beskriver vilka typer som hör ihop.

Problem

Duplicerad kod

def first_text(values: list[str]) -> str:
    return values[0]

def first_number(values: list[int]) -> int:
    return values[0]

Logiken är identisk.

Det enda som skiljer är typen.

Import

Varför importerar vi TypeVar?

from typing import TypeVar
  • typing är en inbyggd Python-modul med verktyg för typhintar.
  • TypeVar är ett verktyg i den modulen.
  • Importen gör namnet TypeVar tillgängligt i vår fil.

Importen skapar inte T. Den hämtar verktyget som kan skapa T.

Skapa typvariabeln

Vad gör T = TypeVar("T")?

T = TypeVar("T")
  • TypeVar("T") skapar en typvariabel med namnet T.
  • Resultatet sparas under Python-namnet T till vänster.
  • T blir en platshållare för en typ som bestäms när funktionen används.

T kan motsvara str i ett anrop och int i ett annat. Det lagrar inte vanliga värden som "hej" eller 10.

Generisk funktion

En funktion räcker

from typing import TypeVar

T = TypeVar("T")

def first(values: list[T]) -> T:
    return values[0]

Funktionen returnerar samma typ som listan innehåller.

Läs raden

Vad betyder list[T] -> T?

def first(values: list[T]) -> T:
    return values[0]
  • values är parameterns namn.
  • list[T] betyder en lista med element av typen T.
  • -> T betyder att ett värde av samma typ ska returneras.

list[str] ger returtypen str. list[int] ger returtypen int.

Typhintarna beskriver sambandet. De omvandlar inga värden.

values[0] kräver att listan inte är tom. Annars uppstår IndexError.

Anropa funktionen

T bestäms av sammanhanget

name = first(["Anna", "Bo", "Cia"])
score = first([10, 20, 30])

print(name)
print(score)
Anna
10

Första anropet ger str. Andra anropet ger int.

Flera parametrar

Vad ska funktionen göra?

contains_value() svarar på frågan:

Finns det här värdet i den här listan?

Samma funktion ska fungera för namn, heltal och andra typer.

Läs koden

Parametrarna kopplas samman

def contains_value(value: T, values: list[T]) -> bool:
    return value in values
  • value: T är värdet vi söker efter.
  • values: list[T] är listan vi söker i.
  • Samma T visar att typerna ska höra ihop.
  • -> bool betyder att svaret blir True eller False.

Sökningen

in gör själva arbetet

return value in values

in undersöker om value finns bland elementen i values.

print(contains_value("Anna", ["Anna", "Bo"]))
print(contains_value(3, [1, 2, 3]))
print(contains_value(8, [1, 2, 3]))
True
True
False

Varför TypeVar?

T beskriver ett samband

value: str
values: list[str]

En sträng söks i en lista med strängar.

value: int
values: list[int]

Ett heltal söks i en lista med heltal.

TypeVar hjälper läsaren, editorn och typkontrollen att se detta samband.

TypeVar söker inte i listan. Det gör in.

Tydligt exempel

Bevara typen till resultatet

def repeat(value: T, times: int) -> list[T]:
    return [value] * times
  • value: T är värdet som ska upprepas.
  • times: int anger antalet upprepningar.
  • list[T] betyder att resultatet innehåller samma typ som value.

Använd funktionen

Exakt returtyp i varje anrop

temperatures = repeat(18.5, 3)
names = repeat("Anna", 2)

print(temperatures)
print(names)
[18.5, 18.5, 18.5]
['Anna', 'Anna']

Första anropet ger list[float].

Andra anropet ger list[str].

Varför generics?

object tappar information

def repeat(value: object, times: int) -> list[object]:
    return [value] * times

temperatures = repeat(18.5, 3)

Editorn ser returtypen list[object].

Den vet inte längre att elementen är float.

Fördelen

TypeVar bevarar informationen

def repeat(value: T, times: int) -> list[T]:
    return [value] * times

temperatures = repeat(18.5, 3)

Editorn ser returtypen list[float].

Den kan föreslå float-metoder och varna vid olämplig användning.

En återanvändbar funktion behåller den exakta typen.

Välj rätt verktyg

Generics behövs inte alltid

  • Använd generics när samma återanvändbara kod ska fungera med flera typer och bevara ett tydligt typsamband.
  • Använd vanlig kod när lösningen är liten, lokal eller tydligare utan generics.
  • Generics ändrar inte algoritmen. De beskriver typerna och förbättrar editorstödet.

Fördjupning

Unionen anger tillåtna typer

mixed_values: list[int | str | float] = [10, "hej", 3.5]

int | str | float betyder att varje element får vara någon av dessa tre typer.

Alla typer som ska vara tillåtna måste skrivas i typhinten.

Men listan måste inte innehålla minst ett värde av varje typ.

also_valid: list[int | str | float] = [10, 20]

Fördjupning

Vad blir T?

mixed_values: list[int | str | float] = [10, "hej", 3.5]

first_value = first(mixed_values)
print(first_value)
10

T kan här motsvara int | str | float.

Returtypen beskriver alla tre möjligheterna, även om det första värdet just här är ett heltal.

Viktigt

Typhintar kör inte programmet

list[T], -> T och Register[Student] är typhintar.

De hjälper människor, editorer och typkontrollverktyg.

Python stoppar normalt inte alla typfel automatiskt vid körning.

Generic

Gör en hel klass generisk

from typing import Generic, TypeVar

Generic hämtas från modulen typing.

class Box(Generic[T]):

Generic[T] betyder att hela klassen använder typvariabeln T.

Då kan klassens attribut, metodparametrar och returtyper kopplas till samma typ.

Generic

Generisk klass

from typing import Generic, TypeVar

T = TypeVar("T")

class Box(Generic[T]):
    def __init__(self, content: T):
        self._content = content

    def get(self) -> T:
        return self._content

Box

Samma klass, olika typer

text_box: Box[str] = Box("Hej")
number_box: Box[int] = Box(42)

print(text_box.get())
print(number_box.get())
Hej
42

Box[str] kopplar innehållet och returtypen till str. Box[int] gör samma sak för int.

Register

Generisk klass för samlingar

class Register(Generic[T]):
    def __init__(self):
        self._items: list[T] = []

    def add(self, item: T) -> None:
        self._items.append(item)

    def all_items(self) -> list[T]:
        return self._items

Register

Ett skolsystem har flera slags objekt

class Student:
    def __init__(self, name: str):
        self.name = name

class Teacher:
    def __init__(self, name: str):
        self.name = name

class Course:
    def __init__(self, title: str):
        self.title = title

Skolsystem

Samma registerklass, tre typer

student_register: Register[Student] = Register()
teacher_register: Register[Teacher] = Register()
course_register: Register[Course] = Register()

Varje register använder samma kod för add() och all_items().

Men varje register behåller sin egen objekttyp.

Skolsystem

Lägg till objekt

student_register.add(Student("Anna"))
teacher_register.add(Teacher("Erik"))
course_register.add(Course("Programmering 2"))
  • Elevregistret är avsett för Student.
  • Lärarregistret är avsett för Teacher.
  • Kursregistret är avsett för Course.

Fördelen

Exakta returtyper

students = student_register.all_items()
teachers = teacher_register.all_items()
courses = course_register.all_items()
  • students blir list[Student].
  • teachers blir list[Teacher].
  • courses blir list[Course].

Registerkoden skrivs och testas en gång men återanvänds i hela skolsystemet.

När?

När passar generisk kod?

  • När samma logik ska fungera för flera datatyper.
  • När du vill undvika duplicerade funktioner.
  • När editorn ska kunna ge bättre typstöd.
  • När du bygger egna register, samlingar eller lagringsklasser.

När inte?

Håll det enkelt

  • När koden bara används på ett ställe.
  • När en vanlig funktion är tydligare.
  • När typmarkeringarna gör lösningen svårare att läsa.

Öva själv

Generics

  1. Skriv last(values) som returnerar sista värdet.
  2. Skapa Pair som lagrar två värden av samma typ.
  3. Skapa Student och använd Register[Student].
  4. Fundera när generisk kod är tydligare än duplicerad kod.

Sammanfattning

Kom ihåg

  • Generisk kod kan återanvändas med flera typer.
  • TypeVar skapar en typvariabel.
  • T visar vilka typer som hör ihop.
  • Generic[T] används för generiska klasser.
  • Generics ska göra koden tydligare, inte svårare.