Streszczenie
Definiowanie klas w Pythonie -- class, __init__, self i tworzenie obiektów

Ten moduł w całości poświęcony jest praktycznemu definiowaniu klas w języku Python -- od podstawowej składni słowa kluczowego class , przez konstruktor __init__ i parametr self , aż po atrybuty instancji i tworzenie obiektów. Szczegółowo omówiono metody instancji z parametrami i wartościami zwracanymi, a także domyślne wartości parametrów z ostrzeżeniem przed pułapką mutowalnych argumentów domyślnych. Przedstawiono techniki symulowania wielu konstruktorów za pomocą parametrów opcjonalnych oraz metod klasowych @classmethod . Moduł wprowadza również konwencję nazewniczą PascalCase dla klas oraz omawia zmienne referencyjne i niezależność obiektów. Całość uzupełnia praktyczny przykład klasy KontoBankowe z walidacją, historią transakcji i czytelnym interfejsem, która łączy wszystkie poznane koncepcje w spójną całość.

  • Składnia definicji klasy -- słowo kluczowe class , konwencja PascalCase, elementy ciała klasy (atrybuty, metody, metody specjalne)
  • Konstruktor __init__ i self -- automatyczne wywołanie przy tworzeniu obiektu, parametry konstruktora, rola self jako odniesienia do bieżącej instancji
  • Atrybuty instancji i tworzenie obiektów -- definiowanie atrybutów przez self.nazwa = wartosc , niezależność atrybutów między obiektami, zmienne referencyjne
  • Metody instancji i parametry -- definiowanie metod, parametry po self , zwracanie wartości za pomocą return , walidacja danych, domyślne wartości parametrów
  • Dobre praktyki i zaawansowane techniki -- unikanie mutowalnych wartości domyślnych ( None zamiast [] ), symulacja wielu konstruktorów ( @classmethod ), praktyczny przykład KontoBankowe
1Wprowadzenie

Część 2: definiowanie klas

Witamy w drugiej części kursu pySIMPLE OOP . W części pierwszej poznaliśmy podstawowe pojęcia programowania obiektowego - czym jest klasa, obiekt, enkapsulacja, dziedziczenie i polimorfizm. Teraz czas przejść do praktyki: nauczymy się definiować własne klasy w języku Python od podstaw.

Programowanie obiektowe (OOP) to paradygmat, w którym grupowujemy dane (atrybuty) i funkcje (metody) w spójne jednostki zwane obiektami. Klasa jest przepisem (szablonem), według którego tworzone są obiekty. W Pythonie wszystko jest obiektem - nawet typy takie jak int , str czy list są klasami, a zmienne tych typów to ich instancje.

W tej części omówimy składnię definicji klasy, konstruktor __init__ , parametr self , atrybuty instancji, tworzenie obiektów oraz metody instancji. Poznasz również zagadnienia takie jak domyślne wartości parametrów, symulacja wielu konstruktorów, oraz praktyczny przykład klasy KontoBankowe łączącej wszystkie poznane koncepcje.

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.

2Cele dydaktyczne

Cele dydaktyczne

Po tej części będziesz umieć:

  • Definiować klasę za pomocą słowa kluczowego class i rozumieć strukturę jej ciała
  • Tworzyć konstruktor __init__ , rozumieć jego rolę w inicjalizacji obiektu oraz moment jego automatycznego wywołania
  • Posługiwać się parametrem self jako odniesieniem do bieżącej instancji - wyjaśniać, dlaczego jest wymagany i jak działa pod maską
  • Definiować i używać atrybutów instancji, odróżniać je od atrybutów klasowych
  • Tworzyć obiekty z klas, operować na nich i rozumieć, że zmienne są referencjami do obiektów w pamięci
  • Pisać metody instancji z parametrami i zwracać wartości, stosować walidację danych wejściowych
  • Stosować domyślne wartości parametrów w konstruktorze, unikać pułapki mutowalnych wartości domyślnych
  • Symulować wiele konstruktorów za pomocą parametrów opcjonalnych oraz metod klasowych @classmethod
  • Zbudować praktyczną klasę KontoBankowe z walidacją, historią transakcji i czytelnym interfejsem

Wymagania wstępne: znajomość podstaw Pythona (funkcje, zmienne, typy danych, instrukcje warunkowe, pętle, listy) oraz podstawowych pojęć OOP z części 1 kursu.

Cele dydaktyczne

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 Składnia definicji klasy

Składnia class Nazwa:

Aby zdefiniować klasę w Pythonie, używamy słowa kluczowego class , po którym podajemy nazwę klasy i dwukropek. Ciało klasy jest wcięte - podobnie jak w przypadku definicji funkcji ( def ) czy instrukcji warunkowych ( if ). Konwencja PEP 8 zaleca wcięcia o 4 spacje.

Ogólna składnia wygląda następująco:

class NazwaKlasy:
    # ciało klasy - atrybuty i metody
    pass

Nazwa klasy powinna być rzeczownikiem w liczbie pojedynczej zapisanym w konwencji PascalCase (zwanej też CapWords lub CamelCase z wielkiej litery). Każdy człon nazwy zaczynamy wielką literą, bez podkreślników między nimi. Przykłady poprawnych nazw: KontoBankowe , AdresDostawy , KlientVIP .

Ciało klasy może zawierać:

  • Atrybuty klasowe - zmienne wspólne dla wszystkich instancji, definiowane bezpośrednio w ciele klasy
  • Atrybuty instancji - zmienne należące do konkretnego obiektu, definiowane w __init__ przez self
  • Metody instancji - funkcje, których pierwszym parametrem jest self
  • Metody specjalne - takie jak __init__ , __str__ , __repr__ , otoczone podwójnymi podkreślnikami (tzw. "dunder methods")
  • Zagnieżdżone klasy - klasy zdefiniowane wewnątrz innych klas (rzadziej stosowane)

Instrukcja pass to tzw. "zaślepka" - nie robi nic, a jedynie pozwala zachować poprawną składnię, gdy ciało klasy jest celowo puste. W rzeczywistym kodzie zamiast pass piszemy właściwe definicje atrybutów i metod.

Slajd zatytułowany "Składnia definicji klasy" 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.

4 Pusta klasa i pierwszy obiekt

Pusta klasa i pierwszy obiekt

Najprostszą klasą jest klasa pusta - zawierająca tylko instrukcję pass . Mimo że nie ma ani atrybutów, ani metod, możemy już tworzyć jej obiekty. To pokazuje fundamentalną własność Pythona: tworzenie instancji nie wymaga żadnego konstruktora - Python zapewnia domyślny.

Przeanalizujmy poniższy kod:

# Definicja pustej klasy
class Pies:
    pass

# Tworzenie obiektu (instancji) klasy Pies
moj_pies = Pies()
print(moj_pies)
print(type(moj_pies))

# Każda instancja jest inna - różne ID w pamięci
inny_pies = Pies()
print(moj_pies is inny_pies)  # False - to różne obiekty
print(id(moj_pies), id(inny_pies))  # różne adresy w pamięci

Po uruchomieniu zobaczymy reprezentację obiektu w pamięci (np. <__main__.Pies object at 0x...> ) oraz informację, że należy on do klasy Pies . Operator is porównuje tożsamość obiektów (czy to ten sam obiekt w pamięci), a funkcja id() zwraca unikalny identyfikator obiektu (adres pamięci w implementacji CPythona). Każdy obiekt jest odrębną instancją z własnym miejscem w pamięci - nawet jeśli są tej samej klasy i nie mają żadnych atrybutów.

Choć pusta klasa jest mało przydatna w praktyce, doskonale ilustruje, że w Pythonie wszystko jest obiektem, a definiowanie klas jest proste i składniowo lekkie.

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.

5 Ciało klasy - co może zawierać

Co może zawierać ciało klasy?

Ciało klasy może zawierać wiele elementów. Oto najważniejsze z nich, wraz z przykładami:

  • Atrybuty klasowe - zmienne wspólne dla wszystkich obiektów (np. licznik = 0 ). Są definiowane bezpośrednio w ciele klasy, poza metodami. Wszystkie instancje współdzielą tę samą wartość, chyba że zostanie przesłonięta przez atrybut instancji.
  • Konstruktor __init__ - metoda wywoływana automatycznie tuż po utworzeniu obiektu. Służy do inicjalizacji atrybutów instancji. Może przyjmować parametry przekazywane podczas tworzenia obiektu.
  • Metody instancji - funkcje operujące na konkretnym obiekcie. Ich pierwszym parametrem jest zawsze self , przez który mają dostęp do atrybutów danej instancji.
  • Metody klasowe (@classmethod) i statyczne (@staticmethod) - zaawansowane rodzaje metod. Metody klasowe otrzymują jako pierwszy parametr cls (odniesienie do klasy), a statyczne nie otrzymują ani self , ani cls - działają jak zwykłe funkcje umieszczone w klasie dla wygody.
  • Właściwości (@property) - enkapsulacja dostępu do atrybutów. Pozwalają definiować metody, które są używane jak atrybuty (bez nawiasów), z możliwością dodania logiki walidacji przy odczycie, zapisie i usuwaniu.
  • Dokumentacja (docstring) - opis klasy w potrójnych cudzysłowach """...""" , umieszczany zaraz po nagłówku klasy. Jest dostępny przez help(NazwaKlasy) lub NazwaKlasy.__doc__ .

Prawidłowo zaprojektowana klasa grupuje dane i funkcje, które są ze sobą ściśle powiązane - to podstawowa zasada programowania obiektowego, zwana kohezją . Wysoka kohezja oznacza, że wszystkie elementy klasy służą jednemu, dobrze zdefiniowanemu celowi.

# Przykład klasy z różnymi elementami
class Przyklad:
    """To jest przykładowa klasa pokazująca różne elementy ciała klasy."""

    atrybut_klasowy = 0  # atrybut klasowy

    def __init__(self, wartosc):  # konstruktor
        self.wartosc = wartosc  # atrybut instancji

    def metoda(self):  # metoda instancji
        return self.wartosc

    @classmethod
    def metoda_klasowa(cls):
        return cls.atrybut_klasowy
Ciało klasy

Slajd zatytułowany "Ciało klasy - co może zawierać" 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.

6 Konwencja nazewnictwa - PascalCase

PascalCase dla klas, snake_case dla wszystkiego innego

W Pythonie przyjęło się, że nazwy klas zapisujemy w konwencji PascalCase (znanej też jako CapWords). Oznacza to, że każdy człon nazwy zaczynamy wielką literą, a resztę piszemy małymi literami - bez podkreślników. Jest to zalecenie zawarte w oficjalnym przewodniku stylu PEP 8.

Nazwy zmiennych, funkcji i metod zapisujemy w snake_case - małymi literami z podkreślnikami między wyrazami. Stałe (wartości, które nie powinny ulegać zmianie) zapisujemy w UPPER_CASE (wielkie litery z podkreślnikami).

Przykłady poprawnych i niepoprawnych konwencji:

  • class KontoBankowe: - PascalCase ✓ (poprawna konwencja dla klas)
  • class kontobankowe: - ✗ (złamana konwencja - wygląda jak zmienna)
  • class Konto_Bankowe: - ✗ (mieszanie PascalCase z podkreślnikami)
  • class osoba: - ✗ (powinno być Osoba)
  • class HTMLParser: - PascalCase dla akronimów ✓ (wielkie litery dla skrótów)
  • class KlientVIP: - PascalCase z akronimem ✓

Dlaczego to ważne? Jednolita konwencja sprawia, że kod jest czytelniejszy - od razu widać, co jest klasą ( KontoBankowe ), a co zmienną ( konto_bankowe ) czy funkcją ( utworz_konto ). Ułatwia to pracę zespołową i przeglądanie kodu.

W Pythonie istnieje też konwencja dla nazw metod specjalnych: __init__ , __str__ , __repr__ - tzw. "dunder methods" (double underscore). Są one zarezerwowane dla metod wbudowanych w język.

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żym 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.

7 Porównanie dobrego i złego kodu - nazwy klas

Dobre i złe nazwy klas

Przeanalizujmy poniższy kod obrazujący różnice między dobrymi a złymi praktykami nazewnictwa klas:

# DOBRZE - PascalCase, czytelne nazwy
class Klient:
    pass

class KoszykZakupowy:
    pass

class AdresDostawy:
    pass

# ŹLE - złe konwencje, mylące nazwy
class klient:        # powinno być Klient (snake_case to konwencja dla zmiennych)
    pass

class koszyk:         # powinno być KoszykZakupowy (zbyt ogólne)
    pass

class x:               # nic nie mówiąca nazwa - zupełnie nieczytelna
    pass

# Jeszcze gorszy przypadek - myląca nazwa klasy
class funkcja_obliczeniowa:  # to nie jest funkcja, to klasa!
    pass

Zasady dobrych nazw klas:

  • Są rzeczownikami (osoba, miejsce, rzecz, koncepcja) - np. Klient , Zamowienie , Silnik
  • Używają PascalCase - każdy człon z wielkiej litery
  • Są konkretne, nie ogólnikowe - KoszykZakupowy zamiast Koszyk , KlientVIP zamiast Klient
  • Są jednoznaczne - nazwa mówi, co klasa reprezentuje w domenie problemu
  • Są w języku angielskim lub polskim (należy być konsekwentnym w całym projekcie)

Złe nazwy są mylące, używają złej konwencji (snake_case zamiast PascalCase), są zbyt ogólne ( Manager , Dane , Utils ) lub wręcz nic nie znaczą ( KlasaA , Temp , x ). Dobra nazwa klasy to pierwszy krok do czytelnego i łatwego w utrzymaniu kodu.

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.

8 Dlaczego potrzebujemy konstruktora?

Konstruktor - wprowadzenie

Pusta klasa nie jest zbyt przydatna w praktyce. Chcemy, aby nasze obiekty miały dane (atrybuty) od razu po utworzeniu - gotowe do użycia, bez konieczności ręcznego dodawania każdego atrybutu po utworzeniu. Do tego służy konstruktor - w Pythonie jest to metoda o nazwie __init__ (czytaj: "dunder init", od "double underscore init").

Konstruktor to specjalna metoda, która:

  • Jest wywoływana automatycznie tuż po utworzeniu obiektu przez __new__ (który alokuje pamięć), ale przed zwróceniem obiektu do kodu wywołującego
  • Służy do inicjalizacji (ustawienia początkowych wartości) atrybutów instancji
  • Może przyjmować parametry przekazywane podczas tworzenia obiektu - to one definiują interfejs konstruktora
  • Nie może zwracać żadnej wartości (poza None ) - próba zwrócenia czegokolwiek innego spowoduje TypeError w Pythonie 3
  • Jego nazwa jest zarezerwowana przez język - nie powinieneś definiować własnych metod o nazwie __init__ do innych celów

Prawidłowa inicjalizacja w konstruktorze to dobra praktyka programistyczna. Zapewnia, że obiekt od razu po utworzeniu jest w spójnym, poprawnym stanie. Na przykład konto bankowe może od razu mieć swój numer i stan konta, a samochód - markę, model i prędkość początkową 0.

W Pythonie istnieje też metoda __new__ , która jest faktycznym konstruktorem (tworzy obiekt), ale w codziennej praktyce programistycznej rzadko ją nadpisujemy. __init__ jest tym, co potocznie nazywamy konstruktorem, choć technicznie jest to inicjalizator.

Wymienione zalety programowania obiektowego mają bezpośrednie przełożenie na praktykę inżynierii oprogramowania i codzienną pracę programisty. Czytelność kodu przekłada się na niższe koszty utrzymania, ponieważ nowi członkowie zespołu szybciej rozumieją strukturę projektu. Wielokrotne użycie klas i dziedziczenie redukują ilość duplikacji kodu, co jest jednym z głównych celów dobrych praktyk programistycznych. Organizacja kodu w klasy ułatwia nawigację po projekcie i przyspiesza wprowadzanie zmian. Łatwość utrzymania oznacza, że modyfikacje w jednym miejscu nie powodują nieoczekiwanych skutków ubocznych w innych. Bezpieczeństwo danych jest zwiększone dzięki enkapsulacji chroniącej stan obiektu przed przypadkową modyfikacją z zewnątrz.

Należy jednak pamiętać, że OOP nie jest srebrną kulą rozwiązującą wszystkie problemy programistyczne. Nadużywanie dziedziczenia prowadzi do głębokich hierarchii trudnych w utrzymaniu i zrozumieniu. Przesadna enkapsulacja może utrudniać testowanie i debugowanie kodu. W przypadku prostych zadań, takich jak jednorazowe skrypty do przetwarzania danych, podejście proceduralne jest w pełni wystarczające i często szybsze w implementacji. Dlatego tak ważne jest zrozumienie nie tylko zalet, ale też potencjalnych pułapek OOP. Świadomy programista wybiera narzędzie odpowiednie do zadania, zamiast ślepo stosować jeden paradygmat.

9 Kod: __init__ z parametrami

Konstruktor z parametrami

Przeanalizujmy poniższy kod - pierwszy praktyczny przykład konstruktora z parametrami:

class KontoBankowe:
    def __init__(self, numer_konta, saldo):
        self.numer_konta = numer_konta
        self.saldo = saldo
        print("Utworzono konto:", numer_konta)

# Tworzymy obiekt - konstruktor przyjmuje 2 argumenty
konto1 = KontoBankowe("12 3456 7890", 1000)
konto2 = KontoBankowe("98 7654 3210", 500)

# Możemy odczytać atrybuty każdego konta osobno
print(konto1.numer_konta)  # "12 3456 7890"
print(konto2.saldo)         # 500

# Możemy też modyfikować atrybuty
konto1.saldo += 200
print(konto1.saldo)  # 1200

Kluczowe obserwacje:

  • Parametry numer_konta i saldo są przekazywane podczas tworzenia obiektu - w nawiasach po nazwie klasy
  • Konstruktor przypisuje je do atrybutów instancji za pomocą składni self.nazwa = nazwa
  • Każde konto ma własne, niezależne dane - konto1.saldo to 1000, konto2.saldo to 500
  • Atrybuty instancji są dostępne z zewnątrz przez notację obiekt.atrybut
  • Możemy je odczytywać i modyfikować, co będzie widoczne w całym obiekcie

To fundamentalny mechanizm OOP - każdy obiekt przechowuje swój własny stan, a konstruktor jest miejscem, w którym ten stan jest inicjalizowany.

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.

10 Automatyczne wywołanie __init__

Automatyczne wywołanie konstruktora

Przeanalizujmy poniższy kod demonstrujący, kiedy dokładnie wywoływany jest konstruktor:

class Test:
    def __init__(self):
        print("Konstruktor został wywołany!")

print("Przed utworzeniem obiektu")
obiekt = Test()  # tutaj automatycznie woła się __init__
print("Po utworzeniu obiektu")

Uruchomienie tego kodu da następujący output:

Przed utworzeniem obiektu
Konstruktor został wywołany!
Po utworzeniu obiektu

Widzimy wyraźnie, że konstruktor wywołuje się automatycznie - między rozpoczęciem tworzenia obiektu a przypisaniem go do zmiennej. Nie musimy go wywoływać ręcznie - Python robi to za nas w momencie, gdy napotka NazwaKlasy() .

Proces tworzenia obiektu w Pythonie krok po kroku:

  1. Wywołanie Test() rozpoczyna proces tworzenia instancji
  2. Python woła metodę __new__ klasy, która alokuje pamięć dla nowego obiektu
  3. Python automatycznie woła __init__ z przekazanymi argumentami (jeśli są) i nowo utworzonym obiektem jako self
  4. __init__ inicjalizuje atrybuty obiektu
  5. Nowy, gotowy obiekt jest zwracany i przypisywany do zmiennej

Dzięki automatycznemu wywołaniu konstruktora nie ma ryzyka, że zapomnimy zainicjalizować obiekt po utworzeniu - dzieje się to zawsze i nie może być pominięte.

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.

11 Kod z życia: klasa Osoba z __init__

Klasa Osoba - praktyczny przykład

Przeanalizujmy poniższy kod - pierwszą pełną klasę z metodą instancji:

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

    def przedstaw_sie(self):
        return f"Nazywam się {self.imie} {self.nazwisko} i mam {self.wiek} lat."

    def czy_pelnoletnia(self):
        return self.wiek >= 18

# Tworzymy osobę
osoba1 = Osoba("Anna", "Kowalska", 28)
print(osoba1.przedstaw_sie())
print("Pełnoletnia?", osoba1.czy_pelnoletnia())

# Tworzymy drugą osobę - każda ma własne dane
osoba2 = Osoba("Piotr", "Nowak", 16)
print(osoba2.przedstaw_sie())
print("Pełnoletni?", osoba2.czy_pelnoletnia())

# Modyfikacja atrybutu - Anna ma urodziny
osoba1.wiek += 1
print("Po urodzinach:", osoba1.przedstaw_sie())

Klasa Osoba przechowuje dane osobowe i udostępnia metody operujące na tych danych. Konstruktor przyjmuje trzy parametry i zapisuje je jako atrybuty instancji. Metoda przedstaw_sie korzysta z atrybutów przez self , zwracając sformatowany napis. Metoda czy_pelnoletnia pokazuje, że metody mogą zawierać logikę warunkową i zwracać wartości logiczne. Atrybuty można modyfikować z zewnątrz (np. zwiększenie wieku o 1), co od razu wpływa na wyniki metod.

Klasa Osoba

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 Porównanie: klasa z i bez __init__

Klasa z __init__ i bez

Przeanalizujmy poniższy kod porównujący oba podejścia:

# Klasa BEZ konstruktora - atrybuty dodawane ręcznie
class Punkt1:
    pass

p1 = Punkt1()
p1.x = 10  # musimy ręcznie dodawać atrybuty
p1.y = 20

# Klasa Z konstruktorem - atrybuty wymuszone
class Punkt2:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p2 = Punkt2(10, 20)  # atrybuty od razu ustawione

# Problem z klasą bez konstruktora - łatwo zapomnieć o atrybucie
p3 = Punkt1()  # zapomnieliśmy ustawić x i y!
# print(p3.x)  # AttributeError: 'Punkt1' object has no attribute 'x'

# Z konstruktorem to niemożliwe - Python wymusi podanie argumentów
# p4 = Punkt2()  # TypeError: missing 2 required positional arguments

Różnice:

  • Klasa bez __init__ - atrybuty są dodawane po utworzeniu obiektu. Łatwo zapomnieć o ustawieniu któregoś atrybutu, co prowadzi do AttributeError . Kod jest podatny na błędy i nie wymusza spójnego stanu obiektu.
  • Klasa z __init__ - atrybuty definiowane są w konstruktorze. Python wymusza podanie wszystkich wymaganych argumentów już podczas tworzenia obiektu. Obiekt od razu jest w spójnym, poprawnym stanie. Kod jest bezpieczniejszy, czytelniejszy i samodokumentujący - od razu widać, jakie dane są potrzebne.

Dodatkową zaletą konstruktora jest to, że możemy w nim dodać walidację parametrów przed przypisaniem ich do atrybutów, co jeszcze bardziej zwiększa bezpieczeństwo kodu.

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.

13Czym jest self?

Parametr self - wprowadzenie

self to pierwszy parametr każdej metody instancji. Jest to odniesienie do konkretnego obiektu , na którym metoda jest wywoływana - czyli do bieżącej instancji klasy.

Gdy piszesz obiekt.metoda() , Python automatycznie przekazuje ten obiekt jako pierwszy argument do metody. Dzięki temu metoda wie, z którym obiektem pracuje i może odczytywać lub modyfikować jego atrybuty.

Nazwa self jest konwencją - możesz teoretycznie użyć innej nazwy (np. this , obiekt , instancja ), ale złamiesz wtedy powszechnie przyjęte standardy PEP 8. Inni programiści czytający Twój kod będą zdezorientowani. Zawsze używaj self .

Jak to działa pod maską? Gdy Python widzi osoba.przedstaw_sie() , przekształca to wewnętrznie na Osoba.przedstaw_sie(osoba) . Pierwszym argumentem jest zawsze instancja, na rzecz której metoda jest wywoływana. To odróżnia Pythona od języków takich jak C++ czy Java, gdzie this jest niejawnym wskaźnikiem - w Pythonie wszystko jest jawne.

Dzięki self Python nie potrzebuje osobnego słowa kluczowego do odnoszenia się do bieżącego obiektu. Jest to zgodne z filozofią Pythona: "Explicit is better than implicit" (Jawne jest lepsze niż niejawne).

Czym jest self

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.

14Kod: self w akcji

Jak działa self - demonstracja

Przeanalizujmy poniższy kod, który wyraźnie pokazuje, że self to ten sam obiekt co zmienna obj :

class Pokaz:
    def __init__(self, wartosc):
        self.wartosc = wartosc
        print(f"self ma ID: {id(self)}")

    def pokaz_self(self):
        print(f"Obiekt to: {self}")
        print(f"Wartość: {self.wartosc}")
        print(f"ID self w metodzie: {id(self)}")

obj = Pokaz(42)
print(f"obj ma ID: {id(obj)}")
obj.pokaz_self()

# Demonstracja, że Python woła metodę z obiektem jako pierwszym arg.
# Te dwa wywołania są równoważne:
obj.pokaz_self()                 # standardowy sposób
Pokaz.pokaz_self(obj)            # jawna wersja - to samo!

ID obiektu wyświetlone w konstruktorze, ID zmiennej obj oraz ID self w metodzie są identyczne. To dowodzi, że self to po prostu ten sam obiekt - Python przekazuje go automatycznie jako pierwszy argument.

Dodatkowo pokazaliśmy, że obj.pokaz_self() jest równoważne Pokaz.pokaz_self(obj) . W drugiej formie jawnie przekazujemy obiekt jako argument - Python robi to samo automatycznie w pierwszej formie. To wyjaśnia, dlaczego metody instancji muszą mieć self jako pierwszy parametr: Python potrzebuje miejsca, gdzie wstawić obiekt, na którym metoda jest wywoływana.

Self w akcji

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 Dlaczego self musi być pierwszy?

Self jako pierwszy parametr

Python wymaga, aby self był pierwszym parametrem w definicji każdej metody instancji. To nie przypadek - ma to głębokie uzasadnienie związane z architekturą języka.

Gdy wywołujesz obiekt.metoda(arg1, arg2) , Python przekształca to wewnętrznie na Klasa.metoda(obiekt, arg1, arg2) . Pierwszym argumentem jest zawsze instancja, na rzecz której metoda jest wywoływana. Reszta argumentów to te, które programista podał w nawiasach.

Porównanie z innymi językami:

  • Python:def metoda(self, arg) - jawne, czytelne
  • C++/Java: void metoda(arg) { this->arg = arg; } - this jest niejawne
  • JavaScript: metoda(arg) { this.arg = arg; } - this jest kontekstowe (zmienne w zależności od wywołania)

Dzięki jawnemu self Python unika problemów znanych z JavaScript, gdzie this zmienia znaczenie w zależności od kontekstu wywołania. W Pythonie self jest zawsze tym samym - pierwszym parametrem metody. Nie ma niejednoznaczności.

Kolejność parametrów w metodzie instancji musi być więc: self , potem parametry wymagane, potem parametry z wartościami domyślnymi, a na końcu *args i **kwargs (jeśli używane).

Wymienione zalety programowania obiektowego mają bezpośrednie przełożenie na praktykę inżynierii oprogramowania i codzienną pracę programisty. Czytelność kodu przekłada się na niższe koszty utrzymania, ponieważ nowi członkowie zespołu szybciej rozumieją strukturę projektu. Wielokrotne użycie klas i dziedziczenie redukują ilość duplikacji kodu, co jest jednym z głównych celów dobrych praktyk programistycznych. Organizacja kodu w klasy ułatwia nawigację po projekcie i przyspiesza wprowadzanie zmian. Łatwość utrzymania oznacza, że modyfikacje w jednym miejscu nie powodują nieoczekiwanych skutków ubocznych w innych. Bezpieczeństwo danych jest zwiększone dzięki enkapsulacji chroniącej stan obiektu przed przypadkową modyfikacją z zewnątrz.

Należy jednak pamiętać, że OOP nie jest srebrną kulą rozwiązującą wszystkie problemy programistyczne. Nadużywanie dziedziczenia prowadzi do głębokich hierarchii trudnych w utrzymaniu i zrozumieniu. Przesadna enkapsulacja może utrudniać testowanie i debugowanie kodu. W przypadku prostych zadań, takich jak jednorazowe skrypty do przetwarzania danych, podejście proceduralne jest w pełni wystarczające i często szybsze w implementacji. Dlatego tak ważne jest zrozumienie nie tylko zalet, ale też potencjalnych pułapek OOP. Świadomy programista wybiera narzędzie odpowiednie do zadania, zamiast ślepo stosować jeden paradygmat.

16 Częsty błąd: brak self w metodzie

Brak self - typowy błąd początkujących

Przeanalizujmy poniższy kod - jeden z najczęstszych błędów przy nauce OOP w Pythonie:

# BŁĘDNA definicja - brak self
class Blad:
    def __init__(self, nazwa):
        self.nazwa = nazwa

    def wyswietl():       # brak self!
        print(f"Nazwa: {nazwa}")

obj = Blad("Test")
obj.wyswietl()  # TypeError: wyswietl() takes 0 positional arguments but 1 was given

# Próba wywołania przez klasę też nie zadziała
# Blad.wyswietl()  # NameError: name 'nazwa' is not defined

Gdy brakuje self , Python nie ma możliwości przekazania obiektu do metody - ale nadal próbuje. Otrzymujemy błąd TypeError: wyswietl() takes 0 positional arguments but 1 was given , który mówi: "metoda oczekuje 0 argumentów, ale dostała 1". Tym jednym argumentem jest właśnie obiekt obj , który Python próbuje przekazać jako self .

Jak naprawić? Wystarczy dodać self jako pierwszy parametr metody i użyć self.nazwa do odwołania się do atrybutu:

# POPRAWNA definicja
class Poprawny:
    def __init__(self, nazwa):
        self.nazwa = nazwa

    def wyswietl(self):
        print(f"Nazwa: {self.nazwa}")

Zawsze dodawaj self jako pierwszy parametr każdej metody instancji!

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 Porównanie: dobry i zły kod z self

Dobre i złe użycie self

Przeanalizujmy poniższy kod porównujący prawidłowe i błędne podejścia do self:

# DOBRZE - prawidłowe użycie self
class Prawidlowy:
    def __init__(self, dana):
        self.dana = dana

    def przetworz(self, mnoznik=2):
        return self.dana * mnoznik

    def aktualizuj(self, nowa_dana):
        self.dana = nowa_dana

# ŹLE - pomijanie self, odwoływanie się do zmiennych globalnych
class Nieprawidlowy:
    def __init__(self, dana):
        self.dana = dana

    def przetworz():    # brak self - błąd!
        return dana * 2  # dana niezdefiniowana - NameError!

# ŹLE - mylenie parametru z atrybutem (brak self.)
class Blad2:
    def __init__(self, x):
        self.x = x

    def pokaz(self):
        print(x)  # odwołanie do zmiennej x, a nie self.x - NameError!

Dobre praktyki z self:

  • Zawsze używaj self jako pierwszego parametru metody instancji
  • Do atrybutów instancji odwołuj się przez self.nazwa - nigdy bez self
  • Gdy wywołujesz inną metodę instancji z wnętrza klasy, używaj self.metoda()
  • Pamiętaj, że self daje dostęp do wszystkich atrybutów i metod obiektu
  • Nie nadpisuj self - nigdy nie przypisuj do self nowej wartości

Prawidłowy kod zawsze używa self jako pierwszego parametru metody i odwołuje się do atrybutów przez self.nazwa . Nieprawidłowy kod pomija self lub myli parametr z atrybutem, co prowadzi do NameError w czasie wykonania.

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.

18 Atrybuty instancji - wprowadzenie

Atrybuty instancji: self.nazwa = wartosc

Atrybuty instancji to zmienne, które należą do konkretnego obiektu. Każdy obiekt ma swoją własną kopię tych zmiennych - są one niezależne od innych instancji tej samej klasy. Definiuje się je zazwyczaj w konstruktorze __init__ za pomocą składni self.nazwa = wartosc .

Charakterystyka atrybutów instancji:

  • Na instancję: Są tworzone dla każdego obiektu osobno - każda instancja ma własną kopię
  • Różne wartości: Mogą mieć różne wartości dla różnych obiektów (np. każde konto ma inne saldo)
  • Dostęp przez self: Są dostępne we wszystkich metodach instancji przez self.nazwa
  • Definiowane w konstruktorze: Najlepiej definiować je wszystkie w __init__ , aby obiekt od razu był w spójnym stanie
  • Dynamiczne: Mogą być dodawane również poza klasą (np. obiekt.nowy_atrybut = 5 ), choć to nie jest zalecane, ponieważ zaburza czytelność

Atrybuty instancji vs atrybuty klasowe:

  • Atrybuty instancji: self.nazwa - należą do obiektu, każdy obiekt ma własne
  • Atrybuty klasowe: NazwaKlasy.atrybut - należą do klasy, współdzielone przez wszystkie obiekty

Atrybuty instancji są fundamentem enkapsulacji w OOP - każdej instancji przypisujemy jej własne dane, a metody operują na tych danych przez self .

Atrybuty instancji

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.

19 Kod: definiowanie atrybutów w __init__

Definiowanie atrybutów w konstruktorze

Przeanalizujmy poniższy kod - klasa Samochod z atrybutami pochodzącymi z parametrów i z wartością domyślną:

class Samochod:
    def __init__(self, marka, model, rok, kolor):
        self.marka = marka
        self.model = model
        self.rok = rok
        self.kolor = kolor
        self.predkosc = 0  # atrybut z wartością domyślną - nie pochodzi z parametru
        self.wlaczony = False  # kolejny atrybut domyślny

    def opis(self):
        return f"{self.marka} {self.model} ({self.rok}), kolor {self.kolor}"

    def uruchom(self):
        self.wlaczony = True
        print("Samochód uruchomiony!")

    def przyspiesz(self, km_h):
        if self.wlaczony:
            self.predkosc += km_h
        else:
            print("Najpierw uruchom samochód!")

auto = Samochod("Toyota", "Corolla", 2020, "czerwony")
print(auto.opis())
print("Prędkość początkowa:", auto.predkosc)
auto.uruchom()
auto.przyspiesz(50)
print("Prędkość po przyspieszeniu:", auto.predkosc)

Konstruktor przyjmuje cztery parametry i zapisuje je jako atrybuty. Dodatkowo tworzy dwa atrybuty predkosc i wlaczony z wartościami domyślnymi - nie są one przekazywane z zewnątrz, ale są niezbędne dla poprawnego stanu obiektu. Wszystkie atrybuty zdefiniowane w konstruktorze są dostępne w każdej metodzie instancji przez self . To kluczowa zasada: w konstruktorze definiujemy pełny, spójny stan początkowy 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.

20 Każdy obiekt ma własne atrybuty

Niezależność atrybutów między obiektami

Przeanalizujmy poniższy kod ilustrujący niezależność atrybutów między różnymi instancjami:

class Magazyn:
    def __init__(self, nazwa):
        self.nazwa = nazwa
        self.towary = []  # każdy obiekt ma własną listę

    def dodaj(self, towar):
        self.towary.append(towar)

    def lista(self):
        return f"{self.nazwa}: {', '.join(self.towary)}"

m1 = Magazyn("Centralny")
m2 = Magazyn("Pomocniczy")

m1.dodaj("Komputer")
m1.dodaj("Monitor")
m2.dodaj("Drukarka")
m2.dodaj("Skaner")

print(m1.lista())  # Centralny: Komputer, Monitor
print(m2.lista())  # Pomocniczy: Drukarka, Skaner

# Sprawdźmy, czy to naprawdę różne listy
print(m1.towary is m2.towary)  # False - to różne obiekty list
print(id(m1.towary), id(m2.towary))  # różne adresy

Każdy magazyn ma własną, niezależną listę towary w pamięci. Dodanie towaru do m1 nie wpływa na m2 - są to odrębne obiekty listy. Operator is i funkcja id() potwierdzają, że są to dwa różne obiekty w pamięci. To kluczowa cecha programowania obiektowego - enkapsulacja danych w obrębie obiektu. Każda instancja ma swój własny, niezależny stan.

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.

21 Różne wartości dla różnych obiektów

Różne wartości atrybutów

Przeanalizujmy poniższy kod - każda instancja przechowuje inne dane, a metoda działa odpowiednio dla każdej z nich:

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

    def srednia(self):
        if not self.oceny:
            return 0.0
        return sum(self.oceny) / len(self.oceny)

    def dodaj_ocene(self, ocena):
        if 1 <= ocena <= 6:
            self.oceny.append(ocena)

    def info(self):
        return f"{self.imie}: średnia {self.srednia():.2f}"

s1 = Student("Ala", [5, 4, 5, 3])
s2 = Student("Bartek", [3, 3, 4])
s3 = Student("Celina", [5, 5, 5])

print(s1.info())
print(s2.info())
print(s3.info())

# Dodajemy ocenę tylko Alinie - nie wpływa to na innych
s1.dodaj_ocene(4)
print("Po dodaniu oceny:")
print(s1.info())
print(s2.info())  # bez zmian

Każdy student ma inne imię i inną listę ocen. Metoda srednia operuje na danych konkretnego studenta przez self i zabezpiecza się przed pustą listą. Dzięki temu jedna definicja metody obsługuje wszystkie obiekty, a zmiana stanu jednego obiektu nie wpływa na pozostałe. To pokazuje siłę OOP - wielokrotne użycie kodu (reuse) przy jednoczesnej niezależności danych.

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ą.

22 Podsumowanie atrybutów instancji

Podsumowanie - atrybuty instancji

Atrybuty instancji to podstawowy mechanizm przechowywania danych w obiektach. Oto najważniejsze fakty i zasady:

  • Definicja: Przypisanie do self w konstruktorze __init__ - self.nazwa = wartosc
  • Niezależność: Każdy obiekt ma własną kopię tych atrybutów - zmiana w jednym obiekcie nie wpływa na inne
  • Dostęp wewnętrzny: Z wnętrza klasy używamy self.nazwa (w każdej metodzie instancji)
  • Dostęp zewnętrzny: Z zewnątrz klasy używamy obiekt.nazwa
  • Enkapsulacja: Atrybuty można dodawać i modyfikować poza klasą ( obiekt.nowy_atrybut = 5 ), ale lepiej robić to wewnątrz klasy - przez metody
  • Konwencja nazewnictwa: Nazwy atrybutów powinny być opisowe i zgodne z konwencją snake_case
  • Widoczność: W Pythonie nie ma prywatnych atrybutów na poziomie języka - konwencja mówi, że atrybuty z prefiksem _ (np. self._wewnetrzny ) są chronione, a z __ (np. self.__tajny ) są name-manglowane

Pamiętaj - dobrze zaprojektowana klasa definiuje wszystkie potrzebne atrybuty w konstruktorze, co zapewnia spójny stan początkowy każdego nowo utworzonego obiektu.

# Przykład: atrybut z prefiksem _ (konwencja "prywatny")
class Konto:
    def __init__(self, saldo):
        self._saldo = saldo  # konwencja: nie modyfikuj bezpośrednio

    def pokaz_saldo(self):
        return self._saldo

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.

23 Tworzenie obiektów - składnia

Składnia: obiekt = Klasa(arg1, arg2)

Aby utworzyć obiekt (instancję) klasy, wywołujemy klasę jak funkcję, podając odpowiednie argumenty. Python wykonuje wtedy następujące kroki:

  1. Alokuje pamięć dla nowego obiektu (przez __new__)
  2. Wywołuje __init__ z nowym obiektem jako self i przekazanymi argumentami
  3. Inicjalizuje atrybuty instancji
  4. Zwraca nowy, gotowy obiekt

Ogólna składnia:

nazwa_zmiennej = NazwaKlasy(argument1, argument2, ...)

Liczba i rodzaj argumentów muszą być zgodne z parametrami konstruktora __init__ (poza self , który jest wypełniany automatycznie). Jeśli podasz złą liczbę argumentów, Python zgłosi TypeError . Możesz też używać argumentów nazwanych (keyword arguments), co zwiększa czytelność:

osoba = Osoba(imie="Anna", nazwisko="Kowalska", wiek=28)

Zmienna osoba przechowuje referencję (odniesienie) do nowo utworzonego obiektu w pamięci, a nie sam obiekt. To oznacza, że przypisanie kopia = osoba nie tworzy nowego obiektu, a jedynie kopiuje referencję.

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ą.

24 Kod: tworzenie wielu obiektów

Tworzenie wielu obiektów tej samej klasy

Przeanalizujmy poniższy kod - z jednej klasy tworzymy wiele niezależnych obiektów:

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

    def wartosc(self):
        return self.cena * self.ilosc

    def zmien_cene(self, nowa_cena):
        if nowa_cena > 0:
            self.cena = nowa_cena

# Tworzymy kilka produktów - każdy z własnymi danymi
p1 = Produkt("Długopis", 2.50, 100)
p2 = Produkt("Zeszyt", 4.00, 50)
p3 = Produkt("Gumka", 1.20, 200)

# Lista produktów - iteracja po obiektach
produkty = [p1, p2, p3]
for p in produkty:
    print(f"{p.nazwa}: {p.wartosc():.2f} zł (cena: {p.cena} zł, sztuk: {p.ilosc})")

# Modyfikacja tylko jednego produktu
p1.zmien_cene(3.00)
print(f"Po zmianie ceny: {p1.nazwa}: {p1.wartosc():.2f} zł")

Z jednej klasy Produkt możemy utworzyć dowolną liczbę obiektów, każdy z własnymi danymi. To pokazuje siłę programowania obiektowego - wielokrotne użycie tej samej definicji (reuse) przy jednoczesnej niezależności danych. Możemy przechowywać obiekty w listach i iterować po nich, wywołując te same metody na każdym z osobna. Modyfikacja jednego obiektu (np. zmiana ceny) nie wpływa na pozostałe.

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ą.

25Obiekty są niezależne

Niezależność obiektów

Przeanalizujmy poniższy kod demonstrujący pełną niezależność instancji:

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

    def zwieksz(self):
        self.wartosc += 1

    def reset(self):
        self.wartosc = 0

l1 = Licznik()
l2 = Licznik()

l1.zwieksz()
l1.zwieksz()
l2.zwieksz()
l2.zwieksz()
l2.zwieksz()

print("Licznik 1:", l1.wartosc)  # 2
print("Licznik 2:", l2.wartosc)  # 3

# Resetujemy tylko l2 - l1 pozostaje bez zmian
l2.reset()
print("Po resecie l2:")
print("Licznik 1:", l1.wartosc)  # 2 (bez zmian)
print("Licznik 2:", l2.wartosc)  # 0

Mimo że oba obiekty są instancjami tej samej klasy Licznik , każdy ma własny, niezależny stan . Zwiększanie wartości w l1 nie wpływa na l2 , a zresetowanie l2 nie wpływa na l1 . Obiekty są w pełni niezależne - każdy żyje własnym życiem.

To kluczowa zaleta OOP: ta sama klasa może produkować wiele niezależnych instancji, każda z własnym stanem, a metody operują na stanie konkretnej instancji przez self .

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ą.

26Zmienne referencyjne

Zmienne referencyjne

W Pythonie zmienne przechowują odniesienia (referencje) do obiektów, a nie same obiekty. Gdy przypisujesz zmienna = Klasa() , zmienna wskazuje na obiekt w pamięci - to jak skrót (alias) do konkretnego miejsca w pamięci.

Przeanalizujmy poniższy kod ilustrujący działanie referencji:

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

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

a = Osoba("Anna")
b = a         # b wskazuje na TEN SAM obiekt co a

print("a:", a)                # Osoba(Anna)
print("b:", b)                # Osoba(Anna)
print(a is b)                # True - to ten sam obiekt
print(id(a), id(b))          # identyczne adresy

b.imie = "Barbara"          # modyfikacja przez b
print("Po zmianie przez b:")
print("a:", a)                # Osoba(Barbara) - a też się zmieniło!
print("b:", b)                # Osoba(Barbara)

# Jak utworzyć prawdziwą kopię?
import copy
c = copy.deepcopy(a)        # głęboka kopia - nowy, niezależny obiekt
c.imie = "Cecylia"
print("a:", a)                # Osoba(Barbara) - bez zmian
print("c:", c)                # Osoba(Cecylia) - niezależna kopia

Przypisanie b = a nie tworzy kopii obiektu - sprawia, że obie zmienne wskazują na ten sam obiekt. Modyfikacja przez jedną zmienną jest widoczna przez drugą. To kluczowe zrozumieć, aby uniknąć efektów ubocznych w kodzie.

Aby rzeczywiście skopiować obiekt, należy użyć copy.deepcopy() z modułu copy (lub copy.copy() dla kopii płytkiej). Wtedy otrzymujemy nowy, niezależny obiekt - zmiana w kopii nie wpływa na oryginał.

Slajd zatytułowany "Zmienne referencyjne" 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.

27 Porównanie: dobre i złe tworzenie obiektów

Dobre i złe praktyki tworzenia obiektów

Przeanalizujmy poniższy kod porównujący dobre i złe praktyki:

# DOBRZE - czytelne nazwy, jeden obiekt na zmienną
konto_anna = KontoBankowe("PL1234", 2000)
konto_bartek = KontoBankowe("PL5678", 1500)

# ŹLE - mylące nazwy, przypadkowe wartości
x = KontoBankowe("abc", "dużo")  # zły typ danych dla salda (str zamiast int)
k = KontoBankowe("PL0000", 0)     # nieczytelna nazwa zmiennej

# ŹLE - niepotrzebne kopiowanie referencji (mylące przy dalszym kodzie)
a = Osoba("Adam")
b = a  # to nie kopia, to alias - mylące dla czytelnika

# DOBRZE - jawna kopia, gdy potrzebna
import copy
kopia_osoby = copy.deepcopy(a)

# ŹLE - tworzenie obiektu bez przypisania do zmiennej
KontoBankowe("PL9999", 100)  # obiekt utworzony, ale zaraz stracony

# ŹLE - nadmiarowe, nieczytelne argumenty pozycyjne
p = Punkt(1, 2, 3, 4, 5)  # co oznaczają te liczby? używaj argumentów nazwanych!

Dobre praktyki tworzenia obiektów:

  • Używaj opisowych nazw zmiennych (np. konto_anna zamiast x)
  • Przekazuj poprawne typy danych zgodne z oczekiwaniami konstruktora
  • Pamiętaj, że przypisanie nie kopiuje obiektu - użyj copy.deepcopy() gdy potrzebujesz kopii
  • Nie twórz obiektów bez przypisania do zmiennej - zostaną natychmiast usunięte przez garbage collector
  • Używaj argumentów nazwanych dla złożonych konstruktorów: Osoba(imie="Anna", nazwisko="Kowalska")
  • Trzymaj się jednej konwencji nazewniczej w całym projekcie

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 Metody instancji - wprowadzenie

Metody instancji - pierwszy parametr zawsze self

Metody instancji to funkcje zdefiniowane wewnątrz klasy, których pierwszym parametrem jest self . Dzięki temu mają dostęp do atrybutów i innych metod konkretnego obiektu. Są podstawowym mechanizmem nadawania zachowania obiektom.

Charakterystyka metod instancji:

  • Definicja: Definiuje się je jak zwykłe funkcje (słowem def ), ale wewnątrz ciała klasy
  • Pierwszy parametr: To zawsze self - odniesienie do obiektu, na którym metoda jest wywoływana
  • Wywołanie: Wywołuje się je na obiekcie przez notację kropkową: obiekt.metoda()
  • Parametry: Mogą przyjmować dodatkowe parametry po self
  • Zwracanie wartości: Mogą zwracać wartości za pomocą return - jeśli brak return , zwracają None
  • Dostęp do atrybutów: Przez self mają pełny dostęp do wszystkich atrybutów instancji
  • Dostęp do innych metod: Mogą wywoływać inne metody instancji przez self.metoda()

Metody instancji to odpowiednik metod składowych w C++ czy metod w Javie. Różnica polega na jawnym przekazywaniu self - w Pythonie wszystko jest jawne ("Explicit is better than implicit").

Metody instancji

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.

29 Kod: definiowanie metody

Definiowanie metody w klasie

Przeanalizujmy poniższy kod - dwie metody instancji: jedna używająca atrybutu, druga działająca na parametrach:

class Kalkulator:
    def __init__(self, nazwa):
        self.nazwa = nazwa
        self.ostatni_wynik = None  # pamięta ostatni wynik

    def dodaj(self, a, b):
        wynik = a + b
        self.ostatni_wynik = wynik
        return wynik

    def info(self):
        stan = ""
        if self.ostatni_wynik is not None:
            stan = f", ostatni wynik: {self.ostatni_wynik}"
        return f"Kalkulator: {self.nazwa}{stan}"

    def ostatni(self):
        return self.ostatni_wynik

kal = Kalkulator("Mój kalkulator")
print(kal.info())
print("5 + 3 =", kal.dodaj(5, 3))
print("10 + 20 =", kal.dodaj(10, 20))
print(kal.info())  # pokazuje też ostatni wynik

Metoda dodaj przyjmuje dwa parametry ( a i b ), oblicza sumę, zapamiętuje ją w atrybucie self.ostatni_wynik i zwraca wynik. Metoda info korzysta z atrybutów self.nazwa i self.ostatni_wynik . Metoda ostatni zwraca zapamiętany wynik. To pokazuje, że metody mogą zarówno modyfikować stan obiektu, jak i go odczytywać, a także wywoływać inne metody (pośrednio) przez dostęp do atrybutów.

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.

30 Wywoływanie metody na obiekcie

Wywoływanie metod na obiektach

Przeanalizujmy poniższy kod - ten sam kod metody, różne efekty dla różnych obiektów:

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

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

    def daj_lape(self):
        print(f"{self.imie} podaje łapę")

    def info(self):
        return f"{self.imie} - rasa: {self.rasa}"

burek = Pies("Burek", "Owczarek")
reks = Pies("Reks", "Jamnik")

# Każdy obiekt wywołuje tę samą metodę, ale działa na własnych danych
burek.szczekaj()    # Burek (Owczarek): Hau! Hau!
reks.szczekaj()     # Reks (Jamnik): Hau! Hau!
burek.daj_lape()     # Burek podaje łapę

# Możemy też wywołać metodę przez klasę - jawnie przekazując obiekt
Pies.szczekaj(burek)  # równoważne: burek.szczekaj()
Pies.szczekaj(reks)   # równoważne: reks.szczekaj()

Każdy obiekt wywołuje tę samą metodę, ale działa na własnych danych. burek.szczekaj() używa self.imie = "Burek" i self.rasa = "Owczarek" , a reks.szczekaj() używa własnych atrybutów. To pokazuje, jak jedna definicja metody obsługuje wiele obiektów - polimorfizm w działaniu.

Dodatkowo pokazaliśmy alternatywną składnię wywołania: Pies.szczekaj(burek) jest równoważne burek.szczekaj() . W pierwszej formie jawnie mówimy Pythonowi: "wywołaj metodę szczekaj klasy Pies, przekazując obiekt burek jako self". To wyjaśnia, dlaczego w definicji metody musi być self .

Wywoływanie 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.

31Metody z parametrami

Metody z dodatkowymi parametrami

Przeanalizujmy poniższy kod - metody mogą przyjmować parametry (po self ) i modyfikować stan obiektu:

class Prostokat:
    def __init__(self, szerokosc, wysokosc):
        self.szerokosc = szerokosc
        self.wysokosc = wysokosc

    def pole(self):
        return self.szerokosc * self.wysokosc

    def obwod(self):
        return 2 * (self.szerokosc + self.wysokosc)

    def czy_kwadrat(self):
        return self.szerokosc == self.wysokosc

    def skaluj(self, wspolczynnik):
        if wspolczynnik <= 0:
            print("Współczynnik musi być dodatni!")
            return
        self.szerokosc *= wspolczynnik
        self.wysokosc *= wspolczynnik

    def info(self):
        return f"Prostokąt {self.szerokosc} x {self.wysokosc}, pole={self.pole()}, obwód={self.obwod()}"

p = Prostokat(4, 6)
print(p.info())
print("Kwadrat?", p.czy_kwadrat())
p.skaluj(2)
print("Po skalowaniu:", p.info())
p.skaluj(-1)  # próba nieprawidłowego skalowania - walidacja odrzuci

Metoda skaluj przyjmuje dodatkowy parametr wspolczynnik i modyfikuje atrybuty obiektu, ale z walidacją - sprawdza, czy współczynnik jest dodatni. Metoda info wewnętrznie wywołuje inne metody ( self.pole() , self.obwod() ), pokazując, że metody mogą współpracować. Metody mogą zarówno odczytywać dane (gettery), jak i je zmieniać (settery), a także zawierać logikę walidacyjną.

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.

32 Metody zwracające wartości

Metody zwracające wartości

Przeanalizujmy poniższy kod - metody wykonują obliczenia na podstawie atrybutów i zwracają wyniki:

class Termometr:
    def __init__(self, stopnie_c):
        self.stopnie_c = stopnie_c

    def do_fahrenheita(self):
        return self.stopnie_c * 9 / 5 + 32

    def do_kelvina(self):
        return self.stopnie_c + 273.15

    def czy_goraco(self, prog=30):
        return self.stopnie_c >= prog

    def ustaw(self, nowa_temp):
        self.stopnie_c = nowa_temp

t = Termometr(25)
print("25°C = ", t.do_fahrenheita(), "°F")
print("25°C = ", t.do_kelvina(), "K")
print("Gorąco?", t.czy_goraco())

# Zmieniamy temperaturę i ponownie sprawdzamy
t.ustaw(35)
print("35°C =", t.do_fahrenheita(), "°F")
print("Gorąco?", t.czy_goraco())
print("Gorąco (prog 25)?", t.czy_goraco(25))

Metody zwracające wartości są niezwykle użyteczne - pozwalają na obliczenia na podstawie atrybutów obiektu. Dzięki return możemy używać wyniku metody w dalszych obliczeniach, przekazywać go do innych funkcji, czy wyświetlać. Metoda czy_goraco pokazuje, że metody mogą mieć parametry opcjonalne z wartościami domyślnymi. Po zmianie temperatury przez ustaw , wszystkie metody od razu używają nowej wartości - stan obiektu jest centralnie przechowywany w atrybutach.

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.

33 Domyślne wartości parametrów w __init__

Parametry domyślne w konstruktorze

Podobnie jak w zwykłych funkcjach, w konstruktorze __init__ możemy ustawić domyślne wartości parametrów. Dzięki temu niektóre argumenty stają się opcjonalne - jeśli nie zostaną podane podczas tworzenia obiektu, używana jest wartość domyślna określona w definicji metody.

Zalety parametrów domyślnych:

  • Mniej wymaganych argumentów - zmniejszają liczbę argumentów, które muszą być podane przy tworzeniu obiektu
  • Elastyczność - umożliwiają elastyczne tworzenie obiektów na różne sposoby
  • Sensowne wartości początkowe - pozwalają na ustawienie rozsądnych wartości początkowych, które mogą być później zmienione
  • Łatwość rozszerzania - ułatwiają dodawanie nowych parametrów w przyszłości bez łamania istniejącego kodu (jeśli nowe parametry mają domyślne wartości)

Zasada: parametry z wartościami domyślnymi muszą występować po parametrach wymaganych w sygnaturze metody. Kolejność: self , potem parametry wymagane, potem parametry z wartościami domyślnymi. Jeśli tej zasady nie zachowasz, Python zgłosi SyntaxError .

# Prawidłowa kolejność parametrów
def __init__(self, wymagany, opcjonalny_a=10, opcjonalny_b="domyślny"):
    pass

# Błędna kolejność - SyntaxError!
# def __init__(self, opcjonalny=10, wymagany):

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. Te fundamentalne rozróżnienie pojawia się w każdym projekcie obiektowym i warto je solidnie utrwalić.

34 Kod: klasa z domyślnymi wartościami

Klasa z domyślnymi wartościami

Przeanalizujmy poniższy kod - klasa Uzytkownik z dwoma opcjonalnymi parametrami:

class Uzytkownik:
    def __init__(self, login, haslo="1234", aktywny=True):
        self.login = login
        self.haslo = haslo
        self.aktywny = aktywny

    def info(self):
        status = "aktywny" if self.aktywny else "nieaktywny"
        return f"Użytkownik {self.login} ({status})"

    def zmien_haslo(self, stare_haslo, nowe_haslo):
        if self.haslo == stare_haslo:
            self.haslo = nowe_haslo
            return True
        return False

    def dezaktywuj(self):
        self.aktywny = False

# Różne sposoby tworzenia użytkowników
u1 = Uzytkownik("admin")                  # tylko login, reszta domyślna
u2 = Uzytkownik("janek", "bezpieczne123")  # login + własne hasło
u3 = Uzytkownik("test", "test", False)    # wszystko jawne
u4 = Uzytkownik("gosia", aktywny=False)     # argument nazwany - pomijamy hasło

print(u1.info())
print(u2.info())
print(u3.info())
print(u4.info())

Domyślne wartości parametrów sprawiają, że konstruktor jest elastyczny. Możemy tworzyć obiekty z różną liczbą argumentów, a brakujące są uzupełniane wartościami domyślnymi. W przypadku u4 użyliśmy argumentu nazwanego aktywny=False , opuszczając haslo (które przyjmuje wartość domyślną). To pokazuje, że argumenty nazwane dają jeszcze większą elastyczność przy wywoływaniu konstruktora.

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. Te fundamentalne rozróżnienie pojawia się w każdym projekcie obiektowym i warto je solidnie utrwalić.

35 Tworzenie obiektów z i bez argumentów

Elastyczne tworzenie obiektów

Przeanalizujmy poniższy kod - klasa, w której wszystkie parametry są opcjonalne:

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

    def przesun(self, dx, dy):
        self.x += dx
        self.y += dy

    def wyswietl(self):
        print(f"Punkt ({self.x}, {self.y})")

    def odleglosc(self, inny):
        """Oblicza odległość euklidesową między dwoma punktami."""
        dx = self.x - inny.x
        dy = self.y - inny.y
        return (dx**2 + dy**2) ** 0.5

# Pięć różnych sposobów tworzenia obiektu Punkt
p1 = Punkt()            # (0, 0) - oba parametry domyślne
p2 = Punkt(3)           # (3, 0) - x=3, y domyślne
p3 = Punkt(3, 7)         # (3, 7) - wszystko podane pozycyjnie
p4 = Punkt(y=5)         # (0, 5) - tylko y podane przez nazwę
p5 = Punkt(x=1, y=1)   # (1, 1) - argumenty nazwane

p1.wyswietl()
p2.wyswietl()
p3.wyswietl()
p4.wyswietl()
p5.wyswietl()

# Użycie metody z parametrem będącym innym obiektem
print("Odległość p1-p3:", p1.odleglosc(p3))

Dzięki domyślnym wartościom możemy tworzyć punkty na wiele sposobów. Możemy pominąć wszystkie argumenty (otrzymując punkt w środku układu współrzędnych), podać tylko pierwszy, wszystkie, a nawet użyć argumentów nazwanych w dowolnej kolejności. Metoda odleglosc pokazuje dodatkową możliwość: przyjmuje jako parametr inny obiekt tej samej klasy i operuje na jego atrybutach - to zaawansowana, ale bardzo użyteczna technika.

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 Ostrzeżenie: mutowalne wartości domyślne

Uwaga na mutowalne wartości domyślne!

To jeden z najsłynniejszych "gotchas" w Pythonie. Przeanalizujmy poniższy kod:

# BŁĘDNE użycie mutowalnej wartości domyślnej
class Koszyk:
    def __init__(self, produkty=[]):  # lista jest tworzona RAZ!
        self.produkty = produkty

    def dodaj(self, produkt):
        self.produkty.append(produkt)

    def lista(self):
        return f"Koszyk: {', '.join(self.produkty)}"

k1 = Koszyk()
k2 = Koszyk()

k1.dodaj("Jabłko")
k1.dodaj("Gruszka")
k2.dodaj("Chleb")

print(k1.lista())  # Koszyk: Jabłko, Gruszka, Chleb - nieoczekiwanie!
print(k2.lista())  # Koszyk: Jabłko, Gruszka, Chleb - ten sam efekt!

Dlaczego tak się dzieje? W Pythonie domyślne wartości parametrów są obliczane raz - w momencie definiowania funkcji/metody, a nie przy każdym wywołaniu. Dlatego lista [] w wartości domyślnej jest tworzona raz i współdzielona przez wszystkie obiekty, które nie podadzą własnej listy.

Mutowalne wartości domyślne (listy, słowniki, zbiory, obiekty klas) są tworzone raz podczas definiowania klasy i współdzielone między wszystkimi instancjami. To prowadzi do subtelnych i trudnych do znalezienia błędów. Zamiast tego używaj None i twórz nowy obiekt wewnątrz __init__ (jak pokazano na następnym slajdzie).

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. Te fundamentalne rozróżnienie pojawia się w każdym projekcie obiektowym i warto je solidnie utrwalić.

37 Porównanie: dobre i złe domyślne wartości

Dobre i złe domyślne wartości

Przeanalizujmy poniższy kod porównujący błędne i prawidłowe podejście:

# ŹLE - mutowalna wartość domyślna (lista współdzielona)
class KoszykZly:
    def __init__(self, produkty=[]):
        self.produkty = produkty

# DOBRZE - None + nowa lista tworzona w init
class KoszykDobry:
    def __init__(self, produkty=None):
        self.produkty = produkty if produkty is not None else []

    def dodaj(self, produkt):
        self.produkty.append(produkt)

    def ile(self):
        return len(self.produkty)

# Dowód: każdy obiekt ma własną listę
k1 = KoszykDobry()
k2 = KoszykDobry()

k1.dodaj("Jabłko")
k2.dodaj("Chleb")

print(k1.produkty)  # ["Jabłko"] - tylko to, co dodaliśmy do k1
print(k2.produkty)  # ["Chleb"] - osobna lista dla k2
print(k1.ile(), k2.ile())  # 1, 1 - każdy ma jeden produkt

# Można też podać własną listę, jeśli potrzeba
lista_wstepna = ["Masło", "Mleko"]
k3 = KoszykDobry(lista_wstepna)
k3.dodaj("Chleb")
print(k3.produkty)  # ["Masło", "Mleko", "Chleb"]

Bezpieczny wzorzec: używaj None jako wartości domyślnej dla parametrów, które mają być mutowalne. Wewnątrz __init__ sprawdź, czy wartość to None i utwórz nowy obiekt (listę, słownik, zbiór). To gwarantuje, że każdy obiekt ma własną, niezależną kopię. Jeśli użytkownik chce podać własną listę, może to zrobić - wtedy użyta zostanie ta podana, a nie nowo utworzona.

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. Te fundamentalne rozróżnienie pojawia się w każdym projekcie obiektowym i warto je solidnie utrwalić.

38 Wiele konstruktorów - wprowadzenie

Wiele konstruktorów - symulacja przez parametry opcjonalne

Python nie pozwala na przeciążanie konstruktorów (przeciążanie metod) w tradycyjnym sensie (jak C++ czy Java), gdzie można zdefiniować wiele metod o tej samej nazwie, ale różnych parametrach. W Pythonie każda klasa ma dokładnie jeden __init__ . Jednak możemy to zasymulować na dwa sprawdzone sposoby:

  • Parametry opcjonalne z logiką warunkową - jeden konstruktor z domyślnymi wartościami ( None ) i instrukcjami if wewnątrz, które dostosowują inicjalizację w zależności od podanych argumentów
  • Metody klasowe (@classmethod) - fabryki (alternatywne konstruktory), które przyjmują dane w innym formacie i zwracają nowe instancje klasy. To bardziej eleganckie i czytelne rozwiązanie

Dzięki tym technikom możemy tworzyć obiekty na różne sposoby, w zależności od dostępnych danych. Na przykład obiekt Data może być utworzony z trzech liczb (dzień, miesiąc, rok) lub z napisu ("2025-03-15"). To zwiększa elastyczność i czytelność kodu, a także ułatwia współpracę z różnymi źródłami danych.

Wiele konstruktorów

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.

39 Kod: jeden __init__ z if logicznym

Jeden konstruktor z logiką warunkową

Przeanalizujmy poniższy kod - jeden konstruktor obsługujący dwa formaty danych wejściowych:

class Data:
    def __init__(self, dzien, miesiac=None, rok=None):
        if miesiac is None and rok is None:
            # Obsługa formatu: Data("2025-03-15")
            czesci = dzien.split("-")
            self.rok = int(czesci[0])
            self.miesiac = int(czesci[1])
            self.dzien = int(czesci[2])
        else:
            # Obsługa formatu: Data(15, 3, 2025)
            self.dzien = dzien
            self.miesiac = miesiac
            self.rok = rok

        # Walidacja po inicjalizacji
        if not (1 <= self.miesiac <= 12):
            raise ValueError(f"Nieprawidłowy miesiąc: {self.miesiac}")

    def __str__(self):
        return f"{self.dzien:02d}.{self.miesiac:02d}.{self.rok}"

    def __repr__(self):
        return f"Data({self.dzien!r}, {self.miesiac!r}, {self.rok!r})"

# Oba sposoby tworzenia działają
d1 = Data("2025-03-15")   # z napisu
d2 = Data(15, 3, 2025)     # z liczb
print(d1, d2)

# Próba nieprawidłowej daty zgłosi wyjątek
# d3 = Data(15, 13, 2025)  # ValueError: Nieprawidłowy miesiąc: 13

Konstruktor sprawdza, czy podano dwa czy trzy argumenty. Jeśli tylko jeden (napis), parsuje go jako datę w formacie ISO. Jeśli trzy (liczby), traktuje je jako dzień, miesiąc, rok. Po inicjalizacji następuje walidacja - nieprawidłowe wartości zgłaszają wyjątek ValueError . Metoda __repr__ zwraca reprezentację, z której można odtworzyć obiekt (przydatne przy debugowaniu).

Init z if logicznym

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.

40 Alternatywa: @classmethod (zapowiedź)

Alternatywny konstruktor przez @classmethod

Przeanalizujmy poniższy kod - metoda klasowa jako fabryka obiektów:

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

    @classmethod
    def z_csv(cls, linia_csv):
        """Tworzy osobę z linii CSV: 'Anna,Kowalska,28'"""
        czesci = linia_csv.split(",")
        return cls(czesci[0], czesci[1], int(czesci[2]))

    @classmethod
    def z_json(cls, dane_json):
        """Tworzy osobę ze słownika JSON."""
        return cls(dane_json["imie"],
                   dane_json["nazwisko"],
                   dane_json["wiek"])

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

# Normalny konstruktor - bezpośrednie parametry
o1 = Osoba("Anna", "Kowalska", 28)

# Alternatywny konstruktor - z linii CSV
o2 = Osoba.z_csv("Bartek,Nowak,35")

# Alternatywny konstruktor - ze słownika (np. z API JSON)
dane = {"imie": "Celina", "nazwisko": "Wiśniewska", "wiek": 42}
o3 = Osoba.z_json(dane)

print(o1)
print(o2)
print(o3)

Metody klasowe ( @classmethod ) działają jako alternatywne konstruktory (wzorzec projektowy "Factory Method"). Przyjmują dane w różnych formatach i zwracają nową instancję klasy. cls to odniesienie do klasy (analogiczne do self , ale dla klasy, nie instancji). Dzięki cls , jeśli stworzymy podklasę Osoby , metody klasowe automatycznie zadziałają dla tej podklasy - to zaleta w porównaniu z twardym kodowaniem nazwy klasy.

Metody klasowe są szczególnie przydatne, gdy dane pochodzą z różnych źródeł: plików CSV, JSON, baz danych, formularzy itp.

Slajd zatytułowany "Alternatywa: @classmethod (zapowiedź)" 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.

41 Porównanie różnych podejść

Porównanie podejść do wielu konstruktorów

Przeanalizujmy poniższy kod porównujący oba podejścia:

# Podejście 1: parametry opcjonalne + if w __init__
class Kolo1:
    def __init__(self, promien=None, srednica=None):
        if promien is not None:
            self.promien = promien
        elif srednica is not None:
            self.promien = srednica / 2
        else:
            raise ValueError("Podaj promień lub średnicę")

        self.srednica = self.promien * 2

    def pole(self):
        return 3.14159 * self.promien ** 2

# Podejście 2: @classmethod (bardziej czytelne i separacja odpowiedzialności)
class Kolo2:
    def __init__(self, promien):
        self.promien = promien
        self.srednica = promien * 2

    @classmethod
    def ze_srednicy(cls, srednica):
        return cls(srednica / 2)

    @classmethod
    def z_obwodu(cls, obwod):
        return cls(obwod / (2 * 3.14159))

    def pole(self):
        return 3.14159 * self.promien ** 2

k1 = Kolo2(5)                   # promień = 5
k2 = Kolo2.ze_srednicy(10)     # promień = 5 (średnica 10 / 2)
k3 = Kolo2.z_obwodu(31.4159)     # promień ≈ 5 (obwód / 2π)

print(k1.promien, k2.promien, k3.promien)
print(k1.pole(), k2.pole(), k3.pole())

Podejście z @classmethod jest czytelniejsze i bardziej zgodne z ideą programowania obiektowego (zasada pojedynczej odpowiedzialności - Single Responsibility Principle). Każda metoda fabryczna ma jasną nazwę i odpowiedzialność: ze_srednicy mówi sama za siebie. Dodanie nowego sposobu tworzenia (np. z_obwodu ) nie wymaga modyfikacji konstruktora - po prostu dodajemy nową metodę klasową. Konstruktor pozostaje prosty i czysty, przyjmując tylko to, co naprawdę potrzebne.

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.

42 Podsumowanie - wiele konstruktorów

Podsumowanie - wiele konstruktorów

Choć Python nie wspiera przeciążania konstruktorów w tradycyjnym sensie (wiele metod o tej samej nazwie, ale różnych sygnaturach), mamy sprawne mechanizmy zastępcze. Oto zestawienie:

Mechanizm Zalety Wady Kiedy stosować
Parametry domyślne Proste, jeden konstruktor, mało kodu Ograniczona elastyczność, problem z None Małe różnice w wartościach początkowych
Logika warunkowa w __init__ Obsługuje różne formaty Konstruktor robi się skomplikowany Różne formaty danych wejściowych
@classmethod Czytelne, separacja odpowiedzialności, łatwe rozszerzanie Więcej kodu, wymaga znajomości @classmethod Złożone przypadki, różne źródła danych

Rekomendacja: Dla prostych przypadków (wartości domyślne) używaj parametrów domyślnych. Dla złożonych przypadków (różne formaty danych, różne sposoby tworzenia) używaj @classmethod - to bardziej eleganckie i zgodne z dobrymi praktykami OOP.

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.

43 Praktyczny przykład: KontoBankowe

Praktyczny przykład - klasa KontoBankowe

Przejdziemy teraz do praktycznego przykładu, który łączy wszystkie poznane wcześniej koncepcje w jednej, spójnej klasie. Zbudujemy klasę KontoBankowe z:

  • Konstruktorem __init__ z parametrami wymaganymi (numer konta, właściciel) i opcjonalnymi (waluta, saldo początkowe)
  • Atrybutami instancji: numer , wlasciciel , saldo , waluta , historia (lista transakcji)
  • Metodami do wykonywania operacji bankowych: wplata , wyplata - z walidacją danych
  • Metodą wyświetlającą informacje o koncie:info
  • Metodą do przeglądania historii: dostęp do self.historia

To realistyczny przykład, który pokazuje, jak projektować klasy w praktyce - z myślą o poprawności danych (walidacja), czytelności (opisowe nazwy) i użyteczności (historia transakcji). Projektowanie klas to sztuka wyboru odpowiednich atrybutów i metod, które razem tworzą spójny, łatwy w użyciu interfejs.

Slajd zatytułowany "Praktyczny przykład: KontoBankowe" 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.

44 Kod: definicja KontoBankowe

Definicja klasy KontoBankowe

Przeanalizujmy poniższy kod - początek definicji klasy, konstruktor i metoda informacyjna:

class KontoBankowe:
    """Klasa reprezentująca konto bankowe z historią transakcji."""

    def __init__(self, numer, wlasciciel, waluta="PLN", saldo_poczatkowe=0):
        self.numer = numer
        self.wlasciciel = wlasciciel
        self.waluta = waluta
        self.saldo = saldo_poczatkowe
        self.historia = []  # lista do przechowywania wpisów transakcji

    def info(self):
        """Zwraca czytelny opis stanu konta."""
        return f"Konto {self.numer} ({self.wlasciciel}): {self.saldo} {self.waluta}"

    def pobierz_historie(self):
        """Zwraca listę wszystkich transakcji."""
        return list(self.historia)  # zwracamy kopię, aby chronić oryginał

Konstruktor przyjmuje numer konta i właściciela jako parametry wymagane , a walutę i saldo początkowe jako opcjonalne (z wartościami domyślnymi). Dodatkowo tworzy pustą listę historia do przechowywania rejestru transakcji - to atrybut inicjalizowany lokalnie, nie pochodzący z parametru. Metoda info zwraca czytelny, sformatowany opis stanu konta. Metoda pobierz_historie zwraca kopię listy, aby chronić wewnętrzne dane przed niekontrolowaną modyfikacją z zewnątrz (enkapsulacja).

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.

45 Kod: tworzenie konta i operacje

Tworzenie konta i wykonywanie operacji

Pełna klasa KontoBankowe z metodami do wpłat i wypłat, zawierającymi walidację:

class KontoBankowe:
    """Klasa reprezentująca konto bankowe z historią transakcji."""

    def __init__(self, numer, wlasciciel, waluta="PLN", saldo_poczatkowe=0):
        self.numer = numer
        self.wlasciciel = wlasciciel
        self.waluta = waluta
        self.saldo = saldo_poczatkowe
        self.historia = []

    def info(self):
        return f"Konto {self.numer} ({self.wlasciciel}): {self.saldo} {self.waluta}"

    def wplata(self, kwota):
        """Wpłaca środki na konto. Zwraca True jeśli operacja się powiodła."""
        if kwota <= 0:
            print("Kwota wpłaty musi być dodatnia!")
            return False
        self.saldo += kwota
        self.historia.append(f"Wpłata: +{kwota} {self.waluta}")
        return True

    def wyplata(self, kwota):
        """Wypłaca środki z konta. Sprawdza saldo i poprawność kwoty."""
        if kwota <= 0:
            print("Kwota wypłaty musi być dodatnia!")
            return False
        if kwota > self.saldo:
            print("Niewystarczające środki!")
            return False
        self.saldo -= kwota
        self.historia.append(f"Wypłata: -{kwota} {self.waluta}")
        return True

Metody wplata i wyplata zawierają pełną walidację: sprawdzają, czy kwota jest dodatnia (obie metody) i czy saldo jest wystarczające (tylko wyplata ). Po udanej operacji aktualizują self.saldo i dodają wpis do self.historia z informacją o kwocie i walucie. Zwracają True lub False w zależności od powodzenia operacji, co pozwala kodowi wywołującemu reagować na sukces lub porażkę. Każda metoda ma docstring opisujący jej działanie.

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ą.

46 Kod: wplata i wyplata w praktyce

Używanie metod wplata i wyplata

Zobaczmy, jak klasa działa w praktyce - tworzymy konto, wykonujemy operacje i sprawdzamy stan:

# Tworzymy konto z początkowym saldem 1000 PLN
konto = KontoBankowe("PL61 1090 1014 0000 0712 1981 2874",
                       "Anna Kowalska",
                       "PLN", 1000)

print(konto.info())  # Konto PL61... (Anna Kowalska): 1000 PLN

# Wpłaty i wypłaty - każda operacja zwraca True/False
udana1 = konto.wplata(500)
udana2 = konto.wplata(200)
udana3 = konto.wyplata(300)
udana4 = konto.wyplata(9999)  # nieudana - brak środków

print("Wpłata 500:", udana1)
print("Wpłata 200:", udana2)
print("Wypłata 300:", udana3)
print("Wypłata 9999:", udana4)  # False

print("\nStan końcowy:", konto.info())
print("\nHistoria transakcji:")
for wpis in konto.historia:
    print("  -", wpis)

Efekt uruchomienia:

Konto PL61 1090 1014 0000 0712 1981 2874 (Anna Kowalska): 1000 PLN
Wpłata 500: True
Wpłata 200: True
Wypłata 300: True
Wypłata 9999: False   (Niewystarczające środki!)

Stan końcowy: Konto PL61... (Anna Kowalska): 1400 PLN

Po wykonaniu operacji saldo wynosi 1400 PLN (1000 + 500 + 200 - 300). Próba wypłaty 9999 została odrzucona z powodu braku środków - metoda zwróciła False . Historia zawiera szczegóły wszystkich udanych operacji. Dzięki zwracaniu wartości logicznych możemy podejmować decyzje w kodzie na podstawie wyniku operacji.

Wplata i wyplata

Slajd zatytułowany "Kod: wplata i wyplata w praktyce" 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.

47 Uruchamianie krok po kroku

Uruchamianie krok po kroku

Przeanalizujmy typowy cykl życia obiektu - od utworzenia, przez operacje, aż po odczyt stanu:

# Krok 1: Import (jeśli klasa jest w osobnym pliku)
# from konto import KontoBankowe

# Krok 2: Tworzenie konta z początkowym saldem 1000 PLN
konto = KontoBankowe("PL61109010140000071219812874",
                       "Jan Kowalski",
                       "PLN", 1000)
# W tym momencie Python: alokuje pamięć → woła __init__ → init ustawia atrybuty → zwraca obiekt

# Krok 3: Wyświetlenie stanu początkowego
print("Stan początkowy:", konto.info())
# Wynik: Stan początkowy: Konto PL611090... (Jan Kowalski): 1000 PLN

# Krok 4: Wykonanie operacji - każda zmienia stan obiektu
konto.wplata(250)   # saldo: 1000 → 1250, historia: ["Wpłata: +250 PLN"]
konto.wyplata(80)    # saldo: 1250 → 1170, historia: +["Wypłata: -80 PLN"]
konto.wplata(120)   # saldo: 1170 → 1290, historia: +["Wpłata: +120 PLN"]

# Krok 5: Wyświetlenie stanu końcowego
print("Stan końcowy:", konto.info())
# Wynik: Stan końcowy: Konto PL611090... (Jan Kowalski): 1290 PLN

# Krok 6: Wyświetlenie pełnej historii transakcji
print("Operacje:")
for i, wpis in enumerate(konto.historia, 1):
    print(f"  {i}.", wpis)

Ten schemat pokazuje typowy cykl życia obiektu: tworzenie (konstruktor inicjalizuje stan), wykonywanie operacji (metody zmieniają stan), odczytywanie stanu (metody zwracają informacje). Każda operacja nie tylko aktualizuje saldo, ale też dodaje wpis do historii - to przykład utrzymywania logu zmian (audit trail). Obiekt przechowuje pełną historię swojego życia, co może być przydatne do audytu, debugowania czy generowania wyciągów.

Slajd zatytułowany "Uruchamianie krok po kroku" 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.

48 Podsumowanie przykładu KontoBankowe

Podsumowanie przykładu KontoBankowe

Klasa KontoBankowe pokazuje wszystkie kluczowe elementy definiowania klas w praktyce:

  • Definicja klasy - class KontoBankowe: z docstringiem opisującym przeznaczenie klasy
  • Konstruktor - __init__ z parametrami wymaganymi ( numer , wlasciciel ) i opcjonalnymi ( waluta , saldo_poczatkowe ) z wartościami domyślnymi
  • Atrybuty instancji - self.numer , self.wlasciciel , self.saldo , self.waluta , self.historia
  • Metody instancji - info (odczyt stanu), wplata (modyfikacja + log), wyplata (modyfikacja + log + walidacja)
  • Walidacja danych - sprawdzanie poprawności kwot (dodatnia) i dostępności środków (przed wypłatą)
  • Zwracanie wartości - metody zwracają True / False informując o powodzeniu operacji
  • Enkapsulacja - atrybuty są przechowywane wewnątrz obiektu, a metody stanowią interfejs do interakcji
  • Historia transakcji - przykład utrzymywania rejestru zmian w obiekcie (audit trail)

To solidna baza, którą można rozbudowywać o przelewy między kontami (metoda przelew przyjmująca inne KontoBankowe ), naliczanie odsetek, generowanie wyciągów miesięcznych, blokadę konta, limity dzienne i wiele więcej. Każda z tych funkcji to kolejna metoda w klasie, korzystająca z istniejących atrybutów i metod.

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.

49 Mapa myśli: definiowanie klas

Mapa myśli - definiowanie klas

Oto graficzne podsumowanie najważniejszych koncepcji z tej części - kompletny obraz definiowania klas w Pythonie:

  • class Nazwa: - podstawowa składnia definicji klasy, zgodna z PEP 8 (PascalCase)
  • __init__(self, ...) - konstruktor (inicjalizator), wywoływany automatycznie przy tworzeniu obiektu
  • self - odniesienie do bieżącego obiektu, pierwszy parametr każdej metody instancji
  • self.atrybut = wartosc - atrybuty instancji - dane należące do konkretnego obiektu
  • obiekt = Klasa(argy) - tworzenie obiektu (instancji) z przekazaniem argumentów do konstruktora
  • def metoda(self, ...) - metody instancji - funkcje wewnątrz klasy operujące na obiekcie przez self
  • return wartosc - zwracanie wartości z metody (jeśli brak return, metoda zwraca None)
  • parametr = wartosc - domyślne wartości parametrów (uważaj na mutowalne wartości domyślne!)
  • @classmethod - alternatywne konstruktory (metody fabryczne) tworzące obiekty w różny sposób
  • Walidacja - sprawdzanie poprawności danych w konstruktorze i metodach przed ich użyciem
  • Docstring - dokumentowanie klasy i metod w potrójnych cudzysłowach

Wszystkie te elementy współpracują ze sobą, tworząc spójny mechanizm programowania obiektowego w Pythonie. Opanowanie tych koncepcji stanowi solidny fundament do dalszej nauki - dziedziczenia, polimorfizmu, enkapsulacji i wzorców projektowych.

Mapa myśli

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 Quiz - sprawdź swoją wiedzę

Quiz: 5 pytań

Pytanie 1: Która nazwa klasy jest zgodna z konwencją PEP 8?

  • A) class konto_bankowe: (snake_case - konwencja dla zmiennych, nie klas)
  • B) class KontoBankowe: ✓ (PascalCase - poprawna konwencja)
  • C) class kontobankowe: (brak wielkich liter)
  • D) class Konto_Bankowe: (mieszanie PascalCase z podkreślnikami)

Pytanie 2: Co się stanie, jeśli w metodzie instancji pominiemy parametr self?

  • A) Metoda będzie działać normalnie (nie - Python nie doda self automatycznie)
  • B) Python doda self automatycznie (nie - Python nie robi tego za nas)
  • C) Otrzymamy TypeError przy wywołaniu ✓ (Python próbuje przekazać obiekt, ale metoda nie przyjmuje argumentów)
  • D) Otrzymamy ostrzeżenie, ale kod zadziała (nie - to błąd krytyczny)

Pytanie 3: Dlaczego poniższy kod jest niebezpieczny?

class Koszyk:
    def __init__(self, produkty=[]):
  • A) Bo lista w wartości domyślnej jest współdzielona między obiektami ✓ (to klasyczny Python gotcha)
  • B) Bo lista jest zbyt wolna (nie - lista jest wydajna)
  • C) Bo nie można dodawać produktów (nie - można, ale efekt będzie nieoczekiwany)
  • D) Bo Python nie akceptuje list jako parametrów (nie - Python akceptuje wszystko)

Pytanie 4: Jaka jest różnica między self.nazwa a nazwa wewnątrz metody instancji?

  • A) Nie ma różnicy - to to samo
  • B) self.nazwa to atrybut obiektu, nazwa to zmienna lokalna ✓
  • C) nazwa to atrybut klasy
  • D) self.nazwa to zmienna globalna

Pytanie 5: Które stwierdzenie o konstruktorze __init__ jest fałszywe ?

  • A) Jest wywoływany automatycznie po utworzeniu obiektu
  • B) Może przyjmować parametry przekazywane podczas tworzenia obiektu
  • C) Może zwracać dowolną wartość za pomocą return ✓ (FAŁSZ - może zwrócić tylko None)
  • D) Służy do inicjalizacji atrybutów instancji
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.