21. Python – Szablony (wzorce) klas – Generics


Szybki quiz – przypomnienie poprzedniej lekcji

Pytanie 1. Jaka jest złożoność czasowa tej funkcji?

def f(lista):
    for a in lista:
        for b in lista:
            print(a, b)
Odpowiedź

Odpowiedź: O(n²). Dwie zagnieżdżone pętle, każda wykonuje się n razy, więc w sumie n · n operacji.

Pytanie 2. Co będzie w liście dane po pierwszym przejściu zewnętrznej pętli bubble_sort dla dane = [3, 1, 2]?

Odpowiedź

Odpowiedź: [1, 2, 3]. Najpierw zamiana 3 i 1 daje [1, 3, 2], potem zamiana 3 i 2 daje [1, 2, 3]. Największy element „wypłynął” na koniec.

Pytanie 3. Co zwróci binary_search(dane, 10) z lekcji i dlaczego wynik jest błędny?

dane = [40, 10, 30, 20, 50]
print(binary_search(dane, 10))
Odpowiedź

Odpowiedź: -1, mimo że 10 jest w liście (indeks 1). Przeszukiwanie binarne wymaga posortowanej listy. Środkowy element to 30, więc algorytm szuka w lewej połowie, ale po porównaniu z 40 odrzuca także indeks 1.

Pytanie 4. Co wyświetli ten kod?

a = [3, 1, 2]
b = a.sort()
print(b)
print(a)
Odpowiedź

Odpowiedź: None i [1, 2, 3]. Metoda sort() sortuje listę w miejscu (in-place) i niczego nie zwraca. Gdybyś chciał nową listę, użyj sorted(a).

Pytanie 5. Masz 1 000 000 rekordów posortowanych po id i wykonujesz wiele wyszukiwań. Którego algorytmu użyjesz: linear_search czy binary_search? Ile porównań wykona w najgorszym przypadku?

Odpowiedź

Odpowiedź: binary_search. Złożoność to O(log n), czyli ok. 20 porównań. Wyszukiwanie liniowe (O(n)) potrzebowałoby do miliona porównań.

Pytanie 6. Który z algorytmów sortowania z lekcji ma zawsze złożoność O(n log n) i jest stabilny? Jaką cenę płaci za tę gwarancję?

Odpowiedź

Odpowiedź: Merge Sort. Cena to dodatkowa pamięć O(n).


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

PODSUMOWANIE

  • Generics pozwalają napisać jedną funkcję lub klasę, która działa z różnymi typami i nadal „wie”, jaki typ przechowuje lub zwraca.
  • Typy w Pythonie to adnotacje (x: int, -> str). Interpreter ich nie sprawdza w trakcie działania programu. Ostrzega o nich edytor (IDE) lub narzędzie do sprawdzania typów. Program z błędnym typem w adnotacji może się wykonać bez błędu.
  • Wszystko pochodzi z modułu typing. Importujesz tylko to, czego używasz.
  • Typowe adnotacje: List[int], Dict[str, int], Tuple[str, int], Optional[T], Union[T, U], Any.
  • Optional[T] oznacza „T albo None” (to samo co Union[T, None]). Metody, które mogą nic nie zwrócić (np. pop() na pustym stosie), zwracają Optional[T].
  • T = TypeVar('T') tworzy zmienną typową, czyli zastępnik typu. Ten sam T w jednej funkcji lub klasie oznacza ten sam typ.
  • Klasa ogólna: class Nazwa(Generic[T]). Przy tworzeniu obiektu podajesz konkretny typ, np. Pudelko[int].
  • Dwa typy w jednej klasie wymagają dwóch zmiennych typowych: Generic[T, U].
  • TypeVar('Liczba', int, float) ogranicza dozwolone typy.
  • Callable[[int, int], int] to typ funkcji: w [] typy argumentów, na końcu typ wyniku.
  • Protocol opisuje, jakie metody obiekt musi mieć. Klasa nie dziedziczy po protokole, wystarczy że ma te metody.
  • Struktury z lekcji (Stos, Kolejka, ListaPowiazana, Repository) to zwykłe klasy, w których typ elementu jest parametrem T.
  • Typowe błędy:
    • użycie T bez T = TypeVar('T'),
    • brak importu z typing,
    • przekonanie, że Python zgłosi błąd przy złym typie,
    • zapomnienie o obsłudze None po pop(), peek(), dequeue() i pobierz().

Minimum, które powinno wejść w pamięć

Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:

  1. Adnotacje typów nie są sprawdzane przez interpreter. Pomagają programiście i edytorowi, ale nie blokują uruchomienia kodu.
  2. Szablon klasy wymaga trzech elementów: from typing import TypeVar, Generic, potem T = TypeVar('T'), potem class Nazwa(Generic[T]):.
  3. T to „jakikolwiek typ, ale ten sam w całej klasie”. Przy tworzeniu obiektu podajesz konkretny typ: Stos[int], Pudelko[str].
  4. Dwa parametry typowe oznaczają Generic[T, U].
  5. Optional[T] to „T albo None”. Jeśli metoda zwraca Optional, sprawdź wynik na None przed użyciem.
  6. Stos = LIFO (push, pop, peek). Kolejka = FIFO (enqueue, dequeue).
  7. Callable[[typy argumentów], typ wyniku] opisuje funkcję przekazywaną jako argument.
  8. TypeVar('X', int, float) ogranicza typ do wymienionych.

Ściągawka – najważniejsze polecenia i składnia

Import:

from typing import List, Dict, Tuple, Optional, Union, Any
from typing import TypeVar, Generic, Callable, Protocol

Adnotacje:

ZapisZnaczenie
x: intzmienna typu int
def f(a: int) -> str:argument int, wynik str
-> Nonefunkcja nic nie zwraca
List[int]lista liczb całkowitych
Dict[str, int]słownik: klucze str, wartości int
Tuple[str, int]krotka: str i int
Optional[str]str albo None
Union[int, str]int albo str
Anydowolny typ
Callable[[int, int], int]funkcja (int, int) → int

Funkcja generyczna:

T = TypeVar('T')

def pierwszy_element(lista: List[T]) -> T:
    return lista[0]

Klasa generyczna (jeden typ):

class Pudelko(Generic[T]):
    def __init__(self, zawartosc: T):
        self.zawartosc = zawartosc

p: Pudelko[int] = Pudelko(42)

Klasa generyczna (dwa typy):

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

class Para(Generic[T, U]):
    def __init__(self, pierwszy: T, drugi: U): ...

Ograniczony TypeVar:

Liczba = TypeVar('Liczba', int, float)

Protocol:

class Drawable(Protocol):
    def draw(self) -> str: ...

Metody struktur z lekcji:

StrukturaDodawaniePobieraniePodglądPusta?
Stos (LIFO)push(x)pop()peek()jest_pusty()
Kolejka (FIFO)enqueue(x)dequeue()–jest_pusta()
Repositorydodaj(obj) → idpobierz(id)wszystkie()usun(id) → bool

Kolejność w pliku: import → T = TypeVar('T') → class ...(Generic[T]).

Quiz – sprawdź się

Pytanie 1. (teoria) Co daje użycie Generic[T] zamiast pisania osobnej klasy dla każdego typu?

  • a) Interpreter Pythona zgłasza błąd, gdy do Pudelko[int] trafi tekst.
  • b) Jedna klasa działa z różnymi typami, a edytor może podpowiadać i ostrzegać o typach.
  • c) Klasa działa szybciej.
  • d) Klasa może przechowywać tylko liczby.

Pytanie 2. (co zostanie wyświetlone)

T = TypeVar('T')

def pierwszy_element(lista: List[T]) -> T:
    return lista[0]

print(pierwszy_element(["a", "b", "c"]))
print(pierwszy_element([10, 20]))

Pytanie 3. (co stanie się po wykonaniu) Co wyświetli program? Czy Python zgłosi błąd?

pudelko: Pudelko[int] = Pudelko(42)
pudelko.ustaw("tekst")
print(pudelko.pobierz())

Pytanie 4. (znajdź błąd) Dlaczego ten kod się nie uruchomi? Popraw go.

from typing import Generic

class Pudelko(Generic[T]):
    def __init__(self, zawartosc: T):
        self.zawartosc = zawartosc

Pytanie 5. (co zostanie wyświetlone) Użyj klasy Stos z lekcji.

stos: Stos[int] = Stos()
stos.push(1)
stos.push(2)
stos.push(3)
print(stos.pop())
print(stos.peek())
print(stos.rozmiar())

pusty: Stos[int] = Stos()
print(pusty.pop())

Pytanie 6. (przewidywanie działania) Użyj klasy Kolejka z lekcji. Co zostanie wyświetlone?

k: Kolejka[str] = Kolejka()
k.enqueue("A")
k.enqueue("B")
k.enqueue("C")
print(k.dequeue())
print(k.dequeue())
print(k)

Pytanie 7. (uzupełnij kod) Wstaw brakujące elementy zamiast ___.

from typing import ___, ___

T = ___('T')
U = ___('U')

class Para(___[T, U]):
    def __init__(self, pierwszy: T, drugi: U):
        self.pierwszy = pierwszy
        self.drugi = drugi

Pytanie 8. (ograniczony TypeVar) Dana jest funkcja:

Liczba = TypeVar('Liczba', int, float)

def suma(a: Liczba, b: Liczba) -> Liczba:
    return a + b

a) Które wywołania są zgodne z adnotacją: suma(2, 3), suma(2.5, 1.5), suma("a", "b")?
b) Co zrobi interpreter Pythona po uruchomieniu print(suma("a", "b"))?

Pytanie 9. (przewidywanie działania) Użyj klasy Repository z lekcji.

repo: Repository[str] = Repository()
a = repo.dodaj("x")
b = repo.dodaj("y")
repo.usun(a)
print(repo.pobierz(a))
print(repo.wszystkie())
print(b)

Pytanie 10. (krótkie zadanie praktyczne) Napisz generyczną klasę Magazyn, która:

  • przechowuje elementy dowolnego typu T w liście,
  • ma metodę dodaj(element),
  • ma metodę ile(), zwracającą liczbę elementów.

Utwórz Magazyn[str], dodaj dwa teksty i wyświetl ile().

Odpowiedzi

1. Poprawna jest odpowiedź b. Odpowiedź a jest błędna, bo interpreter nie sprawdza adnotacji typów. Odpowiedzi c i d są nieprawdziwe.

2. Wyświetli:

a
10

Funkcja zwraca pierwszy element listy, a T przyjmuje typ elementów (tu str, potem int).

3. Wyświetli tekst. Python nie zgłosi błędu, bo adnotacje nie są sprawdzane w czasie działania. Ostrzeżenie pokaże edytor lub narzędzie do sprawdzania typów, ale sam program się wykona.

4. Zmienna T nie została zdefiniowana (NameError: name 'T' is not defined). Poprawnie:

from typing import TypeVar, Generic

T = TypeVar('T')

class Pudelko(Generic[T]):
    def __init__(self, zawartosc: T):
        self.zawartosc = zawartosc

5. Wyświetli:

3
2
2
None

pop() zdejmuje ze szczytu (3). peek() pokazuje nowy szczyt (2) bez usuwania. Zostały 2 elementy. pop() na pustym stosie zwraca None, dlatego metoda ma typ Optional[T].

6. Wyświetli:

A
B
Kolejka(['C'])

Kolejka działa w zasadzie FIFO: pierwszy dodany element wychodzi pierwszy.

7.

from typing import TypeVar, Generic

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

class Para(Generic[T, U]):
    ...

8.
a) Zgodne z adnotacją są suma(2, 3) i suma(2.5, 1.5). Wywołanie suma("a", "b") jest niezgodne, bo str nie należy do dozwolonych typów, więc ostrzeże o tym edytor lub narzędzie do sprawdzania typów.
b) Interpreter wykona kod bez błędu i wyświetli ab, bo operator + działa na tekstach. Ograniczenie typów jest informacją dla programisty i narzędzi, a nie zabezpieczeniem w czasie działania.

9. Wyświetli:

None
['y']
2

Obiekt "x" dostał id 1 i został usunięty, więc pobierz(1) zwraca None (metoda .get() słownika). W repozytorium został tylko "y" z id 2.

10. Przykładowe rozwiązanie:

from typing import TypeVar, Generic, List

T = TypeVar('T')

class Magazyn(Generic[T]):
    def __init__(self):
        self._elementy: List[T] = []

    def dodaj(self, element: T) -> None:
        self._elementy.append(element)

    def ile(self) -> int:
        return len(self._elementy)

m: Magazyn[str] = Magazyn()
m.dodaj("pierwszy")
m.dodaj("drugi")
print(m.ile())  # 2

Elementy oceny: poprawny import i TypeVar, klasa dziedziczy po Generic[T], pole z listą utworzone w __init__, poprawne dodaj i ile, wynik 2.