1/30
Hermetyzacja i właściwości

Wykład 3: Kontrola dostępu do danych

W poprzednich częściach nauczyliśmy się tworzyć klasy i zarządzać ich atrybutami. Jednak dotychczas każdy mógł dowolnie zmieniać dane w naszych obiektach. Czas to uporządkować.

Co to oznacza w praktyce? Wyobraź sobie, że tworzysz klasę reprezentującą konto bankowe. Jeśli ktoś może wpisać ujemny stan konta (np. -1000 zł), to jest błąd w logiczny działania programu. Hermetyzacja pomaga nam tego uniknąć.

  • Hermetyzacja (Enkapsulacja): Ukrywanie wewnętrznego stanu obiektu. To znaczy, że pewne dane wewnątrz klasy są dostępne tylko dla samej klasy, a nie bezpośrednio z zewnątrz.
  • Konwencje nazewnicze: Podkreślnik jako sygnał dla innych programistów. To nie jest zakaz, ale ostrzeżenie "nie dotykaj tego bez powodu".
  • Pseudo-prywatność: Mechanizm name mangling. Specjalna zasada Pythona, która utrudnia (ale nie uniemożliwia) dostęp do niektórych zmiennych.
  • Properties (Właściwości): Nowoczesne podejście do getterów i setterów. Pozwalają kontrolować dostęp do danych bez zmiany sposobu ich używania w kodzie.
  • Walidacja: Jak upewnić się, że dane są poprawne przed ich zapisaniem. Np. czy wiek jest większy od zera, czy hasło ma odpowiednią długość.
2/30
Problem: Nieograniczony dostęp

Dlaczego swoboda bywa groźna?

Jeśli wszystkie atrybuty są publiczne, użytkownik Twojej klasy może wprowadzić obiekt w stan nielogiczny.

Wyjaśnienie kodu: W poniższym przykładzie tworzymy klasę Osoba, która ma atrybut wiek. Nic nie stoi na przeszkodzie, żeby ktoś wpisał wiek -500, co jest oczywiście błędem w realnym świecie.

class Osoba:
    def __init__(self, wiek):
        self.wiek = wiek  # Ten atrybut jest publiczny - każdy może go zmienić

ktos = Osoba(20)  # Tworzymy osobę w wieku 20 lat
ktos.wiek = -500  # Błąd logiczny - wiek nie może być ujemny!
print(ktos.wiek)  # Wyświetli -500 - to jest błąd w programie

Publiczne atrybuty sprawiają, że nie masz żadnej kontroli nad tym, co dzieje się z danymi od razu po ich przypisaniu. Program działa, ale wynik jest nielogiczny.

3/30
Czym jest Hermetyzacja?

Ochrona wnętrza

Hermetyzacja (enkapsulacja) to jeden z filarów programowania obiektowego. Polega na:

  1. Łączeniu danych i metod w jedną jednostkę (klasę). Dane (zmienne) i funkcje (metody) tworzą razem spójną całość.
  2. Ograniczaniu bezpośredniego dostępu do niektórych komponentów. Nie wszystko, co jest w klasie, musi być dostępne dla każdego.

Obiekt powinien być jak "czarna skrzynka" – wiemy co robi, ale nie musimy (i nie powinniśmy) wiedzieć, jak dokładnie przechowuje swoje zmienne.

Przykład z życia: Pilot do telewizora ma przyciski (interfejs), ale ty nie widzisz w środku, jak działają elektroniczne układy (implementacja). Wystarczy, że wiesz, co robi każdy przycisk.

Dzięki hermetyzacji możesz zmienić wewnętrzną strukturę klasy, nie psując kodu osobom, które z niej korzystają. To bardzo ważne przy rozwijaniu oprogramowania.
4/30
Konwencja: Atrybut Chroniony (_)

Sygnał "Nie dotykaj"

W Pythonie, aby oznaczyć atrybut jako wewnętrzny (chroniony), dodajemy przed jego nazwą pojedynczy podkreślnik: _nazwa.

Co to oznacza? To jest tylko umowa między programistami. Python pozwala nadal używać tego atrybutu, ale przyjęło się, że nie powinno się tego robić poza klasą lub jej podklasami.

class Konto:
    def __init__(self, balans):
        self._balans = balans # Znak _ oznacza: "uwaga, to jest zmienna wewnętrzna"

moje = Konto(100)  # Tworzymy konto ze 100 zł
print(moje._balans) # Technicznie nadal działa, ale to zła praktyka!

To jest Porozumienie programistów (ang. Gentlemen's Agreement). Python nie zabroni Ci dostępu do _balans, ale każdy programista wie, że nie powinien go zmieniać bezpośrednio. To ostrzeżenie: "to jest szczegół implementacji, może się zmienić".

5/30
Atrybut "Prywatny" (__)

Silniejsza ochrona - Name Mangling

Jeśli użyjesz podwójnego podkreślnika __nazwa, Python zastosuje mechanizm zmiany nazwy, aby utrudnić przypadkowy dostęp.

Jak to działa? Python automatycznie zmienia nazwę takiego atrybutu, dodając przed nią nazwę klasy. Dzięki temu z zewnątrz nie można go łatwo znaleźć.

class Tajne:
    def __init__(self):
        self.__sekret = "Hasło123"  # Podwójny podkreślnik - "prywatna" zmienna

t = Tajne()
print(t.__sekret) # BŁĄD! Python nie pozwoli tego odczytać wprost

Python zmienia nazwę na _Tajne__sekret. To nie jest prawdziwe zabezpieczenie (nadal można się tam dostać, znając tę zasadę), ale wyraźnie odcina publiczne API od prywatnych wnętrzności. To przydatne, gdy nie chcesz, żeby użytkownik przypadkowo użył czegoś, czego nie powinien.

6/30
Pythonowa filozofia dostępu

"Consenting Adults"

W językach takich jak Java czy C++ mamy twarde słowa kluczowe (private, protected), których kompilator pilnuje rygorystycznie. Jeśli napiszesz private int wiek, to kompilacja się nie powiedzie, jeśli ktoś spróbuje to zmienić z zewnątrz.

Twórca Pythona, Guido van Rossum, uważał, że programiści to "dorośli ludzie, którzy wiedzą co robią" (We are all consenting adults here). Zamiast zakazywać, Python zaufał programistom i dał im narzędzia (konwencje z podkreślnikami), a decyzję o ich respektowaniu pozostawił ludziom.

  • Brak ścisłych blokad zachęca do otwartości - możesz zajrzeć do środka, gdy naprawdę musisz.
  • Daje elastyczność w debugowaniu - łatwiej znaleźć błąd, gdy możesz zobaczyć wszystkie dane.
  • Odpowiedzialność za błędy spoczywa na programiście, który zignorował ostrzeżenie (podkreślnik). Jeśli zmienisz zmienną z _, sam ponosisz konsekwencje.
To nie jest słabość Pythona - to świadomy wybór filozofii. Python ufa programistom, że użyją narzędzi we właściwy sposób.
7/30
Gettery i Settery - Stary styl

Jak to robiono kiedyś (i w innych językach)

Aby kontrolować dostęp, tworzyło się dedykowane metody do pobierania i ustawiania wartości. To podejście pochodzi z języków takich jak Java.

Wyjaśnienie kodu:

  • get_wiek() - metoda do pobierania (odczytu) wartości, nazywana getter
  • set_wiek(wartosc) - metoda do ustawiania (zapisu) wartości, nazywana setter
class Osoba:
    def __init__(self, wiek):
        self._wiek = wiek  # Zmienna wewnętrzna (z podkreślnikiem)

    def get_wiek(self):  # Getter - oddaje wartość
        return self._wiek

    def set_wiek(self, wartosc):  # Setter - sprawdza i ustawia wartość
        if wartosc > 0:  # Walidacja - tylko dodatni wiek
            self._wiek = wartosc
        else:
            print("Wiek musi być dodatni!")

# Użycie:
o = Osoba(25)
print(o.get_wiek())  # Musisz wywołać metodę z nawiasami()
o.set_wiek(30)  # Też z nawiasami i argumentem

Wadą jest to, że kod staje się "gadatliwy" – zamiast o.wiek trzeba pisać o.get_wiek(). To mniej naturalne i wymaga więcej pisania.

8/30
@property - Nowoczesne podejście

Właściwość zamiast metody

Dekorator @property pozwala zamienić metodę w coś, co z zewnątrz wygląda jak zwykły atrybut, ale pod spodem wykonuje kod. To pozwala na kontrolowany dostęp bez zmiany sposobu użycia.

Co to jest dekorator? Dekorator to specjalne słowo (zaczynające się od @), które modyfikuje działanie funkcji lub metody. @property mówi Pythonowi: "ta metoda ma zachowywać się jak atrybut".

class Osoba:
    def __init__(self, wiek):
        self._wiek = wiek  # Wewnętrzna zmienna (z podkreślnikiem)

    @property  # To jest dekorator - zamienia metodę w "właściwość"
    def wiek(self):  # Ta metoda zachowuje się jak atrybut
        return self._wiek  # Zwraca wartość zmiennej _wiek

o = Osoba(25)
print(o.wiek) # Wygląda jak zmienna, ale wywołuje metodę!
# Nie musisz pisać o.wiek() - nawiasy są niepotrzebne!

Teraz możesz czytać o.wiek jak zwykłą zmienną, ale pod spodem działa kod (metoda). To jest "magia" Pythona.

9/30
Setter we właściwościach

Kontrola zapisu

Aby umożliwić zmianę wartości przez @property, musimy zdefiniować tzw. setter. Setter to część właściwości, która obsługuje przypisanie wartości (użycie znaku =).

Jak to działa? Setter musi mieć taką samą nazwę jak właściwość (tutaj wiek) i musi używać dekoratora @nazwa.setter.

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

    @property
    def wiek(self):
        return self._wiek

    @wiek.setter  # Dekorator do ustawiania wartości
    def wiek(self, wartosc):  # Ta metoda obsługuje przypisanie o.wiek = ...
        if wartosc < 0:  # Sprawdzamy czy wartość jest poprawna
            raise ValueError("Wiek nie może być ujemny!")  # Zgłaszamy błąd
        self._wiek = wartosc  # Jeśli OK, zapisujemy

o = Osoba(20)
o.wiek = 30 # OK - 30 jest dodatnie
print(o.wiek)  # Wyświetli 30
o.wiek = -5 # WYJĄTEK! Program się zatrzyma z błędem

Użytkownik klasy używa prostej składni o.wiek = 30, a my w tle pilnujemy poprawności danych. Nie wie, że pod spodem działa walidacja!

10/30
Dlaczego @property jest lepsze?

Zasada Jednolitego Dostępu

Wyobraź sobie, że stworzyłeś klasę z publicznym atrybutem score. Po roku chcesz dodać walidację wyników. Problem polega na tym, jak to zrobić, nie psując kodu wszystkim, którzy już używają Twojej klasy.

  • Bez @property: Musisz zmienić score na metodę. Wszyscy użytkownicy Twojej klasy muszą teraz poprawić swój kod na set_score() i get_score(). To może być setki miejsc w kodzie!
  • Z @property: Zmieniasz atrybut w property. Zewnętrzny kod pozostaje identyczny (obj.score = 10), mimo że dodałeś skomplikowaną logikę walidacji. Nikt nie musi zmieniać swojego kodu!

Co to jest kompatybilność wsteczna? To zdolność nowej wersji programu do współpracy ze starszym kodem. Dzięki @property możesz zmieniać "wnętrze" klasy bez zmiany "wyglądu" na zewnątrz.

To pozwala na ewolucję kodu bez psucia jego kompatybilności. Możesz dodawać nowe funkcje bez zmiany sposobu użycia klasy.
11/30
Właściwości tylko do odczytu

Ochrona przed zmianą

Jeśli zdefiniujesz tylko @property (getter), a pominiesz setter, atrybut stanie się niemożliwy do zmiany z zewnątrz. To tworzy atrybut tylko do odczytu (read-only).

Po co to potrzebne? Czasami chcesz, żeby pewna wartość była dostępna do czytania, ale nie do zmieniania. Np. pole powierzchni koła, które jest obliczane na podstawie promienia - nie ma sensu pozwalać na jego ręczną zmianę.

class Kolo:
    def __init__(self, promien):
        self._promien = promien  # Promień jest wewnętrzny

    @property
    def promien(self):
        return self._promien  # Getter - można czytać
        # Brak @promien.setter = nie można zmieniać!

k = Kolo(10)
print(k.promien)  # OK - 10
k.promien = 20 # BŁĄD! AttributeError: can't set attribute

To świetna metoda na wystawianie danych, których nikt nie powinien modyfikować "ręcznie". Chronisz w ten sposób spójność danych.

12/30
Właściwości wyliczane

Dane generowane "w locie"

Właściwości nie muszą odnosić się bezpośrednio do żadnej ukrytej zmiennej. Mogą obliczać wynik na podstawie innych danych - to się nazywa właściwość wyliczana (computed property).

Po co to? Czasami nie chcesz przechowywać pewnej wartości, bo łatwo ją obliczyć z innych danych. Np. pole prostokąta to po prostu a * b - nie musisz tego przechowywać osobno.

class Prostokat:
    def __init__(self, a, b):
        self.a = a  # Długość boku a
        self.b = b  # Długość boku b

    @property  # Właściwość wyliczana - nie ma zmiennej _pole
    def pole(self):
        return self.a * self.b  # Oblicza pole przy każdym wywołaniu

p = Prostokat(4, 5)
print(p.pole) # 20 - obliczone na podstawie a i b
# Nie ma zmiennej p._pole - pole jest zawsze aktualne!

Użytkownik widzi pole jako cechę obiektu, mimo że jest ono wynikiem działania matematycznego. Zawsze ma aktualną wartość.

13/30
Deleter - Usuwanie właściwości

Sprzątanie zasobów

Oprócz gettera i settera, istnieje rzadziej używany deleter, wywoływany przy użyciu słowa kluczowego del. Słowo del w Pythonie służy do usuwania zmiennych lub atrybutów.

Kiedy to jest potrzebne? Czasami chcesz wykonać jakieś działanie, gdy atrybut jest usuwany. Np. wylogować użytkownika, zamknąć plik, zwolnić zasoby.

class Sesja:
    def __init__(self):
        self._user = "Gość"  # Domyślny użytkownik

    @property
    def user(self):
        return self._user

    @user.deleter  # Dekorator do obsługi usuwania
    def user(self):
        print("Wylogowywanie użytkownika...")  # Wykonaj "sprzątanie"
        self._user = None  # Wyczyść dane

s = Sesja()
print(s.user)  # Gość
del s.user  # Wywoła deleter - wyświetli komunikat i wyczyści
print(s.user)  # None

Deleter jest rzadko używany, ale warto wiedzieć, że taka możliwość istnieje. Najczęściej spotkasz to przy zaawansowanych wzorcach projektowych.

14/30
Atrybut Chroniony i Dziedziczenie

Praca w rodzinie

Zasada pojedynczego podkreślnika (_) mówi: "ten atrybut jest dla twórcy klasy i twórców klas, które po niej dziedziczą".

Co to jest dziedziczenie? To temat następnego wykładu, ale w skrócie: jeśli tworzysz nową klasę na podstawie istniejącej, ta nowa klasa (podklasa) dziedziczy wszystkie cechy klasy rodzica.

  • Podklasy mogą i powinny mieć dostęp do atrybutów chronionych rodzica. Służą one do współdzielenia stanu wewnętrznego wewnątrz hierarchii klas.
  • To pozwala podklasie na korzystanie z danych rodzica bez naruszania hermetyzacji wobec świata zewnętrznego.
Jeśli użyjesz podwójnego podkreślnika (__), podklasa będzie miała problem z dostępem do tego pola ze względu na name mangling. Python zmieni nazwę na _Rodzic__pole, więc podklasa musiałaby znać dokładną nazwę klasy rodzica.
15/30
Przykład zaawansowany: Termometr

Konwersja jednostek we właściwościach

To jest praktyczny przykład zastosowania właściwości. Mamy temperaturę wewnętrznie zapisaną w stopniach Celsjusza, ale udostępniamy ją także w Fahrenheitach.

Jak to działa?

  • Wewnątrz przechowujemy tylko _celsius
  • Właściwość fahrenheit przelicza wartość przy odczycie (getter)
  • Setter dla fahrenheit przelicza podaną wartość na Celsjusza i zapisuje w _celsius
class Termometr:
    def __init__(self, celsius):
        self._celsius = celsius  # Wewnątrz trzymamy tylko Celsjusze

    @property
    def fahrenheit(self):
        # Wzór: °F = °C × 9/5 + 32
        return (self._celsius * 9/5) + 32

    @fahrenheit.setter  # Możemy też ustawić przez Fahrenheita
    def fahrenheit(self, wartosc):
        # Wzór odwrotny: °C = (°F - 32) × 5/9
        self._celsius = (wartosc - 32) * 5/9

t = Termometr(20)  # Tworzymy termometr z 20°C
print(t.fahrenheit) # 68.0°F
t.fahrenheit = 100  # Ustawiamy 100°F
print(t._celsius)  # 37.78...°C - Python przeliczył za nas!
16/30
Publiczne vs Ukryte - Kiedy co?

Zasady dobrego designu

Hermetyzacja w Pythonie jest elastyczna, ale warto stosować pewne zasady, aby kod był czytelny i łatwy w utrzymaniu.

  1. Domyślnie używaj publicznych atrybutów (bez podkreślnika). Python to nie Java, nie twórz getterów "na zapas". Jeśli dane nie wymagają ochrony, nie ukrywaj ich.
  2. Użyj podkreślnika (_), gdy atrybut jest detalem implementacji, którego zmiana mogłaby coś popsuć. Np. wewnętrzne liczniki, tymczasowe dane.
  3. Użyj @property, gdy musisz dodać walidację lub obliczenia do atrybutu, który był wcześniej publiczny. To pozwala na zachowanie kompatybilności.
  4. Użyj podwójnego podkreślnika (__) głównie po to, by uniknąć konfliktów nazw w bardzo głębokiej hierarchii dziedziczenia. To rzadziej spotykane zastosowanie.
Pamiętaj: W Pythonie "prywatne" to tylko konwencja. Zawsze możesz się dostać do danych, jeśli naprawdę musisz. Chodzi o to, żeby robić to świadomie, a nie przypadkowo.
17/30
Magia name mangling - Dowód

Zaglądamy pod maskę

Sprawdźmy co się dzieje, gdy wyświetlimy słownik atrybutów obiektu (__dict__). Słownik atrybutów to specjalny słownik, który zawiera wszystkie atrybuty obiektu.

class Test:
    def __init__(self):
        self.__sekret = 42  # Zmienna z podwójnym podkreślnikiem

t = Test()
print(t.__dict__)  # Wyświetla słownik wszystkich atrybutów
# WYNIK: {'_Test__sekret': 42}
# Python zmienił nazwę! Zamiast __sekret jest _Test__sekret

# Możemy go odczytać, jeśli znamy nową nazwę:
print(t._Test__sekret) # 42 - działa!

Jak widać, "prywatność" w Pythonie polega tylko na zmianie etykiety, a nie na fizycznym zablokowaniu pamięci. To jest właśnie mechanizm name mangling (przemianowywanie nazw).

Po co to komu? Głównie po to, żeby uniknąć przypadkowych konfliktów nazw w dużych projektach, gdzie wiele klas może mieć zmienne o tych samych nazwach.

18/30
Hermetyzacja metod

Ukrywanie zachowań

Nie tylko dane mogą być ukryte. Często chcemy ukryć metody pomocnicze, które są potrzebne klasie "wewnątrz", ale nie powinny być wywoływane przez użytkownika.

Przykład z życia: Wyobraź sobie klasę do płatności. Metoda zaplac() jest publiczna - kliencie ją wywołujesz. Ale metoda _polacz_z_bankiem() jest wewnętrzna - klient nie musi wiedzieć, jak dokładnie łączycie się z bankiem.

class Platnosc:
    def zaplac(self, kwota):  # Publiczna metoda - do użytku zewnętrznego
        self._polacz_z_bankiem()  # Wywołujemy metodę wewnętrzną
        print(f"Opłata {kwota} zł przyjęta")

    def _polacz_z_bankiem(self):  # Metoda wewnętrzna - z podkreślnikiem
        print("Łączenie z serwerem bankowym...")
        # Tutaj byłaby skomplikowana logika połączenia

# Użycie:
p = Platnosc()
p.zaplac(100)  # OK - używamy publicznej metody
p._polacz_z_bankiem()  # Działa, ale to zła praktyka!

Metody z podkreślnikiem to sygnał dla innych programistów: "nie używaj tego bezpośrednio, to szczegół implementacji".

19/30
Pułapki i błędy

Czego nie robić?

Pracując z właściwościami, łatwo popełnić błędy. Oto najczęstsze pułapki, w które wpadają początkujący programiści:

  • Nadpisywanie @property w podklasie: Jeśli chcesz zmienić zachowanie właściwości w podklasie, musisz uważać, by nie "zgubić" settera. W podklasie trzeba zdefiniować go ponownie, inaczej nie będzie działał.
  • Zmienne publiczne i podkreślniki bez powodu: Unikaj mieszania konwencji w obrębie jednej klasy (np. self.name i self._age bez wyraźnego powodu). To wprowadza chaos.
  • Zbyt skomplikowane @property: Właściwość powinna być szybka. Jeśli wymaga ciężkich obliczeń lub zapytań do bazy danych, lepiej zrób z tego zwykłą metodę (np. pobierz_dane()), by użytkownik wiedział, że to kosztowna operacja. Użycie kropki sugeruje prosty dostęp do danych.
Zasada: Jeśli coś wymaga dużo pracy (obliczenia, czas), nie ukrywaj tego w @property. Użytkownik powinien wiedzieć, że to może chwilę potrwać.
20/30
Przykład: Klasa SmartLight

Wszystko w praktyce

Zobaczmy kompletny przykład klasy, która wykorzystuje hermetyzację i właściwości w praktyce. To klasa reprezentująca smart żarówkę (można sterować jej jasnością).

class SmartLight:
    def __init__(self):
        self._brightness = 0  # Początkowa jasność 0%

    @property
    def brightness(self):
        return self._brightness  # Zwróć aktualną jasność

    @brightness.setter
    def brightness(self, val):
        if 0 <= val <= 100:  # Sprawdź czy wartość jest w zakresie 0-100
            self._brightness = val  # Jeśli OK, ustaw
        else:
            print("Błędna jasność (0-100)")  # Komunikat błędu

# Użycie:
lamp = SmartLight()
lamp.brightness = 75  # OK - ustaw 75%
print(lamp.brightness)  # 75
lamp.brightness = 150  # Wyświetli ostrzeżenie - za duża wartość!
print(lamp.brightness)  # 75 - wartość się nie zmieniła

Ten przykład pokazuje, jak walidacja chroni nasze dane przed nieprawidłowymi wartościami.

21/30
Leniwa inicjalizacja (Lazy Loading)

Sprytne właściwości

Właściwości pozwalają na wczytanie danych dopiero wtedy, gdy są naprawdę potrzebne. To się nazywa leniwa inicjalizacja (lazy loading).

Po co to jest? Czasami pobieranie danych jest kosztowne (np. z bazy danych, z pliku, z internetu). Nie warto tego robić od razu przy tworzeniu obiektu, jeśli użytkownik może tych danych w ogóle nie potrzebować.

class BazaDanych:
    def __init__(self):
        self._dane = None  # Na początku brak danych

    @property
    def dane(self):
        if self._dane is None:  # Jeśli dane jeszcze nie są pobrane
            print("Pobieranie danych z bazy...")  # Symulacja operacji
            self._dane = [1, 2, 3]  # Pobierz dane
        return self._dane  # Zwróć dane

# Użycie:
baza = BazaDanych()  # Obiekt stworzony, ale dane NIE zostały pobrane!
print("Teraz pobiorę dane...")
print(baza.dane)  # Dopiero TERAZ dane są pobierane
print(baza.dane)  # Drugi raz - już nie pobiera, zwraca z pamięci
22/30
Interfejs vs Implementacja

Najważniejszy podział

Dzięki hermetyzacji oddzielasz to, co obiekt udostępnia (Public Interface), od tego jak to robi (Implementation Details). To fundamentalna zasada dobrego programowania.

  • Interfejs: To, co widzi użytkownik (atrybuty publiczne, properties, metody publiczne). Powinien być stabilny - nie zmieniaj go bez powodu, bo możesz zepsuć kod innym.
  • Implementacja: To, co ukryte (zmienne z _). Może być dowolnie zmieniana podczas optymalizacji kodu, naprawiania błędów czy dodawania nowych funkcji.

Analogia: Interfejs to ekran telefonu (przyciski, aplikacje), a implementacja to wszystko, co jest w środku (procesor, pamięć, układy). Producent może zmienić wnętrze, ale ekran musi pozostawać taki sam.

23/30
Dobre nazwy właściwości

Czytelność przede wszystkim

Właściwości powinny brzmieć jak rzeczowniki (cechy obiektu), a nie jak czasowniki (akcje). To ważne dla czytelności kodu.

  • Dobrze: user.full_name (pełne imię - cecha), car.is_running (czy jest włączony - cecha), doc.page_count (liczba stron - cecha)
  • Źle: user.get_full_name (jako property - to jest czasownik!), car.calculate_speed (to brzmi jak obliczanie, nie jak cecha)

Pamiętaj, że dla kogoś, kto czyta kod, użycie kropki (obiekt.cecha) sugeruje szybki dostęp do danych, a nie ciężką pracę procesora. Jeśli coś wymaga dużo obliczeń, lepiej nazwać to metodą z nawiasami.

Zasada: Jeśli nazwa brzmi jak pytanie o cechę (is_, has_, może...), to prawdopodobnie powinna być właściwością. Jeśli brzmi jak polecenie (pobierz_, oblicz_, zrób...), lepiej zrobić z tego metodę.
24/30
Property a dokumentacja

Gdzie pisać docstringi?

Dokumentację (docstring) dla property piszemy zawsze w getterze (tej metodzie z dekoratorem @property). Większość narzędzi do dokumentacji (jak Sphinx czy VS Code) poradzi sobie z tym poprawnie.

Co to jest docstring? To tekst w potrójnych cudzysłowach pod definicją funkcji lub metody. Służy jako dokumentacja, którą można później wyświetlić poleceniem help().

@property  # Dokumentację piszemy TUTAJ, w getterze
def email(self):
    """Zwraca adres e-mail użytkownika w formacie tekstowym."""
    # To jest docstring - opis działania właściwości
    return self._email

# Nie piszemy docstringa w setterze ani deleterze!
@email.setter
def email(self, val):
    self._email = val

# Sprawdzenie:
# help(TwojaKlasa.email) wyświetli dokumentację
25/30
Caching we właściwościach

Zapamiętywanie wyników

Jeśli obliczenie właściwości zajmuje dużo czasu (np. przetwarzanie dużej ilości danych), warto zapisać wynik po pierwszym wywołaniu. To się nazywa caching (zapamiętywanie).

Jak to działa? Po pierwszym wywołaniu zapisujemy wynik w ukrytej zmiennej (np. _cache). Przy kolejnych wywołaniach zwracamy zapamiętany wynik zamiast liczyć go ponownie.

class Analizator:
    @property
    def raport_wynik(self):
        # Sprawdź czy wynik jest już zapamiętany
        if not hasattr(self, "_cache"):
            print("Obliczam raport (to może chwilę potrwać)...")
            self._cache = sum(range(1000000))  # Symulacja ciężkiej pracy
            print("Obliczenia zakończone!")
        return self._cache  # Zwróć zapamiętany wynik

a = Analizator()
print(a.raport_wynik)  # Najpierw oblicza, potem zapisuje
print(a.raport_wynik)  # Już tylko zwraca z pamięci!

W nowszych wersjach Pythona (3.8+) mamy do tego dedykowany dekorator @cached_property z modułu functools, który robi to automatycznie.

26/30
Hermetyzacja a JSON / Serializacja

Eksportowanie danych

Kiedy zamieniamy obiekt na słownik lub JSON (tzw. serializacja), atrybuty chronione (jak _wiek) mogą pojawić się w wyniku, co nie zawsze jest pożądane. To może być problem, jeśli te dane są wrażliwe.

Co to jest serializacja? To zamiana obiektu Python na tekst (JSON) lub inny format, który można zapisać w pliku lub przesłać przez sieć.

Przykład problemu: Jeśli masz klasę z hasłem w zmiennej _password, to przy zapisie do JSON to hasło może "wyciec" - zależy nam na tym, żeby było ukryte.

Omawiając później serializację (wykład 6 o dataclass), dowiemy się, jak właściwości pomagają kontrolować, które dane "wyciekają" na zewnątrz do plików. Poznamy specjalne metody, które pozwalają wybrać, co ma być zapisane.

27/30
Ciekawostka: Slots

Blokowanie możliwości dodawania atrybutów

Domyślnie w Pythonie do każdego obiektu możesz dodać nową zmienną "z zewnątrz" (o.nowy = 5). To czasami prowadzi do błędów, gdy przypadkowo pomylisz nazwę atrybutu.

__slots__ to specjalny mechanizm, który pozwala określić z góry, jakie atrybuty dany obiekt może mieć. To forma hermetyzacji (kontroli) i jednocześnie optymalizacji pamięci.

class Punkt:
    __slots__ = ('x', 'y')  # Tylko te pola są dozwolone!

p = Punkt()
p.x = 10  # OK
p.y = 20  # OK
p.z = 30  # BŁĄD! AttributeError - 'z' nie jest dozwolony

To sprawia, że obiekt nie ma __dict__ (słownika atrybutów) i nie można mu dopisać niczego innego. Używa się tego rzadko, głównie dla oszczędności pamięci w przypadku tworzenia milionów obiektów.

28/30
Porównanie z Java/C++

Tabela różnic

Zobaczmy, jak hermetyzacja wygląda w Pythonie w porównaniu z innymi popularnymi językami programowania.

Cecha Python Java / C++
Blokada dostępu Brak (tylko konwencja z _ i __) Pełna (słowa kluczowe private, protected)
Gettery/Settery Dekorator @property (automatyczne) Jawne metody getX(), setX()
Filozofia Zaufanie (Consenting Adults) Kontrola i bezpieczeństwo

Każde podejście ma swoje zalety. Python pozwala na większą elastyczność, ale wymaga dyscypliny od programisty. Java/C++ wymuszają zasady, ale dają więcej gwarancji.

29/30
Zadanie do przemyślenia

Sprawdź się

Zaprojektuj klasę KontoUzytkownika, która:

  1. Przechowuje hasło jako atrybut "prywatny" (__password). Użyj podwójnego podkreślnika, żeby utrudnić dostęp.
  2. Ma właściwość password, która przy odczycie zwraca same gwiazdki (****). To chroni hasło przed podglądnięciem.
  3. Umożliwia zmianę hasła przez setter tylko wtedy, gdy nowe hasło ma minimum 8 znaków. To podstawowa walidacja bezpieczeństwa.

Takie ćwiczenie pomoże Ci zrozumieć, jak chronić wrażliwe dane i jak łączyć wszystkie elementy hermetyzacji w jednym miejscu.

Wskazówka: Pamiętaj, że getter ma zwracać **** niezależnie od długości hasła. Setter musi sprawdzić długość przed zapisem.
30/30
Podsumowanie wykładu

Co zapamiętać?

  • Używaj (_) do oznaczenia zmiennych, które nie są częścią publicznego API. To sygnał dla innych programistów: "to jest szczegół implementacji".
  • Używaj (@property) do kontroli dostępu do danych i walidacji. To pozwala na zachowanie prostego składni (kropka), a jednocześnie dodaje logikę.
  • Pamiętaj, że enkapsulacja to nie tylko zakazy, to przede wszystkim dbanie o czystość i stabilność Twojego kodu. Dobrze zaprojektowana klasa chroni swoje dane przed błędami.
  • Właściwości pozwalają zmieniać sposób działania klasy bez psucia programów, które z niej korzystają. To klucz do tworzenia oprogramowania, które łatwo rozwijać.

Znasz już fundamenty struktur danych w klasach. W następnym wykładzie: Dziedziczenie – jak budować nowe klasy na podstawie już istniejących, aby nie powtarzać kodu!

Ćwicz! Najlepiej uczysz się przez praktykę. Spróbuj przepisać przykłady z tego wykładu i zmodyfikuj je według własnego pomysłu.