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ówAny– dowolny typ
Generics:
TypeVar('T')– zmienna typowaGeneric[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