Streszczenie Wstęp do programowania obiektowego w Pythonie - klasa, obiekt, atrybuty i metody

Ten moduł wprowadza w podstawy programowania obiektowego (OOP) w języku Python. Wyjaśnia, czym jest paradygmat obiektowy i jak różni się od proceduralnego, a następnie krok po kroku prowadzi przez definiowanie klas, tworzenie obiektów oraz stosowanie konstruktora __init__ i parametru self . Omówione są atrybuty przechowujące stan obiektu oraz metody definiujące jego zachowania, a także zagadnienia tożsamości i równości obiektów ( id() , is , == ). Moduł prezentuje praktyczne wzorce, takie jak przechowywanie obiektów w kolekcjach, kompozycja obiektów oraz funkcje fabryczne. Całość uzupełniają ćwiczenia, mapy myśli, quiz oraz omówienie typowych błędów i dobrych praktyk.

  • Klasa i obiekt - klasa jako szablon, obiekt jako instancja; składnia class i tworzenie obiektów przez NazwaKlasy()
  • Konstruktor__init__  i  self - inicjalizacja atrybutów w momencie tworzenia obiektu, rola self jako odnośnika do bieżącej instancji
  • Atrybuty i metody - atrybuty przechowują stan obiektu, metody określają jego zachowania; dostęp przez notację kropkową
  • Tożsamość i stan obiektów - id() , is vs == , porównywanie obiektów ( __eq__ , __repr__ ), mutowalność stanu
  • Kompozycja i kolekcje obiektów - przechowywanie obiektów w listach, iterowanie, obiekty jako argumenty i zwracane wartości, kompozycja (obiekt w obiekcie)

Moduł ten stanowi pierwszą część kompleksowego kursu programowania obiektowego w języku Python. Jego celem jest wprowadzenie studentów w paradygmat obiektowy oraz przedstawienie fundamentalnych koncepcji, takich jak klasy, obiekty, atrybuty i metody. Materiał został zaprojektowany tak, aby stopniowo budować zrozumienie od prostych analogii do konkretnych implementacji w kodzie. Każdy slajd zawiera przykłady, które można samodzielnie przetestować w środowisku REPL. Zaleca się aktywne uczestnictwo poprzez modyfikowanie przykładów i wykonywanie ćwiczeń. Systematyczna praca z materiałem gwarantuje solidne opanowanie podstaw OOP. W kolejnych modułach wiedza ta będzie rozwijana o bardziej zaawansowane zagadnienia.

Programowanie obiektowe to nie tylko zestaw reguł składniowych, ale przede wszystkim sposób myślenia o problemach programistycznych. Kluczowe jest zrozumienie, że klasy służą do modelowania rzeczywistych bytów i relacji między nimi. Dzięki OOP kod staje się bardziej modularny, łatwiejszy w utrzymaniu i bardziej odporny na błędy. Współczesne aplikacje webowe, systemy bazodanowe i frameworki w dużym stopniu opierają się na paradygmacie obiektowym. Opanowanie OOP otwiera drzwi do zrozumienia zaawansowanych wzorców projektowych. Zachęcamy do cierpliwej i systematycznej nauki - każde nowe pojęcie będzie szczegółowo wyjaśnione i zilustrowane przykładami.

1/50Wprowadzenie

Część 1: wstęp do obiektowości

Programowanie obiektowe (OOP) to jeden z najważniejszych paradygmatów we współczesnym tworzeniu oprogramowania. Python, jako język wieloparadygmatowy, wspiera OOP w sposób elastyczny i przystępny dla początkujących.

W tej części poznasz fundamentalne pojęcia: klasę , obiekt , atrybuty , metody oraz konstruktor . Zrozumiesz, czym różni się myślenie obiektowe od proceduralnego i dlaczego OOP dominuje w nowoczesnych systemach informatycznych.

Kurs obejmuje 50 slajdów z przykładami kodu, ćwiczeniami, quizem i mapami myśli. Każdy slajd został zaprojektowany tak, by stopniowo budować Twoje zrozumienie - od analogii ze świata rzeczywistego po konkretne implementacje w Pythonie.

Jak korzystać z kursu: przeglądaj slajdy po kolei, eksperymentuj z kodem w REPL, wykonuj ćwiczenia. Nie pomijaj slajdów z błędami - ucz się na cudzych pomyłkach, zanim popełnisz własne. Powtarzaj przykłady samodzielnie, modyfikując je i obserwując efekty.
Wprowadzenie do OOP

Moduł ten stanowi pierwszą część kompleksowego kursu programowania obiektowego w języku Python. Jego celem jest wprowadzenie studentów w paradygmat obiektowy oraz przedstawienie fundamentalnych koncepcji, takich jak klasy, obiekty, atrybuty i metody. Materiał został zaprojektowany tak, aby stopniowo budować zrozumienie od prostych analogii do konkretnych implementacji w kodzie. Każdy slajd zawiera przykłady, które można samodzielnie przetestować w środowisku REPL. Zaleca się aktywne uczestnictwo poprzez modyfikowanie przykładów i wykonywanie ćwiczeń. Systematyczna praca z materiałem gwarantuje solidne opanowanie podstaw OOP. W kolejnych modułach wiedza ta będzie rozwijana o bardziej zaawansowane zagadnienia.

Programowanie obiektowe to nie tylko zestaw reguł składniowych, ale przede wszystkim sposób myślenia o problemach programistycznych. Kluczowe jest zrozumienie, że klasy służą do modelowania rzeczywistych bytów i relacji między nimi. Dzięki OOP kod staje się bardziej modularny, łatwiejszy w utrzymaniu i bardziej odporny na błędy. Współczesne aplikacje webowe, systemy bazodanowe i frameworki w dużym stopniu opierają się na paradygmacie obiektowym. Opanowanie OOP otwiera drzwi do zrozumienia zaawansowanych wzorców projektowych. Zachęcamy do cierpliwej i systematycznej nauki - każde nowe pojęcie będzie szczegółowo wyjaśnione i zilustrowane przykładami.

2/50Cele dydaktyczne
Po tej części będziesz umieć:
  1. Wyjaśnić, czym jest paradygmat obiektowy i jak różni się od proceduralnego - zrozumiesz fundamentalną różnicę w organizacji kodu
  2. Zdefiniować własną klasę w Pythonie i utworzyć jej obiekty - poznasz składnię class i mechanizm tworzenia instancji
  3. Stosować konstruktor __init__ i parametr self - nauczysz się inicjalizować stan obiektu w momencie jego powstania
  4. Dodawać atrybuty i metody do klasy - zarówno poprzez konstruktor, jak i dynamicznie w trakcie działania programu
  5. Posługiwać się obiektami - porównywać je (przez == i is ), przechowywać w kolekcjach, przekazywać jako argumenty i zwracać z funkcji

Cele te realizujemy stopniowo: najpierw teoria i analogie, potem proste przykłady, a na końcu bardziej złożone scenariusze z kompozycją obiektów. Każdy cel jest poparty ćwiczeniem praktycznym.

Cele części

Przedstawione cele dydaktyczne zostały opracowane zgodnie z zasadą stopniowania trudności, co pozwala na systematyczne budowanie kompetencji. Każdy cel koncentruje się na konkretnym aspekcie programowania obiektowego, od teorii przez praktykę aż po zaawansowane zastosowania. Realizacja wszystkich założeń gwarantuje solidne podstawy do dalszego rozwoju w kierunku bardziej złożonych zagadnień, takich jak wzorce projektowe. Materiał został dostosowany do potrzeb studentów kierunków informatycznych. Oczekuje się, że po ukończeniu modułu student będzie potrafił samodzielnie projektować proste hierarchie klas i implementować je w Pythonie. Regularne sprawdzanie postępów pomoże w identyfikacji obszarów wymagających dodatkowej uwagi i powtórki.

Struktura kursu przewiduje płynne przejście od zagadnień podstawowych do bardziej złożonych, z licznymi przykładami i ćwiczeniami praktycznymi. Zaleca się aktywne uczestnictwo poprzez samodzielne eksperymenty z kodem. Środowisko REPL Pythona jest doskonałym narzędziem do natychmiastowego testowania omawianych koncepcji bez konieczności tworzenia plików projektowych. Warto również korzystać z dodatkowych materiałów, takich jak dokumentacja oficjalna Pythona oraz społecznościowe fora programistyczne. Systematyczna praca i regularne powtórki są kluczem do sukcesu w opanowaniu programowania obiektowego.

3/50 Czym jest programowanie obiektowe?

OOP (Object-Oriented Programming) to paradygmat programowania, w którym kod organizuje się wokół obiektów - bytów łączących dane (atrybuty) i zachowania (metody). W przeciwieństwie do programowania proceduralnego, gdzie dane i funkcje są od siebie oddzielone, OOP grupuje je w spójne jednostki zwane klasami.

Zamiast pisać długie sekwencje instrukcji operujących na luźnych zmiennych, grupujemy powiązane ze sobą dane i funkcje w jednym miejscu - w klasie . Dzięki temu kod staje się bardziej modularny, łatwiejszy do zrozumienia i utrzymania.

Python jest językiem wieloparadygmatowym - wspiera OOP, ale także programowanie proceduralne i funkcyjne. Nie musisz wybierać jednego stylu na zawsze - możesz łączyć podejścia w ramach jednego projektu, wybierając to, które najlepiej pasuje do danego problemu.

Historia OOP sięga lat 60. XX wieku (symulacje w Simuli), ale prawdziwy rozkwit nastąpił w latach 90. wraz z językami C++ i Java. Dziś OOP to standard w takich językach jak Python, Java, C#, Ruby, C++ i JavaScript.

Filary OOP: enkapsulacja (ukrywanie szczegółów implementacji), dziedziczenie (budowanie klas na bazie innych), polimorfizm (jednolity interfejs dla różnych typów), abstrakcja (koncentracja na istotnych cechach, pomijanie nieistotnych). Każdy z tych filarów będzie szczegółowo omówiony w kolejnych częściach kursu.
Paradygmat OOP

Programowanie obiektowe wywodzi się z języka Simula z lat 60. XX wieku, a jego rozwój przyspieszył w latach 70. wraz z językiem Smalltalk, który wprowadził wiele koncepcji używanych do dziś, takich jak klasy, metody i dziedziczenie. Współcześnie OOP jest wspierane przez większość popularnych języków programowania, choć każdy z nich implementuje ten paradygmat nieco inaczej. Python przyjął podejście pragmatyczne, w którym wszystko jest obiektem, ale programista nie jest zmuszany do obiektowego stylu. Dzięki temu Python jest językiem wieloparadygmatowym, łączącym OOP z programowaniem proceduralnym i funkcyjnym w elastyczny sposób. Zrozumienie genezy i ewolucji OOP pomaga docenić jego zalety i świadomie stosować go we własnych projektach programistycznych.

Cztery filary OOP - enkapsulacja, dziedziczenie, polimorfizm i abstrakcja - są ze sobą ściśle powiązane i wzajemnie się uzupełniają. Enkapsulacja chroni stan obiektu przed niekontrolowanym dostępem z zewnątrz, co zwiększa bezpieczeństwo i spójność danych. Dziedziczenie pozwala na budowanie hierarchii klas i wielokrotne wykorzystanie kodu bez jego kopiowania. Polimorfizm umożliwia jednolity interfejs dla różnych typów obiektów, co upraszcza projektowanie rozszerzalnych systemów. Abstrakcja koncentruje się na istotnych cechach, pomijając nieistotne szczegóły implementacyjne. Opanowanie tych czterech koncepcji stanowi fundament biegłości w OOP.

4/50 Analogia: dom jako obiekt

Wyobraź sobie dom. Każdy dom ma cechy (w terminologii OOP: atrybuty), które go opisują i odróżniają od innych domów:

  • kolor elewacji - np. biały, żółty, czerwony
  • liczba pięter - parterowy, piętrowy, z poddaszem
  • powierzchnia w m2 - od małych 50m2 po rezydencje 300m2
  • liczba okien - wpływa na ilość światła w pomieszczeniach

Oraz zachowania (w terminologii OOP: metody), które zmieniają stan domu lub dostarczają informacji:

  • otworz_drzwi() - zmienia stan (drzwi otwarte - zamknięte)
  • wlacz_ogrzewanie() - podnosi temperaturę wewnątrz
  • zamknij_okno() - blokuje dostęp powietrza, zmienia stan wentylacji
  • oblicz_powierzchnie() - zwraca całkowitą powierzchnię użytkową

W OOP łączymy cechy i zachowania w jeden byt - obiekt . Nie myślimy już "mam kolor domu i funkcję malującą dom", tylko "mam obiekt domu, który ma kolor i potrafi się pomalować". To subtelna, ale kluczowa różnica w sposobie myślenia o kodzie.

Co ważne: plan domu (projekt architektoniczny) to klasa , a każdy konkretny dom zbudowany według tego planu to obiekt . Jeden projekt może posłużyć do zbudowania wielu domów, różniących się szczegółami (kolor, wykończenie), tak jak jedna klasa może mieć wiele instancji.

Dom jako obiekt

Stosowanie analogii ze świata rzeczywistego to sprawdzona metoda dydaktyczna ułatwiająca zrozumienie abstrakcyjnych koncepcji programistycznych. Analogia domu i foremki do ciastek doskonale ilustruje relację między klasą a obiektem, ponieważ każdy ma intuicyjne pojęcie o tych przedmiotach. Dzięki takiemu podejściu student może łatwiej przejść od myślenia w kategoriach przedmiotów fizycznych do myślenia w kategoriach bytów programistycznych. Warto jednak pamiętać, że każda analogia ma swoje ograniczenia i nie należy jej rozciągać zbyt daleko poza zamierzony obszar. Mimo tych ograniczeń, analogie pozostają jednym z najskuteczniejszych narzędzi dydaktycznych w nauczaniu programowania. Kluczem jest umiejętne łączenie analogii z konkretnymi przykładami kodu.

W świecie fizycznym przedmioty nie zmieniają swoich właściwości tak dynamicznie, jak obiekty w programie. W kodzie atrybuty obiektu mogą się zmieniać w każdej chwili poprzez wywołanie metod, a metody mogą uruchamiać złożoną logikę biznesową. Mimo tych różnic, rozumienie relacji między projektem a gotowym produktem jest kluczowe dla zrozumienia OOP. Każdy programista powinien umieć odróżnić definicję klasy od konkretnej instancji, tak jak odróżnia plan architektoniczny od zbudowanego według niego domu. To fundamentalne rozróżnienie pojawia się w każdym projekcie obiektowym i warto je solidnie utrwalić.

5/50 Kod proceduralny vs obiektowy

Dwa podejścia do tego samego zadania - zarządzanie danymi studenta. W podejściu proceduralnym dane studenta są przechowywane w oddzielnych zmiennych lub słownikach, a funkcje operujące na tych danych są globalne. W podejściu obiektowym dane i funkcje są zebrane w klasie.

AspektProceduralnyObiektowy
DaneOddzielne zmienne lub słownikiAtrybuty obiektu (spójne, zawsze razem)
Funkcje Globalne funkcje przyjmujące dane jako argumenty Metody w klasie, operujące na self
Organizacja Plik z funkcjami, często rosnący bez kontroli Klasa jako autonomiczna jednostka
Modyfikacja Trudna przy wielu danych - trzeba zmieniać każdą funkcję osobno Łatwa - zmiana w klasie propaguje się na wszystkie obiekty
Ponowne użycie Kopiowanie funkcji lub używanie modułów Dziedziczenie klas - budujesz na istniejącym kodzie
Stan Zmienne globalne lub przekazywane między funkcjami Stan przechowywany w obiekcie, metody go modyfikują
Testowanie Trudniejsze - funkcje zależą od stanu globalnego Łatwiejsze - obiekt można testować w izolacji

W praktyce podejście obiektowe nie zawsze jest lepsze - dla prostych skryptów proceduralny kod bywa szybszy w napisaniu. Jednak przy większych projektach (powyżej kilkuset linii) OOP zaczyna błyszczeć.

Porównanie podejść

Porównanie programowania proceduralnego i obiektowego na konkretnych przykładach pokazuje fundamentalne różnice w organizacji kodu. Podejście proceduralne koncentruje się na sekwencji operacji i funkcjach przetwarzających dane, podczas gdy obiektowe grupuje powiązane dane i funkcje w spójne jednostki zwane klasami. W praktyce zawodowej rzadko spotyka się czyste implementacje jednego paradygmatu - nowoczesne aplikacje łączą różne podejścia w zależności od potrzeb konkretnego modułu. Python, jako język wieloparadygmatowy, doskonale wspiera taki hybrydowy styl programowania, pozwalając programiście na elastyczny wybór narzędzia. Wybór paradygmatu zależy przede wszystkim od charakteru problemu - proste skrypty często lepiej napisać proceduralnie, a złożone systemy obiektowo.

Tabelaryczne zestawienie cech obu podejść ułatwia zrozumienie, kiedy które z nich jest bardziej odpowiednie. Programowanie proceduralne sprawdza się w małych, liniowych skryptach, gdzie ważna jest prostota i szybkość wykonania. OOP dominuje w dużych projektach, gdzie kluczowe są organizacja kodu, wielokrotne użycie i łatwość utrzymania. Warto również zauważyć, że wiele języków nowej generacji, takich jak Rust czy Kotlin, łączy elementy obu podejść, oferując programistom najlepsze cechy każdego z nich. Świadomy wybór paradygmatu jest oznaką dojrzałości programistycznej i pozwala na podejmowanie optymalnych decyzji projektowych w codziennej pracy.

6/50 Zalety programowania obiektowego
  • Czytelność - kod odzwierciedla strukturę rzeczywistych bytów: jeżeli w systemie mamy Pracownika, Projekt i Zadanie, to klasy o tych nazwach są intuicyjne dla każdego programisty
  • Wielokrotne użycie - raz napisana klasa służy do tworzenia wielu obiektów; raz napisana hierarchia klas może być rozszerzana przez dziedziczenie
  • Organizacja - powiązane dane i funkcje są obok siebie, w jednym pliku, co ułatwia nawigację po kodzie
  • Łatwość utrzymania - zmiany w klasie propagują się na wszystkie obiekty; poprawiasz błąd w jednym miejscu, a nie w 50 funkcjach
  • Rozszerzalność - dziedziczenie pozwala budować na istniejącym kodzie bez modyfikowania go (zasada Open-Closed)
  • Bezpieczeństwo - enkapsulacja chroni dane przed przypadkową modyfikacją; kontrolujesz, jak zmienia się stan obiektu
  • Modelowanie złożoności - OOP pozwala radzić sobie ze złożonością poprzez dekompozycję problemu na mniejsze, powiązane ze sobą obiekty

Nie oznacza to, że OOP jest zawsze najlepszym wyborem. Dla prostych zadań (np. jednorazowy skrypt do przetworzenia pliku) podejście proceduralne jest w pełni wystarczające. OOP sprawdza się tam, gdzie mamy do czynienia ze złożonym stanem, wieloma bytami i relacjami między nimi.

Zalety OOP

Programowanie obiektowe wywodzi się z języka Simula z lat 60. XX wieku, a jego rozwój przyspieszył w latach 70. wraz z językiem Smalltalk, który wprowadził wiele koncepcji używanych do dziś, takich jak klasy, metody i dziedziczenie. Współcześnie OOP jest wspierane przez większość popularnych języków programowania, choć każdy z nich implementuje ten paradygmat nieco inaczej. Python przyjął podejście pragmatyczne, w którym wszystko jest obiektem, ale programista nie jest zmuszany do obiektowego stylu. Dzięki temu Python jest językiem wieloparadygmatowym, łączącym OOP z programowaniem proceduralnym i funkcyjnym w elastyczny sposób. Zrozumienie genezy i ewolucji OOP pomaga docenić jego zalety i świadomie stosować go we własnych projektach programistycznych.

Cztery filary OOP - enkapsulacja, dziedziczenie, polimorfizm i abstrakcja - są ze sobą ściśle powiązane i wzajemnie się uzupełniają. Enkapsulacja chroni stan obiektu przed niekontrolowanym dostępem z zewnątrz, co zwiększa bezpieczeństwo i spójność danych. Dziedziczenie pozwala na budowanie hierarchii klas i wielokrotne wykorzystanie kodu bez jego kopiowania. Polimorfizm umożliwia jednolity interfejs dla różnych typów obiektów, co upraszcza projektowanie rozszerzalnych systemów. Abstrakcja koncentruje się na istotnych cechach, pomijając nieistotne szczegóły implementacyjne. Opanowanie tych czterech koncepcji stanowi fundament biegłości w OOP.

7/50Klasa jako przepis

Klasa to przepis lub foremka do ciastek . Określa, jakie cechy i zachowania będą miały wszystkie obiekty z niej utworzone. Definiuje strukturę, ale sama w sobie nie zawiera konkretnych danych - to dopiero obiekty będą miały swoje własne wartości.

Foremka sama w sobie nie jest ciastkiem - dopiero gdy użyjesz jej z ciastem, otrzymasz konkretny wyrób. Analogicznie: klasa to nie obiekt, ale szablon do jego tworzenia. Możesz mieć jedną foremkę i upiec z niej sto ciasteczek, każde z innym lukrem i posypką - tak jak możesz mieć jedną klasę i sto obiektów, każdy z innymi wartościami atrybutów.

W Pythonie klasa to blok kodu zaczynający się od słowa kluczowego class , po którym następuje nazwa klasy (konwencja: PascalCase) i dwukropek. Wewnątrz bloku definiujemy atrybuty i metody. Klasa może być tak prosta, jak pusta definicja z pass , ale w praktyce zawiera konstruktor i metody.

Klasa = szablon: definiuje strukturę, ale nie zajmuje pamięci na dane. Dopiero obiekt (instancja) alokuje pamięć na swoje atrybuty. Klasa istnieje w kodzie źródłowym, obiekt istnieje w pamięci RAM podczas działania programu.
Foremka do ciastek

Stosowanie analogii ze świata rzeczywistego to sprawdzona metoda dydaktyczna ułatwiająca zrozumienie abstrakcyjnych koncepcji programistycznych. Analogia domu i foremki do ciastek doskonale ilustruje relację między klasą a obiektem, ponieważ każdy ma intuicyjne pojęcie o tych przedmiotach. Dzięki takiemu podejściu student może łatwiej przejść od myślenia w kategoriach przedmiotów fizycznych do myślenia w kategoriach bytów programistycznych. Warto jednak pamiętać, że każda analogia ma swoje ograniczenia i nie należy jej rozciągać zbyt daleko poza zamierzony obszar. Mimo tych ograniczeń, analogie pozostają jednym z najskuteczniejszych narzędzi dydaktycznych w nauczaniu programowania. Kluczem jest umiejętne łączenie analogii z konkretnymi przykładami kodu.

W świecie fizycznym przedmioty nie zmieniają swoich właściwości tak dynamicznie, jak obiekty w programie. W kodzie atrybuty obiektu mogą się zmieniać w każdej chwili poprzez wywołanie metod, a metody mogą uruchamiać złożoną logikę biznesową. Mimo tych różnic, rozumienie relacji między projektem a gotowym produktem jest kluczowe dla zrozumienia OOP. Każdy programista powinien umieć odróżnić definicję klasy od konkretnej instancji, tak jak odróżnia plan architektoniczny od zbudowanego według niego domu. To fundamentalne rozróżnienie pojawia się w każdym projekcie obiektowym i warto je solidnie utrwalić.

8/50Obiekt jako wyrób

Obiekt to konkretna instancja klasy - tak jak ciasteczko upieczone z foremki. To właśnie na obiektach operuje program: wywołuje ich metody, odczytuje i modyfikuje ich atrybuty.

Każdy obiekt ma własne dane (atrybuty) i dostęp do metod zdefiniowanych w klasie. Jeżeli zmienisz atrybut w jednym obiekcie, inne obiekty tej samej klasy pozostają niezmienione - każdy ma swoją własną kopię danych.

W Pythonie obiekt tworzy się przez wywołanie klasy jak funkcji: nazwa_obiektu = NazwaKlasy() . W momencie wywołania Python alokuje pamięć dla nowego obiektu, uruchamia konstruktor __init__ (jeżeli istnieje) i zwraca referencję do nowego obiektu.

Możesz mieć wiele obiektów tej samej klasy, każdy z innymi wartościami atrybutów:

  • ciasteczko_1 = czekoladowe, z posypką
  • ciasteczko_2 = waniliowe, bez posypki
  • Oba są ciasteczkami - oba mają ten sam kształt (klasę), ale różnić się szczegółami (atrybutami)

Obiekty są bytami pierwszorzędnymi w Pythonie - możesz je przechowywać w zmiennych, listach, słownikach, przekazywać do funkcji, zwracać z funkcji, a nawet tworzyć ich dynamicznie w trakcie działania programu. W Pythonie wszystko jest obiektem - nawet liczby, napisy i funkcje.

Ciasteczka z foremki

Tworzenie obiektu przez wywołanie klasy to jeden z najważniejszych wzorców w Pythonie. Składnia NazwaKlasy() może być myląca dla początkujących, ponieważ wygląda jak wywołanie funkcji, ale w rzeczywistości uruchamia złożony proces alokacji i inicjalizacji. Python, jako język dynamiczny, pozwala na tworzenie obiektów w dowolnym momencie działania programu, w tym wewnątrz pętli, warunków czy wyrażeń listowych. Każde wywołanie klasy tworzy nowy, niezależny obiekt w pamięci, nawet jeżeli klasa nie ma zdefiniowanego konstruktora. Zmienna przechowuje referencję do obiektu, a nie sam obiekt, co ma kluczowe znaczenie dla zrozumienia mechanizmu przypisań i kopiowania.

Proces tworzenia obiektu składa się z kilku etapów: najpierw wywoływana jest metoda __new__, która alokuje pamięć dla nowej instancji, następnie uruchamiany jest __init__ do inicjalizacji atrybutów, a na końcu zwracana jest referencja do gotowego obiektu. Większość programistów modyfikuje wyłącznie __init__, pozostawiając __new__ w domyślnej implementacji. Zrozumienie tej kolejności jest ważne przy tworzeniu bardziej zaawansowanych wzorców, takich jak Singleton czy Fabryka. Gdy tworzymy wiele obiektów w pętli, każdy z nich jest niezależną instancją z własnym stanem i tożsamością.

9/50 Klasa a obiekt - zestawienie

Poniższa tabela zestawia najważniejsze różnice między klasą a obiektem. Zapamiętanie tych różnic to klucz do zrozumienia OOP:

CechaKlasaObiekt
DefinicjaSzablon / przepis / planKonkretna instancja / wyrób
Istnienie Istnieje w kodzie źródłowym Istnieje w pamięci RAM w trakcie działania programu
LiczbaJedna definicja w kodzieWiele egzemplarzy (potencjalnie miliony)
Dane Nie zawiera danych - tylko definicję struktury Zawiera konkretne wartości atrybutów
Tworzenie class Nazwa: - definiujemy raz nazwa = Nazwa() - tworzymy wielokrotnie
Przykład class Auto: - plan samochodu moje_auto = Auto() - konkretny samochód
Miejsce w pamięci Przechowuje definicje metod (wspólne dla wszystkich obiektów) Przechowuje własne wartości atrybutów (unikalne dla każdego obiektu)

W Pythonie klasa jest też obiektem (tak, klasa to również obiekt - typu type ), ale na razie nie musisz się tym przejmować. Na potrzeby tego kursu: klasa to szablon, obiekt to instancja.

Klasa a obiekt

Porównanie programowania proceduralnego i obiektowego na konkretnych przykładach pokazuje fundamentalne różnice w organizacji kodu. Podejście proceduralne koncentruje się na sekwencji operacji i funkcjach przetwarzających dane, podczas gdy obiektowe grupuje powiązane dane i funkcje w spójne jednostki zwane klasami. W praktyce zawodowej rzadko spotyka się czyste implementacje jednego paradygmatu - nowoczesne aplikacje łączą różne podejścia w zależności od potrzeb konkretnego modułu. Python, jako język wieloparadygmatowy, doskonale wspiera taki hybrydowy styl programowania, pozwalając programiście na elastyczny wybór narzędzia. Wybór paradygmatu zależy przede wszystkim od charakteru problemu - proste skrypty często lepiej napisać proceduralnie, a złożone systemy obiektowo.

Tabelaryczne zestawienie cech obu podejść ułatwia zrozumienie, kiedy które z nich jest bardziej odpowiednie. Programowanie proceduralne sprawdza się w małych, liniowych skryptach, gdzie ważna jest prostota i szybkość wykonania. OOP dominuje w dużych projektach, gdzie kluczowe są organizacja kodu, wielokrotne użycie i łatwość utrzymania. Warto również zauważyć, że wiele języków nowej generacji, takich jak Rust czy Kotlin, łączy elementy obu podejść, oferując programistom najlepsze cechy każdego z nich. Świadomy wybór paradygmatu jest oznaką dojrzałości programistycznej i pozwala na podejmowanie optymalnych decyzji projektowych w codziennej pracy.

10/50 Ćwiczenie intelektualne

Rozejrzyj się wokół siebie. Znajdź 5 obiektów, które możesz opisać przez cechy i zachowania. To ćwiczenie ma Cię nauczyć myślenia obiektowego - dostrzegania, że wszystko w otoczeniu można opisać jako byt ze stanem i zachowaniem.

Przykład: długopis - cechy: kolor ("niebieski"), grubość końcówki (0.7mm), poziom atramentu (75%), marka ("Pilot"); zachowania: pisz(tekst), zmień_kolor(nowy), wymień_atrament()

Twoja kolej:

  1. Kubek na biurku - atrybuty: pojemność, kolor, temperatura, poziom napełnienia; metody: nalej(plyn), wypij(ilosc), umyj()
  2. Telefon - atrybuty: marka, model, poziom_baterii, czy_wlaczony; metody: wlacz(), zadzwon(numer), zrob_zdjecie()
  3. Krzesło - atrybuty: liczba_nog, kolor, material, czy_zajete; metody: usiadz(), wstan(), zloz()
  4. Lampa - atrybuty: typ_zarowki, moc, wlaczona, jasnosc; metody: wlacz(), wylacz(), sciemnij(poziom)
  5. Okno - atrybuty: szerokosc, wysokosc, otwarte, material_ramy; metody: otworz(), zamknij()

Dla każdego określ 2-3 cechy (atrybuty) i 1-2 zachowania (metody). To myślenie obiektowe w czystej postaci - zanim napiszesz pierwszą linię kodu, musisz umieć myśleć w kategoriach obiektów.

Obiekty w otoczeniu

Ćwiczenia praktyczne są niezbędnym elementem nauki programowania, ponieważ pozwalają na bezpośrednie zastosowanie poznanej teorii. Samodzielne pisanie kodu, nawet prostych klas i obiektów, utrwala wiedzę znacznie skuteczniej niż bierne czytanie przykładów. Każde ćwiczenie w tym kursie zostało zaprojektowane tak, aby sprawdzić konkretne umiejętności i przygotować do bardziej złożonych zadań. Zaleca się wykonanie ćwiczenia bez podglądania rozwiązania, a dopiero potem porównanie własnego kodu z przykładowym. W przypadku trudności warto wrócić do odpowiedniego slajdu i przeanalizować omawiane na nim koncepcje jeszcze raz.

Nauka programowania przypomina naukę języków obcych - teoria jest ważna, ale to praktyka decyduje o biegłości. Im więcej samodzielnie napiszesz kodu, tym szybciej nabierzesz wprawy w myśleniu obiektowym. Eksperymentuj z przykładami, dodawaj nowe atrybuty i metody, testuj różne scenariusze użycia. Nie bój się błędów - każdy komunikat błędu to cenna lekcja, która przybliża Cię do mistrzostwa. Systematyczna praca i regularne programowanie są kluczem do sukcesu w nauce Pythona. Wykorzystaj REPL do szybkiego testowania hipotez i pomysłów.

11/50 Atrybuty - cechy obiektu

Atrybuty (zwane też polami lub właściwościami) przechowują dane opisujące stan obiektu. Są to zmienne, które należą do konkretnego obiektu - każdy obiekt ma swoją własną kopię atrybutów, niezależną od innych instancji.

Atrybuty definiujemy najczęściej w konstruktorze __init__ , ale Python pozwala też dodawać je dynamicznie po utworzeniu obiektu (choć to niezalecane). Atrybutem może być dowolny typ danych: liczba, napis, lista, słownik, a nawet inny obiekt.

Przykłady atrybutów dla różnych klas:

  • Student : imie (str), nazwisko (str), indeks (int), kierunek (str), oceny (list)
  • Samochod : marka (str), model (str), rok (int), predkosc (float), silnik_wlaczony (bool)
  • Ksiazka : tytul (str), autor (str), isbn (str), liczba_stron (int), wypozyczona (bool)

Dostęp do atrybutów uzyskujemy przez notację kropkową: obiekt.atrybut . Możemy zarówno odczytywać, jak i przypisywać nowe wartości:

s = Student()
s.imie = "Jan"         # przypisanie wartości
print(s.imie)          # odczytanie wartości - Jan
s.oceny = [4.5, 5.0, 3.5]  # atrybut może być listą
Atrybuty obiektu

Atrybuty są podstawowym mechanizmem przechowywania stanu obiektu i mogą zawierać dane dowolnego typu dostępnego w Pythonie. Każdy obiekt ma własną, niezależną kopię atrybutów zdefiniowanych w konstruktorze, co oznacza, że zmiana wartości w jednej instancji nie wpływa na pozostałe. W Pythonie atrybuty są przechowywane w słowniku __dict__ każdego obiektu, co umożliwia dynamiczne dodawanie i usuwanie atrybutów w czasie wykonania. Chociaż dynamiczne dodawanie atrybutów jest możliwe, w praktyce zaleca się definiowanie wszystkich atrybutów w konstruktorze __init__ dla zachowania przewidywalności kodu. Taki nawyk sprawia, że struktura obiektu jest jasna dla każdego programisty czytającego definicję klasy. Ponadto wiele narzędzi do statycznej analizy kodu wymaga jawnego deklarowania atrybutów.

Konstruktor __init__ jest wywoływany automatycznie po utworzeniu obiektu przez metodę __new__, która alokuje pamięć dla nowej instancji. Zadaniem __init__ jest przygotowanie obiektu do użycia poprzez ustawienie początkowych wartości atrybutów i wykonanie ewentualnych czynności inicjalizacyjnych. W przeciwieństwie do niektórych innych języków, Python nie wspiera przeciążania konstruktorów - zamiast tego stosuje się parametry opcjonalne z wartościami domyślnymi oraz wzorzec fabryki. Parametr self, obowiązkowy w każdej metodzie instancyjnej, jest referencją do konkretnego obiektu, na którym wywołano metodę. Dzięki self metoda ma dostęp do wszystkich atrybutów i innych metod danego obiektu.

12/50 Metody - zachowania obiektu

Metody to funkcje zdefiniowane wewnątrz klasy, które operują na danych obiektu. Określają, co obiekt potrafi zrobić - jakie ma zachowania. Metody mają dostęp do atrybutów obiektu przez parametr self i mogą je odczytywać, modyfikować, a także wywoływać inne metody tego samego obiektu.

Przykłady metod:

  • Student.przedstaw_sie()- wypisuje dane studenta, korzystając z jego atrybutów
  • Samochod.przyspiesz(wartosc) - zmienia prędkość samochodu, modyfikując atrybut predkosc
  • Ksiazka.czy_gruba() - zwraca True, jeżeli liczba stron > 400, wykonując logiczną operację na danych
  • Konto.wplac(kwota)- zwiększa saldo konta po weryfikacji poprawności kwoty

Metody zawsze przyjmują self jako pierwszy argument - to odnośnik do konkretnego obiektu. Gdy wywołujesz obiekt.metoda() , Python automatycznie przekazuje obiekt jako self , więc nie podajesz go jawnie w wywołaniu.

Metody mogą:

  • zwracać wartość (przez return) - jak funkcje
  • mieć parametry (oprócz self) - np.ustaw_wiek(self, nowy_wiek)
  • mieć wartości domyślne parametrów - np.przyspiesz(self, wartosc=5)
  • wywoływać inne metody tego samego obiektu - przez self.inna_metoda()
Metody obiektu

Metody są funkcjami zdefiniowanymi w przestrzeni nazw klasy, które otrzymują automatycznie pierwszy argument w postaci referencji do obiektu. Dzięki temu mogą odczytywać i modyfikować stan obiektu, na którym zostały wywołane. W Pythonie istnieje kilka rodzajów metod: instancyjne (z self), klasowe (z cls i dekoratorem @classmethod), statyczne (z @staticmethod) oraz abstrakcyjne (z @abstractmethod w klasach ABC). Metody instancyjne są najczęściej używane i to one będą głównym tematem tej części kursu. Każda metoda instancyjna przyjmuje self jako pierwszy parametr, a Python automatycznie przekazuje obiekt w momencie wywołania przez notację kropkową. Metody mogą przyjmować dodatkowe argumenty, mieć wartości domyślne i zwracać wartości za pomocą instrukcji return.

Dobrą praktyką projektowania metod jest zasada pojedynczej odpowiedzialności - każda metoda powinna wykonywać jedną, dobrze określoną operację. Metody tylko do odczytu stanu, nazywane getterami, nie powinny modyfikować atrybutów obiektu. Metody modyfikujące stan, nazywane setterami, powinny walidować dane wejściowe przed wprowadzeniem zmian. Przestrzeganie tych zasad prowadzi do kodu łatwiejszego w testowaniu i debugowaniu. Warto również stosować metody pomocnicze, aby uniknąć powielania kodu wewnątrz klasy. Dobrze zaprojektowane metody sprawiają, że klasa jest intuicyjna w użyciu i trudna do niepoprawnego wykorzystania.

13/50 Najprostsza klasa w Pythonie

Najmniejsza możliwa klasa w Pythonie to klasa pusta - taka, która nie ma ani atrybutów, ani metod. Mimo swojej prostoty jest poprawna składniowo i można tworzyć jej obiekty.

Przeanalizujmy poniższy kod:

class Pusta:
    pass  # nic nie robi, ale klasa jest poprawna składniowo

# Możemy tworzyć obiekty pustej klasy
p1 = Pusta()
p2 = Pusta()

# Sprawdźmy typ
print(type(p1))  # <class '__main__.Pusta'>
print(type(p2))  # <class '__main__.Pusta'>

# Każdy obiekt ma unikalny identyfikator
print(id(p1))     # np. 2387612345678
print(id(p2))     # inny numer

Słowo pass jest potrzebne, bo Python wymaga przynajmniej jednej instrukcji w bloku klasy. pass to instrukcja pusta - nie robi nic, ale spełnia wymóg składniowy. Klasa Pusta nie ma ani atrybutów, ani metod - to tylko szkielet, który dziedziczy po object (domyślna klasa bazowa w Pythonie 3).

Mimo pustej definicji, obiekty tej klasy mają już kilka domyślnych metod odziedziczonych po object : __str__ , __repr__ , __eq__ , __hash__ i inne. Możesz je wywołać, choć ich domyślne implementacje nie są zbyt użyteczne.

Pusta klasa

Klasa pusta z samym pass to najprostszy możliwy przykład ilustrujący podstawową składnię klas w Pythonie. Nawet tak minimalna definicja jest poprawną klasą, z której można tworzyć obiekty. Każda klasa w Pythonie 3 domyślnie dziedziczy po object, co zapewnia zestaw podstawowych metod specjalnych, takich jak __str__, __repr__, __eq__ czy __new__. Obiekty pustej klasy mają unikalny identyfikator w pamięci, a operator is pozwala sprawdzić ich tożsamość. Mimo że praktyczne znaczenie pustych klas jest ograniczone, stanowią one dobry punkt wyjścia do zrozumienia mechanizmów tworzenia i zarządzania obiektami.

Słowo kluczowe pass jest potrzebne, ponieważ Python wymaga, aby każdy blok kodu zawierał co najmniej jedną instrukcję. W przypadku klas, funkcji czy pętli, pass spełnia ten wymóg bez wykonywania jakiejkolwiek operacji. Jest to przydatne podczas szkicowania struktury projektu, gdy chcemy zdefiniować szkielet klasy bez implementacji jej wnętrza. Pusta klasa może być później rozszerzana o atrybuty, metody i bardziej zaawansowane mechanizmy. Wzorzec ten jest często używany w fazie projektowania, gdy najpierw definiuje się strukturę klas, a dopiero potem implementuje ich zachowania.

14/50 Tworzenie obiektu (instancji)

Obiekt tworzymy przez wywołanie klasy jak funkcji - używamy nawiasów okrągłych, tak jak przy wywoływaniu funkcji. To wywołanie uruchamia proces tworzenia instancji.

Przeanalizujmy poniższy kod:

class Pusta:
    pass

# Tworzymy obiekt klasy Pusta
moj_obiekt = Pusta()

# Sprawdzamy typ
print(type(moj_obiekt))  # <class '__main__.Pusta'>

# Możemy tworzyć dowolnie wiele instancji
obj_a = Pusta()
obj_b = Pusta()
obj_c = Pusta()

# Każda instancja to osobny byt w pamięci
print(obj_a is obj_b)  # False - różne obiekty

# Obiekty można też tworzyć dynamicznie w pętli
lista_obiektow = [Pusta() for _ in range(5)]
print(len(lista_obiektow))  # 5

Zmienna moj_obiekt przechowuje referencję do nowo utworzonego obiektu w pamięci, a nie sam obiekt. Gdy przypisujesz a = Pusta() , a potem b = a , obie zmienne wskazują na ten sam obiekt.

Każde wywołanie klasy tworzy nowy, niezależny obiekt. Nawet jeżeli klasa nie ma konstruktora, Python i tak alokuje pamięć i zwraca nową instancję.

Tworzenie obiektu

Atrybuty są podstawowym mechanizmem przechowywania stanu obiektu i mogą zawierać dane dowolnego typu dostępnego w Pythonie. Każdy obiekt ma własną, niezależną kopię atrybutów zdefiniowanych w konstruktorze, co oznacza, że zmiana wartości w jednej instancji nie wpływa na pozostałe. W Pythonie atrybuty są przechowywane w słowniku __dict__ każdego obiektu, co umożliwia dynamiczne dodawanie i usuwanie atrybutów w czasie wykonania. Chociaż dynamiczne dodawanie atrybutów jest możliwe, w praktyce zaleca się definiowanie wszystkich atrybutów w konstruktorze __init__ dla zachowania przewidywalności kodu. Taki nawyk sprawia, że struktura obiektu jest jasna dla każdego programisty czytającego definicję klasy. Ponadto wiele narzędzi do statycznej analizy kodu wymaga jawnego deklarowania atrybutów.

Konstruktor __init__ jest wywoływany automatycznie po utworzeniu obiektu przez metodę __new__, która alokuje pamięć dla nowej instancji. Zadaniem __init__ jest przygotowanie obiektu do użycia poprzez ustawienie początkowych wartości atrybutów i wykonanie ewentualnych czynności inicjalizacyjnych. W przeciwieństwie do niektórych innych języków, Python nie wspiera przeciążania konstruktorów - zamiast tego stosuje się parametry opcjonalne z wartościami domyślnymi oraz wzorzec fabryki. Parametr self, obowiązkowy w każdej metodzie instancyjnej, jest referencją do konkretnego obiektu, na którym wywołano metodę. Dzięki self metoda ma dostęp do wszystkich atrybutów i innych metod danego obiektu.

15/50 Dodawanie atrybutów "na żywo"

Python, jako język dynamiczny, pozwala dodawać atrybuty do obiektu nawet poza klasą, w dowolnym momencie działania programu. To cecha, która odróżnia go od języków statycznych jak Java czy C++.

Przeanalizujmy poniższy kod:

class Pusta:
    pass

p1 = Pusta()
p2 = Pusta()

# Dodajemy atrybuty tylko do p1
p1.imie = "Anna"
p1.wiek = 22
p1.aktywny = True
p1.ulubione_liczby = [7, 13, 42]

print(p1.imie)                # Anna
print(p1.wiek)                # 22
print(p1.ulubione_liczby)     # [7, 13, 42]
print(vars(p1))              # {'imie': 'Anna', 'wiek': 22, ...}

# print(p2.imie)  # AttributeError! - p2 nie ma atrybutu 'imie'

Każdy obiekt ma własną, niezależną przestrzeń atrybutów ( __dict__ ). Możesz w dowolnej chwili dodać nowy atrybut do dowolnego obiektu - nie wpływa to na inne instancje tej samej klasy.

To wygodne, ale niezalecane w praktyce - lepiej definiować wszystkie atrybuty w konstruktorze __init__ , nawet jeżeli początkowo mają wartość None . Dlaczego? Bo kod staje się przewidywalny - wiesz, jakie atrybuty ma każdy obiekt, patrząc tylko na definicję klasy.

Atrybuty na żywo

Atrybuty są podstawowym mechanizmem przechowywania stanu obiektu i mogą zawierać dane dowolnego typu dostępnego w Pythonie. Każdy obiekt ma własną, niezależną kopię atrybutów zdefiniowanych w konstruktorze, co oznacza, że zmiana wartości w jednej instancji nie wpływa na pozostałe. W Pythonie atrybuty są przechowywane w słowniku __dict__ każdego obiektu, co umożliwia dynamiczne dodawanie i usuwanie atrybutów w czasie wykonania. Chociaż dynamiczne dodawanie atrybutów jest możliwe, w praktyce zaleca się definiowanie wszystkich atrybutów w konstruktorze __init__ dla zachowania przewidywalności kodu. Taki nawyk sprawia, że struktura obiektu jest jasna dla każdego programisty czytającego definicję klasy. Ponadto wiele narzędzi do statycznej analizy kodu wymaga jawnego deklarowania atrybutów.

Konstruktor __init__ jest wywoływany automatycznie po utworzeniu obiektu przez metodę __new__, która alokuje pamięć dla nowej instancji. Zadaniem __init__ jest przygotowanie obiektu do użycia poprzez ustawienie początkowych wartości atrybutów i wykonanie ewentualnych czynności inicjalizacyjnych. W przeciwieństwie do niektórych innych języków, Python nie wspiera przeciążania konstruktorów - zamiast tego stosuje się parametry opcjonalne z wartościami domyślnymi oraz wzorzec fabryki. Parametr self, obowiązkowy w każdej metodzie instancyjnej, jest referencją do konkretnego obiektu, na którym wywołano metodę. Dzięki self metoda ma dostęp do wszystkich atrybutów i innych metod danego obiektu.

16/50 Konstruktor __init__

Metoda __init__ (inicjalizator, potocznie konstruktor) jest wywoływana automatycznie tuż po utworzeniu obiektu, w momencie wywołania klasy. To tutaj ustawiamy początkowe wartości atrybutów i wykonujemy wszelkie czynności przygotowawcze.

Przeanalizujmy poniższy kod:

class Student:
    def __init__(self, imie, wiek, kierunek="Informatyka"):
        self.imie = imie
        self.wiek = wiek
        self.kierunek = kierunek
        self.oceny = []         # domyślnie pusta lista
        self.aktywny = True    # domyślna wartość logiczna

s1 = Student("Jan", 20)
s2 = Student("Anna", 22, "Matematyka")

print(s1.imie)       # Jan
print(s1.kierunek)   # Informatyka (wartość domyślna)
print(s2.kierunek)   # Matematyka
print(s1.oceny)      # []

__init__ nie zwraca wartości (zwraca None ). Jego zadaniem jest przygotowanie obiektu do użycia - ustawienie wszystkich atrybutów w znany, spójny stan. Jeżeli atrybut ma wartość domyślną, podajemy ją bezpośrednio w ciele metody (jak self.oceny = [] ).

Nazwa __init__ to jedna z wielu "magicznych metod" (dunder methods) w Pythonie. Są to metody o specjalnym znaczeniu, rozpoznawane przez Python przez podwójne podkreślenia z obu stron. Nie wywołujemy ich bezpośrednio - Python robi to automatycznie w odpowiednim momencie (tu: przy tworzeniu obiektu).

Uwaga: W Pythonie prawdziwym konstruktorem jest __new__ , ale __init__ jest tym, czego używasz w 99% przypadków. __new__ tworzy obiekt, __init__ go inicjalizuje. Nie musisz implementować __new__ , chyba że tworzysz wzorzec Singleton lub dziedziczysz po typach niemutowalnych.
Konstruktor

Atrybuty są podstawowym mechanizmem przechowywania stanu obiektu i mogą zawierać dane dowolnego typu dostępnego w Pythonie. Każdy obiekt ma własną, niezależną kopię atrybutów zdefiniowanych w konstruktorze, co oznacza, że zmiana wartości w jednej instancji nie wpływa na pozostałe. W Pythonie atrybuty są przechowywane w słowniku __dict__ każdego obiektu, co umożliwia dynamiczne dodawanie i usuwanie atrybutów w czasie wykonania. Chociaż dynamiczne dodawanie atrybutów jest możliwe, w praktyce zaleca się definiowanie wszystkich atrybutów w konstruktorze __init__ dla zachowania przewidywalności kodu. Taki nawyk sprawia, że struktura obiektu jest jasna dla każdego programisty czytającego definicję klasy. Ponadto wiele narzędzi do statycznej analizy kodu wymaga jawnego deklarowania atrybutów.

Konstruktor __init__ jest wywoływany automatycznie po utworzeniu obiektu przez metodę __new__, która alokuje pamięć dla nowej instancji. Zadaniem __init__ jest przygotowanie obiektu do użycia poprzez ustawienie początkowych wartości atrybutów i wykonanie ewentualnych czynności inicjalizacyjnych. W przeciwieństwie do niektórych innych języków, Python nie wspiera przeciążania konstruktorów - zamiast tego stosuje się parametry opcjonalne z wartościami domyślnymi oraz wzorzec fabryki. Parametr self, obowiązkowy w każdej metodzie instancyjnej, jest referencją do konkretnego obiektu, na którym wywołano metodę. Dzięki self metoda ma dostęp do wszystkich atrybutów i innych metod danego obiektu.

17/50Parametr self

self to pierwszy parametr każdej metody instancji. Odnosi się do konkretnego obiektu , na którym wywołano metodę. Bez self metoda nie miałaby dostępu do atrybutów obiektu ani do innych jego metod.

Gdy piszesz s.przedstaw_sie() , Python automatycznie przekazuje s jako pierwszy argument ( self ) metody przedstaw_sie . To dzieje się "pod maską" - nie widzisz tego w kodzie wywołania.

Przeanalizujmy poniższy kod:

class Demo:
    def __init__(self, nazwa):
        self.nazwa = nazwa  # self = ten konkretny obiekt

    def pokaz(self):
        print(f"Nazywam si\u0119 {self.nazwa}")

    def zmien_nazwe(self, nowa_nazwa):
        self.nazwa = nowa_nazwa  # modyfikujemy atrybut przez self

a = Demo("A")
b = Demo("B")
a.pokaz()          # self to a - "Nazywam się A"
b.pokaz()          # self to b - "Nazywam się B"

a.zmien_nazwe("Alfa")
a.pokaz()          # "Nazywam się Alfa" - zmiana tylko w obiekcie a
b.pokaz()          # "Nazywam się B" - b bez zmian

Nazwa self to konwencja , nie wymóg języka. Możesz użyć dowolnej innej nazwy (np. this , moj , obiekt ), ale nie powinieneś - cała społeczność Pythonowa używa self , a odstępstwo od tej konwencji wprowadzi chaos i zdziwi innych programistów czytających Twój kod. PEP 8 jasno zaleca używanie self .

Parametr self

Atrybuty są podstawowym mechanizmem przechowywania stanu obiektu i mogą zawierać dane dowolnego typu dostępnego w Pythonie. Każdy obiekt ma własną, niezależną kopię atrybutów zdefiniowanych w konstruktorze, co oznacza, że zmiana wartości w jednej instancji nie wpływa na pozostałe. W Pythonie atrybuty są przechowywane w słowniku __dict__ każdego obiektu, co umożliwia dynamiczne dodawanie i usuwanie atrybutów w czasie wykonania. Chociaż dynamiczne dodawanie atrybutów jest możliwe, w praktyce zaleca się definiowanie wszystkich atrybutów w konstruktorze __init__ dla zachowania przewidywalności kodu. Taki nawyk sprawia, że struktura obiektu jest jasna dla każdego programisty czytającego definicję klasy. Ponadto wiele narzędzi do statycznej analizy kodu wymaga jawnego deklarowania atrybutów.

Konstruktor __init__ jest wywoływany automatycznie po utworzeniu obiektu przez metodę __new__, która alokuje pamięć dla nowej instancji. Zadaniem __init__ jest przygotowanie obiektu do użycia poprzez ustawienie początkowych wartości atrybutów i wykonanie ewentualnych czynności inicjalizacyjnych. W przeciwieństwie do niektórych innych języków, Python nie wspiera przeciążania konstruktorów - zamiast tego stosuje się parametry opcjonalne z wartościami domyślnymi oraz wzorzec fabryki. Parametr self, obowiązkowy w każdej metodzie instancyjnej, jest referencją do konkretnego obiektu, na którym wywołano metodę. Dzięki self metoda ma dostęp do wszystkich atrybutów i innych metod danego obiektu.

18/50 Definiowanie metody w klasie

Metody definiujemy wewnątrz klasy za pomocą def - tak samo jak zwykłe funkcje, ale pierwszym parametrem jest self . Poza tym mogą przyjmować dowolną liczbę argumentów, mieć wartości domyślne, zwracać wartości - wszystko jak w zwykłych funkcjach.

Przeanalizujmy poniższy kod:

class Przyklad:
    def __init__(self, wartosc):
        self.wartosc = wartosc
        self.historia = [wartosc]  # lista zmian

    def pomnoz(self, czynnik):
        return self.wartosc * czynnik  # metoda tylko do odczytu

    def ustaw(self, nowa):
        self.wartosc = nowa            # metoda modyfikująca stan
        self.historia.append(nowa)     # zapisujemy zmianę

    def pobierz_historie(self):
        return self.historia[:]         # zwracamy kopię dla bezpieczeństwa

p = Przyklad(10)
print(p.pomnoz(3))        # 30 - metoda nie zmienia stanu
p.ustaw(7)                 # zmienia stan
print(p.pomnoz(3))        # 21 - teraz 7 * 3
print(p.pobierz_historie())  # [10, 7]

Metody mogą przyjmować argumenty, zwracać wartości i modyfikować stan obiektu. Ważne rozróżnienie: metody, które tylko odczytują stan (jak pomnoz ), nie zmieniają obiektu - takie metody nazywamy często "getterami" lub metodami tylko do odczytu. Metody, które zmieniają atrybuty (jak ustaw ), nazywamy "setterami" lub metodami modyfikującymi.

Definiowanie metody

Metody są funkcjami zdefiniowanymi w przestrzeni nazw klasy, które otrzymują automatycznie pierwszy argument w postaci referencji do obiektu. Dzięki temu mogą odczytywać i modyfikować stan obiektu, na którym zostały wywołane. W Pythonie istnieje kilka rodzajów metod: instancyjne (z self), klasowe (z cls i dekoratorem @classmethod), statyczne (z @staticmethod) oraz abstrakcyjne (z @abstractmethod w klasach ABC). Metody instancyjne są najczęściej używane i to one będą głównym tematem tej części kursu. Każda metoda instancyjna przyjmuje self jako pierwszy parametr, a Python automatycznie przekazuje obiekt w momencie wywołania przez notację kropkową. Metody mogą przyjmować dodatkowe argumenty, mieć wartości domyślne i zwracać wartości za pomocą instrukcji return.

Dobrą praktyką projektowania metod jest zasada pojedynczej odpowiedzialności - każda metoda powinna wykonywać jedną, dobrze określoną operację. Metody tylko do odczytu stanu, nazywane getterami, nie powinny modyfikować atrybutów obiektu. Metody modyfikujące stan, nazywane setterami, powinny walidować dane wejściowe przed wprowadzeniem zmian. Przestrzeganie tych zasad prowadzi do kodu łatwiejszego w testowaniu i debugowaniu. Warto również stosować metody pomocnicze, aby uniknąć powielania kodu wewnątrz klasy. Dobrze zaprojektowane metody sprawiają, że klasa jest intuicyjna w użyciu i trudna do niepoprawnego wykorzystania.

19/50 Wywoływanie metody na obiekcie

Metodę wywołujemy przez notację kropkową: obiekt.metoda(argumenty) . Python w tle przekazuje obiekt jako pierwszy argument ( self ), więc liczba argumentów w wywołaniu jest o jeden mniejsza niż w definicji metody.

Przeanalizujmy poniższy kod:

class Licznik:
    def __init__(self, start=0):
        self.wartosc = start

    def inkrementuj(self, o_ile=1):
        self.wartosc += o_ile

    def pokaz(self):
        print(f"Aktualna warto\u015b\u0107: {self.wartosc}")

    def zresetuj(self):
        self.wartosc = 0

licz = Licznik(10)
licz.pokaz()           # Aktualna wartość: 10
licz.inkrementuj()     # +1 (domyślnie)
licz.inkrementuj(5)   # +5
licz.pokaz()           # Aktualna wartość: 16
licz.zresetuj()
licz.pokaz()           # Aktualna wartość: 0

# Wywołanie metody bez nawiasów zwraca referencję do metody, a nie wynik
ref_do_metody = licz.pokaz  # to nie wywołuje metody!
ref_do_metody()             # to dopiero wywołuje

Wywołanie metody to wysłanie komunikatu do obiektu: "wykonaj swoją operację". Obiekt odpowiada, wykonując odpowiedni kod i opcjonalnie zwracając wynik.

Ważne: pominięcie nawiasów () przy wywołaniu metody nie wywołuje jej - zwraca obiekt metody (bound method), który możesz wywołać później. To przydaje się np. przy callbackach.

Wywołanie metody

Metody są funkcjami zdefiniowanymi w przestrzeni nazw klasy, które otrzymują automatycznie pierwszy argument w postaci referencji do obiektu. Dzięki temu mogą odczytywać i modyfikować stan obiektu, na którym zostały wywołane. W Pythonie istnieje kilka rodzajów metod: instancyjne (z self), klasowe (z cls i dekoratorem @classmethod), statyczne (z @staticmethod) oraz abstrakcyjne (z @abstractmethod w klasach ABC). Metody instancyjne są najczęściej używane i to one będą głównym tematem tej części kursu. Każda metoda instancyjna przyjmuje self jako pierwszy parametr, a Python automatycznie przekazuje obiekt w momencie wywołania przez notację kropkową. Metody mogą przyjmować dodatkowe argumenty, mieć wartości domyślne i zwracać wartości za pomocą instrukcji return.

Dobrą praktyką projektowania metod jest zasada pojedynczej odpowiedzialności - każda metoda powinna wykonywać jedną, dobrze określoną operację. Metody tylko do odczytu stanu, nazywane getterami, nie powinny modyfikować atrybutów obiektu. Metody modyfikujące stan, nazywane setterami, powinny walidować dane wejściowe przed wprowadzeniem zmian. Przestrzeganie tych zasad prowadzi do kodu łatwiejszego w testowaniu i debugowaniu. Warto również stosować metody pomocnicze, aby uniknąć powielania kodu wewnątrz klasy. Dobrze zaprojektowane metody sprawiają, że klasa jest intuicyjna w użyciu i trudna do niepoprawnego wykorzystania.

20/50 Kod z życia: klasa Student

Przyjrzyjmy się teraz bardziej realistycznej klasie - modelującej studenta w systemie uczelnianym. Klasa zawiera zarówno atrybuty opisujące stan, jak i metody do manipulowania tym stanem.

Przeanalizujmy poniższy kod:

class Student:
    def __init__(self, imie, kierunek, rok=1):
        self.imie = imie
        self.kierunek = kierunek
        self.rok = rok
        self.aktywny = True
        self.ects = 0

    def awansuj(self):
        self.rok += 1
        print(f"{self.imie} jest teraz na {self.rok}. roku")

    def dodaj_ects(self, punkty):
        self.ects += punkty
        return self.ects

    def przedstaw_sie(self):
        return (f"{self.imie}, kierunek: {self.kierunek}, "
                f"rok {self.rok}, ECTS: {self.ects}")

s1 = Student("Kasia", "Informatyka")
s2 = Student("Tomek", "Matematyka", 2)

s1.dodaj_ects(30)
s1.dodaj_ects(25)
s1.awansuj()
print(s1.przedstaw_sie())
print(s2.przedstaw_sie())

Każdy student ma własne imię, kierunek, rok, pulę ECTS i status aktywności - to stan obiektu. Metody pozwalają zmieniać ten stan w kontrolowany sposób. Zwróć uwagę, że s2 ma rok 2 (przekazany w konstruktorze), podczas gdy s1 zaczął od roku 1 (wartość domyślna) i awansował.

Klasa Student

Slajd zatytułowany "Kod z życia: klasa Student" przedstawia istotne zagadnienie z zakresu programowania obiektowego w Pythonie. Zrozumienie przedstawionych tu koncepcji jest niezbędne do dalszej nauki i praktycznego stosowania OOP w codziennej pracy programisty. Zaleca się dokładne przeanalizowanie przykładów kodu i samodzielne ich przetestowanie w środowisku REPL lub edytorze. Warto również zwrócić uwagę na powiązania między tym tematem a innymi zagadnieniami omawianymi w kursie. Systematyczne budowanie wiedzy krok po kroku to klucz do opanowania programowania obiektowego w Pythonie. Nie pomijaj żadnego slajdu, nawet jeśli wydaje Ci się, że temat jest Ci już znany - powtórka utrwala wiedzę i pozwala dostrzec nowe szczegóły w znajomym materiale.

Zachęcamy do samodzielnego eksperymentowania z omawianymi mechanizmami i modyfikowania przykładowego kodu. Praktyczne ćwiczenia i własne projekty to najskuteczniejsza metoda nauki programowania, ponieważ wymagają aktywnego stosowania wiedzy. Pamiętaj, że błędy są naturalną częścią procesu uczenia się i każda pomyłka przybliża Cię do mistrzostwa. Warto prowadzić własny dziennik błędów, w którym zapisujesz napotkane problemy i ich rozwiązania. Taki zeszyt stanie się z czasem bezcennym źródłem wiedzy i referencją na przyszłość. Korzystaj z dokumentacji oficjalnej Pythona oraz społeczności programistycznych - to nieocenione źródła wiedzy i wsparcia w trudnych momentach.

21/50 Uruchamianie kodu - REPL

Python REPL (Read-Eval-Print Loop) to interaktywna konsola, w której możesz testować kod na bieżąco - wpisujesz instrukcję, Python ją wykonuje i od razu pokazuje wynik. To doskonałe narzędzie do eksperymentowania z OOP bez tworzenia plików.

Przeanalizujmy poniższy kod (sesja REPL):

>>> class Student:
...     def __init__(self, imie):
...         self.imie = imie
...     def przedstaw_sie(self):
...         return f"Jestem {self.imie}"
...
>>> s = Student("Ola")
>>> s.imie
'Ola'
>>> type(s)
<class '__main__.Student'>
>>> print(s)
<__main__.Student object at 0x0000023A...>
>>> s.przedstaw_sie()
'Jestem Ola'
>>> print(vars(s))
{'imie': 'Ola'}
>>> print(dir(s))
['__class__', '__delattr__', ..., 'imie', 'przedstaw_sie']

Uruchom Python w terminalu ( python lub py w Windows) i eksperymentuj samodzielnie! REPL daje natychmiastową informację zwrotną - to najszybszy sposób na naukę. Funkcja dir(obiekt) pokazuje wszystkie atrybuty i metody dostępne na obiekcie, a vars(obiekt) wyświetla słownik atrybutów. Używaj tych narzędzi do debugowania i eksploracji.

REPL Python

Slajd zatytułowany "Uruchamianie kodu - REPL" przedstawia istotne zagadnienie z zakresu programowania obiektowego w Pythonie. Zrozumienie przedstawionych tu koncepcji jest niezbędne do dalszej nauki i praktycznego stosowania OOP w codziennej pracy programisty. Zaleca się dokładne przeanalizowanie przykładów kodu i samodzielne ich przetestowanie w środowisku REPL lub edytorze. Warto również zwrócić uwagę na powiązania między tym tematem a innymi zagadnieniami omawianymi w kursie. Systematyczne budowanie wiedzy krok po kroku to klucz do opanowania programowania obiektowego w Pythonie. Nie pomijaj żadnego slajdu, nawet jeśli wydaje Ci się, że temat jest Ci już znany - powtórka utrwala wiedzę i pozwala dostrzec nowe szczegóły w znajomym materiale.

Zachęcamy do samodzielnego eksperymentowania z omawianymi mechanizmami i modyfikowania przykładowego kodu. Praktyczne ćwiczenia i własne projekty to najskuteczniejsza metoda nauki programowania, ponieważ wymagają aktywnego stosowania wiedzy. Pamiętaj, że błędy są naturalną częścią procesu uczenia się i każda pomyłka przybliża Cię do mistrzostwa. Warto prowadzić własny dziennik błędów, w którym zapisujesz napotkane problemy i ich rozwiązania. Taki zeszyt stanie się z czasem bezcennym źródłem wiedzy i referencją na przyszłość. Korzystaj z dokumentacji oficjalnej Pythona oraz społeczności programistycznych - to nieocenione źródła wiedzy i wsparcia w trudnych momentach.

22/50 Mapa myśli: klasa - obiekt
KLASA
class Nazwa: def __init__ atrybuty (wzór) metody (wzór) self
OBIEKT (instancja)
stan (wartości) tożsamość (id) zachowanie (metody) id() / is

Klasa to definicja - istnieje w kodzie źródłowym, określa strukturę. Obiekt to konkretyzacja - istnieje w pamięci podczas działania programu, ma własne wartości atrybutów. Z jednej klasy powstaje wiele obiektów, każdy z unikalnym stanem i tożsamością.

Kluczowa różnica: klasa definiuje co i jak , obiekt przechowuje jakie wartości . Gdy piszesz kod, pracujesz z klasami i obiektami - tworzysz klasy jako szablony, a potem operujesz na obiektach jako konkretnych instancjach.

Mapa myśli

Tworzenie obiektu przez wywołanie klasy to jeden z najważniejszych wzorców w Pythonie. Składnia NazwaKlasy() może być myląca dla początkujących, ponieważ wygląda jak wywołanie funkcji, ale w rzeczywistości uruchamia złożony proces alokacji i inicjalizacji. Python, jako język dynamiczny, pozwala na tworzenie obiektów w dowolnym momencie działania programu, w tym wewnątrz pętli, warunków czy wyrażeń listowych. Każde wywołanie klasy tworzy nowy, niezależny obiekt w pamięci, nawet jeżeli klasa nie ma zdefiniowanego konstruktora. Zmienna przechowuje referencję do obiektu, a nie sam obiekt, co ma kluczowe znaczenie dla zrozumienia mechanizmu przypisań i kopiowania.

Proces tworzenia obiektu składa się z kilku etapów: najpierw wywoływana jest metoda __new__, która alokuje pamięć dla nowej instancji, następnie uruchamiany jest __init__ do inicjalizacji atrybutów, a na końcu zwracana jest referencja do gotowego obiektu. Większość programistów modyfikuje wyłącznie __init__, pozostawiając __new__ w domyślnej implementacji. Zrozumienie tej kolejności jest ważne przy tworzeniu bardziej zaawansowanych wzorców, takich jak Singleton czy Fabryka. Gdy tworzymy wiele obiektów w pętli, każdy z nich jest niezależną instancją z własnym stanem i tożsamością.

23/50 Odczyt atrybutów obiektu

Atrybuty odczytujemy przez notację kropkową - to najprostszy i najczęściej używany sposób. Python udostępnia też funkcje wbudowane do bezpiecznego odczytu i eksploracji atrybutów.

Przeanalizujmy poniższy kod:

class Auto:
    def __init__(self, marka, model, rok=2024):
        self.marka = marka
        self.model = model
        self.rok = rok

a = Auto("Toyota", "Corolla")

# Sposób 1: notacja kropkowa (najczęstszy)
print(a.marka)    # Toyota
print(a.model)    # Corolla

# Sposób 2: getattr - bezpieczny odczyt z wartością domyślną
print(getattr(a, "model"))           # Corolla
print(getattr(a, "kolor", "brak"))    # brak (nie ma atrybutu)

# Sposób 3: vars() - wszystkie atrybuty jako słownik
print(vars(a))  # {"marka": "Toyota", "model": "Corolla", "rok": 2024}

# Sposób 4: hasattr - sprawdzenie czy atrybut istnieje
print(hasattr(a, "marka"))  # True
print(hasattr(a, "kolor"))  # False

vars() zwraca słownik __dict__ obiektu - wszystkie atrybuty instancji jako pary klucz-wartość. To bardzo przydatne narzędzie do debugowania i introspekcji. getattr z wartością domyślną pozwala uniknąć wyjątku AttributeError , gdy nie jesteś pewien, czy atrybut istnieje.

Odczyt atrybutów

Atrybuty są podstawowym mechanizmem przechowywania stanu obiektu i mogą zawierać dane dowolnego typu dostępnego w Pythonie. Każdy obiekt ma własną, niezależną kopię atrybutów zdefiniowanych w konstruktorze, co oznacza, że zmiana wartości w jednej instancji nie wpływa na pozostałe. W Pythonie atrybuty są przechowywane w słowniku __dict__ każdego obiektu, co umożliwia dynamiczne dodawanie i usuwanie atrybutów w czasie wykonania. Chociaż dynamiczne dodawanie atrybutów jest możliwe, w praktyce zaleca się definiowanie wszystkich atrybutów w konstruktorze __init__ dla zachowania przewidywalności kodu. Taki nawyk sprawia, że struktura obiektu jest jasna dla każdego programisty czytającego definicję klasy. Ponadto wiele narzędzi do statycznej analizy kodu wymaga jawnego deklarowania atrybutów.

Konstruktor __init__ jest wywoływany automatycznie po utworzeniu obiektu przez metodę __new__, która alokuje pamięć dla nowej instancji. Zadaniem __init__ jest przygotowanie obiektu do użycia poprzez ustawienie początkowych wartości atrybutów i wykonanie ewentualnych czynności inicjalizacyjnych. W przeciwieństwie do niektórych innych języków, Python nie wspiera przeciążania konstruktorów - zamiast tego stosuje się parametry opcjonalne z wartościami domyślnymi oraz wzorzec fabryki. Parametr self, obowiązkowy w każdej metodzie instancyjnej, jest referencją do konkretnego obiektu, na którym wywołano metodę. Dzięki self metoda ma dostęp do wszystkich atrybutów i innych metod danego obiektu.

24/50 Modyfikacja atrybutów przez metodę

Zmiana stanu obiektu odbywa się przez metody, które modyfikują self.atrybut . To podstawowa forma enkapsulacji - kontrolujemy, jak zmienia się stan obiektu, zamiast pozwalać na bezpośrednią modyfikację z zewnątrz.

Przeanalizujmy poniższy kod:

class Konto:
    def __init__(self, wlasciciel, waluta="PLN"):
        self.wlasciciel = wlasciciel
        self.saldo = 0.0
        self.waluta = waluta
        self.historia = []  # lista transakcji

    def wplac(self, kwota):
        if kwota > 0:
            self.saldo += kwota
            self.historia.append(f"Wp\u0142ata: +{kwota}")

    def wyplac(self, kwota):
        if kwota <= self.saldo and kwota > 0:
            self.saldo -= kwota
            self.historia.append(f"Wyp\u0142ata: -{kwota}")
            return True
        print("Brak \u015brodk\u00f3w lub nieprawid\u0142owa kwota!")
        return False

    def pokaz_historie(self):
        return self.historia[:]  # zwracamy kopię listy

k = Konto("Ala")
k.wplac(1000)
k.wyplac(200)
k.wyplac(900)   # Brak środków!
print(k.saldo)              # 800.0
print(k.pokaz_historie())   # ["Wpłata: +1000", "Wypłata: -200"]

Metody kontrolują, jak zmienia się stan - walidują dane wejściowe, aktualizują stan w spójny sposób i logują zmiany. Dzięki temu obiekt nigdy nie znajdzie się w niepoprawnym stanie (np. ujemne saldo). To jest właśnie enkapsulacja w praktyce.

Atrybuty są podstawowym mechanizmem przechowywania stanu obiektu i mogą zawierać dane dowolnego typu dostępnego w Pythonie. Każdy obiekt ma własną, niezależną kopię atrybutów zdefiniowanych w konstruktorze, co oznacza, że zmiana wartości w jednej instancji nie wpływa na pozostałe. W Pythonie atrybuty są przechowywane w słowniku __dict__ każdego obiektu, co umożliwia dynamiczne dodawanie i usuwanie atrybutów w czasie wykonania. Chociaż dynamiczne dodawanie atrybutów jest możliwe, w praktyce zaleca się definiowanie wszystkich atrybutów w konstruktorze __init__ dla zachowania przewidywalności kodu. Taki nawyk sprawia, że struktura obiektu jest jasna dla każdego programisty czytającego definicję klasy. Ponadto wiele narzędzi do statycznej analizy kodu wymaga jawnego deklarowania atrybutów.

Konstruktor __init__ jest wywoływany automatycznie po utworzeniu obiektu przez metodę __new__, która alokuje pamięć dla nowej instancji. Zadaniem __init__ jest przygotowanie obiektu do użycia poprzez ustawienie początkowych wartości atrybutów i wykonanie ewentualnych czynności inicjalizacyjnych. W przeciwieństwie do niektórych innych języków, Python nie wspiera przeciążania konstruktorów - zamiast tego stosuje się parametry opcjonalne z wartościami domyślnymi oraz wzorzec fabryki. Parametr self, obowiązkowy w każdej metodzie instancyjnej, jest referencją do konkretnego obiektu, na którym wywołano metodę. Dzięki self metoda ma dostęp do wszystkich atrybutów i innych metod danego obiektu.

25/50 Metoda przedstaw_sie()

Typowa metoda, która zwraca lub wyświetla informacje o obiekcie. To jeden z najczęstszych wzorców w OOP - metoda, która agreguje stan obiektu w czytelną formę. Później poznamy metodę __str__ , która robi to samo automatycznie przy print() .

Przeanalizujmy poniższy kod:

class Student:
    def __init__(self, imie, nazwisko, kierunek):
        self.imie = imie
        self.nazwisko = nazwisko
        self.kierunek = kierunek
        self.srednia = 0.0

    def przedstaw_sie(self):
        return (f"Cze\u015b\u0107, jestem {self.imie} {self.nazwisko}, "
                f"studiuj\u0119 {self.kierunek}, "
                f"moja \u015brednia to {self.srednia:.2f}")

    def __str__(self):
        return self.przedstaw_sie()

s = Student("Micha\u0142", "Kowalski", "Informatyk\u0119")
print(s.przedstaw_sie())
print(s)  # to samo - dzięki __str__

Dzięki self metoda ma dostęp do wszystkich atrybutów obiektu. Metoda __str__ to magiczna metoda, która jest wywoływana przez print() i str() - definiuje "nieformalną" reprezentację tekstową obiektu.

przedstaw_sie

Metody są funkcjami zdefiniowanymi w przestrzeni nazw klasy, które otrzymują automatycznie pierwszy argument w postaci referencji do obiektu. Dzięki temu mogą odczytywać i modyfikować stan obiektu, na którym zostały wywołane. W Pythonie istnieje kilka rodzajów metod: instancyjne (z self), klasowe (z cls i dekoratorem @classmethod), statyczne (z @staticmethod) oraz abstrakcyjne (z @abstractmethod w klasach ABC). Metody instancyjne są najczęściej używane i to one będą głównym tematem tej części kursu. Każda metoda instancyjna przyjmuje self jako pierwszy parametr, a Python automatycznie przekazuje obiekt w momencie wywołania przez notację kropkową. Metody mogą przyjmować dodatkowe argumenty, mieć wartości domyślne i zwracać wartości za pomocą instrukcji return.

Dobrą praktyką projektowania metod jest zasada pojedynczej odpowiedzialności - każda metoda powinna wykonywać jedną, dobrze określoną operację. Metody tylko do odczytu stanu, nazywane getterami, nie powinny modyfikować atrybutów obiektu. Metody modyfikujące stan, nazywane setterami, powinny walidować dane wejściowe przed wprowadzeniem zmian. Przestrzeganie tych zasad prowadzi do kodu łatwiejszego w testowaniu i debugowaniu. Warto również stosować metody pomocnicze, aby uniknąć powielania kodu wewnątrz klasy. Dobrze zaprojektowane metody sprawiają, że klasa jest intuicyjna w użyciu i trudna do niepoprawnego wykorzystania.

26/50 Zmiana stanu obiektu w czasie

Obiekty są dynamiczne - ich stan zmienia się w trakcie działania programu. To odróżnia je od niemutowalnych typów danych (jak int , str , tuple ), których nie można zmienić po utworzeniu. Obiekty klas użytkownika są domyślnie mutowalne.

Przeanalizujmy poniższy kod:

class Termometr:
    def __init__(self, nazwa):
        self.nazwa = nazwa
        self.temperatura = 20.0
        self.wlaczony = False
        self.min_temp = 20.0
        self.max_temp = 20.0

    def wlacz(self):
        self.wlaczony = True

    def wylacz(self):
        self.wlaczony = False

    def ustaw_temp(self, temp):
        if self.wlaczony:
            self.temperatura = temp
            self.min_temp = min(self.min_temp, temp)
            self.max_temp = max(self.max_temp, temp)
            print(f"{self.nazwa}: temp. ustawiona na {temp}")
        else:
            print(f"{self.nazwa}: w\u0142\u0105cz termometr najpierw!")

    def podsumowanie(self):
        return (f"Termometr {self.nazwa}: "
                f"min={self.min_temp}, max={self.max_temp}, "
                f"aktualna={self.temperatura}")

t = Termometr("Salon")
t.ustaw_temp(24.5)     # "Salon: włącz termometr najpierw!"
t.wlacz()
t.ustaw_temp(24.5)
t.ustaw_temp(19.0)
print(t.podsumowanie())   # min=19.0, max=24.5, aktualna=19.0

Metoda ustaw_temp zmienia stan tylko gdy termometr jest włączony - to przykład kontroli dostępu. Obiekt pilnuje swojego stanu i nie pozwala na niepoprawne operacje. Dodatkowo śledzi wartości minimalne i maksymalne.

Zmiana stanu

Atrybuty są podstawowym mechanizmem przechowywania stanu obiektu i mogą zawierać dane dowolnego typu dostępnego w Pythonie. Każdy obiekt ma własną, niezależną kopię atrybutów zdefiniowanych w konstruktorze, co oznacza, że zmiana wartości w jednej instancji nie wpływa na pozostałe. W Pythonie atrybuty są przechowywane w słowniku __dict__ każdego obiektu, co umożliwia dynamiczne dodawanie i usuwanie atrybutów w czasie wykonania. Chociaż dynamiczne dodawanie atrybutów jest możliwe, w praktyce zaleca się definiowanie wszystkich atrybutów w konstruktorze __init__ dla zachowania przewidywalności kodu. Taki nawyk sprawia, że struktura obiektu jest jasna dla każdego programisty czytającego definicję klasy. Ponadto wiele narzędzi do statycznej analizy kodu wymaga jawnego deklarowania atrybutów.

Konstruktor __init__ jest wywoływany automatycznie po utworzeniu obiektu przez metodę __new__, która alokuje pamięć dla nowej instancji. Zadaniem __init__ jest przygotowanie obiektu do użycia poprzez ustawienie początkowych wartości atrybutów i wykonanie ewentualnych czynności inicjalizacyjnych. W przeciwieństwie do niektórych innych języków, Python nie wspiera przeciążania konstruktorów - zamiast tego stosuje się parametry opcjonalne z wartościami domyślnymi oraz wzorzec fabryki. Parametr self, obowiązkowy w każdej metodzie instancyjnej, jest referencją do konkretnego obiektu, na którym wywołano metodę. Dzięki self metoda ma dostęp do wszystkich atrybutów i innych metod danego obiektu.

27/50 Porównanie obiektów - tożsamość vs równość

Dwa obiekty mogą być równe (mieć te same wartości atrybutów), ale nie być tym samym obiektem w pamięci. To fundamentalne rozróżnienie w programowaniu obiektowym.

PojęcieZnaczenieOperatorPrzykład
Tożsamość ten sam obiekt w pamięci is a is b - True gdy id(a) == id(b)
Równość te same wartości atrybutów == a == b - True gdy wartości równe

Przeanalizujmy poniższy kod na listach (które są obiektami):

a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a == b)  # True - te same wartości
print(a is b)  # False - różne obiekty w pamięci
print(a is c)  # True - c wskazuje na ten sam obiekt co a

a.append(4)
print(c)       # [1, 2, 3, 4] - c "widzi" zmianę
print(b)       # [1, 2, 3] - b bez zmian

To kluczowe zrozumieć: gdy przypisujesz obiekt do zmiennej, nie kopiujesz go - tworzysz nową referencję do tego samego obiektu. Dlatego c = a nie tworzy kopii, a jedynie nową "etykietę" na ten sam obiekt.

Tożsamość vs równość

Porównanie programowania proceduralnego i obiektowego na konkretnych przykładach pokazuje fundamentalne różnice w organizacji kodu. Podejście proceduralne koncentruje się na sekwencji operacji i funkcjach przetwarzających dane, podczas gdy obiektowe grupuje powiązane dane i funkcje w spójne jednostki zwane klasami. W praktyce zawodowej rzadko spotyka się czyste implementacje jednego paradygmatu - nowoczesne aplikacje łączą różne podejścia w zależności od potrzeb konkretnego modułu. Python, jako język wieloparadygmatowy, doskonale wspiera taki hybrydowy styl programowania, pozwalając programiście na elastyczny wybór narzędzia. Wybór paradygmatu zależy przede wszystkim od charakteru problemu - proste skrypty często lepiej napisać proceduralnie, a złożone systemy obiektowo.

Tabelaryczne zestawienie cech obu podejść ułatwia zrozumienie, kiedy które z nich jest bardziej odpowiednie. Programowanie proceduralne sprawdza się w małych, liniowych skryptach, gdzie ważna jest prostota i szybkość wykonania. OOP dominuje w dużych projektach, gdzie kluczowe są organizacja kodu, wielokrotne użycie i łatwość utrzymania. Warto również zauważyć, że wiele języków nowej generacji, takich jak Rust czy Kotlin, łączy elementy obu podejść, oferując programistom najlepsze cechy każdego z nich. Świadomy wybór paradygmatu jest oznaką dojrzałości programistycznej i pozwala na podejmowanie optymalnych decyzji projektowych w codziennej pracy.

28/50 is vs == dla własnych obiektów

Dla własnych klas domyślnie operator == zachowuje się jak is - porównuje tożsamość, a nie wartości atrybutów. Aby to zmienić, definiujemy metodę __eq__ .

Przeanalizujmy poniższy kod:

class Osoba:
    def __init__(self, imie, wiek):
        self.imie = imie
        self.wiek = wiek

    def __eq__(self, other):
        if not isinstance(other, Osoba):
            return NotImplemented
        return self.imie == other.imie and self.wiek == other.wiek

    def __repr__(self):
        return f"Osoba('{self.imie}', {self.wiek})"

p1 = Osoba("Adam", 25)
p2 = Osoba("Adam", 25)
p3 = p1

print(p1 == p2)  # True - dzięki __eq__ porównujemy atrybuty
print(p1 is p2)  # False - różne obiekty w pamięci
print(p1 is p3)  # True - ten sam obiekt
print(p1)         # Osoba('Adam', 25) - dzięki __repr__

Domyślnie == dla obiektów porównuje tożsamość. Definiując własne __eq__ , mówimy Pythonowi, co znaczy "równe" dla naszej klasy. Metoda __repr__ daje oficjalną, jednoznaczną reprezentację tekstową obiektu - przydatną przy debugowaniu.

is vs porównanie

Tworzenie obiektu przez wywołanie klasy to jeden z najważniejszych wzorców w Pythonie. Składnia NazwaKlasy() może być myląca dla początkujących, ponieważ wygląda jak wywołanie funkcji, ale w rzeczywistości uruchamia złożony proces alokacji i inicjalizacji. Python, jako język dynamiczny, pozwala na tworzenie obiektów w dowolnym momencie działania programu, w tym wewnątrz pętli, warunków czy wyrażeń listowych. Każde wywołanie klasy tworzy nowy, niezależny obiekt w pamięci, nawet jeżeli klasa nie ma zdefiniowanego konstruktora. Zmienna przechowuje referencję do obiektu, a nie sam obiekt, co ma kluczowe znaczenie dla zrozumienia mechanizmu przypisań i kopiowania.

Proces tworzenia obiektu składa się z kilku etapów: najpierw wywoływana jest metoda __new__, która alokuje pamięć dla nowej instancji, następnie uruchamiany jest __init__ do inicjalizacji atrybutów, a na końcu zwracana jest referencja do gotowego obiektu. Większość programistów modyfikuje wyłącznie __init__, pozostawiając __new__ w domyślnej implementacji. Zrozumienie tej kolejności jest ważne przy tworzeniu bardziej zaawansowanych wzorców, takich jak Singleton czy Fabryka. Gdy tworzymy wiele obiektów w pętli, każdy z nich jest niezależną instancją z własnym stanem i tożsamością.

29/50 Unikalny identyfikator obiektu id()

Każdy obiekt w Pythonie ma unikalny identyfikator zwracany przez wbudowaną funkcję id() . Identyfikator ten jest stały przez cały czas życia obiektu i jednoznacznie go identyfikuje. W implementacji CPython id() zwraca adres pamięci obiektu.

Przeanalizujmy poniższy kod:

class Punkt:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Punkt({self.x}, {self.y})"

a = Punkt(3, 4)
b = Punkt(3, 4)
c = a

print(id(a))    # np. 2387612345678
print(id(b))    # np. 2387612345790 - inny numer
print(id(c))    # np. 2387612345678 - ten sam co a

print(a is b)    # False - różne id
print(a is c)    # True - ten sam id

# operator is to nic innego jak porównanie id
print(id(a) == id(c))  # True

id() zwraca liczbę całkowitą unikalną dla danego obiektu w danym momencie. Operator is porównuje właśnie te identyfikatory - a is b jest równoważne id(a) == id(b) .

Funkcja id

Tworzenie obiektu przez wywołanie klasy to jeden z najważniejszych wzorców w Pythonie. Składnia NazwaKlasy() może być myląca dla początkujących, ponieważ wygląda jak wywołanie funkcji, ale w rzeczywistości uruchamia złożony proces alokacji i inicjalizacji. Python, jako język dynamiczny, pozwala na tworzenie obiektów w dowolnym momencie działania programu, w tym wewnątrz pętli, warunków czy wyrażeń listowych. Każde wywołanie klasy tworzy nowy, niezależny obiekt w pamięci, nawet jeżeli klasa nie ma zdefiniowanego konstruktora. Zmienna przechowuje referencję do obiektu, a nie sam obiekt, co ma kluczowe znaczenie dla zrozumienia mechanizmu przypisań i kopiowania.

Proces tworzenia obiektu składa się z kilku etapów: najpierw wywoływana jest metoda __new__, która alokuje pamięć dla nowej instancji, następnie uruchamiany jest __init__ do inicjalizacji atrybutów, a na końcu zwracana jest referencja do gotowego obiektu. Większość programistów modyfikuje wyłącznie __init__, pozostawiając __new__ w domyślnej implementacji. Zrozumienie tej kolejności jest ważne przy tworzeniu bardziej zaawansowanych wzorców, takich jak Singleton czy Fabryka. Gdy tworzymy wiele obiektów w pętli, każdy z nich jest niezależną instancją z własnym stanem i tożsamością.

30/50 Co to znaczy, że obiekt ma "stan"?

Stan obiektu to aktualne wartości wszystkich jego atrybutów w danym momencie. Stan może się zmieniać w czasie życia obiektu - w przeciwieństwie do klas, które są statyczne. Programowanie obiektowe polega w dużej mierze na zarządzaniu stanem obiektów poprzez wywoływanie ich metod.

Przeanalizujmy poniższy kod - klasa modelująca światło z regulacją jasności:

class Swiatlo:
    def __init__(self, nazwa, max_jasnosc=100):
        self.nazwa = nazwa
        self.max_jasnosc = max_jasnosc
        self.wlaczone = False
        self.jasnosc = 0
        self.tryb = "normalny"

    def wlacz(self):
        self.wlaczone = True
        self.jasnosc = self.max_jasnosc

    def wylacz(self):
        self.wlaczone = False
        self.jasnosc = 0

    def sciemnij(self):
        if self.wlaczone:
            self.jasnosc //= 2
            self.tryb = "przyciemniony"

    def rozwidnij(self):
        if self.wlaczone:
            self.jasnosc = self.max_jasnosc
            self.tryb = "normalny"

    def opis_stanu(self):
        return (f"{self.nazwa}: w\u0142\u0105czone={self.wlaczone}, "
                f"jasno\u015b\u0107={self.jasnosc}, tryb={self.tryb}")

l = Swiatlo("Lampa biurkowa")
print(l.opis_stanu())    # włączone=False, jasność=0, tryb=normalny
l.wlacz()
print(l.opis_stanu())    # włączone=True, jasność=100, tryb=normalny
l.sciemnij()
print(l.opis_stanu())    # włączone=True, jasność=50, tryb=przyciemniony

Stan obiektu zmienia się wyłącznie przez wywołanie jego metod. Każda metoda może zmienić jeden lub więcej atrybutów, przenosząc obiekt z jednego stanu do drugiego. Ważne, by metody utrzymywały stan spójnym.

Stan obiektu

Programowanie obiektowe wywodzi się z języka Simula z lat 60. XX wieku, a jego rozwój przyspieszył w latach 70. wraz z językiem Smalltalk, który wprowadził wiele koncepcji używanych do dziś, takich jak klasy, metody i dziedziczenie. Współcześnie OOP jest wspierane przez większość popularnych języków programowania, choć każdy z nich implementuje ten paradygmat nieco inaczej. Python przyjął podejście pragmatyczne, w którym wszystko jest obiektem, ale programista nie jest zmuszany do obiektowego stylu. Dzięki temu Python jest językiem wieloparadygmatowym, łączącym OOP z programowaniem proceduralnym i funkcyjnym w elastyczny sposób. Zrozumienie genezy i ewolucji OOP pomaga docenić jego zalety i świadomie stosować go we własnych projektach programistycznych.

Cztery filary OOP - enkapsulacja, dziedziczenie, polimorfizm i abstrakcja - są ze sobą ściśle powiązane i wzajemnie się uzupełniają. Enkapsulacja chroni stan obiektu przed niekontrolowanym dostępem z zewnątrz, co zwiększa bezpieczeństwo i spójność danych. Dziedziczenie pozwala na budowanie hierarchii klas i wielokrotne wykorzystanie kodu bez jego kopiowania. Polimorfizm umożliwia jednolity interfejs dla różnych typów obiektów, co upraszcza projektowanie rozszerzalnych systemów. Abstrakcja koncentruje się na istotnych cechach, pomijając nieistotne szczegóły implementacyjne. Opanowanie tych czterech koncepcji stanowi fundament biegłości w OOP.

31/50 Przechowywanie obiektów w liście

Obiekty, jak wszystkie dane w Pythonie, mogą być przechowywane w kolekcjach - listach, słownikach, zbiorach, krotkach. Lista obiektów to jeden z najczęstszych wzorców: masz wiele instancji tej samej klasy i chcesz operować na nich zbiorowo.

Przeanalizujmy poniższy kod:

class Produkt:
    def __init__(self, nazwa, cena, kategoria="Og\u00f3lna"):
        self.nazwa = nazwa
        self.cena = cena
        self.kategoria = kategoria

    def __repr__(self):
        return f"{self.nazwa}: {self.cena}z\u0142"

p1 = Produkt("D\u0142ugopis", 3.50, "Biurowe")
p2 = Produkt("Zeszyt", 5.00, "Biurowe")
p3 = Produkt("Gumka", 2.00, "Szko\u0142a")

lista = [p1, p2, p3]
print(lista[0].nazwa)     # Długopis
print(lista[1].cena)      # 5.0

koszyk = [
    Produkt("Chleb", 3.20, "Spo\u017cywcze"),
    Produkt("Mas\u0142o", 5.80, "Spo\u017cywcze"),
]

# Lista przechowuje referencje
lista[0].cena = 4.00
print(p1.cena)  # 4.0 - zmiana widoczna przez oryginalną zmienną

Lista przechowuje referencje do obiektów - możesz je indeksować, sortować, filtrować, dodawać i usuwać. Każdy element listy to odniesienie do obiektu w pamięci, a nie kopia obiektu.

Obiekty w liście

Tworzenie obiektu przez wywołanie klasy to jeden z najważniejszych wzorców w Pythonie. Składnia NazwaKlasy() może być myląca dla początkujących, ponieważ wygląda jak wywołanie funkcji, ale w rzeczywistości uruchamia złożony proces alokacji i inicjalizacji. Python, jako język dynamiczny, pozwala na tworzenie obiektów w dowolnym momencie działania programu, w tym wewnątrz pętli, warunków czy wyrażeń listowych. Każde wywołanie klasy tworzy nowy, niezależny obiekt w pamięci, nawet jeżeli klasa nie ma zdefiniowanego konstruktora. Zmienna przechowuje referencję do obiektu, a nie sam obiekt, co ma kluczowe znaczenie dla zrozumienia mechanizmu przypisań i kopiowania.

Proces tworzenia obiektu składa się z kilku etapów: najpierw wywoływana jest metoda __new__, która alokuje pamięć dla nowej instancji, następnie uruchamiany jest __init__ do inicjalizacji atrybutów, a na końcu zwracana jest referencja do gotowego obiektu. Większość programistów modyfikuje wyłącznie __init__, pozostawiając __new__ w domyślnej implementacji. Zrozumienie tej kolejności jest ważne przy tworzeniu bardziej zaawansowanych wzorców, takich jak Singleton czy Fabryka. Gdy tworzymy wiele obiektów w pętli, każdy z nich jest niezależną instancją z własnym stanem i tożsamością.

32/50 Iterowanie po liście obiektów

Możemy przejrzeć wszystkie obiekty w pętli i wywołać ich metody lub odczytać atrybuty. To potęga OOP - jeden kawałek kodu działa na wielu obiektach, każdy z własnymi danymi.

Przeanalizujmy poniższy kod:

class Produkt:
    def __init__(self, nazwa, cena):
        self.nazwa = nazwa
        self.cena = cena

    def opis(self):
        return f"{self.nazwa}: {self.cena:.2f} z\u0142"

    def z_podatkiem(self, stawka=0.23):
        return self.cena * (1 + stawka)

produkty = [
    Produkt("D\u0142ugopis", 3.50),
    Produkt("Zeszyt", 5.00),
    Produkt("Gumka", 2.00),
]

for p in produkty:
    print(p.opis())

# Filtrowanie po atrybucie
tansze = [p for p in produkty if p.cena < 4.00]
for p in tansze:
    print(f"Promocja: {p.opis()}")

# Sortowanie po atrybucie
produkty.sort(key=lambda p: p.cena, reverse=True)
for p in produkty:
    print(f"{p.nazwa}: {p.z_podatkiem():.2f} z\u0142 brutto")

Każdy obiekt w pętli ma własne dane - to piękno OOP: jedna klasa, wiele instancji, wspólny interfejs. Możesz sortować, filtrować, agregować, przekształcać kolekcje obiektów, używając tych samych technik co dla list liczb czy napisów.

Tworzenie obiektu przez wywołanie klasy to jeden z najważniejszych wzorców w Pythonie. Składnia NazwaKlasy() może być myląca dla początkujących, ponieważ wygląda jak wywołanie funkcji, ale w rzeczywistości uruchamia złożony proces alokacji i inicjalizacji. Python, jako język dynamiczny, pozwala na tworzenie obiektów w dowolnym momencie działania programu, w tym wewnątrz pętli, warunków czy wyrażeń listowych. Każde wywołanie klasy tworzy nowy, niezależny obiekt w pamięci, nawet jeżeli klasa nie ma zdefiniowanego konstruktora. Zmienna przechowuje referencję do obiektu, a nie sam obiekt, co ma kluczowe znaczenie dla zrozumienia mechanizmu przypisań i kopiowania.

Proces tworzenia obiektu składa się z kilku etapów: najpierw wywoływana jest metoda __new__, która alokuje pamięć dla nowej instancji, następnie uruchamiany jest __init__ do inicjalizacji atrybutów, a na końcu zwracana jest referencja do gotowego obiektu. Większość programistów modyfikuje wyłącznie __init__, pozostawiając __new__ w domyślnej implementacji. Zrozumienie tej kolejności jest ważne przy tworzeniu bardziej zaawansowanych wzorców, takich jak Singleton czy Fabryka. Gdy tworzymy wiele obiektów w pętli, każdy z nich jest niezależną instancją z własnym stanem i tożsamością.

33/50 Obiekty jako argumenty funkcji

Obiekty można przekazywać do funkcji jak każdą inną wartość. Ponieważ Python przekazuje obiekty przez referencję, funkcja może modyfikować stan obiektu wewnątrz siebie - zmiany będą widoczne na zewnątrz.

Przeanalizujmy poniższy kod:

class Produkt:
    def __init__(self, nazwa, cena):
        self.nazwa = nazwa
        self.cena = cena

    def __repr__(self):
        return f"{self.nazwa}({self.cena}z\u0142)"

def suma_koszyka(produkty):
    """Oblicza sumę cen produktów na liście."""
    return sum(p.cena for p in produkty)

def nalicz_rabat(produkty, procent):
    """Zmniejsza cenę każdego produktu o zadany procent."""
    for p in produkty:
        p.cena *= (1 - procent / 100)

koszyk = [
    Produkt("Chleb", 3.20),
    Produkt("Mas\u0142o", 5.80),
    Produkt("Mleko", 2.40),
]

print(f"Do zap\u0142aty: {suma_koszyka(koszyk):.2f} z\u0142")
nalicz_rabat(koszyk, 10)
print(f"Po rabacie: {suma_koszyka(koszyk):.2f} z\u0142")
for p in koszyk:
    print(f"  {p}")

Funkcja operuje na obiektach przez ich atrybuty - nie musi wiedzieć, jak są zbudowane wewnątrz. Uwaga: funkcja nalicz_rabat modyfikuje oryginalne obiekty. Jeżeli chcesz uniknąć skutków ubocznych, rozważ zwrócenie nowych obiektów lub utworzenie kopii przed modyfikacją.

Tworzenie obiektu przez wywołanie klasy to jeden z najważniejszych wzorców w Pythonie. Składnia NazwaKlasy() może być myląca dla początkujących, ponieważ wygląda jak wywołanie funkcji, ale w rzeczywistości uruchamia złożony proces alokacji i inicjalizacji. Python, jako język dynamiczny, pozwala na tworzenie obiektów w dowolnym momencie działania programu, w tym wewnątrz pętli, warunków czy wyrażeń listowych. Każde wywołanie klasy tworzy nowy, niezależny obiekt w pamięci, nawet jeżeli klasa nie ma zdefiniowanego konstruktora. Zmienna przechowuje referencję do obiektu, a nie sam obiekt, co ma kluczowe znaczenie dla zrozumienia mechanizmu przypisań i kopiowania.

Proces tworzenia obiektu składa się z kilku etapów: najpierw wywoływana jest metoda __new__, która alokuje pamięć dla nowej instancji, następnie uruchamiany jest __init__ do inicjalizacji atrybutów, a na końcu zwracana jest referencja do gotowego obiektu. Większość programistów modyfikuje wyłącznie __init__, pozostawiając __new__ w domyślnej implementacji. Zrozumienie tej kolejności jest ważne przy tworzeniu bardziej zaawansowanych wzorców, takich jak Singleton czy Fabryka. Gdy tworzymy wiele obiektów w pętli, każdy z nich jest niezależną instancją z własnym stanem i tożsamością.

34/50 Zwracanie obiektów z funkcji

Funkcje mogą tworzyć i zwracać obiekty. To wygodny wzorzec, gdy tworzenie obiektu wymaga logiki (np. walidacji danych, formatowania). Taki wzorzec nazywamy fabryką (factory pattern).

Przeanalizujmy poniższy kod:

class Uzytkownik:
    def __init__(self, login, email, aktywny=True):
        self.login = login
        self.email = email
        self.aktywny = aktywny
        self.data_rejestracji = "2024-01-15"

    def __repr__(self):
        status = "aktywny" if self.aktywny else "nieaktywny"
        return f"Uzytkownik({self.login}, {self.email}, {status})"

def utworz_uzytkownika(login, domena="example.com"):
    """Funkcja fabryczna - tworzy użytkownika z emailem z danej domeny."""
    if not login or not login.isidentifier():
        raise ValueError(f"Nieprawid\u0142owy login: {login}")
    email = f"{login}@{domena}"
    return Uzytkownik(login, email)

def utworz_uzytkownika_z_csv(linia_csv):
    login, domena = linia_csv.strip().split(",")
    return utworz_uzytkownika(login, domena)

u1 = utworz_uzytkownika("jan")
u2 = utworz_uzytkownika("ala", "mail.pl")
u3 = utworz_uzytkownika_z_csv("ola,mojafirma.com\n")

print(u1)
print(u2)
print(u3)

Funkcja zwraca gotowy obiekt z wypełnionymi atrybutami. Wzorzec fabryki jest szczególnie przydatny, gdy tworzenie obiektu wymaga skomplikowanej logiki lub gdy chcesz ukryć szczegóły konstruktora przed użytkownikiem.

Zwracanie obiektów

Tworzenie obiektu przez wywołanie klasy to jeden z najważniejszych wzorców w Pythonie. Składnia NazwaKlasy() może być myląca dla początkujących, ponieważ wygląda jak wywołanie funkcji, ale w rzeczywistości uruchamia złożony proces alokacji i inicjalizacji. Python, jako język dynamiczny, pozwala na tworzenie obiektów w dowolnym momencie działania programu, w tym wewnątrz pętli, warunków czy wyrażeń listowych. Każde wywołanie klasy tworzy nowy, niezależny obiekt w pamięci, nawet jeżeli klasa nie ma zdefiniowanego konstruktora. Zmienna przechowuje referencję do obiektu, a nie sam obiekt, co ma kluczowe znaczenie dla zrozumienia mechanizmu przypisań i kopiowania.

Proces tworzenia obiektu składa się z kilku etapów: najpierw wywoływana jest metoda __new__, która alokuje pamięć dla nowej instancji, następnie uruchamiany jest __init__ do inicjalizacji atrybutów, a na końcu zwracana jest referencja do gotowego obiektu. Większość programistów modyfikuje wyłącznie __init__, pozostawiając __new__ w domyślnej implementacji. Zrozumienie tej kolejności jest ważne przy tworzeniu bardziej zaawansowanych wzorców, takich jak Singleton czy Fabryka. Gdy tworzymy wiele obiektów w pętli, każdy z nich jest niezależną instancją z własnym stanem i tożsamością.

35/50 Obiekty w obiektach (zapowiedź kompozycji)

Atrybutem obiektu może być inny obiekt. To podstawa kompozycji - potężnej techniki, w której budujemy złożone obiekty z prostszych.

Przeanalizujmy poniższy kod:

class Adres:
    def __init__(self, miasto, ulica, kod, kraj="Polska"):
        self.miasto = miasto
        self.ulica = ulica
        self.kod = kod
        self.kraj = kraj

    def pelny_adres(self):
        return f"{self.ulica}, {self.kod} {self.miasto}, {self.kraj}"

class Osoba:
    def __init__(self, imie, adres):
        self.imie = imie
        self.adres = adres  # to jest obiekt Adres

    def przedstaw_sie(self):
        return f"{self.imie}, zamieszka\u0142y: {self.adres.pelny_adres()}"

adres_jana = Adres("Warszawa", "Marsza\u0142kowska 1", "00-001")
osoba = Osoba("Jan", adres_jana)

print(osoba.adres.miasto)
print(osoba.adres.pelny_adres())
print(osoba.przedstaw_sie())

# Adres w locie -- bez zmiennej pośredniej
osoba2 = Osoba("Anna", Adres("Krak\u00f3w", "Rynek 5", "30-001"))
print(osoba2.przedstaw_sie())

Notacja osoba.adres.miasto pokazuje, jak łańcuchowo odwołujemy się do zagnieżdżonych obiektów. To naturalny sposób modelowania rzeczywistości: osoba "ma" adres, adres "ma" miasto. Tę strukturę nazywamy grafem obiektów.

Obiekty w obiektach

Tworzenie obiektu przez wywołanie klasy to jeden z najważniejszych wzorców w Pythonie. Składnia NazwaKlasy() może być myląca dla początkujących, ponieważ wygląda jak wywołanie funkcji, ale w rzeczywistości uruchamia złożony proces alokacji i inicjalizacji. Python, jako język dynamiczny, pozwala na tworzenie obiektów w dowolnym momencie działania programu, w tym wewnątrz pętli, warunków czy wyrażeń listowych. Każde wywołanie klasy tworzy nowy, niezależny obiekt w pamięci, nawet jeżeli klasa nie ma zdefiniowanego konstruktora. Zmienna przechowuje referencję do obiektu, a nie sam obiekt, co ma kluczowe znaczenie dla zrozumienia mechanizmu przypisań i kopiowania.

Proces tworzenia obiektu składa się z kilku etapów: najpierw wywoływana jest metoda __new__, która alokuje pamięć dla nowej instancji, następnie uruchamiany jest __init__ do inicjalizacji atrybutów, a na końcu zwracana jest referencja do gotowego obiektu. Większość programistów modyfikuje wyłącznie __init__, pozostawiając __new__ w domyślnej implementacji. Zrozumienie tej kolejności jest ważne przy tworzeniu bardziej zaawansowanych wzorców, takich jak Singleton czy Fabryka. Gdy tworzymy wiele obiektów w pętli, każdy z nich jest niezależną instancją z własnym stanem i tożsamością.

36/50 Przykład: Ksiazka i Biblioteka

Klasyczny przykład kompozycji: biblioteka zawiera wiele książek. Klasa Biblioteka przechowuje listę obiektów Ksiazka i udostępnia metody do zarządzania nimi.

Przeanalizujmy poniższy kod:

class Ksiazka:
    def __init__(self, tytul, autor, rok, isbn):
        self.tytul = tytul
        self.autor = autor
        self.rok = rok
        self.isbn = isbn
        self.wypozyczona = False

    def wypozycz(self):
        if not self.wypozyczona:
            self.wypozyczona = True
            return True
        return False

    def zwroc(self):
        self.wypozyczona = False

    def __repr__(self):
        status = "[wypo\u017cyczona]" if self.wypozyczona else "[dost\u0119pna]"
        return f"{self.tytul} ({self.autor}, {self.rok}) {status}"

class Biblioteka:
    def __init__(self, nazwa):
        self.nazwa = nazwa
        self.ksiazki = []

    def dodaj(self, ksiazka):
        self.ksiazki.append(ksiazka)

    def dostepne(self):
        return [k for k in self.ksiazki if not k.wypozyczona]

    def wypozycz_ksiazke(self, tytul):
        for k in self.ksiazki:
            if k.tytul == tytul and not k.wypozyczona:
                k.wypozycz()
                return True
        return False

bib = Biblioteka("Biblioteka G\u0142\u00f3wna")
bib.dodaj(Ksiazka("1984", "George Orwell", 1949, "978-83-7278-000-1"))
bib.dodaj(Ksiazka("Lalka", "Boles\u0142aw Prus", 1890, "978-83-7278-000-2"))
bib.wypozycz_ksiazke("1984")
for k in bib.dostepne():
    print(k)

Biblioteka przechowuje listę obiektów Ksiazka - to kompozycja w praktyce. Każda książka jest niezależnym obiektem z własnym stanem, a biblioteka zarządza kolekcją tych obiektów.

Książka i Biblioteka

Slajd zatytułowany "Przykład: Ksiazka i Biblioteka" przedstawia istotne zagadnienie z zakresu programowania obiektowego w Pythonie. Zrozumienie przedstawionych tu koncepcji jest niezbędne do dalszej nauki i praktycznego stosowania OOP w codziennej pracy programisty. Zaleca się dokładne przeanalizowanie przykładów kodu i samodzielne ich przetestowanie w środowisku REPL lub edytorze. Warto również zwrócić uwagę na powiązania między tym tematem a innymi zagadnieniami omawianymi w kursie. Systematyczne budowanie wiedzy krok po kroku to klucz do opanowania programowania obiektowego w Pythonie. Nie pomijaj żadnego slajdu, nawet jeśli wydaje Ci się, że temat jest Ci już znany - powtórka utrwala wiedzę i pozwala dostrzec nowe szczegóły w znajomym materiale.

Zachęcamy do samodzielnego eksperymentowania z omawianymi mechanizmami i modyfikowania przykładowego kodu. Praktyczne ćwiczenia i własne projekty to najskuteczniejsza metoda nauki programowania, ponieważ wymagają aktywnego stosowania wiedzy. Pamiętaj, że błędy są naturalną częścią procesu uczenia się i każda pomyłka przybliża Cię do mistrzostwa. Warto prowadzić własny dziennik błędów, w którym zapisujesz napotkane problemy i ich rozwiązania. Taki zeszyt stanie się z czasem bezcennym źródłem wiedzy i referencją na przyszłość. Korzystaj z dokumentacji oficjalnej Pythona oraz społeczności programistycznych - to nieocenione źródła wiedzy i wsparcia w trudnych momentach.

37/50 Wielość obiektów jednej klasy

Z jednej klasy możesz utworzyć dowolnie wiele niezależnych obiektów - to jedna z głównych zalet OOP. Każdy obiekt ma własną przestrzeń atrybutów, więc zmiana w jednym nie wpływa na pozostałe.

Przeanalizujmy poniższy kod:

class Pies:
    liczba_psow = 0  # atrybut klasy - wspólny

    def __init__(self, imie, rasa):
        self.imie = imie
        self.rasa = rasa
        Pies.liczba_psow += 1

    def szczekaj(self):
        print(f"{self.imie}: Hau!")

psy = [
    Pies("Burek", "Owczarek"),
    Pies("\u0141atka", "Kundel"),
    Pies("Reksio", "Jamnik"),
    Pies("Azor", "Kokus"),
]

print(f"Liczba ps\u00f3w: {Pies.liczba_psow}")

for p in psy:
    p.szczekaj()

print(psy[0].imie)  # Burek
print(psy[1].imie)  # Łatka

Wszystkie cztery psy to obiekty tej samej klasy Pies , ale każdy ma własne imię i rasę. Atrybut klasy liczba_psow jest współdzielony - istnieje w jednej kopii na poziomie klasy, a nie w każdym obiekcie.

Wielość obiektów

Tworzenie obiektu przez wywołanie klasy to jeden z najważniejszych wzorców w Pythonie. Składnia NazwaKlasy() może być myląca dla początkujących, ponieważ wygląda jak wywołanie funkcji, ale w rzeczywistości uruchamia złożony proces alokacji i inicjalizacji. Python, jako język dynamiczny, pozwala na tworzenie obiektów w dowolnym momencie działania programu, w tym wewnątrz pętli, warunków czy wyrażeń listowych. Każde wywołanie klasy tworzy nowy, niezależny obiekt w pamięci, nawet jeżeli klasa nie ma zdefiniowanego konstruktora. Zmienna przechowuje referencję do obiektu, a nie sam obiekt, co ma kluczowe znaczenie dla zrozumienia mechanizmu przypisań i kopiowania.

Proces tworzenia obiektu składa się z kilku etapów: najpierw wywoływana jest metoda __new__, która alokuje pamięć dla nowej instancji, następnie uruchamiany jest __init__ do inicjalizacji atrybutów, a na końcu zwracana jest referencja do gotowego obiektu. Większość programistów modyfikuje wyłącznie __init__, pozostawiając __new__ w domyślnej implementacji. Zrozumienie tej kolejności jest ważne przy tworzeniu bardziej zaawansowanych wzorców, takich jak Singleton czy Fabryka. Gdy tworzymy wiele obiektów w pętli, każdy z nich jest niezależną instancją z własnym stanem i tożsamością.

38/50 Ćwiczenie: zaprojektuj klasę Pies
Zadanie: Zaprojektuj klasę Pies z:
  • 3 atrybuty:imie, rasa, wiek
  • 2 metody:szczekaj() i majtek_ogonem()

Wskazówki:

  1. Użyj __init__ do ustawienia atrybutów - pamiętaj o self!
  2. Metoda szczekaj() niech wypisuje coś w stylu "Hau! Jestem [imie]"
  3. Metoda majtek_ogonem() niech informuje, że pies merda ogonem, gdy jest szczęśliwy
  4. Utwórz 2-3 obiekty i przetestuj metody

Spróbuj samodzielnie przed przejściem na następny slajd! To ćwiczenie utrwala wszystkie poznane koncepcje: definicję klasy, konstruktor, atrybuty, metody i tworzenie obiektów.

ćwiczenie Pies

Ćwiczenia praktyczne są niezbędnym elementem nauki programowania, ponieważ pozwalają na bezpośrednie zastosowanie poznanej teorii. Samodzielne pisanie kodu, nawet prostych klas i obiektów, utrwala wiedzę znacznie skuteczniej niż bierne czytanie przykładów. Każde ćwiczenie w tym kursie zostało zaprojektowane tak, aby sprawdzić konkretne umiejętności i przygotować do bardziej złożonych zadań. Zaleca się wykonanie ćwiczenia bez podglądania rozwiązania, a dopiero potem porównanie własnego kodu z przykładowym. W przypadku trudności warto wrócić do odpowiedniego slajdu i przeanalizować omawiane na nim koncepcje jeszcze raz.

Nauka programowania przypomina naukę języków obcych - teoria jest ważna, ale to praktyka decyduje o biegłości. Im więcej samodzielnie napiszesz kodu, tym szybciej nabierzesz wprawy w myśleniu obiektowym. Eksperymentuj z przykładami, dodawaj nowe atrybuty i metody, testuj różne scenariusze użycia. Nie bój się błędów - każdy komunikat błędu to cenna lekcja, która przybliża Cię do mistrzostwa. Systematyczna praca i regularne programowanie są kluczem do sukcesu w nauce Pythona. Wykorzystaj REPL do szybkiego testowania hipotez i pomysłów.

39/50 Rozwiązanie krok po kroku

Przeanalizujmy poniższy kod:

class Pies:
    def __init__(self, imie, rasa, wiek):
        self.imie = imie
        self.rasa = rasa
        self.wiek = wiek
        self.szczesliwy = True

    def szczekaj(self):
        print(f"Hau! Jestem {self.imie}, {self.rasa}")

    def majtek_ogonem(self):
        if self.szczesliwy:
            print(f"{self.imie} merda ogonem!")
        else:
            print(f"{self.imie} jest smutny...")

p = Pies("Burek", "Owczarek", 3)
p.szczekaj()        # Hau! Jestem Burek, Owczarek
p.majtek_ogonem()    # Burek merda ogonem!

Gotowe! Klasa ma 3 atrybuty i 2 metody. Możesz tworzyć kolejne psy. Zwróć uwagę na atrybut szczesliwy - to dodatkowy atrybut stanu, którego nie było w ćwiczeniu, ale który wzbogaca zachowanie obiektu.

Rozwiązanie Pies

Przedstawione rozwiązanie ćwiczenia jest tylko jedną z wielu poprawnych implementacji. W programowaniu obiektowym rzadko istnieje jedyne słuszne rozwiązanie - różni programiści mogą zaprojektować klasę w odmienny sposób, w zależności od potrzeb i preferencji. Ważne jest, aby kod był czytelny, spójny i poprawny logicznie. Porównaj swoje rozwiązanie z przykładowym i zastanów się nad różnicami - każda alternatywna implementacja może Cię czegoś nauczyć. Z czasem wyrobisz sobie własny styl projektowania klas, który będzie dla Ciebie najbardziej naturalny i efektywny. Warto również sprawdzić, jak Twoje rozwiązanie radzi sobie z przypadkami brzegowymi, których nie przewidziałeś na początku.

Proces refaktoryzacji i ulepszania własnego kodu jest równie ważny jak jego początkowe napisanie. Po porównaniu z przykładowym rozwiązaniem, zastanów się, co możesz poprawić w swoim kodzie. Czy wszystkie atrybuty są potrzebne? Czy metody mają jasne nazwy? Czy klasa jest łatwa w testowaniu? Taka autoanaliza jest jednym z najlepszych sposobów na szybki rozwój umiejętności programistycznych. W codziennej pracy programiści często wracają do swojego kodu i wprowadzają ulepszenia - to naturalna część procesu wytwarzania oprogramowania. Nie oczekuj, że pierwsza wersja będzie idealna.

40/50Podsumowanie bloku
  • Klasa to szablon - definiuje strukturę i zachowania
  • Obiekt to konkretna instancja klasy z własnym stanem
  • Konstruktor __init__ inicjalizuje atrybuty obiektu
  • self to odnośnik do bieżącego obiektu
  • Atrybuty przechowują dane (stan), metody definiują zachowania
  • Obiekty mają tożsamość ( id() , is ) i stan (wartości atrybutów)
  • Obiekty można przechowywać w listach, przekazywać do funkcji, zwracać
  • Kompozycja- obiekt może zawierać inne obiekty jako atrybuty
  • Wzorzec fabryki- funkcje zwracające gotowe obiekty
  • __eq__ i __repr__- metody do porównywania i reprezentacji obiektów
Podsumowanie bloku

Podsumowanie to dobry moment na refleksję nad przyswojonym materiałem i identyfikację obszarów wymagających dodatkowej pracy. Wymienione punkty stanowią esencję przerobionej części kursu - od definicji klasy, przez tworzenie obiektów, aż po kompozycję i wzorzec fabryki. Każdy z tych punktów będzie rozwijany w kolejnych modułach, dlatego warto upewnić się, że są dobrze zrozumiane. Zachęcamy do tworzenia własnych notatek i map myśli, które pomagają w usystematyzowaniu wiedzy. Regularne powtórki są kluczowe dla trwałego zapamiętania materiału.

Mapy myśli są skutecznym narzędziem wizualizacji złożonych koncepcji i relacji między nimi. Przedstawiona mapa obrazuje najważniejsze pojęcia omówione w tej części kursu oraz ich wzajemne powiązania. Tworzenie własnych map myśli podczas nauki programowania aktywuje inne obszary mózgu niż czytanie liniowego tekstu, co przekłada się na lepsze zapamiętywanie. Studenci, którzy regularnie tworzą mapy myśli, osiągają lepsze wyniki w testach koncepcyjnych i szybciej łączą nowe informacje z już posiadaną wiedzą. Zachęcamy do wykorzystania tej techniki.

41/50 Błąd: brak self w parametrach metody

Najczęstszy błąd początkujących - pominięcie self w definicji metody. Python automatycznie przekazuje obiekt jako pierwszy argument - jeżeli metoda nie przyjmuje self , dostajesz TypeError .

Przeanalizujmy poniższy kod:

# ŹLE - brak self
class Pies:
    def __init__(self, imie):
        self.imie = imie

    def szczekaj():          # BŁĄD! brak self
        print("Hau!")

p = Pies("Burek")
p.szczekaj()
# TypeError: szczekaj() takes 0 positional arguments but 1 was given

Python automatycznie przekazuje obiekt jako pierwszy argument - jeżeli metoda nie przyjmuje self , dostajesz błąd. Zawsze dodawaj self jako pierwszy parametr każdej metody instancji!

Błąd brak self

Atrybuty są podstawowym mechanizmem przechowywania stanu obiektu i mogą zawierać dane dowolnego typu dostępnego w Pythonie. Każdy obiekt ma własną, niezależną kopię atrybutów zdefiniowanych w konstruktorze, co oznacza, że zmiana wartości w jednej instancji nie wpływa na pozostałe. W Pythonie atrybuty są przechowywane w słowniku __dict__ każdego obiektu, co umożliwia dynamiczne dodawanie i usuwanie atrybutów w czasie wykonania. Chociaż dynamiczne dodawanie atrybutów jest możliwe, w praktyce zaleca się definiowanie wszystkich atrybutów w konstruktorze __init__ dla zachowania przewidywalności kodu. Taki nawyk sprawia, że struktura obiektu jest jasna dla każdego programisty czytającego definicję klasy. Ponadto wiele narzędzi do statycznej analizy kodu wymaga jawnego deklarowania atrybutów.

Konstruktor __init__ jest wywoływany automatycznie po utworzeniu obiektu przez metodę __new__, która alokuje pamięć dla nowej instancji. Zadaniem __init__ jest przygotowanie obiektu do użycia poprzez ustawienie początkowych wartości atrybutów i wykonanie ewentualnych czynności inicjalizacyjnych. W przeciwieństwie do niektórych innych języków, Python nie wspiera przeciążania konstruktorów - zamiast tego stosuje się parametry opcjonalne z wartościami domyślnymi oraz wzorzec fabryki. Parametr self, obowiązkowy w każdej metodzie instancyjnej, jest referencją do konkretnego obiektu, na którym wywołano metodę. Dzięki self metoda ma dostęp do wszystkich atrybutów i innych metod danego obiektu.

42/50 Błąd: zapomnienie () przy tworzeniu obiektu

Bez nawiasów nie tworzysz obiektu - tylko przypisujesz klasę do zmiennej (referencję do klasy, a nie instancję).

Przeanalizujmy poniższy kod:

class Student:
    def __init__(self, imie):
        self.imie = imie

# ŹLE - brak nawiasów
s = Student           # to NIE jest obiekt, to klasa!
# print(s.imie)      # AttributeError

# DOBRZE
s = Student("Ala")  # tworzy obiekt
print(s.imie)        # Ala

# Ciekawostka: bez nawiasów można przypisać klasę do innej zmiennej
KlasaStudent = Student
s2 = KlasaStudent("Ola")  # to działa!

Rozróżnij: Student (klasa) od Student() (wywołanie klasy = nowy obiekt). To kluczowe rozróżnienie.

Błąd nawiasy

Tworzenie obiektu przez wywołanie klasy to jeden z najważniejszych wzorców w Pythonie. Składnia NazwaKlasy() może być myląca dla początkujących, ponieważ wygląda jak wywołanie funkcji, ale w rzeczywistości uruchamia złożony proces alokacji i inicjalizacji. Python, jako język dynamiczny, pozwala na tworzenie obiektów w dowolnym momencie działania programu, w tym wewnątrz pętli, warunków czy wyrażeń listowych. Każde wywołanie klasy tworzy nowy, niezależny obiekt w pamięci, nawet jeżeli klasa nie ma zdefiniowanego konstruktora. Zmienna przechowuje referencję do obiektu, a nie sam obiekt, co ma kluczowe znaczenie dla zrozumienia mechanizmu przypisań i kopiowania.

Proces tworzenia obiektu składa się z kilku etapów: najpierw wywoływana jest metoda __new__, która alokuje pamięć dla nowej instancji, następnie uruchamiany jest __init__ do inicjalizacji atrybutów, a na końcu zwracana jest referencja do gotowego obiektu. Większość programistów modyfikuje wyłącznie __init__, pozostawiając __new__ w domyślnej implementacji. Zrozumienie tej kolejności jest ważne przy tworzeniu bardziej zaawansowanych wzorców, takich jak Singleton czy Fabryka. Gdy tworzymy wiele obiektów w pętli, każdy z nich jest niezależną instancją z własnym stanem i tożsamością.

43/50 Błąd: nadpisywanie klasy zamiast tworzenia obiektów

Klasę definiujemy raz, potem tworzymy obiekty. Nie nadpisuj klasy! Jeżeli przypiszesz obiekt do zmiennej o tej samej nazwie co klasa, klasa przestaje istnieć w tej przestrzeni nazw.

Przeanalizujmy poniższy kod:

class Auto:
    def __init__(self, model):
        self.model = model

# ŹLE - nadpisanie klasy
Auto = Auto("Fiat")  # zmienna Auto to teraz obiekt, nie klasa!

# Kolejna próba tworzenia auta rzuci błędem
# a2 = Auto("Toyota") - TypeError: "Auto" object is not callable

Używaj małych liter dla zmiennych (obiektów) i PascalCase dla klas - to zapobiega takim błędom. Konwencja nazewnicza to nie tylko estetyka, ale ochrona przed poważnymi błędami.

Błąd nadpisywanie

Tworzenie obiektu przez wywołanie klasy to jeden z najważniejszych wzorców w Pythonie. Składnia NazwaKlasy() może być myląca dla początkujących, ponieważ wygląda jak wywołanie funkcji, ale w rzeczywistości uruchamia złożony proces alokacji i inicjalizacji. Python, jako język dynamiczny, pozwala na tworzenie obiektów w dowolnym momencie działania programu, w tym wewnątrz pętli, warunków czy wyrażeń listowych. Każde wywołanie klasy tworzy nowy, niezależny obiekt w pamięci, nawet jeżeli klasa nie ma zdefiniowanego konstruktora. Zmienna przechowuje referencję do obiektu, a nie sam obiekt, co ma kluczowe znaczenie dla zrozumienia mechanizmu przypisań i kopiowania.

Proces tworzenia obiektu składa się z kilku etapów: najpierw wywoływana jest metoda __new__, która alokuje pamięć dla nowej instancji, następnie uruchamiany jest __init__ do inicjalizacji atrybutów, a na końcu zwracana jest referencja do gotowego obiektu. Większość programistów modyfikuje wyłącznie __init__, pozostawiając __new__ w domyślnej implementacji. Zrozumienie tej kolejności jest ważne przy tworzeniu bardziej zaawansowanych wzorców, takich jak Singleton czy Fabryka. Gdy tworzymy wiele obiektów w pętli, każdy z nich jest niezależną instancją z własnym stanem i tożsamością.

44/50 Błąd: używanie nazwy klasy jak zmiennej

Nie używaj nazwy klasy jako tymczasowej zmiennej. Gdy przypiszesz coś do nazwy klasy, tracisz dostęp do definicji klasy.

Przeanalizujmy poniższy kod:

class Pracownik:
    def __init__(self, imie):
        self.imie = imie

# ŹLE - nadpisanie zmiennej
Pracownik = "Jan Kowalski"  # Klasa Pracownik już nie istnieje!

# Teraz nie możesz utworzyć obiektu Pracownik
# p = Pracownik("Ala")  # TypeError: "str" object is not callable

Zawsze używaj różnych nazw dla klasy i zmiennych. Konwencja: PascalCase dla klas, snake_case dla obiektów (np. pracownik = Pracownik() ).

Błąd nazwa klasy

Błędy są naturalną i nieodłączną częścią procesu uczenia się programowania. Każdy, nawet najbardziej doświadczony programista, popełnia błędy na co dzień - kluczowa jest umiejętność ich szybkiego identyfikowania i poprawiania. Przedstawione na tym slajdzie typowe pomyłki zostały zebrane na podstawie wieloletnich doświadczeń nauczycieli programowania i występują u większości początkujących. Zapamiętanie ich i zrozumienie przyczyn pomoże Ci uniknąć frustracji i straconego czasu na debugowanie. Warto również zapoznać się z technikami debugowania, takimi jak użycie print(), logging czy debuggera wbudowanego w IDE. Świadomość typowych pułapek to pierwszy krok do ich unikania.

Nowoczesne edytory kodu i IDE oferują wiele narzędzi pomagających w wykrywaniu błędów jeszcze przed uruchomieniem programu. PyCharm, VS Code z wtyczką Python, a nawet zaawansowane edytory tekstu z obsługą lintingu potrafią ostrzegać przed wieloma typowymi pomyłkami w czasie rzeczywistym. Korzystanie z type hintów, narzędzi takich jak mypy do statycznej analizy typów oraz systemów CI/CD z automatycznym uruchamianiem testów znacząco redukuje liczbę błędów w kodzie produkcyjnym. Warto od początku nauki wyrobić sobie nawyk korzystania z tych narzędzi, ponieważ znacząco podnoszą one jakość i niezawodność tworzonego oprogramowania.

45/50 Dobra praktyka: PascalCase dla nazw klas

W Pythonie przyjęto konwencję PascalCase (zwany też CapWords) dla nazw klas. To część oficjalnego przewodnika stylu - PEP 8 . Dzięki temu od razu widać, co jest klasą, a co obiektem lub funkcją.

Przeanalizujmy poniższy kod:

# Dobrze - PascalCase
class KontoBankowe:
    pass

class KlientVIP:
    pass

class AdresDomowy:
    pass

# źle - nie używaj
# class konto_bankowe:
# class Klient_Vip:
# class adresdomowy:

# Dla obiektów używamy snake_case
konto = KontoBankowe()
vip = KlientVIP()
adres = AdresDomowy()

Dzięki temu od razu widać, co jest klasą, a co obiektem lub funkcją. To nie fanaberia - w dużych projektach ta konwencja znacząco ułatwia czytanie kodu.

PascalCase

Stosowanie dobrych praktyk i konwencji nazewniczych od samego początku nauki programowania to inwestycja, która procentuje w miarę rozwoju umiejętności. PEP 8, oficjalny przewodnik stylu Pythona, definiuje zalecenia dotyczące formatowania kodu, które są powszechnie akceptowane w społeczności programistów. Przestrzeganie PEP 8 jest ważne nie tylko ze względów estetycznych, ale przede wszystkim dla zapewnienia spójności kodu w zespołach. Konsekwentne stosowanie PascalCase dla klas i snake_case dla zmiennych sprawia, że kod jest czytelny i przewidywalny. Automatyczne narzędzia takie jak black, autopep8 czy isort pomagają utrzymać spójny styl bez ręcznego formatowania.

Umieszczanie każdej klasy w osobnym pliku to praktyka, która ułatwia nawigację po projekcie i zarządzanie kodem. Plik z klasą staje się modułem, który można importować w innych częściach projektu, co promuje modularność i ponowne użycie kodu. Docstringi, czyli dokumentacja wbudowana w kod, są dostępne przez atrybut __doc__ i narzędzie help(), co czyni je nieocenionymi w dużych projekcie. Dobrze napisany docstring wyjaśnia cel i sposób użycia klasy bez konieczności czytania całej implementacji. System kontroli wersji Git lepiej radzi sobie z małymi plikami niż z jednym ogromnym.

46/50 Dobra praktyka: każda klasa w osobnym pliku

W większych projektach każdą klasę umieszczamy w osobnym pliku. To ułatwia nawigację, utrzymanie kodu i współpracę w zespole. Struktura katalogów jest wtedy czytelna i przewidywalna.

Struktura katalogów:

project/
├── main.py
├── klasy/
│   ├── student.py
│   ├── pracownik.py
│   └── adres.py
└── README.md

Przykład pliku student.py:

# student.py
class Student:
    def __init__(self, imie):
        self.imie = imie

Importujemy potem: from klasy.student import Student . Dzięki temu kod jest modularny i każdą klasę można testować i modyfikować niezależnie.

Dla małych projektów (1-2 klasy) możesz trzymać wszystko w jednym pliku. Z czasem, gdy projekt rośnie, warto podzielić go na osobne moduły.

Osobny plik

Stosowanie dobrych praktyk i konwencji nazewniczych od samego początku nauki programowania to inwestycja, która procentuje w miarę rozwoju umiejętności. PEP 8, oficjalny przewodnik stylu Pythona, definiuje zalecenia dotyczące formatowania kodu, które są powszechnie akceptowane w społeczności programistów. Przestrzeganie PEP 8 jest ważne nie tylko ze względów estetycznych, ale przede wszystkim dla zapewnienia spójności kodu w zespołach. Konsekwentne stosowanie PascalCase dla klas i snake_case dla zmiennych sprawia, że kod jest czytelny i przewidywalny. Automatyczne narzędzia takie jak black, autopep8 czy isort pomagają utrzymać spójny styl bez ręcznego formatowania.

Umieszczanie każdej klasy w osobnym pliku to praktyka, która ułatwia nawigację po projekcie i zarządzanie kodem. Plik z klasą staje się modułem, który można importować w innych częściach projektu, co promuje modularność i ponowne użycie kodu. Docstringi, czyli dokumentacja wbudowana w kod, są dostępne przez atrybut __doc__ i narzędzie help(), co czyni je nieocenionymi w dużych projekcie. Dobrze napisany docstring wyjaśnia cel i sposób użycia klasy bez konieczności czytania całej implementacji. System kontroli wersji Git lepiej radzi sobie z małymi plikami niż z jednym ogromnym.

47/50 Dobra praktyka: dokumentowanie klasy (docstring)

Każda klasa powinna mieć docstring - opis tego, co robi, jak jej używać i czego się spodziewać. To nie komentarz, a formalna dokumentacja dostępna przez __doc__ i narzędzia typu help() .

Przeanalizujmy poniższy kod:

class Kalkulator:
    """Prosty kalkulator wykonuj\u0105cy podstawowe dzia\u0142ania
    matematyczne: dodawanie, odejmowanie, mno\u017cenie, dzielenie.

    Przyk\u0142ad:
        k = Kalkulator()
        print(k.dodaj(2, 3))  # 5
    """

    def dodaj(self, a, b):
        """Zwraca sum\u0119 a + b."""
        return a + b

    def dziel(self, a, b):
        """Zwraca wynik dzielenia a / b.

        Args:
            a: dzielna
            b: dzielnik (nie mo\u017ce by\u0107 0)

        Raises:
            ValueError: gdy b = 0
        """
        if b == 0:
            raise ValueError("Nie dziel przez zero!")
        return a / b

help(Kalkulator)

Docstring umieszczamy zaraz po nagłówku klasy, w trzech podwójnych cudzysłowach. Format dokumentacji może być dowolny, ale warto trzymać się konwencji (np. Google, NumPy, Sphinx). Pomaga innym (i tobie za 6 miesięcy) zrozumieć kod bez czytania implementacji.

Docstring

Stosowanie dobrych praktyk i konwencji nazewniczych od samego początku nauki programowania to inwestycja, która procentuje w miarę rozwoju umiejętności. PEP 8, oficjalny przewodnik stylu Pythona, definiuje zalecenia dotyczące formatowania kodu, które są powszechnie akceptowane w społeczności programistów. Przestrzeganie PEP 8 jest ważne nie tylko ze względów estetycznych, ale przede wszystkim dla zapewnienia spójności kodu w zespołach. Konsekwentne stosowanie PascalCase dla klas i snake_case dla zmiennych sprawia, że kod jest czytelny i przewidywalny. Automatyczne narzędzia takie jak black, autopep8 czy isort pomagają utrzymać spójny styl bez ręcznego formatowania.

Umieszczanie każdej klasy w osobnym pliku to praktyka, która ułatwia nawigację po projekcie i zarządzanie kodem. Plik z klasą staje się modułem, który można importować w innych częściach projektu, co promuje modularność i ponowne użycie kodu. Docstringi, czyli dokumentacja wbudowana w kod, są dostępne przez atrybut __doc__ i narzędzie help(), co czyni je nieocenionymi w dużych projekcie. Dobrze napisany docstring wyjaśnia cel i sposób użycia klasy bez konieczności czytania całej implementacji. System kontroli wersji Git lepiej radzi sobie z małymi plikami niż z jednym ogromnym.

48/50 Kod proceduralny vs obiektowy dla tego samego zadania

Porównajmy dwa podejścia dla zadania "zarządzanie kontem bankowym" - zobacz, jak zmienia się organizacja kodu:

Przeanalizujmy poniższy kod:

# PODEJŚCIE PROCEDURALNE
saldo = 0
def wplac(kwota):
    global saldo
    saldo += kwota
    return saldo

def wyplac(kwota):
    global saldo
    if kwota <= saldo:
        saldo -= kwota
        return saldo
    return "Brak \u015brodk\u00f3w"

# Problem: co jeżeli mamy dwa konta?
# Musimy używać słowników lub osobnych zmiennych

# PODEJŚCIE OBIEKTOWE
class Konto:
    def __init__(self, wlasciciel, saldo_poczatkowe=0):
        self.wlasciciel = wlasciciel
        self.saldo = saldo_poczatkowe

    def wplac(self, kwota):
        self.saldo += kwota
        return self.saldo

    def wyplac(self, kwota):
        if kwota <= self.saldo:
            self.saldo -= kwota
            return self.saldo
        return "Brak \u015brodk\u00f3w"

# Łatwo tworzymy wiele niezależnych kont
konto1 = Konto("Ala", 1000)
konto2 = Konto("Jan")

Obiektowe jest czytelniejsze i nie wymaga globalnych zmiennych - każde konto ma własne saldo. W wersji proceduralnej dodanie drugiego konta wymagałoby kopiowania funkcji i zmiennych, podczas gdy w OOP po prostu tworzysz drugą instancję.

Porównanie programowania proceduralnego i obiektowego na konkretnych przykładach pokazuje fundamentalne różnice w organizacji kodu. Podejście proceduralne koncentruje się na sekwencji operacji i funkcjach przetwarzających dane, podczas gdy obiektowe grupuje powiązane dane i funkcje w spójne jednostki zwane klasami. W praktyce zawodowej rzadko spotyka się czyste implementacje jednego paradygmatu - nowoczesne aplikacje łączą różne podejścia w zależności od potrzeb konkretnego modułu. Python, jako język wieloparadygmatowy, doskonale wspiera taki hybrydowy styl programowania, pozwalając programiście na elastyczny wybór narzędzia. Wybór paradygmatu zależy przede wszystkim od charakteru problemu - proste skrypty często lepiej napisać proceduralnie, a złożone systemy obiektowo.

Tabelaryczne zestawienie cech obu podejść ułatwia zrozumienie, kiedy które z nich jest bardziej odpowiednie. Programowanie proceduralne sprawdza się w małych, liniowych skryptach, gdzie ważna jest prostota i szybkość wykonania. OOP dominuje w dużych projektach, gdzie kluczowe są organizacja kodu, wielokrotne użycie i łatwość utrzymania. Warto również zauważyć, że wiele języków nowej generacji, takich jak Rust czy Kotlin, łączy elementy obu podejść, oferując programistom najlepsze cechy każdego z nich. Świadomy wybór paradygmatu jest oznaką dojrzałości programistycznej i pozwala na podejmowanie optymalnych decyzji projektowych w codziennej pracy.

49/50 Mapa myśli całej części
OOP w Pythonie - Cz\u0119\u015b\u0107 1
Czym jest OOP Klasa vs Obiekt class Nazwa: __init__
Atrybuty (stan) Metody (zachowania) self Stan obiektu
id()is vs ==Lista obiektówKompozycja
Błędy (self, ()) PascalCase Docstring Osobne pliki

Od tej pory myślisz obiektowo! Każdy byt w kodzie to potencjalna klasa z atrybutami i metodami. Przed napisaniem kodu zastanów się: jakie obiekty występują w moim problemie? Jakie mają cechy i zachowania?

Podsumowanie to dobry moment na refleksję nad przyswojonym materiałem i identyfikację obszarów wymagających dodatkowej pracy. Wymienione punkty stanowią esencję przerobionej części kursu - od definicji klasy, przez tworzenie obiektów, aż po kompozycję i wzorzec fabryki. Każdy z tych punktów będzie rozwijany w kolejnych modułach, dlatego warto upewnić się, że są dobrze zrozumiane. Zachęcamy do tworzenia własnych notatek i map myśli, które pomagają w usystematyzowaniu wiedzy. Regularne powtórki są kluczowe dla trwałego zapamiętania materiału.

Mapy myśli są skutecznym narzędziem wizualizacji złożonych koncepcji i relacji między nimi. Przedstawiona mapa obrazuje najważniejsze pojęcia omówione w tej części kursu oraz ich wzajemne powiązania. Tworzenie własnych map myśli podczas nauki programowania aktywuje inne obszary mózgu niż czytanie liniowego tekstu, co przekłada się na lepsze zapamiętywanie. Studenci, którzy regularnie tworzą mapy myśli, osiągają lepsze wyniki w testach koncepcyjnych i szybciej łączą nowe informacje z już posiadaną wiedzą. Zachęcamy do wykorzystania tej techniki.

50/50 Quiz: sprawdź swoją wiedzę
Pytanie 1: Czym różni się klasa od obiektu?

Odpowiedź: Klasa to szablon definiujący strukturę i zachowania (jak foremka do ciastek). Obiekt to konkretna instancja klasy, mająca własny stan w pamięci (jak upieczone ciastko). Z jednej klasy powstaje wiele obiektów.

Pytanie 2: Do czego służy parametrselfw metodach?

Odpowiedź: self odwołuje się do konkretnego obiektu, na którym wywołano metodę. Pozwala na dostęp do atrybutów i innych metod tego obiektu. Python przekazuje go automatycznie przy wywołaniu metody.

Pytanie 3: Jaka jest różnica między == a is przy porównywaniu obiektów?

Odpowiedź: == sprawdza równość wartości (może być nadpisane przez __eq__ ). is sprawdza tożsamość - czy dwie zmienne wskazują na ten sam obiekt w pamięci. Domyślnie dla własnych klas == działa jak is .

Pytanie 4: Co się stanie, gdy zapomnisz nawiasów () przy tworzeniu obiektu?

Odpowiedź: Nie utworzysz obiektu - przypiszesz do zmiennej referencję do klasy, a nie instancji. s = Student to klasa, s = Student() to obiekt.

Gratulacje! Ukończyłeś część 1 kursu pyMID OOP. Poznałeś fundamenty programowania obiektowego: klasy, obiekty, atrybuty, metody, konstruktor, self, porównywanie obiektów, kompozycję i dobre praktyki. Przejdź do części 2, aby poznać enkapsulację i właściwości.

Quiz

Quiz podsumowujący to nie tylko sprawdzenie wiedzy, ale także okazja do utrwalenia najważniejszych koncepcji poprzez aktywne przypominanie sobie materiału. Badania z zakresu neurodydaktyki pokazują, że testowanie własnej wiedzy jest jedną z najskuteczniejszych metod uczenia się, znacznie efektywniejszą niż bierne czytanie. Każde pytanie quizu zostało starannie zaprojektowane, aby sprawdzić zrozumienie konkretnego zagadnienia, a nie tylko pamięciowe odtworzenie definicji. Jeżeli któreś pytanie sprawiło Ci trudność, potraktuj to jako sygnał, że dany temat wymaga powtórzenia. Wróć do odpowiedniego slajdu i przeanalizuj go jeszcze raz, tym razem zwracając uwagę na szczegóły, które mogły Ci umknąć.

Sukces w nauce programowania polega na systematycznym budowaniu wiedzy - każda kolejna część opiera się na fundamentach z części poprzednich. Jeżeli masz wątpliwości co do którejkolwiek koncepcji, nie przechodź dalej, dopóki jej nie wyjaśnisz. Wykorzystaj dostępne zasoby: dokumentację Pythona, fora społecznościowe, tutoriale wideo i oczywiście możliwość eksperymentowania we własnym środowisku programistycznym. Pamiętaj, że każdy zaawansowany programista zaczynał od podstaw i pokonał te same trudności co Ty teraz. Gratulujemy ukończenia kolejnego etapu nauki i życzymy powodzenia w dalszej części kursu.