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
TypeVari 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
TypeVartillgä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 namnetT.- Resultatet sparas under Python-namnet
Ttill vänster. Tblir 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 typenT.-> Tbetyder 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
Tvisar att typerna ska höra ihop. -> boolbetyder att svaret blirTrueellerFalse.
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: intanger antalet upprepningar.list[T]betyder att resultatet innehåller samma typ somvalue.
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()
studentsblirlist[Student].teachersblirlist[Teacher].coursesblirlist[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
- Skriv
last(values)som returnerar sista värdet. - Skapa
Pairsom lagrar två värden av samma typ. - Skapa
Studentoch användRegister[Student]. - Fundera när generisk kod är tydligare än duplicerad kod.
Sammanfattning
Kom ihåg
- Generisk kod kan återanvändas med flera typer.
TypeVarskapar en typvariabel.Tvisar vilka typer som hör ihop.Generic[T]används för generiska klasser.- Generics ska göra koden tydligare, inte svårare.