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, Generic
  • TypeVar används för att skapa en symbol för en okänd typ, t.ex. T.
  • Generic gö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.content

Anvä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())  # -> 42

Redan 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 vara str.
  • Om du skapar Box(42), kommer T vara int.

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 klassen Box ä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.
  • -> bool betyder att funktionen kommer att returnera en boolean (antingen True eller False).

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 = message

Nä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 utanNär Generic är bra
Små, enkla programStörre program med många objekt
När du inte bryr dig om typkontrollNär du vill ha tydlig, förutsägbar kod
Du bara skickar in 1–2 attribut utan logikDu bygger dataklasser, verktyg, containers etc

När är det användbart?

Här är fler verkliga fall där generics används i praktiken:

ExempelBeskrivning
List[str], List[int]Python-listor är generiska. Du kan ange vilken typ listan innehåller.
Response[T] från APIEn klass som läser svar från ett API och innehåller olika typer av data beroende på endpoint.
Repository[T] i större apparEn 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.