{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "k00",
   "metadata": {},
   "source": [
    "# Programowanie obiektowe w Pythonie\n",
    "\n",
    "*Wersja notatnika: 2026-10-08 · na bazie wersji z 2025-10-11 · przygotowanie: dr inż. Szczepan Górtowski*\n",
    "\n",
    "Wprowadzenie do **programowania obiektowego (OOP)**: klasy, obiekty, atrybuty, metody, konstruktor `__init__`,\n",
    "metody specjalne `__str__` i `__repr__`, atrybuty klasy, metody statyczne i dziedziczenie. Po tym notatniku\n",
    "napiszesz własną strukturę danych jako klasę — na następnych zajęciach będzie to lista z dowiązaniami.\n",
    "\n",
    "### Jak korzystać\n",
    "1. Uruchamiaj komórki po kolei: `Ctrl + Enter` albo `Shift + Enter`.\n",
    "2. Przy ramkach **Przewidź, zanim uruchomisz** najpierw zgadnij, potem uruchom, na końcu rozwiń odpowiedź.\n",
    "3. W zadaniach zamień `pass` i `TODO` na swój kod i uruchom komórkę **Sprawdź**."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k01",
   "metadata": {},
   "source": [
    "## Spis treści\n",
    "0. **Nowe:** wszystko w Pythonie jest obiektem\n",
    "1. Czym jest programowanie obiektowe?\n",
    "2. Klasa i obiekt: podstawowe pojęcia\n",
    "3. Metody instancyjne i rola parametru `self`\n",
    "4. Konstruktor `__init__`\n",
    "5. Atrybuty obiektu i dostęp do nich\n",
    "6. Reprezentacja tekstowa: `__str__` — i **nowe:** `__repr__`\n",
    "7. Atrybut klasy i metoda statyczna (`@staticmethod`) — i **nowe:** pułapka listy w atrybucie klasy\n",
    "8. Dziedziczenie: idea i implementacja\n",
    "9. **Nowe:** `@dataclass` i metody specjalne (`__len__`, `__eq__`)\n",
    "10. Zadania 5.0–5.3 i zadanie 5.4 dla chętnych\n",
    "11. Co dalej?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k02",
   "metadata": {},
   "source": [
    "## 0) Nowe: wszystko w Pythonie jest obiektem\n",
    "\n",
    "Dokumentacja Pythona zaczyna opis modelu danych od zdania: *„Objects are Python's abstraction for data. All data\n",
    "in a Python program is represented by objects or by relations between objects.”* Liczba, napis i lista to obiekty\n",
    "— mają swój **typ** (klasę) i **metody**. `lista.append(…)` z zajęć 2 to właśnie wywołanie metody obiektu."
   ]
  },
  {
   "cell_type": "code",
   "id": "k03",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "liczba = 5\n",
    "napis = \"ala\"\n",
    "lista = [3, 1, 2]\n",
    "\n",
    "print(type(liczba), type(napis), type(lista))\n",
    "print(liczba.bit_length())   # metoda obiektu int: ile bitów potrzeba\n",
    "print(napis.upper())         # metoda obiektu str\n",
    "lista.sort()                 # metoda obiektu list\n",
    "print(lista)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k04",
   "metadata": {},
   "source": [
    "## 1) Czym jest programowanie obiektowe?\n",
    "\n",
    "**Programowanie obiektowe (OOP)** organizuje kod wokół **obiektów**, które łączą **dane** (*atrybuty*) i **zachowania**\n",
    "(*metody*). Łatwiej wtedy modelować byty ze świata: *student*, *konto*, *zamówienie* — albo *węzeł listy*.\n",
    "Zalety: kod blisko dziedziny, możliwość ponownego użycia i łączenia obiektów w większe całości."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k05",
   "metadata": {},
   "source": [
    "## 2) Klasa i obiekt: podstawowe pojęcia\n",
    "\n",
    "- **Klasa** — *wzorzec* (szablon), który określa atrybuty i metody wspólne dla wszystkich obiektów.\n",
    "- **Obiekt (instancja)** — konkretny egzemplarz klasy z własnymi wartościami atrybutów.\n",
    "- Z jednej klasy tworzysz wiele obiektów: zestaw cech i metod jest wspólny, wartości cech — indywidualne."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k06",
   "metadata": {},
   "source": [
    "## 3) Metody instancyjne i rola parametru `self`\n",
    "\n",
    "Metoda zdefiniowana w klasie działa na konkretnym obiekcie — Python przekazuje go jako pierwszy argument, który\n",
    "**umownie** nazywamy `self`. `self` znaczy „ja”: bieżący obiekt, na którym metoda pracuje.\n",
    "`ola.hello()` to w praktyce `Student.hello(ola)`."
   ]
  },
  {
   "cell_type": "code",
   "id": "k07",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "class Student:\n",
    "    # prosta metoda instancyjna; Python przekaże self automatycznie\n",
    "    def hello(self):\n",
    "        return \"Hello! Jestem instancja klasy Student\"\n",
    "\n",
    "ola = Student()\n",
    "print(ola.hello())\n",
    "print(Student.hello(ola))   # to samo wywołanie, zapisane wprost"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k08",
   "metadata": {},
   "source": [
    "## 4) Konstruktor `__init__`\n",
    "\n",
    "`__init__` to **metoda inicjalizująca**, którą Python wywołuje sam tuż po utworzeniu obiektu. Zwykle nadaje\n",
    "atrybutom wartości początkowe."
   ]
  },
  {
   "cell_type": "code",
   "id": "k09",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "class Student:\n",
    "    def __init__(self):\n",
    "        print(\"Zarejestrowano nowego studenta w systemie!\")\n",
    "\n",
    "ola = Student()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k10",
   "metadata": {},
   "source": [
    "## 5) Atrybuty obiektu i dostęp do nich\n",
    "\n",
    "Atrybut to **cecha** zapisana w obiekcie. Tworzysz je zwykle w `__init__`. Wewnątrz metod czytasz je przez\n",
    "`self.atrybut`, na zewnątrz — przez `obiekt.atrybut`."
   ]
  },
  {
   "cell_type": "code",
   "id": "k11",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "class Student:\n",
    "    def __init__(self, firstname, lastname, niu):\n",
    "        self.name = firstname\n",
    "        self.lastname = lastname\n",
    "        self.niu = niu\n",
    "        self.grades = []  # atrybut z wartością początkową\n",
    "\n",
    "    def hello(self):\n",
    "        return f\"Cześć, mam na imię {self.name}\"\n",
    "\n",
    "jan = Student(\"Jan\", \"Kowalski\", 12312)\n",
    "ania = Student(\"Anna\", \"Nowak\", 32132)\n",
    "print(jan.name)       # 'Jan'\n",
    "print(ania.lastname)  # 'Nowak'\n",
    "print(jan.hello())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k12",
   "metadata": {},
   "source": [
    "## 6) Reprezentacja tekstowa: `__str__`\n",
    "\n",
    "Domyślnie `print(obiekt)` pokazuje szczegóły techniczne (typ i adres w pamięci). Czytelny opis daje metoda\n",
    "specjalna `__str__`."
   ]
  },
  {
   "cell_type": "code",
   "id": "k13",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "class Student:\n",
    "    def __init__(self, firstname, lastname):\n",
    "        self.name = firstname\n",
    "        self.lastname = lastname\n",
    "\n",
    "    def __str__(self):\n",
    "        return f\"Student {self.name} {self.lastname}\"\n",
    "\n",
    "jan = Student(\"Jan\", \"Kowalski\")\n",
    "print(jan)  # Student Jan Kowalski"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k14",
   "metadata": {},
   "source": [
    "### Nowe: `__str__` dla ludzi, `__repr__` dla programisty\n",
    "\n",
    "`print(obiekt)` używa `__str__`. Ale lista, słownik i konsola pokazują elementy przez `__repr__` — opis\n",
    "techniczny, najlepiej taki, który wygląda jak kod tworzący obiekt."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k15",
   "metadata": {},
   "source": [
    "> **Przewidź, zanim uruchomisz:** co wypisze `print([jan])` — lista z jednym studentem, który ma `__str__`?\n",
    ">\n",
    "> <details><summary>Odpowiedź</summary>\n",
    ">\n",
    "> Nie „Student Jan Kowalski”, tylko coś w stylu `[<__main__.Student object at 0x…>]`: lista pokazuje elementy przez `__repr__`, a tej metody klasa nie ma.\n",
    ">\n",
    "> </details>"
   ]
  },
  {
   "cell_type": "code",
   "id": "k16",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "print([jan])\n",
    "\n",
    "class Student:\n",
    "    def __init__(self, firstname, lastname):\n",
    "        self.name = firstname\n",
    "        self.lastname = lastname\n",
    "\n",
    "    def __str__(self):\n",
    "        return f\"Student {self.name} {self.lastname}\"\n",
    "\n",
    "    def __repr__(self):\n",
    "        return f\"Student({self.name!r}, {self.lastname!r})\"\n",
    "\n",
    "jan = Student(\"Jan\", \"Kowalski\")\n",
    "print(jan)\n",
    "print([jan])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k17",
   "metadata": {},
   "source": [
    "## 7) Atrybut klasy i metoda statyczna (`@staticmethod`)\n",
    "\n",
    "Atrybut **klasy** należy do klasy jako całości, a nie do pojedynczego obiektu — np. licznik utworzonych obiektów.\n",
    "**Metoda statyczna** to funkcja zdefiniowana w klasie, ale niezwiązana z obiektem: nie dostaje `self`."
   ]
  },
  {
   "cell_type": "code",
   "id": "k18",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "class Student:\n",
    "    total = 0  # atrybut klasy\n",
    "\n",
    "    @staticmethod\n",
    "    def status():\n",
    "        print(f\"Liczba studentów: {Student.total}\")\n",
    "\n",
    "    def __init__(self, firstname, lastname):\n",
    "        self.name = firstname\n",
    "        self.lastname = lastname\n",
    "        Student.total += 1\n",
    "\n",
    "    def __str__(self):\n",
    "        return f\"Student {self.name} {self.lastname}\"\n",
    "\n",
    "jan = Student(\"Jan\", \"Kowalski\")\n",
    "ania = Student(\"Anna\", \"Nowak\")\n",
    "Student.status()      # Liczba studentów: 2\n",
    "print(Student.total)  # 2\n",
    "print(jan.total)      # obiekt widzi atrybut klasy"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k19",
   "metadata": {},
   "source": [
    "### Nowe: pułapka — lista w atrybucie klasy\n",
    "\n",
    "Atrybut klasy jest **jeden dla wszystkich obiektów**. Liczba w nim (jak `total`) to zwykle zamierzone zachowanie.\n",
    "Ale lista w atrybucie klasy to jedna lista na wszystkie obiekty — znasz to z zajęć 2 (dwie etykiety, jedna lista).\n",
    "Oficjalny tutorial Pythona pokazuje ten błąd na przykładzie psów i ich sztuczek (`Dog.tricks`)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k20",
   "metadata": {},
   "source": [
    "> **Przewidź, zanim uruchomisz:** Ola dostaje ocenę 5.0, Jan — 3.0. Co wypisze `print(jan.oceny)`?\n",
    ">\n",
    "> <details><summary>Odpowiedź</summary>\n",
    ">\n",
    "> `[5.0, 3.0]` — obie oceny, bo `oceny` jest jedną listą klasy, wspólną dla Oli i Jana. Poprawka: `self.oceny = []` w `__init__`.\n",
    ">\n",
    "> </details>"
   ]
  },
  {
   "cell_type": "code",
   "id": "k21",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "class StudentZBledem:\n",
    "    oceny = []      # BŁĄD: jedna lista dla wszystkich\n",
    "\n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "\n",
    "    def dodaj_ocene(self, ocena):\n",
    "        self.oceny.append(ocena)\n",
    "\n",
    "ola = StudentZBledem(\"Ola\")\n",
    "jan = StudentZBledem(\"Jan\")\n",
    "ola.dodaj_ocene(5.0)\n",
    "jan.dodaj_ocene(3.0)\n",
    "print(jan.oceny, \"| ta sama lista?\", ola.oceny is jan.oceny)\n",
    "\n",
    "class StudentDobry:\n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "        self.oceny = []        # nowa lista dla każdego obiektu\n",
    "\n",
    "    def dodaj_ocene(self, ocena):\n",
    "        self.oceny.append(ocena)\n",
    "\n",
    "ola = StudentDobry(\"Ola\")\n",
    "jan = StudentDobry(\"Jan\")\n",
    "ola.dodaj_ocene(5.0)\n",
    "jan.dodaj_ocene(3.0)\n",
    "print(jan.oceny, \"| ta sama lista?\", ola.oceny is jan.oceny)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k22",
   "metadata": {},
   "source": [
    "## 8) Dziedziczenie: idea i implementacja\n",
    "\n",
    "**Dziedziczenie** buduje nową klasę na bazie istniejącej. Klasa pochodna **dziedziczy** atrybuty i metody klasy\n",
    "bazowej, może dodać własne albo je **nadpisać**. `super().__init__(…)` wywołuje konstruktor klasy bazowej."
   ]
  },
  {
   "cell_type": "code",
   "id": "k23",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "class Czlowiek:\n",
    "    def __init__(self, firstname, lastname):\n",
    "        self.name = firstname\n",
    "        self.lastname = lastname\n",
    "\n",
    "    def __str__(self):\n",
    "        return f\"{self.name} {self.lastname}\"\n",
    "\n",
    "    def wypozycz_ksiazke(self, book_id):\n",
    "        return f\"Książka o id {book_id} wypożyczona\"\n",
    "\n",
    "class Student(Czlowiek):\n",
    "    def zloz_wniosek_o_ios(self, wniosek):\n",
    "        return \"Wniosek przyjęty do procesowania\"\n",
    "\n",
    "class Wykladowca(Czlowiek):\n",
    "    def __init__(self, firstname, lastname, academic_title):\n",
    "        super().__init__(firstname, lastname)  # konstruktor klasy bazowej\n",
    "        self.academic_title = academic_title\n",
    "\n",
    "    def napisz_sylabus(self, sylabus):\n",
    "        return \"Sylabus przyjęty do systemu\"\n",
    "\n",
    "jan_s = Student(\"Jan\", \"Kowalski\")\n",
    "ania_w = Wykladowca(\"Anna\", \"Nowak\", \"dr\")\n",
    "print(jan_s.zloz_wniosek_o_ios(\"wniosek\"))\n",
    "print(ania_w.wypozycz_ksiazke(6879))\n",
    "print(ania_w, \"|\", ania_w.academic_title)\n",
    "print(isinstance(ania_w, Czlowiek), isinstance(ania_w, Student))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k24",
   "metadata": {},
   "source": [
    "## 9) Nowe: `@dataclass` i metody specjalne\n",
    "\n",
    "Metody z dwoma podkreślnikami (`__init__`, `__str__`, `__len__`, `__eq__`, `__lt__`…) to **metody specjalne**:\n",
    "Python woła je sam. `len(x)` wywołuje `x.__len__()`, `a == b` — `a.__eq__(b)`, a `sorted` korzysta z `__lt__`.\n",
    "Dzięki temu twoja klasa działa z wbudowanymi funkcjami jak lista czy słownik — przyda się przy liście z dowiązaniami.\n",
    "\n",
    "Dekorator `@dataclass` (zajęcia 4) pisze `__init__`, `__repr__` i `__eq__` za ciebie."
   ]
  },
  {
   "cell_type": "code",
   "id": "k25",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "from dataclasses import dataclass\n",
    "\n",
    "@dataclass\n",
    "class Przedmiot:\n",
    "    nazwa: str\n",
    "    ects: int\n",
    "\n",
    "asd = Przedmiot(\"Algorytmy i struktury danych\", 5)\n",
    "print(asd)\n",
    "print(asd == Przedmiot(\"Algorytmy i struktury danych\", 5))\n",
    "\n",
    "\n",
    "class Plecak:\n",
    "    def __init__(self):\n",
    "        self.rzeczy = []\n",
    "\n",
    "    def wloz(self, rzecz):\n",
    "        self.rzeczy.append(rzecz)\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self.rzeczy)\n",
    "\n",
    "plecak = Plecak()\n",
    "plecak.wloz(\"laptop\")\n",
    "plecak.wloz(\"notatki\")\n",
    "print(\"len(plecak) =\", len(plecak))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k26",
   "metadata": {},
   "source": [
    "## 10) Zadanie 5.0 — klasa `Student` (podstawy)\n",
    "\n",
    "**Cel:** zaimplementuj klasę `Student` z atrybutami `name`, `lastname`, `niu` oraz metodami `hello()` i `__str__()`.\n",
    "- `hello()` zwraca napis `\"Cześć, mam na imię {name}\"`,\n",
    "- `__str__()` zwraca `\"Student {name} {lastname} (NIU: {niu})\"`."
   ]
  },
  {
   "cell_type": "code",
   "id": "k27",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "class Student:\n",
    "    def __init__(self, name: str, lastname: str, niu: int):\n",
    "        pass  # TODO: zapisz name, lastname i niu w self\n",
    "\n",
    "    def hello(self) -> str:\n",
    "        pass  # TODO\n",
    "\n",
    "    def __str__(self) -> str:\n",
    "        return \"\"  # TODO"
   ]
  },
  {
   "cell_type": "code",
   "id": "k28",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Sprawdź\n",
    "try:\n",
    "    s = Student(\"Jan\", \"Kowalski\", 111)\n",
    "    assert s.hello() == \"Cześć, mam na imię Jan\"\n",
    "    assert str(s) == \"Student Jan Kowalski (NIU: 111)\"\n",
    "    assert hasattr(s, 'niu') and isinstance(s.niu, int)\n",
    "except AssertionError:\n",
    "    print(\"❌ Testy nie przeszły — popraw rozwiązanie.\")\n",
    "except Exception as e:\n",
    "    print(\"❌ Kod kończy się błędem:\", type(e).__name__, e)\n",
    "else:\n",
    "    print(\"✅ Zadanie 5.0 zaliczone!\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k29",
   "metadata": {},
   "source": [
    "## Zadanie 5.1 — licznik obiektów (atrybut klasy + `@staticmethod`)\n",
    "\n",
    "**Cel:** dodaj do klasy `Student` atrybut **klasy** `total`, który liczy utworzone obiekty, i metodę statyczną\n",
    "`status()`, która drukuje `\"Liczba studentów: {Student.total}\"`. *Wskazówka:* zwiększaj `Student.total` w `__init__`."
   ]
  },
  {
   "cell_type": "code",
   "id": "k30",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "class Student:\n",
    "    # TODO: atrybut klasy total\n",
    "\n",
    "    @staticmethod\n",
    "    def status():\n",
    "        pass  # TODO\n",
    "\n",
    "    def __init__(self, name: str, lastname: str, niu: int):\n",
    "        self.name = name\n",
    "        self.lastname = lastname\n",
    "        self.niu = niu\n",
    "        # TODO: zwiększ licznik\n",
    "\n",
    "    def __str__(self) -> str:\n",
    "        return f\"Student {self.name} {self.lastname} (NIU: {self.niu})\""
   ]
  },
  {
   "cell_type": "code",
   "id": "k31",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Sprawdź\n",
    "try:\n",
    "    Student.total = 0\n",
    "    a = Student('Ala', 'Nowak', 1)\n",
    "    b = Student('Jan', 'Kowalski', 2)\n",
    "    assert Student.total == 2\n",
    "    Student.status()\n",
    "except AssertionError:\n",
    "    print(\"❌ Testy nie przeszły — popraw rozwiązanie.\")\n",
    "except Exception as e:\n",
    "    print(\"❌ Kod kończy się błędem:\", type(e).__name__, e)\n",
    "else:\n",
    "    print(\"✅ Zadanie 5.1 zaliczone!\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k32",
   "metadata": {},
   "source": [
    "## Zadanie 5.2 — dziedziczenie: `Czlowiek`, `Student`, `Wykladowca`\n",
    "\n",
    "**Cel:** uzupełnij klasy `Wykladowca` i `Student`, które dziedziczą po `Czlowiek` (gotowej).\n",
    "- `Student` ma metodę `zloz_wniosek_o_ios(wniosek)`, która **zwraca** `\"Wniosek przyjęty do procesowania\"`.\n",
    "- `Wykladowca` ma własny `__init__(firstname, lastname, academic_title)` — użyj `super().__init__` —\n",
    "  i metodę `napisz_sylabus(sylabus)`, która **zwraca** `\"Sylabus przyjęty do systemu\"`.\n",
    "\n",
    "*W oryginalnej wersji polecenie mówiło „drukującą”, a test sprawdzał wartość zwracaną — teraz obie strony mówią to samo.*"
   ]
  },
  {
   "cell_type": "code",
   "id": "k33",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "class Czlowiek:\n",
    "    def __init__(self, firstname, lastname):\n",
    "        self.name = firstname\n",
    "        self.lastname = lastname\n",
    "\n",
    "    def __str__(self):\n",
    "        return f\"{self.name} {self.lastname}\"\n",
    "\n",
    "    def wypozycz_ksiazke(self, book_id):\n",
    "        return f\"Książka o id {book_id} wypożyczona\"\n",
    "\n",
    "\n",
    "class Wykladowca(Czlowiek):\n",
    "    def __init__(self, firstname, lastname, academic_title):\n",
    "        pass  # TODO: super().__init__(...) i academic_title\n",
    "\n",
    "    def napisz_sylabus(self, sylabus):\n",
    "        pass  # TODO\n",
    "\n",
    "\n",
    "# Tutaj nic nie podpowiadam\n",
    "class Student(Czlowiek):\n",
    "    pass  # TODO"
   ]
  },
  {
   "cell_type": "code",
   "id": "k34",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Sprawdź\n",
    "try:\n",
    "    s = Student('Jan', 'Kowalski')\n",
    "    w = Wykladowca('Anna', 'Nowak', 'dr')\n",
    "    assert s.zloz_wniosek_o_ios('X') == 'Wniosek przyjęty do procesowania'\n",
    "    assert w.napisz_sylabus('S') == 'Sylabus przyjęty do systemu'\n",
    "    assert w.wypozycz_ksiazke(100).startswith('Książka o id 100')\n",
    "    assert str(w) == 'Anna Nowak' and w.academic_title == 'dr'\n",
    "    assert isinstance(w, Czlowiek) and isinstance(s, Czlowiek)\n",
    "except AssertionError:\n",
    "    print(\"❌ Testy nie przeszły — popraw rozwiązanie.\")\n",
    "except Exception as e:\n",
    "    print(\"❌ Kod kończy się błędem:\", type(e).__name__, e)\n",
    "else:\n",
    "    print(\"✅ Zadanie 5.2 zaliczone!\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k35",
   "metadata": {},
   "source": [
    "## Zadanie 5.3 — `__str__` i walidacja danych\n",
    "\n",
    "*Tego zadania brakowało w oryginalnej wersji, choć było w spisie treści.*\n",
    "\n",
    "**Cel:** klasa `Student`, która nie przyjmie błędnych danych.\n",
    "- `__init__(name, lastname, niu)` — gdy `niu` nie jest dodatnią liczbą całkowitą, podnieś `ValueError`;\n",
    "  utwórz pustą listę `self.oceny` (pamiętaj o pułapce z sekcji 7).\n",
    "- `dodaj_ocene(ocena)` — przyjmuje tylko oceny ze skali UEP `(2.0, 3.0, 3.5, 4.0, 4.5, 5.0)`, inaczej `ValueError`.\n",
    "- `srednia()` — średnia ocen zaokrąglona do 2 miejsc; bez ocen zwraca `0.0`.\n",
    "- `__str__()` — `\"Student {name} {lastname} (NIU: {niu}), średnia: {srednia}\"`."
   ]
  },
  {
   "cell_type": "code",
   "id": "k36",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "class Student:\n",
    "    SKALA = (2.0, 3.0, 3.5, 4.0, 4.5, 5.0)\n",
    "\n",
    "    def __init__(self, name: str, lastname: str, niu: int):\n",
    "        pass  # TODO: walidacja niu, atrybuty, pusta lista oceny\n",
    "\n",
    "    def dodaj_ocene(self, ocena: float) -> None:\n",
    "        pass  # TODO: walidacja i dopisanie oceny\n",
    "\n",
    "    def srednia(self) -> float:\n",
    "        return 0.0  # TODO\n",
    "\n",
    "    def __str__(self) -> str:\n",
    "        return \"\"  # TODO"
   ]
  },
  {
   "cell_type": "code",
   "id": "k37",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Sprawdź\n",
    "try:\n",
    "    s = Student(\"Jan\", \"Kowalski\", 111)\n",
    "    s.dodaj_ocene(4.0)\n",
    "    s.dodaj_ocene(4.5)\n",
    "    assert s.srednia() == 4.25\n",
    "    assert str(s) == \"Student Jan Kowalski (NIU: 111), średnia: 4.25\"\n",
    "    assert Student(\"Ola\", \"Nowak\", 5).srednia() == 0.0\n",
    "    try:\n",
    "        s.dodaj_ocene(6)\n",
    "        blad_oceny = False\n",
    "    except ValueError:\n",
    "        blad_oceny = True\n",
    "    assert blad_oceny, 'ocena 6 powinna dać ValueError'\n",
    "    try:\n",
    "        Student(\"Ala\", \"Nowak\", -5)\n",
    "        blad_niu = False\n",
    "    except ValueError:\n",
    "        blad_niu = True\n",
    "    assert blad_niu, 'NIU -5 powinien dać ValueError'\n",
    "except AssertionError:\n",
    "    print(\"❌ Testy nie przeszły — popraw rozwiązanie.\")\n",
    "except Exception as e:\n",
    "    print(\"❌ Kod kończy się błędem:\", type(e).__name__, e)\n",
    "else:\n",
    "    print(\"✅ Zadanie 5.3 zaliczone!\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k38",
   "metadata": {},
   "source": [
    "## Zadanie 5.4 dla chętnych — porównywanie i sortowanie obiektów\n",
    "\n",
    "**Cel:** naucz klasę `Student` porównywania.\n",
    "- `__eq__(self, inny)` — dwóch studentów jest równych, gdy mają ten sam `niu`,\n",
    "- `__lt__(self, inny)` — „mniejszy” jest ten, kto stoi wcześniej alfabetycznie po nazwisku, a potem imieniu\n",
    "  (wskazówka: porównaj krotki `(lastname, name)` — krotki porównują się element po elemencie, zajęcia 4),\n",
    "- `__repr__` — np. `Student('Jan', 'Kowalski', 1)`. Wtedy `sorted(lista_studentow)` działa samo."
   ]
  },
  {
   "cell_type": "code",
   "id": "k39",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "class Student:\n",
    "    def __init__(self, name: str, lastname: str, niu: int):\n",
    "        self.name = name\n",
    "        self.lastname = lastname\n",
    "        self.niu = niu\n",
    "\n",
    "    # TODO: __eq__, __lt__ i __repr__"
   ]
  },
  {
   "cell_type": "code",
   "id": "k40",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Sprawdź\n",
    "try:\n",
    "    a = Student('Jan', 'Kowalski', 1)\n",
    "    assert a == Student('Janek', 'Kowalski', 1)\n",
    "    assert a != Student('Jan', 'Kowalski', 2)\n",
    "    grupa = [Student('Ola', 'Nowak', 3), a, Student('Ewa', 'Kowalska', 2)]\n",
    "    nazwiska = [s.lastname for s in sorted(grupa)]\n",
    "    assert nazwiska == ['Kowalska', 'Kowalski', 'Nowak']\n",
    "except AssertionError:\n",
    "    print(\"❌ Testy nie przeszły — popraw rozwiązanie.\")\n",
    "except Exception as e:\n",
    "    print(\"❌ Kod kończy się błędem:\", type(e).__name__, e)\n",
    "else:\n",
    "    print(\"✅ Zadanie 5.4 zaliczone!\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "k41",
   "metadata": {},
   "source": [
    "## Co dalej?\n",
    "\n",
    "- **Atrybuty „prywatne”** i konwencje hermetyzacji: `_nazwa` (umowa), `__nazwa` (zmiana nazwy przez Pythona).\n",
    "- **Właściwości** (`@property`, `@nazwa.setter`) — walidacja przy każdym przypisaniu, a nie tylko w `__init__`.\n",
    "- **Dziedziczenie wielokrotne** i **klasy abstrakcyjne** (`abc.ABC`).\n",
    "- Następne zajęcia: **lista z dowiązaniami** — struktura danych zbudowana z dwóch klas: `Node` i `LinkedList`.\n",
    "\n",
    "## Zadania dodatkowe\n",
    "\n",
    "1. Czym różni się atrybut klasy od atrybutu obiektu? Kiedy użyjesz każdego z nich?\n",
    "2. Dlaczego `print([jan])` nie korzysta z `__str__`?\n",
    "3. Napisz klasę `Konto` z metodami `wplac` i `wyplac`; `wyplac` podnosi `ValueError`, gdy brakuje środków."
   ]
  }
 ],
 "nbformat": 4,
 "nbformat_minor": 5,
 "metadata": {
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3",
   "language": "python"
  },
  "language_info": {
   "name": "python"
  }
 }
}
