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ó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
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 „TalboNone” (to samo coUnion[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 samTw 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.Protocolopisuje, 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
TbezT = TypeVar('T'), - brak importu z
typing, - przekonanie, że Python zgłosi błąd przy złym typie,
- zapomnienie o obsłudze
Nonepopop(),peek(),dequeue()ipobierz().
- użycie
Minimum, które powinno wejść w pamięć
Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:
- Adnotacje typów nie są sprawdzane przez interpreter. Pomagają programiście i edytorowi, ale nie blokują uruchomienia kodu.
- Szablon klasy wymaga trzech elementów:
from typing import TypeVar, Generic, potemT = TypeVar('T'), potemclass Nazwa(Generic[T]):. Tto „jakikolwiek typ, ale ten sam w całej klasie”. Przy tworzeniu obiektu podajesz konkretny typ:Stos[int],Pudelko[str].- Dwa parametry typowe oznaczają
Generic[T, U]. Optional[T]to „TalboNone”. Jeśli metoda zwracaOptional, sprawdź wynik naNoneprzed użyciem.- Stos = LIFO (
push,pop,peek). Kolejka = FIFO (enqueue,dequeue). Callable[[typy argumentów], typ wyniku]opisuje funkcję przekazywaną jako argument.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:
| Zapis | Znaczenie |
|---|---|
x: int | zmienna typu int |
def f(a: int) -> str: | argument int, wynik str |
-> None | funkcja 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 |
Any | dowolny 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:
| Struktura | Dodawanie | Pobieranie | Podgląd | Pusta? |
|---|---|---|---|---|
Stos (LIFO) | push(x) | pop() | peek() | jest_pusty() |
Kolejka (FIFO) | enqueue(x) | dequeue() | – | jest_pusta() |
Repository | dodaj(obj) → id | pobierz(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
Tw 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.