21. Python – Szablony (wzorce) klas – Generics


Generics (Typy ogólne) to mechanizm pozwalający na tworzenie klas, funkcji i struktur danych działających z różnymi typami, zachowując bezpieczeństwo typów.

Dlaczego Generics?

  • Wielokrotne użycie kodu
  • Bezpieczeństwo typów
  • Czytelność kodu
  • Lepsze wsparcie IDE (autouzupełnianie)

W Pythonie: Moduł typing (Python 3.5+)

Moduł typing

from typing import List, Dict, Tuple, Optional, Union, Any

# Proste typy
imie: str = "Jan"
wiek: int = 25
cena: float = 19.99
aktywny: bool = True

# Kolekcje
liczby: List[int] = [1, 2, 3, 4]
slownik: Dict[str, int] = {"Jan": 25, "Anna": 30}
para: Tuple[str, int] = ("Jan", 25)

# Optional - może być None
imie_opcjonalne: Optional[str] = None  # To samo co: Union[str, None]

# Union - jeden z typów
identyfikator: Union[int, str] = 123  # Może być int lub str

# Any - dowolny typ
cokolwiek: Any = "może być wszystko"

Funkcje z typami

from typing import List

def suma_liczb(liczby: List[int]) -> int:
    """
    Sumuje listę liczb.
    
    Args:
        liczby: Lista liczb całkowitych
    
    Returns:
        Suma wszystkich liczb
    """
    return sum(liczby)

# Użycie
wynik: int = suma_liczb([1, 2, 3, 4, 5])
print(wynik)  # 15

# IDE podpowie błąd
# suma_liczb(["a", "b"])  # Błąd typu!

TypeVar – zmienne typowe

TypeVar pozwala stworzyć „placeholder” dla typu.

from typing import TypeVar, List

# Stwórz zmienną typową
T = TypeVar('T')

def pierwszy_element(lista: List[T]) -> T:
    """
    Zwraca pierwszy element listy.
    Typ zwracany jest taki sam jak typ elementów.
    """
    return lista[0]

# Użycie
liczby = [1, 2, 3]
wynik1 = pierwszy_element(liczby)  # wynik1: int

teksty = ["a", "b", "c"]
wynik2 = pierwszy_element(teksty)  # wynik2: str

Generic – klasa ogólna

from typing import TypeVar, Generic

T = TypeVar('T')

class Pudelko(Generic[T]):
    """
    Generyczna klasa pudełka - może przechowywać dowolny typ.
    """
    def __init__(self, zawartosc: T):
        self.zawartosc = zawartosc
    
    def pobierz(self) -> T:
        return self.zawartosc
    
    def ustaw(self, nowa_zawartosc: T) -> None:
        self.zawartosc = nowa_zawartosc

# Użycie z int
pudelko_int: Pudelko[int] = Pudelko(42)
liczba = pudelko_int.pobierz()  # liczba: int

# Użycie z str
pudelko_str: Pudelko[str] = Pudelko("Hello")
tekst = pudelko_str.pobierz()  # tekst: str

# IDE ostrzeże o błędzie
# pudelko_int.ustaw("tekst")  # Błąd - oczekiwano int!

Para – generic z dwoma typami

from typing import TypeVar, Generic

T = TypeVar('T')
U = TypeVar('U')

class Para(Generic[T, U]):
    """Generyczna para dwóch wartości."""
    
    def __init__(self, pierwszy: T, drugi: U):
        self.pierwszy = pierwszy
        self.drugi = drugi
    
    def pobierz_pierwszy(self) -> T:
        return self.pierwszy
    
    def pobierz_drugi(self) -> U:
        return self.drugi
    
    def __str__(self) -> str:
        return f"Para({self.pierwszy}, {self.drugi})"

# Użycie
para1: Para[str, int] = Para("Jan", 25)
print(para1)  # Para(Jan, 25)

para2: Para[int, str] = Para(1, "jeden")
print(para2)  # Para(1, jeden)

Stos (Stack) – praktyczny przykład

from typing import TypeVar, Generic, List, Optional

T = TypeVar('T')

class Stos(Generic[T]):
    """
    Generyczny stos (LIFO - Last In First Out).
    """
    def __init__(self):
        self._elementy: List[T] = []
    
    def push(self, element: T) -> None:
        """Dodaj element na szczyt stosu."""
        self._elementy.append(element)
    
    def pop(self) -> Optional[T]:
        """Usuń i zwróć element ze szczytu."""
        if self.jest_pusty():
            return None
        return self._elementy.pop()
    
    def peek(self) -> Optional[T]:
        """Sprawdź szczyt bez usuwania."""
        if self.jest_pusty():
            return None
        return self._elementy[-1]
    
    def jest_pusty(self) -> bool:
        """Czy stos jest pusty?"""
        return len(self._elementy) == 0
    
    def rozmiar(self) -> int:
        """Liczba elementów."""
        return len(self._elementy)
    
    def __str__(self) -> str:
        return f"Stos({self._elementy})"

# Użycie
stos_int: Stos[int] = Stos()
stos_int.push(1)
stos_int.push(2)
stos_int.push(3)
print(stos_int)  # Stos([1, 2, 3])
print(stos_int.pop())  # 3

stos_str: Stos[str] = Stos()
stos_str.push("Python")
stos_str.push("Java")
print(stos_str)  # Stos(['Python', 'Java'])

Kolejka (Queue) – przykład

from typing import TypeVar, Generic, List, Optional

T = TypeVar('T')

class Kolejka(Generic[T]):
    """
    Generyczna kolejka (FIFO - First In First Out).
    """
    def __init__(self):
        self._elementy: List[T] = []
    
    def enqueue(self, element: T) -> None:
        """Dodaj element na koniec kolejki."""
        self._elementy.append(element)
    
    def dequeue(self) -> Optional[T]:
        """Usuń i zwróć element z początku."""
        if self.jest_pusta():
            return None
        return self._elementy.pop(0)
    
    def jest_pusta(self) -> bool:
        return len(self._elementy) == 0
    
    def rozmiar(self) -> int:
        return len(self._elementy)
    
    def __str__(self) -> str:
        return f"Kolejka({self._elementy})"

# Użycie
kolejka: Kolejka[str] = Kolejka()
kolejka.enqueue("Pierwszy")
kolejka.enqueue("Drugi")
kolejka.enqueue("Trzeci")
print(kolejka)  # Kolejka(['Pierwszy', 'Drugi', 'Trzeci'])
print(kolejka.dequeue())  # Pierwszy

Linked List – lista powiązana

from typing import TypeVar, Generic, Optional

T = TypeVar('T')

class Wezel(Generic[T]):
    """Węzeł listy powiązanej."""
    def __init__(self, dane: T):
        self.dane = dane
        self.nastepny: Optional[Wezel[T]] = None

class ListaPowiazana(Generic[T]):
    """Generyczna lista powiązana."""
    
    def __init__(self):
        self.glowa: Optional[Wezel[T]] = None
    
    def dodaj_na_poczatek(self, dane: T) -> None:
        """Dodaj element na początek."""
        nowy = Wezel(dane)
        nowy.nastepny = self.glowa
        self.glowa = nowy
    
    def dodaj_na_koniec(self, dane: T) -> None:
        """Dodaj element na koniec."""
        nowy = Wezel(dane)
        
        if self.glowa is None:
            self.glowa = nowy
            return
        
        aktualny = self.glowa
        while aktualny.nastepny is not None:
            aktualny = aktualny.nastepny
        aktualny.nastepny = nowy
    
    def wyswietl(self) -> None:
        """Wyświetl wszystkie elementy."""
        aktualny = self.glowa
        elementy = []
        while aktualny is not None:
            elementy.append(str(aktualny.dane))
            aktualny = aktualny.nastepny
        print(" -> ".join(elementy))

# Użycie
lista: ListaPowiazana[int] = ListaPowiazana()
lista.dodaj_na_koniec(1)
lista.dodaj_na_koniec(2)
lista.dodaj_na_koniec(3)
lista.wyswietl()  # 1 -> 2 -> 3

Ograniczenia typów (Bounded TypeVar)

from typing import TypeVar

# Ograniczenie do typów numerycznych
Liczba = TypeVar('Liczba', int, float)

def suma(a: Liczba, b: Liczba) -> Liczba:
    """Sumuje dwie liczby (int lub float)."""
    return a + b

print(suma(5, 3))      # 8 (int)
print(suma(5.5, 3.2))  # 8.7 (float)
# suma("a", "b")  # Błąd - string nie jest dozwolony!

Protocol – duck typing z typami

from typing import Protocol

class Drawable(Protocol):
    """Protokół dla obiektów które można narysować."""
    def draw(self) -> str:
        ...

class Circle:
    def draw(self) -> str:
        return "Rysujęokrąg"

class Square:
    def draw(self) -> str:
        return "Rysuję kwadrat"

def render(shape: Drawable) -> None:
    """Rysuje kształt."""
    print(shape.draw())

# Użycie - Circle i Square automatycznie spełniają Protocol
circle = Circle()
square = Square()

render(circle)  # Rysuję okrąg
render(square)  # Rysuję kwadrat

Callable – typ funkcji

from typing import Callable

def wykonaj_operacje(a: int, b: int, operacja: Callable[[int, int], int]) -> int:
    """
    Wykonuje operację na dwóch liczbach.
    
    operacja: funkcja przyjmująca dwa int i zwracająca int
    """
    return operacja(a, b)

def dodaj(x: int, y: int) -> int:
    return x + y

def pomnoz(x: int, y: int) -> int:
    return x * y

print(wykonaj_operacje(5, 3, dodaj))    # 8
print(wykonaj_operacje(5, 3, pomnoz))   # 15

Praktyczny przykład – Repository

from typing import TypeVar, Generic, List, Optional, Dict

T = TypeVar('T')

class Repository(Generic[T]):
    """
    Generyczne repozytorium do przechowywania obiektów.
    """
    def __init__(self):
        self._dane: Dict[int, T] = {}
        self._next_id: int = 1
    
    def dodaj(self, obiekt: T) -> int:
        """Dodaje obiekt i zwraca jego ID."""
        id_obiektu = self._next_id
        self._dane[id_obiektu] = obiekt
        self._next_id += 1
        return id_obiektu
    
    def pobierz(self, id_obiektu: int) -> Optional[T]:
        """Pobiera obiekt po ID."""
        return self._dane.get(id_obiektu)
    
    def wszystkie(self) -> List[T]:
        """Zwraca wszystkie obiekty."""
        return list(self._dane.values())
    
    def usun(self, id_obiektu: int) -> bool:
        """Usuwa obiekt po ID."""
        if id_obiektu in self._dane:
            del self._dane[id_obiektu]
            return True
        return False

# Użycie z różnymi typami
class Uzytkownik:
    def __init__(self, imie: str):
        self.imie = imie
    
    def __str__(self):
        return f"Uzytkownik({self.imie})"

repo_uzytkownikow: Repository[Uzytkownik] = Repository()
id1 = repo_uzytkownikow.dodaj(Uzytkownik("Jan"))
id2 = repo_uzytkownikow.dodaj(Uzytkownik("Anna"))

print(repo_uzytkownikow.pobierz(id1))  # Uzytkownik(Jan)
print(repo_uzytkownikow.wszystkie())   # [Uzytkownik(Jan), Uzytkownik(Anna)]

Podsumowanie

Typing:

  • List[T]Dict[K, V]Tuple[T, ...]
  • Optional[T] = Union[T, None]
  • Union[T, U] – jeden z typów
  • Any – dowolny typ

Generics:

  • TypeVar('T') – zmienna typowa
  • Generic[T] – klasa ogólna
  • Wielokrotne użycie kodu
  • Bezpieczeństwo typów

Przykłady:

  • Pudełko, Para
  • Stos, Kolejka
  • Lista powiązana
  • Repository

Korzyści:

  • IDE podpowiada typy
  • Wykrywanie błędów przed uruchomieniem
  • Lepszaczytelność kodu
  • Dokumentacja w kodzie