Vad är en generisk klass eller metod?
En generisk klass eller metod är en mall som fungerar med olika datatyper – utan att du behöver skriva om samma kod flera gånger.
Tänk på det som en receptbok där du inte skriver ”äpplen” i varje recept, utan säger ”frukt”. Då kan receptet funka för både äpplen, bananer och päron – beroende på vad du stoppar in.
I programmering kallar vi detta för generics – kod som fungerar för många olika typer, men som ändå behåller typsäkerhet.
Varför använda generiska klasser?
- Återanvändbar kod: Du slipper duplicera samma klass för olika datatyper.
- Typsäkerhet: Kodeditorn (t.ex. VS Code) kan hjälpa dig med rätt typ – även om du använder en mall.
- Renare kod: Mindre risk för fel, bättre struktur.
Vad behöver vi importera?
För att skapa generiska klasser/metoder behöver vi importera typer från Pythons typing-modul:
from typing import TypeVar, GenericTypeVaranvänds för att skapa en symbol för en okänd typ, t.ex.T.Genericgör klassen medveten om att den är generisk och vilka typvariabler som används.
Exempel: En generisk behållare
Vi skapar en klass Box som kan innehålla valfri typ av objekt.
from typing import TypeVar, Generic
T = TypeVar('T') # T är en platshållare för en datatyp
class Box(Generic[T]):
def __init__(self, content: T):
self.content = content
def get_content(self) -> T:
return self.contentAnvändning:
# En Box som håller en sträng
str_box = Box("hello")
print(str_box.get_content()) # -> hello
# En Box som håller ett heltal
int_box = Box(42)
print(int_box.get_content()) # -> 42Redan här ser vi fördelen: Vi slipper skriva en StringBox och en IntBox – en generisk klass räcker!
Jämföra typer i generiska klasser
När vi skapar generiska klasser kan det ibland vara intressant att veta vilken typ av data objektet innehåller – särskilt om vi vill jämföra det med ett annat objekt.
Vi kan använda funktionen type() för att få reda på vilken typ ett objekt eller ett attribut har.
Exempel – jämföra två Box-objekt
from typing import Generic, TypeVar
# Vi skapar en generisk typ T som kan ersättas med vilken datatyp som helst
T = TypeVar("T")
class Box(Generic[T]):
def __init__(self, content: T): # T betyder att vi förväntar oss innehåll av generisk typ
self.content = content
def is_same_type(self, other: 'Box') -> bool: # Jämför typ mellan två Box-objekt och returnerar True/False
return type(self.content) == type(other.content)
# Skapa olika boxar
box1 = Box("hej")
box2 = Box(42)
box3 = Box("test")
print(box1.is_same_type(box2)) # False, olika typer (str vs int)
print(box1.is_same_type(box3)) # True, båda är str
Förklaring av raderna:
def __init__(self, content: T):
Här använder vi T som en generisk typ. Det innebär att content kan vara vilken datatyp som helst (t.ex. int, str, bool), men att klassen kommer hantera den typen konsekvent för det aktuella objektet.
- Om du skapar
Box("hej"), kommer T varastr. - Om du skapar
Box(42), kommer T varaint.
T fungerar alltså som en mall för datatypen.
def is_same_type(self, other: 'Box') -> bool:
'Box'är en framåtreferens (forward reference). Eftersom klassenBoxännu inte är helt definierad vid tillfället när metoden tolkas, skriver vi'Box'som en sträng. Detta är vanligt i typannoteringar för att undvika fel.-> boolbetyder att funktionen kommer att returnera en boolean (antingenTrueellerFalse).
Om man använder Python 3.10 eller senare, kan man ofta skriva other: Box direkt utan citationstecken, eftersom språket hanterar detta bättre numera. Men strängvarianten funkar alltid och är bakåtkompatibel.
När använder man detta?
Det kan vara användbart om du vill kontrollera:
- att två objekt är av samma typ innan du jämför dem
- att vissa funktioner bara körs om typen stämmer
- eller vid validering i mer komplexa program.
Ett annat exempel: Generisk funktion
Vi kan också skapa generiska funktioner:
from typing import TypeVar
T = TypeVar("T")
def echo(item: T) -> T:
return item
Funktionen echo() fungerar för vilken typ som helst – och den returnerar samma typ som du skickade in.
print(echo("hej")) # str
print(echo(123)) # int
print(echo([1, 2, 3])) # list[int]När är det smart att använda generics?
- När du bygger datastrukturer (listor, stackar, träd, etc.)
- När du skriver verktygsklasser/funktioner som inte beror på en viss typ
- När du vill få typhjälp i IDE:n, men ändå ha flexibel kod
Varför ska man använda Generic i en klass när Python ändå kan hantera olika datatyper?
Scenario: En notifieringstjänst för användare
Tänk att du bygger ett system där du skickar notifieringar till användare. Ibland är det mejl, ibland är det sms, och ibland kanske det är ett popup-meddelande i en app.
Alternativ utan generics:
Du kan skapa en notifieringsklass som tar vilken typ som helst – men då tappar du all typkontroll.
class Notifier:
def __init__(self, message):
self.message = message
def send(self):
print(f"Sending: {self.message}")Du kan skicka in vad som helst, men du vet aldrig om det är rätt format, och IDE:n hjälper dig inte.
Med Generic – typtrygg notifiering
Vi skapar en generisk notifieringsklass som fungerar med olika typer av meddelanden, men är tydlig med vilken typ som används:
from typing import Generic, TypeVar
T = TypeVar("T")
class Notifier(Generic[T]):
def __init__(self, message: T):
self.message = message
def send(self):
print(f"Sending: {self.message}")Nu kan du skapa notifierare för olika datatyper, med tydlighet och kontroll:
email_notifier = Notifier[str]("Welcome to our service!")
email_notifier.send()
sms_notifier = Notifier[int](0712345678) # kanske en verifieringskod
sms_notifier.send()Sammanfattning varför
1. Tydlighet & förutsägbarhet för utvecklaren
Med Generic kan du tydligt visa vad en klass förväntas arbeta med, vilket gör det enklare för andra (eller framtida du) att förstå:
from typing import Generic, TypeVar
T = TypeVar("T")
class Notifier(Generic[T]):
def __init__(self, message: T):
self.message = messageNär någon skapar ett objekt med Notifier[str], så förstår man direkt: Den här instansen är tänkt att skicka strängar.
2. Få hjälp av editor och verktyg (typkontroll)
Med Generic fungerar verktyg som mypy och VS Code:s Intellisense mycket bättre. De varnar dig om du skickar in fel typ, vilket:
- Förebygger buggar
- Gör koden lättare att felsöka
- Underlättar för större team att hålla koll på vad som förväntas
Sammanfattning: Ska vi använda Generic?
| När det räcker utan | När Generic är bra |
|---|---|
| Små, enkla program | Större program med många objekt |
| När du inte bryr dig om typkontroll | När du vill ha tydlig, förutsägbar kod |
| Du bara skickar in 1–2 attribut utan logik | Du bygger dataklasser, verktyg, containers etc |
När är det användbart?
Här är fler verkliga fall där generics används i praktiken:
| Exempel | Beskrivning |
|---|---|
List[str], List[int] | Python-listor är generiska. Du kan ange vilken typ listan innehåller. |
Response[T] från API | En klass som läser svar från ett API och innehåller olika typer av data beroende på endpoint. |
Repository[T] i större appar | En datalager-klass som kan spara, hämta och uppdatera objekt av valfri typ (t.ex. User, Product, Order). |
Queue[T] eller Stack[T] | Datastrukturer som fungerar oavsett vilken datatyp som används. |