Programmering nivå 2
Kap 3.9 – Generiska klasser och metoder
Skriv återanvändbar kod som kan fungera med flera olika datatyper utan att tappa tydlighet.
Mål med lektionen
När du har arbetat klart med denna lektion ska du:
- Förstå vad generisk kod betyder i Python.
- Kunna skriva enkla generiska funktioner och metoder med
TypeVar. - Kunna skapa en enkel generisk klass med
Generic. - Förstå när generiska klasser och metoder gör kod mer återanvändbar och testbar.
Så här lär du dig bäst
Börja med att jämföra två funktioner som gör samma sak för olika datatyper. Fundera sedan på vad som egentligen är gemensamt. Generiska typer är inte främst till för att göra koden svårare, utan för att beskriva tydligare vilka typer som hänger ihop.
Centrala begrepp
- Generisk kod: kod som kan användas med flera datatyper.
- Typvariabel: en symbolisk typ, ofta
T, som betyder "samma typ som används här". TypeVar: används för att skapa en typvariabel i Python.Generic: används när en klass ska vara generisk.- Typhint: en markering som hjälper läsaren och editorn förstå vilka typer koden arbetar med.
Varför behövs generiska typer?
Tänk att du vill skriva en funktion som hämtar första värdet ur en lista. Funktionen fungerar på samma sätt oavsett om listan innehåller tal, text eller objekt.
def first_text(values: list[str]) -> str:
return values[0]
def first_number(values: list[int]) -> int:
return values[0]
Det här ger duplicerad kod. Logiken är identisk. Det enda som skiljer är typen. Med en generisk funktion kan vi beskriva samma idé en gång.
Steg 1: importera TypeVar
from typing import TypeVar
TypeVar finns i Pythons inbyggda modul typing. Importen hämtar verktyget
och gör namnet TypeVar tillgängligt i filen. Importen skapar ännu ingen typvariabel.
Steg 2: skapa typvariabeln T
T = TypeVar("T")
TypeVar("T") skapar en typvariabel. Resultatet sparas under Python-namnet
T till vänster. Texten "T" är namnet som typverktyg använder för
typvariabeln. Man brukar ge båda samma namn.
T är en platshållare för en typ. Den kan motsvara str i ett anrop och
int i ett annat. Den lagrar inte vanliga värden som "hej" eller
10.
Steg 3: skriv den generiska funktionen
from typing import TypeVar
T = TypeVar("T")
def first(values: list[T]) -> T:
return values[0]
Läs funktionshuvudet del för del
valuesär parameterns namn.- Kolonet efter
valuesbörjar parameterns typhint. list[T]betyder en lista vars element har typen somTrepresenterar.-> Tbetyder att funktionen returnerar ett värde av samma typ.
Samma T kopplar ihop listans elementtyp och returtypen. Typhintarna beskriver sambandet;
de omvandlar inga värden.
Funktionen använder values[0] och förutsätter därför att listan innehåller minst ett
element. En tom lista skulle ge IndexError. TypeVar skyddar inte mot det;
den beskriver bara typernas samband.
Steg 4: anropa funktionen
name = first(["Anna", "Bo", "Cia"])
score = first([10, 20, 30])
print(name)
print(score)
Anna
10
I det första anropet motsvarar T typen str. I det andra motsvarar
T typen int.
Generisk funktion med flera parametrar
Funktionen contains_value() svarar på frågan: Finns det här värdet i den här
listan? Vi vill kunna använda samma funktion för exempelvis namn, heltal och andra typer
utan att skriva en särskild funktion för varje typ.
from typing import TypeVar
T = TypeVar("T")
def contains_value(value: T, values: list[T]) -> bool:
return value in values
print(contains_value("Anna", ["Anna", "Bo"]))
print(contains_value(3, [1, 2, 3]))
print(contains_value(8, [1, 2, 3]))
Vad gör koden?
value: Tär värdet som funktionen ska söka efter.values: list[T]är listan som funktionen ska söka i.- Samma
Tanvänds på båda platserna. Det visar att sökvärdet och listans element ska ha samma typ. -> boolvisar att svaret alltid ärTrueellerFalse.- Uttrycket
value in valuesundersöker om värdet finns i listan.
True
True
False
"Anna" finns i den första listan och 3 finns i den andra. Talet
8 finns inte i den sista listan, så det anropet ger False.
Varför används TypeVar här?
Utan TypeVar kan vi fortfarande skriva en funktion som körs, men typhintarna beskriver
inte lika tydligt att parametrarna måste höra ihop. T kopplar samman typerna:
- Skickar vi in en
strska den andra parametern vara enlist[str]. - Skickar vi in en
intska den andra parametern vara enlist[int].
Det hjälper läsaren, editorn och typkontrollverktyg att upptäcka tveksamma anrop, till exempel att
söka efter ett heltal i en lista som är avsedd för strängar. TypeVar utför däremot
inte själva sökningen och stoppar normalt inte felaktiga typer när Python-programmet körs. Det är
operatorn in som gör sökningen.
Tydligt exempel: bevara typen från indata till resultat
Funktionen repeat() upprepar ett värde och returnerar en lista. Samma funktion ska
fungera för många typer, men editorn ska fortfarande kunna veta exakt vad den returnerade listan
innehåller.
from typing import TypeVar
T = TypeVar("T")
def repeat(value: T, times: int) -> list[T]:
return [value] * times
temperatures = repeat(18.5, 3)
names = repeat("Anna", 2)
print(temperatures)
print(names)
[18.5, 18.5, 18.5]
['Anna', 'Anna']
value: Tbetyder att värdet kan ha olika typer i olika anrop.-> list[T]betyder att listan innehåller samma typ somvalue.repeat(18.5, 3)får returtypenlist[float].repeat("Anna", 2)får returtypenlist[str].
Den konkreta fördelen med TypeVar
Vi skulle kunna använda typen object, eftersom alla Pythonvärden är objekt:
def repeat(value: object, times: int) -> list[object]:
return [value] * times
temperatures = repeat(18.5, 3) # list[object]
Koden fungerar, men returtypen blir bara list[object]. Editorn har då tappat
informationen att listan faktiskt innehåller float.
Med TypeVar kopplas returtypen till typen på argumentet:
def repeat(value: T, times: int) -> list[T]:
return [value] * times
temperatures = repeat(18.5, 3) # list[float]
Nu vet editorn att elementen är float och kan föreslå float-metoder samt varna om koden
försöker använda elementen som en olämplig typ. Vi får alltså både en återanvändbar funktion och
en exakt returtyp.
Använd generisk kod när samma återanvändbara funktion eller klass ska bevara ett tydligt samband mellan flera typer. Använd vanlig kod när lösningen är liten, endast används på ett ställe eller blir lättare att förstå utan generics. Generics förändrar inte vad algoritmen gör; de gör typförhållandet tydligare och ger bättre stöd från editorn och typkontrollverktyg.
Fördjupning: listor med blandade typer
Det här avsnittet bygger vidare på grunderna ovan. En lista kan också vara avsedd att innehålla flera tillåtna typer.
mixed_values: list[int | str | float] = [10, "hej", 3.5]
first_value = first(mixed_values)
int | str | float är en union och betyder att varje element får vara
int, str eller float. Alla tillåtna typer ska skrivas i
typhinten. Typhinten kräver däremot inte att minst ett värde av varje typ finns i listan. Även
[10, 20] är därför tillåten av samma typhint.
Om listan senare även ska få innehålla bytes måste det anges i typhinten, exempelvis
list[int | str | float | bytes]. Utan en uttrycklig typhint kan en typkontroll ibland
härleda typer från värdena, men det är inte samma sak som att typer automatiskt läggs till i den
skrivna koden.
När den blandade listan skickas till first() kan T motsvara unionen
int | str | float. Returtypen blir då också int | str | float, även om det
första värdet i just detta exempel är heltalet 10.
Typhintar kör inte programmet åt dig
list[T], -> T och andra typhintar är främst hjälp för människor,
editorer och typkontrollverktyg. Python stoppar normalt inte automatiskt ett anrop med fel typ
när programmet körs. Koden behöver fortfarande testas och vid behov kontrollera värden själv.
Generisk klass
En generisk klass passar när klassen ska kunna lagra eller hantera olika typer av objekt, men med samma logik. Här är en enkel låda som kan innehålla vad som helst.
Generic importeras från typing. När klassen ärver från
Generic[T] talar vi om att hela klassen använder typvariabeln T. Då kan
attribut, parametrar och returtyper i klassens metoder kopplas till samma typ.
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
text_box: Box[str] = Box("Hej")
number_box: Box[int] = Box(42)
print(text_box.get())
print(number_box.get())
Box[str] betyder en låda för text. Därför är både parametern content,
attributet _content och returvärdet från get() kopplade till
str. Box[int] skapar motsvarande samband för heltal. Python tvingar
normalt inte detta vid körning, men typhintarna hjälper programmeraren, editorn och testningen.
Exempel: register i ett skolsystem
Ett skolsystem kan behöva separata register för elever, lärare och kurser. Alla registren behöver
samma grundfunktioner, till exempel att lägga till objekt och hämta alla objekt. I stället för att
skriva tre nästan identiska registerklasser kan vi återanvända Register[T].
from typing import Generic, TypeVar
T = TypeVar("T")
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
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
student_register: Register[Student] = Register()
teacher_register: Register[Teacher] = Register()
course_register: Register[Course] = Register()
student_register.add(Student("Anna"))
teacher_register.add(Teacher("Erik"))
course_register.add(Course("Programmering 2"))
first_student = student_register.all_items()[0]
first_teacher = teacher_register.all_items()[0]
first_course = course_register.all_items()[0]
print(first_student.name)
print(first_teacher.name)
print(first_course.title)
Anna
Erik
Programmering 2
Vad vinner vi på detta?
Registerbehöver bara skrivas och testas en gång.student_register.add()är avsedd förStudent-objekt.student_register.all_items()får returtypenlist[Student].- Lärarregistret och kursregistret återanvänder samma logik men behåller sina egna typer.
Det här är användbart i ett verkligt skolsystem eftersom programmet hanterar flera slags objekt med samma registerfunktioner. Om registren hörde till helt separata program skulle en gemensam generisk klass inte nödvändigtvis vara motiverad.
Interaktiv övning: generiska typer
Välj exempel och kör koden. Lägg märke till att T används för att visa vilka typer
som hör ihop: listans innehåll, värdet som skickas in och värdet som returneras.
Programmering 2-koppling: generiska typer ändrar inte själva körningen av Python-koden här. De gör däremot sambandet mellan typer tydligare för programmeraren och editorn.
När ska du använda generiska klasser och metoder?
- När samma logik ska fungera för flera datatyper.
- När du vill undvika duplicerade funktioner som bara skiljer sig i typ.
- När du vill att editorn ska kunna ge bättre stöd och varna för fel typ.
- När du bygger egna samlingar, register, köer eller lagringsklasser.
När behövs det inte?
- När koden bara används på ett enda ställe.
- När en vanlig funktion eller klass är tydligare.
- När generiska typer gör lösningen svårare att läsa än problemet kräver.
Öva själv
- Skriv en generisk funktion
last(values)som returnerar sista värdet i en lista. - Skapa en generisk klass
Pairsom lagrar två värden av samma typ. - Skapa en klass
Studentoch användRegister[Student]för att lagra elever. - Fundera: när blir generisk kod tydligare än duplicerad kod?
När du har försökt själv kan du ladda ner ett lösningsförslag: kap3-9-generics-losningsforslag.py
Sammanfattning
- Generisk kod kan återanvändas med flera datatyper.
TypeVarbeskriver en typ som återkommer på flera platser.Generic[T]används när en klass ska fungera med valfri men sammanhängande typ.- Generiska klasser och metoder kan göra kod mer läsbar, testbar och återanvändbar.