1/20
Atrybuty i metody klasowe

Wykład 2: Co będziemy omawiać?

W poprzednim wykładzie dowiedzieliśmy się, jak tworzyć proste klasy i obiekty. Teraz wejdziemy głębiej w mechanizmy, które pozwalają zarządzać danymi na poziomie całej klasy, a nie tylko pojedynczego obiektu.

  • Atrybuty instancji vs atrybuty klasy: Dane specyficzne dla obiektu kontra dane wspólne dla wszystkich.
  • Metody klasowe (@classmethod): Funkcje, które operują na samej klasie.
  • Metody statyczne (@staticmethod): Narzędzia pomocnicze "przyczepione" do klasy.
  • Praktyczne zastosowania: Kiedy użyć którego rozwiązania?
Drzewo hierarchii - od ogólnej klasy do konkretnych liści/obiektów
2/20
Przypomnienie: Atrybuty instancji

Dane "prywatne" każdego obiektu

Atrybuty instancji to zmienne, które należą do konkretnego obiektu. Każdy obiekt ma swoją własną kopię tych danych. Definiujemy je najczęściej wewnątrz metody __init__, która jest wywoływana automatycznie podczas tworzenia nowego obiektu.

Słowo self w Pythonie oznacza "ten konkretny obiekt, na którym wywołuję metodę". Dzięki temu możemy rozróżnić dane różnych obiektów tej samej klasy.

class Pracownik:
    def __init__(self, imie):
        self.imie = imie # Atrybut instancji - każdy obiekt ma swoje własne imie

p1 = Pracownik("Alicja")  # Tworzymy pierwszego pracownika
p2 = Pracownik("Bartek")  # Tworzymy drugiego pracownika

print(p1.imie)  # Wyświetli: Alicja
print(p2.imie)  # Wyświetli: Bartek

Jak widać, zmiana imienia Alicji nie wpłynie na Bartka, ponieważ to są ich osobne, niezależne dane. Każdy obiekt przechowuje swoje własne wartości w tzw. słowniku (słownik to po prostu sposób Pythona na przechowywanie par klucz-wartość).

3/20
Atrybuty klasy (Class Attributes)

Dane wspólne dla wszystkich

Atrybuty klasy są definiowane bezpośrednio wewnątrz bloku class, ale poza jakąkolwiek metodą. Są dzielone przez wszystkie obiekty danej klasy.

class Pracownik:
    firma = "TechCorp" # Atrybut klasy

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

p1 = Pracownik("Jan")
print(p1.firma) # TechCorp
Jeśli zmienimy Pracownik.firma, zmiana będzie widoczna dla wszystkich pracowników jednocześnie (tych już istniejących i nowych).
4/20
Po co używać atrybutów klasy?

Praktyczne zastosowania

Atrybuty klasy świetnie nadają się do przechowywania stałych lub zmiennych, które charakteryzują całą grupę obiektów.

  • Stałe konfiguracyjne: np. VAT, nazwa firmy, limit miejsc.
  • Liczniki: Śledzenie liczby stworzonych obiektów.
  • Zasoby wspólne: np. połączenie z bazą danych współdzielone przez instancje.

Oszczędza to pamięć – wartość jest przechowywana tylko raz, a nie w każdym obiekcie z osobna.

Schemat: Jeden centralny magazyn (Klasa) i wiele powiązań (Obiekty)
5/20
Przykład: Licznik obiektów

Automatyczne zliczanie

Atrybuty klasy świetnie nadają się do zliczania, ile razy dana klasa została użyta do stworzenia obiektów. W tym przykładzie tworzymy licznik, który rośnie za każdym razem, gdy ktoś tworzy nowego robota.

class Robot:
    populacja = 0 # Atrybut klasy - zaczynamy od zera

    def __init__(self, nazwa):
        self.nazwa = nazwa  # Atrybut instancji - każdy robot ma swoją nazwę
        Robot.populacja += 1  # Zwiększamy licznik o 1
        print(f"Zbudowano robota {self.nazwa}.")

r1 = Robot("R2D2")  # Tworzymy pierwszego robota
r2 = Robot("C3PO")  # Tworzymy drugiego robota
print(f"Liczba robotów: {Robot.populacja}") # Wyświetli: 2

Zauważ, że używamy Robot.populacja (z nazwą klasy), a nie self.populacja. Dzięki temu mamy pewność, że zmieniamy tę samą zmienną dla całej klasy, a nie tworzymy nowego atrybutu tylko dla jednego robota.

Każdy nowy robot zwiększa wartość populacja o 1. Wszystkie roboty dzielą tę samą wartość, ponieważ jest to atrybut klasy, a nie atrybut instancji.

6/20
Pułapka: Nadpisywanie przez instancję

Uważaj na przypisanie do self

Jeśli spróbujesz zmienić atrybut klasy przez self, Python stworzy nowy atrybut instancji o tej samej nazwie, zamiast zmienić ten wspólny.

p1 = Pracownik("Anna")
p1.firma = "Google" # Tworzy p1.firma - TYLKO DLA ANNY!

print(Pracownik.firma) # Nadal TechCorp
Aby zmienić wartość dla wszystkich, zawsze używaj NazwaKlasy.atrybut = nowa_wartosc.
7/20
Metody instancji (Przypomnienie)

Działania na konkretnym obiekcie

Metody instancji to najczęstszy typ metod w Pythonie. Każda metoda zdefiniowana w klasie (bez specjalnych dekoratorów) jest domyślnie metodą instancji. Pierwszy parametr takiej metody to zawsze self, który reprezentuje konkretny obiekt, na którym wywołujemy metodę.

Dzięki self możemy w metodzie odwoływać się do atrybutów tego konkretnego obiektu i wykonywać operacje tylko na jego danych.

class Samochod:
    def __init__(self, bak):
        self.paliwo = bak  # Ilość paliwa w tym konkretnym samochodzie

    def tankuj(self, litry):
        # self oznacza "ten samochód, na którym wywołuję metodę"
        self.paliwo += litry  # Zwiększamy paliwo TYLKO tego samochodu

moj_samochod = Samochod(30)  # Tworzymy samochód z 30 litrami
moj_samochod.tankuj(20)  # Tankujemy 20 litrów - tylko do naszego samochodu
print(moj_samochod.paliwo)  # Wyświetli: 50

To jest najczęstszy typ metod, z którymi będziesz pracować w Pythonie. Używamy ich, gdy chcemy wykonać jakąś operację na danych konkretnego obiektu.

8/20
Metody klasowe (Class Methods)

Działania na poziomie klasy

Metody klasowe są oznaczone dekoratorem @classmethod. Zamiast self (obiekt), przyjmują cls (sama klasa) jako pierwszy parametr.

class Pracownik:
    firma = "TechCorp"

    @classmethod
    def zmien_firme(cls, nowa_nazwa):
        cls.firma = nowa_nazwa

Pracownik.zmien_firme("InnovateIT")

Możemy je wywoływać bezpośrednio na klasie, bez tworzenia jakiegokolwiek obiektu!

9/20
Zastosowanie: Alternatywne konstruktory

Tworzenie obiektów w niestandardowy sposób

Bardzo częstym zastosowaniem metod klasowych jest tworzenie tzw. alternatywnych konstruktorów. Chodzi o to, że oprócz standardowego tworzenia obiektu (np. Data(12, 3, 2026)) chcemy czasem stworzyć obiekt w inny sposób - na przykład z tekstu (stringa), z pliku JSON, z danych z internetu itp.

Metoda klasowa pozwala nam zdefiniować taki alternatywny sposób tworzenia obiektu. Używamy do tego parametru cls, który reprezentuje klasę, i na końcu wywołujemy cls(...), aby faktycznie stworzyć obiekt.

class Data:
    def __init__(self, d, m, r):
        self.d, self.m, self.r = d, m, r  # dzień, miesiąc, rok

    @classmethod
    def z_napisu(cls, napis):
        # Zamieniamy string "12-03-2026" na listę liczb [12, 3, 2026]
        d, m, r = map(int, napis.split("-"))
        return cls(d, m, r)  # Tworzymy nowy obiekt Data

# Standardowe tworzenie:
dzis = Data(12, 3, 2026)

# Alternatywne - z stringa:
dzis2 = Data.z_napisu("12-03-2026")

Zauważ, że wywołujemy metodę na klasie (Data.z_napisu), a nie na obiekcie. To jest właśnie charakterystyczne dla metod klasowych.

10/20
Metody statyczne (Static Methods)

Metody bez powiązań

Metody statyczne (@staticmethod) nie przyjmują ani self, ani cls. Zachowują się jak zwykłe funkcje, ale mieszkają wewnątrz klasy.

class Kalkulator:
    @staticmethod
    def dodaj(a, b):
        return a + b

wynik = Kalkulator.dodaj(5, 3)

Używamy ich, gdy funkcja logicznie pasuje do klasy, ale nie potrzebuje dostępu do jej danych ani instancji.

Pudełko z narzędziami (Klasa) i luźny klucz (Statyczna metoda)
11/20
Porównanie: Trzy rodzaje metod

Kiedy wybrać którą?

  1. Instancji (self): Potrzebujesz dostępu do danych konkretnego obiektu (np. self.nazwisko).
  2. Klasowa (cls): Potrzebujesz dostępu do danych klasy (np. cls.populacja) lub chcesz stworzyć nową instancję.
  3. Statyczna: Funkcja pomocnicza, która nie potrzebuje żadnych danych z klasy ani instancji.
Wybór odpowiedniego typu metody poprawia czytelność kodu i sugeruje innym programistom, jak dana funkcja jest używana.
12/20
Przykład zbiorczy: Uniwersytet

Wszystko w jednym miejscu

Ten przykład pokazuje, jak można połączyć wszystkie trzy typy atrybutów i metod w jednej klasie. Zwróć uwagę, jak każdy element ma inne zastosowanie:

  • Atrybut klasy (uniwersytet) - wspólny dla wszystkich studentów
  • Metoda instancji (__init__) - tworzy dane konkretnego studenta
  • Metoda klasowa (info_uczelnia) - operuje na poziomie całej klasy
  • Metoda statyczna (czy_pelnoletni) - pomocnicza funkcja, która nie potrzebuje danych klasy
class Student:
    uniwersytet = "Politechnika"  # Atrybut klasy - wspólny dla wszystkich

    def __init__(self, imie):
        self.imie = imie  # Atrybut instancji - każdy student ma swoje imie

    @classmethod
    def info_uczelnia(cls):
        print(f"Wszyscy studiujemy na {cls.uniwersytet}")

    @staticmethod
    def czy_pelnoletni(wiek):
        return wiek >= 18  # Zwraca True lub False

# Przykłady użycia:
s1 = Student("Anna")
Student.info_uczelnia()  # Wyświetli: Wszyscy studiujemy na Politechnika
print(Student.czy_pelnoletni(20))  # Wyświetli: True
13/20
Atrybuty klasowe - Zaawansowane

Dziedziczenie atrybutów

Atrybuty klasy są dziedziczone, co oznacza, że jeśli podklasa (klasa, która dziedziczy z innej klasy) nie zdefiniuje własnego atrybutu o danej nazwie, to "pożyczy" go od klasy nadrzędnej.

Wyobraź sobie to jak w rodzinie: dziecko dziedziczy pewne cechy od rodzica. Jeśli rodzic ma jakieś wspólne mienie (atrybut klasy), dziecko może z niego korzystać, dopóki nie zdefiniuje własnego.

O dziedziczeniu będziemy mówić szczegółowo w osobnym wykładzie, ale warto już teraz pamiętać, że atrybuty klasowe "płyną" w dół hierarchii klas - od klas rodzica do klas dziecka.

Schemat: Rodzic(atrybut) -> Dziecko(korzysta z atrybutu)
14/20
Przestrzeń nazw (Namespace)

Gdzie Python szuka zmiennych?

Przestrzeń nazw (namespace) to po prostu "słownik", w którym Python przechowuje wszystkie zmienne (atrybuty) dostępne w danym miejscu. Każdy obiekt i każda klasa ma swoją własną przestrzeń nazw.

Kiedy piszesz obiekt.atrybut (czyli próbujesz dostać się do atrybutu), Python szuka go w określonej kolejności, jakby przeszukiwał kolejne szuflady:

  1. Najpierw sprawdza w słowniku instancji (czyli w danych tego konkretnego obiektu).
  2. Następnie sprawdza w słowniku klasy (czyli w danych wspólnych dla wszystkich obiektów tej klasy).
  3. Na końcu sprawdza w słownikach klas nadrzędnych (jeśli istnieje dziedziczenie).

Zrozumienie tej kolejności jest bardzo ważne, aby unikać błędów. Dzięki temu wiesz, czy zmieniasz atrybut jednego obiektu, czy wszystkich naraz.

15/20
Metody klasowe a Dziedziczenie

Dlaczego cls jest ważne?

Użycie parametru cls (zamiast wpisywania na sztywno nazwy klasy) jest bardzo ważne przy dziedziczeniu. Dzięki cls Python zawsze wie, na jakiej klasie wywołujesz metodę, nawet jeśli to jest klasa dziedzicząca.

Wyobraź sobie klasę A i klasę B, która dziedziczy z A. Jeśli wywołasz metodę klasową na klasie B, to cls będzie wskazywało na B, a nie na A. Dzięki temu metoda działa poprawnie niezależnie od tego, na której klasie ją wywołujesz.

class A:
    @classmethod
    def pokaz(cls):
        print(f"Jestem klasą {cls.__name__}")

class B(A): # B dziedziczy z A
    pass  # Pusta klasa - nic nowego nie dodaje

A.pokaz()  # Wyświetli: Jestem klasą A
B.pokaz()  # Wyświetli: Jestem klasą B (nie A!)

Dzięki temu kod jest bardziej elastyczny i nie musisz go zmieniać, gdy dodajesz nowe klasy dziedziczące.

16/20
Dobre praktyki

Pisz czysty kod

  • Nie nadużywaj @staticmethod: Jeśli funkcja nie potrzebuje klasy, może powinna być zwykłą funkcją poza nią?
  • Używaj @classmethod dla fabryk: To standardowy sposób tworzenia obiektów z różnych źródeł danych.
  • Dokumentuj zmienne klasowe: Inni programiści muszą wiedzieć, że zmiana tej wartości wpłynie na cały system.
Grafika: Zatwierdzone (Checkmark) przy czystym przykładzie kodu
17/20
Interakcja: Atrybut vs Metoda

Czy to musi być metoda?

Czasami nie wiesz, czy daną rzecz zaimplementować jako atrybut (zmienną), czy jako metodę (funkcję). Oto prosta zasada:

  • Jeśli chcesz tylko przechować jakąś wartość (np. imię pracownika, cenę produktu), użyj atrybutu.
  • Jeśli chcesz wykonać jakąś akcję (np. obliczyć coś, zmienić stan, sprawdzić warunek), użyj metody.

W Pythonie często kierujemy się zasadą EAFP (Easier to Ask for Forgiveness than Permission - Łatwiej prosić o wybaczenie niż o pozwolenie). Oznacza to, że zamiast sprawdzać "czy mogę coś zrobić", po prostu próbujemy to zrobić, a jeśli się nie uda, to obsługujemy błąd. To często wpływa na to, jak projektujemy nasze klasy.

W praktyce oznacza to, że nie musisz tworzyć wielu metod typu "getter" (do odczytu) i "setter" (do zapisu) - w Pythonie często po prostu bezpośrednio operujemy na atrybutach.

18/20
Przykład: Walidacja danych

Statyczna pomoc

Metody statyczne są bardzo przydatne do tworzenia funkcji pomocniczych (helperów), które logicznie należą do klasy, ale nie potrzebują żadnych jej danych. Klasycznym przykładem jest walidacja - sprawdzanie, czy dane są poprawne.

W tym przykładzie tworzymy metodę validate_email, która sprawdza, czy podany tekst zawiera znak @ (co jest podstawowym wyznacznikiem poprawnego adresu e-mail). Nie potrzebujemy do tego żadnego konkretnego użytkownika - to po prostu narzędzie do sprawdzania.

class User:
    @staticmethod
    def validate_email(email):
        # Prosta walidacja: czy jest znak @ w tekście
        return "@" in email

# Wywołujemy metodę bezpośrednio na klasie, bez tworzenia obiektu:
if User.validate_email("test@example.com"):
    print("Email poprawny")  # Wyświetli ten tekst

if not User.validate_email("zly-email"):
    print("Email niepoprawny")  # Wyświetli ten tekst

Dzięki temu logika sprawdzania e-maila jest w tym samym miejscu co definicja użytkownika, ale nie wymaga posiadania już stworzonego obiektu. To ułatwia organizację kodu.

19/20
Częste błędy początkujących

Czego unikać?

Oto lista najczęstszych błędów, które popełniają początkujący programiści uczący się OOP w Pythonie. Zrozumienie tych błędów pomoże Ci ich uniknąć:

  • Zapominanie o dekoratorze @classmethod lub @staticmethod. Bez dekoratora Python traktuje metodę jako metodę instancji i pierwszy argument musi być self.
  • Używanie self w metodzie klasowej - jeśli użyjesz self zamiast cls w metodzie z dekoratorem @classmethod, Python zgłosi błąd.
  • Modyfikowanie atrybutów klasowych przez instancje - przypisanie czegoś do self.atrybut tworzy nowy atrybut instancji, a nie zmienia atrybut klasy (tzw. "cień" / shadowing).
  • Próba wywołania metody instancji bezpośrednio na klasie - metody instancji wymagają obiektu, więc Klasa.metoda() bez obiektu zakończy się błędem.

Te błędy mogą początkowo frustrować, ale z czasem rozpoznasz je i będziesz wiedział, jak je naprawić.

Ikona: Ostrzeżenie / Uwaga
20/20
Podsumowanie i co dalej?

Główne wnioski

Podsumujmy to, czego nauczyliśmy się w tym wykładzie:

  • Atrybuty klasy to dane wspólne dla wszystkich obiektów danej klasy. Definiujemy je poza metodami, bezpośrednio w ciele klasy. Używamy ich do przechowywania stałych, liczników lub innych wartości, które mają być współdzielone.
  • Metody klasowe (@classmethod) operują na samej klasie, a nie na konkretnym obiekcie. Pierwszy parametr to cls (klasa). Są świetne jako alternatywne konstruktory (np. tworzenie obiektu z stringa) lub do modyfikowania atrybutów klasy.
  • Metody statyczne (@staticmethod) to po prostu funkcje wewnątrz klasy, które nie zależą od jej stanu. Nie przyjmują ani self, ani cls. Używamy ich do tworzenia funkcji pomocniczych, które logicznie pasują do klasy, ale nie potrzebują jej danych.

W następnym wykładzie zajmiemy się Hermetyzacją (enkapsulacją) – czyli tym, jak ukrywać dane przed światem zewnętrznym, by nasz kod był bezpieczniejszy i bardziej uporządkowany. Nauczysz się, jak chronić atrybuty przed niepożądanym dostępem i modyfikacją.

Grafika: Zapowiedź kolejnego tematu - kłódka i tarcza (Hermetyzacja)