Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Enkapsulacja - ochrona siedlisk

Witaj ponownie, @name! Darwin tutaj z kluczową lekcją o bezpieczeństwie danych.

Wyobraź sobie, że prowadzisz katalog gatunków zagrożonych. Dane o lokalizacji rzadkich zwierząt są wrażliwe - nie chcesz, aby kłusownicy mieli do nich bezpośredni dostęp! Potrzebujesz kontrolowanego dostępu, walidacji, ochrony.

W OOP ten mechanizm to enkapsulacja (encapsulation) - ukrywanie szczegółów implementacji i chronieniu danych przed nieautoryzowanym dostępem!

Czym jest enkapsulacja?

Enkapsulacja to jeden z 4 filarów OOP, który polega na:

  1. Grupowaniu danych i metod w jednej klasie
  2. Ukrywaniu szczegółów implementacji
  3. Kontrolowaniu dostępu do danych przez metody publiczne

Analogia Safari: Wrażliwe dane o lokalizacji gatunków zagrożonych są chronione - dostęp tylko dla autoryzowanego personelu, z logowaniem każdej zmiany!

1# BEZ enkapsulacji - niebezpieczne!
2class Species:
3    def __init__(self, name, population):
4        self.name = name
5        self.population = population  # Publiczne - każdy może zmienić!
6
7lion = Species("Lew", 500)
8lion.population = -999999  # ❌ Nikt nie zatrzyma tego błędu!
9
10# Z enkapsulacją - bezpieczne!
11class ProtectedSpecies:
12    def __init__(self, name, population):
13        self.name = name
14        self.__population = population  # Prywatne!
15
16    def set_population(self, value):
17        """Kontrolowany dostęp z walidacją"""
18        if value < 0:
19            raise ValueError("Populacja nie może być ujemna!")
20        self.__population = value
21
22    def get_population(self):
23        return self.__population
24
25lion = ProtectedSpecies("Lew", 500)
26# lion.__population = -999  # ❌ Nie działa - chronione!
27lion.set_population(550)  # ✅ Działa - walidowane!

Konwencje dostępu w Pythonie

Python używa konwencji nazewniczych do oznaczania poziomów dostępu:

| Konwencja | Znaczenie | Przykład | |-----------|-----------|----------| |

name
| Publiczny - dostęp z każdego miejsca |
self.name
| |
_name
| Chroniony - tylko klasa i podklasy |
self._internal_id
| |
__name
| Prywatny - tylko wewnątrz klasy |
self.__password
|

UWAGA: W Pythonie to są konwencje, nie twarde ograniczenia! Python ufa programiście.

Atrybuty publiczne

Domyślnie wszystkie atrybuty są publiczne - dostępne z każdego miejsca:

1class Animal:
2    def __init__(self, name, age):
3        self.name = name  # Publiczny
4        self.age = age    # Publiczny
5
6lion = Animal("Simba", 3)
7
8# Pełny dostęp
9print(lion.name)  # "Simba"
10lion.age = 4      # Można zmienić
11print(lion.age)   # 4

Użyj publicznych atrybutów gdy:

  • ✅ Dane są bezpieczne do bezpośredniej modyfikacji
  • ✅ Nie potrzebujesz walidacji
  • ✅ To część publicznego API klasy

Atrybuty chronione -
_single_underscore

Konwencja: Atrybut z jednym podkreśleniem (

_name
) oznacza "chroniony" - używaj tylko wewnątrz klasy i w podklasach.

1class Species:
2    def __init__(self, name):
3        self.name = name           # Publiczny
4        self._internal_id = 12345  # Chroniony - konwencja!
5
6    def _calculate_risk(self):
7        """Metoda chroniona - konwencja"""
8        return self._internal_id * 2
9
10lion = Species("Lew")
11
12# Technicznie możliwe, ale narusza konwencję
13print(lion._internal_id)  # 12345 - działa, ale NIE POWINNO!
14
15# Prawidłowe użycie
16class EndangeredSpecies(Species):
17    def get_risk(self):
18        # Podklasa może używać _internal_id
19        return self._calculate_risk()

Konwencja _underscore:

  • ⚠️ Sygnał: "To wewnętrzna implementacja, nie używaj bezpośrednio"
  • 🔓 Techniczne: Wciąż dostępne, Python nie blokuje
  • 📚 Dokumentacja: Nie pojawia się w
    help()
    domyślnie

Atrybuty prywatne -
__double_underscore

Name Mangling: Atrybut z dwoma podkreśleniami (

__name
) jest "prywatny" - Python zmienia nazwę, żeby utrudnić dostęp!

1class SecretData:
2    def __init__(self, value):
3        self.__secret = value  # Prywatny!
4
5    def get_secret(self):
6        """Kontrolowany dostęp"""
7        return self.__secret
8
9data = SecretData("Top Secret")
10
11# Bezpośredni dostęp nie działa
12# print(data.__secret)  # ❌ AttributeError!
13
14# Działa przez metodę
15print(data.get_secret())  # "Top Secret"
16
17# Name mangling - Python zmienił nazwę!
18print(data._SecretData__secret)  # "Top Secret" - można, ale NIE RÓB TAK!

Mechanizm Name Mangling:

  • Python zmienia
    __name
    na
    _ClassName__name
  • To utrudnia dostęp, ale nie uniemożliwia
  • Chroni przed przypadkowym nadpisaniem w podklasach
1class Parent:
2    def __init__(self):
3        self.__private = "Rodzic"
4
5class Child(Parent):
6    def __init__(self):
7        super().__init__()
8        self.__private = "Dziecko"  # Nie koliduje!
9
10child = Child()
11print(child._Parent__private)  # "Rodzic"
12print(child._Child__private)   # "Dziecko"

Properties -
@property

Property to elegancki sposób na tworzenie getterów i setterów, które wyglądają jak atrybuty!

Podstawowe property (tylko odczyt)

1class Circle:
2    def __init__(self, radius):
3        self.radius = radius
4
5    @property
6    def area(self):
7        """Oblicz pole - automatycznie!"""
8        return 3.14159 * self.radius ** 2
9
10    @property
11    def diameter(self):
12        """Oblicz średnicę"""
13        return self.radius * 2
14
15circle = Circle(5)
16
17# Używaj jak atrybutu, ale wywołuje metodę!
18print(circle.area)      # 78.53975 - wygląda jak atrybut
19print(circle.diameter)  # 10
20
21circle.radius = 10
22print(circle.area)      # 314.159 - automatycznie przeliczone!

Property z getterem i setterem

1class Temperature:
2    def __init__(self, celsius):
3        self._celsius = celsius  # Wewnętrzne przechowywanie
4
5    @property
6    def celsius(self):
7        """Getter - odczyt temperatury"""
8        return self._celsius
9
10    @celsius.setter
11    def celsius(self, value):
12        """Setter - zapis z walidacją"""
13        if value < -273.15:
14            raise ValueError("Temperatura poniżej zera bezwzględnego!")
15        self._celsius = value
16
17    @property
18    def fahrenheit(self):
19        """Automatyczna konwersja"""
20        return self._celsius * 9/5 + 32
21
22    @fahrenheit.setter
23    def fahrenheit(self, value):
24        """Ustawienie przez Fahrenheita"""
25        self._celsius = (value - 32) * 5/9
26
27temp = Temperature(25)
28
29# Getter
30print(temp.celsius)     # 25
31
32# Setter z walidacją
33temp.celsius = 30       # OK
34print(temp.celsius)     # 30
35
36# temp.celsius = -300  # ValueError: Temperatura poniżej zera bezwzględnego!
37
38# Konwersja automatyczna
39print(temp.fahrenheit)  # 86.0
40temp.fahrenheit = 32    # Ustaw przez Fahrenheita
41print(temp.celsius)     # 0.0 - automatyczna konwersja!

Property tylko do odczytu (read-only)

1class Person:
2    def __init__(self, birth_year):
3        self._birth_year = birth_year
4
5    @property
6    def birth_year(self):
7        """Tylko odczyt - brak settera"""
8        return self._birth_year
9
10    @property
11    def age(self):
12        """Obliczane dynamicznie"""
13        from datetime import datetime
14        return datetime.now().year - self._birth_year
15
16person = Person(1990)
17print(person.age)  # 35 (w 2025)
18# person.age = 40  # ❌ AttributeError: can't set attribute

Safari przykład - System zarządzania gatunkami zagrożonymi

1from datetime import datetime
2from typing import List, Dict
3
4class EndangeredSpecies:
5    """
6    Klasa zarządzająca gatunkiem zagrożonym z pełną enkapsulacją
7
8    Chroni wrażliwe dane o lokalizacji, populacji, obserwacjach
9    """
10
11    # Klasa przechowuje wszystkie instancje
12    _all_species: Dict[str, 'EndangeredSpecies'] = {}
13    _next_id = 1
14
15    def __init__(self, scientific_name, common_name, conservation_status):
16        # Publiczne dane - bezpieczne do udostępnienia
17        self.scientific_name = scientific_name
18        self.common_name = common_name
19
20        # Chronione dane - konwencja _underscore
21        self._id = EndangeredSpecies._next_id
22        EndangeredSpecies._next_id += 1
23        self._conservation_status = conservation_status
24        self._created_at = datetime.now()
25
26        # Prywatne dane - wrażliwe informacje
27        self.__population = 0
28        self.__secret_locations = []  # Lokalizacje - NIE dla publiki!
29        self.__access_log = []        # Log dostępu
30
31        # Rejestruj w katalogu
32        EndangeredSpecies._all_species[scientific_name] = self
33
34    # === PROPERTY: Population (z walidacją) ===
35
36    @property
37    def population(self):
38        """Odczyt populacji - logowany"""
39        self.__log_access("population", "read")
40        return self.__population
41
42    @population.setter
43    def population(self, value):
44        """Zapis populacji - walidacja + logowanie"""
45        self.__log_access("population", "write", f"Zmiana z {self.__population} na {value}")
46
47        if not isinstance(value, int):
48            raise TypeError("Populacja musi być liczbą całkowitą")
49
50        if value < 0:
51            raise ValueError("Populacja nie może być ujemna")
52
53        self.__population = value
54        self.__update_conservation_status()
55
56    # === PROPERTY: Conservation Status (tylko odczyt) ===
57
58    @property
59    def conservation_status(self):
60        """Status ochrony - tylko odczyt"""
61        return self._conservation_status
62
63    # === PROPERTY: Threat Level (obliczane dynamicznie) ===
64
65    @property
66    def threat_level(self):
67        """
68        Poziom zagrożenia (1-10) - obliczane na podstawie populacji
69        Tylko odczyt - brak settera
70        """
71        if self.__population == 0:
72            return 10  # Krytycznie zagrożony
73        elif self.__population < 50:
74            return 9
75        elif self.__population < 250:
76            return 7
77        elif self.__population < 1000:
78            return 5
79        elif self.__population < 5000:
80            return 3
81        else:
82            return 1  # Bezpieczny
83
84    # === METODY PRYWATNE ===
85
86    def __log_access(self, field, action, details=""):
87        """Prywatna metoda - logowanie dostępu"""
88        log_entry = {
89            "timestamp": datetime.now(),
90            "field": field,
91            "action": action,
92            "details": details
93        }
94        self.__access_log.append(log_entry)
95
96    def __update_conservation_status(self):
97        """Prywatna metoda - aktualizacja statusu na podstawie populacji"""
98        if self.__population == 0:
99            self._conservation_status = "Extinct in the Wild"
100        elif self.__population < 50:
101            self._conservation_status = "Critically Endangered"
102        elif self.__population < 250:
103            self._conservation_status = "Endangered"
104        elif self.__population < 1000:
105            self._conservation_status = "Vulnerable"
106        else:
107            self._conservation_status = "Least Concern"
108
109    # === METODY PUBLICZNE - Kontrolowany dostęp ===
110
111    def add_secret_location(self, latitude, longitude, authorization_code):
112        """
113        Dodaj wrażliwą lokalizację - wymaga autoryzacji!
114
115        Args:
116            latitude: Szerokość geograficzna
117            longitude: Długość geograficzna
118            authorization_code: Kod dostępu (w produkcji: hash, token, etc.)
119        """
120        # Walidacja autoryzacji
121        if authorization_code != "SAFARI_2024_SECURE":
122            self.__log_access("secret_locations", "denied", "Nieautoryzowany dostęp")
123            raise PermissionError("Nieautoryzowany dostęp do wrażliwych danych!")
124
125        # Walidacja współrzędnych
126        if not (-90 <= latitude <= 90):
127            raise ValueError("Nieprawidłowa szerokość geograficzna")
128        if not (-180 <= longitude <= 180):
129            raise ValueError("Nieprawidłowa długość geograficzna")
130
131        location = {
132            "lat": latitude,
133            "lon": longitude,
134            "added_at": datetime.now()
135        }
136
137        self.__secret_locations.append(location)
138        self.__log_access("secret_locations", "add", f"Dodano lokalizację: {latitude}, {longitude}")
139
140        return f"✓ Lokalizacja dodana (ID: {len(self.__secret_locations)})"
141
142    def get_location_count(self):
143        """Zwróć liczbę lokalizacji bez ujawniania szczegółów"""
144        return len(self.__secret_locations)
145
146    def get_general_area(self):
147        """Zwróć ogólny obszar bez precyzyjnych współrzędnych"""
148        if not self.__secret_locations:
149            return "Brak danych o lokalizacji"
150
151        # Uśrednij współrzędne dla ogólnego obszaru
152        avg_lat = sum(loc["lat"] for loc in self.__secret_locations) / len(self.__secret_locations)
153        avg_lon = sum(loc["lon"] for loc in self.__secret_locations) / len(self.__secret_locations)
154
155        # Zwróć zaokrąglone do 1 miejsca (mniej precyzyjne)
156        return f"Obszar: {avg_lat:.1f}°, {avg_lon:.1f}°"
157
158    def get_access_log(self, authorization_code):
159        """Zwróć log dostępu - wymaga autoryzacji"""
160        if authorization_code != "SAFARI_2024_SECURE":
161            raise PermissionError("Nieautoryzowany dostęp do logów!")
162
163        return self.__access_log.copy()  # Zwróć kopię, nie oryginał!
164
165    def generate_public_report(self):
166        """Raport publiczny - bez wrażliwych danych"""
167        return f"""
168╔════════════════════════════════════════════════╗
169  {self.common_name} ({self.scientific_name})
170╠════════════════════════════════════════════════╣
171  Status ochrony: {self.conservation_status}
172  Populacja: {self.population} osobników
173  Poziom zagrożenia: {self.threat_level}/10
174  Znane lokalizacje: {self.get_location_count()}
175  Ogólny obszar: {self.get_general_area()}
176╚════════════════════════════════════════════════╝
177        """.strip()
178
179    def generate_classified_report(self, authorization_code):
180        """Raport szczegółowy - wymaga autoryzacji"""
181        if authorization_code != "SAFARI_2024_SECURE":
182            raise PermissionError("Nieautoryzowany dostęp do danych niejawnych!")
183
184        locations_str = "\n".join(
185            f"    - Lat: {loc['lat']:.6f}, Lon: {loc['lon']:.6f} (dodano: {loc['added_at'].strftime('%Y-%m-%d %H:%M')})"
186            for loc in self.__secret_locations
187        )
188
189        return f"""
190╔════════════════════════════════════════════════╗
191  🔒 RAPORT NIEJAWNY - {self.common_name}
192╠════════════════════════════════════════════════╣
193  ID: {self._id}
194  Status: {self.conservation_status}
195  Populacja: {self.__population}
196  Zagrożenie: {self.threat_level}/10
197
198  LOKALIZACJE (NIEJAWNE):
199{locations_str if locations_str else "    Brak danych"}
200
201  Dostępy do danych: {len(self.__access_log)}
202╚════════════════════════════════════════════════╝
203        """.strip()
204
205    # === METODY KLASOWE ===
206
207    @classmethod
208    def get_species_by_threat_level(cls, min_level):
209        """Zwróć gatunki o zagrożeniu >= min_level"""
210        return [
211            species for species in cls._all_species.values()
212            if species.threat_level >= min_level
213        ]
214
215# === DEMONSTRACJA UŻYCIA ===
216
217print("=== SYSTEM ZARZĄDZANIA GATUNKAMI ZAGROŻONYMI ===\n")
218
219# Tworzenie gatunków
220rhino = EndangeredSpecies(
221    scientific_name="Diceros bicornis",
222    common_name="Nosorożec czarny",
223    conservation_status="Critically Endangered"
224)
225
226mountain_gorilla = EndangeredSpecies(
227    scientific_name="Gorilla beringei beringei",
228    common_name="Goryl górski",
229    conservation_status="Endangered"
230)
231
232# Ustawienie populacji (z walidacją)
233rhino.population = 45  # OK
234mountain_gorilla.population = 880  # OK
235
236# Próba nieprawidłowej wartości
237try:
238    rhino.population = -10  # ValueError!
239except ValueError as e:
240    print(f"❌ Błąd walidacji: {e}\n")
241
242# Dodanie lokalizacji (wymaga autoryzacji)
243try:
244    rhino.add_secret_location(
245        latitude=-1.2345,
246        longitude=36.7890,
247        authorization_code="WRONG_CODE"
248    )
249except PermissionError as e:
250    print(f"❌ Brak autoryzacji: {e}\n")
251
252# Prawidłowa autoryzacja
253rhino.add_secret_location(-1.2345, 36.7890, "SAFARI_2024_SECURE")
254rhino.add_secret_location(-1.5678, 36.9012, "SAFARI_2024_SECURE")
255mountain_gorilla.add_secret_location(-1.4321, 29.6543, "SAFARI_2024_SECURE")
256
257print("✓ Lokalizacje dodane\n")
258
259# Bezpośredni dostęp do prywatnych danych - nie działa!
260try:
261    print(rhino.__population)  # AttributeError
262except AttributeError:
263    print("❌ Nie można uzyskać bezpośredniego dostępu do __population\n")
264
265# Dostęp przez property - działa!
266print(f"✓ Populacja nosorożca (przez property): {rhino.population}\n")
267
268# Raport publiczny - bezpieczny
269print("=== RAPORT PUBLICZNY ===")
270print(rhino.generate_public_report())
271
272# Raport niejawny - wymaga autoryzacji
273print("\n=== RAPORT NIEJAWNY ===")
274print(rhino.generate_classified_report("SAFARI_2024_SECURE"))
275
276# Property tylko do odczytu
277print(f"\n=== WŁAŚCIWOŚCI TYLKO DO ODCZYTU ===")
278print(f"Status ochrony: {rhino.conservation_status}")
279print(f"Poziom zagrożenia: {rhino.threat_level}/10")
280
281# Próba zmiany - nie działa!
282try:
283    rhino.threat_level = 1  # AttributeError - brak settera!
284except AttributeError as e:
285    print(f"❌ Nie można ustawić threat_level: {e}")
286
287# Automatyczna aktualizacja statusu przy zmianie populacji
288print(f"\n=== AUTOMATYCZNA AKTUALIZACJA ===")
289print(f"Przed: populacja={rhino.population}, status={rhino.conservation_status}")
290rhino.population = 280  # Zwiększ populację
291print(f"Po: populacja={rhino.population}, status={rhino.conservation_status}")
292
293# Gatunki o wysokim zagrożeniu
294print(f"\n=== GATUNKI KRYTYCZNIE ZAGROŻONE ===")
295critical = EndangeredSpecies.get_species_by_threat_level(7)
296for species in critical:
297    print(f"  ⚠️  {species.common_name}: Zagrożenie {species.threat_level}/10")

Zalety enkapsulacji

1. Walidacja danych

1class Temperature:
2    def __init__(self):
3        self.__celsius = 0
4
5    @property
6    def celsius(self):
7        return self.__celsius
8
9    @celsius.setter
10    def celsius(self, value):
11        if value < -273.15:
12            raise ValueError("Poniżej zera bezwzględnego!")
13        self.__celsius = value
14
15temp = Temperature()
16# temp.celsius = -300  # ValueError - walidacja!

2. Kontrola dostępu

1class BankAccount:
2    def __init__(self, balance):
3        self.__balance = balance
4
5    def withdraw(self, amount):
6        """Kontrolowany dostęp"""
7        if amount > self.__balance:
8            raise ValueError("Niewystarczające środki")
9        self.__balance -= amount
10
11account = BankAccount(1000)
12# account.__balance = 999999  # Nie działa!
13account.withdraw(500)  # Tylko przez metodę!

3. Elastyczność implementacji

1class Circle:
2    def __init__(self, radius):
3        self.__radius = radius
4
5    @property
6    def area(self):
7        """Użytkownik nie wie, że to obliczane!"""
8        return 3.14159 * self.__radius ** 2
9
10# Możesz zmienić implementację bez zmiany interfejsu
11circle = Circle(5)
12print(circle.area)  # Użytkownik używa jak atrybutu

4. Ukrywanie złożoności

1class Database:
2    def __init__(self):
3        self.__connection = None
4        self.__cache = {}
5
6    def __connect(self):
7        """Prywatna - użytkownik nie musi znać szczegółów"""
8        print("Łączenie z bazą...")
9
10    def query(self, sql):
11        """Publiczny interfejs - prosty"""
12        if not self.__connection:
13            self.__connect()
14        # ... wykonaj zapytanie

Dobre praktyki enkapsulacji

1. Użyj property zamiast getterów/setterów

1# ❌ Styl Java - rozwlekły
2class Person:
3    def __init__(self, name):
4        self.__name = name
5
6    def get_name(self):
7        return self.__name
8
9    def set_name(self, value):
10        self.__name = value
11
12# ✅ Pythonowy sposób - elegancki
13class Person:
14    def __init__(self, name):
15        self._name = name
16
17    @property
18    def name(self):
19        return self._name
20
21    @name.setter
22    def name(self, value):
23        self._name = value

2. Nie nadużywaj prywatnych atrybutów

1# ❌ Zbyt paranoidalne
2class Person:
3    def __init__(self, name):
4        self.__name = name  # Zbędne __
5
6# ✅ Zaufaj konwencjom
7class Person:
8    def __init__(self, name):
9        self.name = name  # Prosty atrybut, jeśli nie potrzebujesz ochrony

3. Używaj enkapsulacji dla wrażliwych danych

1# ✅ Chroniąc wrażliwe dane
2class User:
3    def __init__(self, username, password):
4        self.username = username         # Publiczny
5        self.__password_hash = hash(password)  # Prywatny!
6
7    def check_password(self, password):
8        return hash(password) == self.__password_hash

Podsumowanie

W tej lekcji nauczyłeś/aś się:

  • ✅ Czym jest enkapsulacja i dlaczego jest ważna
  • ✅ Różnicy między publicznymi, chronionymi i prywatnymi atrybutami
  • ✅ Mechanizmu name mangling (
    __name
    )
  • ✅ Tworzenia properties z
    @property
    , getterów i setterów
  • ✅ Walidacji danych przy zapisie
  • ✅ Kontrolowanego dostępu do wrażliwych informacji
  • ✅ Praktycznych przykładów z systemu zarządzania gatunkami zagrożonymi

Checkpoint

Przed przejściem dalej:

  • [ ] Rozumiesz różnicę między
    name
    ,
    _name
    i
    __name
  • [ ] Potrafisz stworzyć property z getterem i setterem
  • [ ] Wiesz kiedy używać enkapsulacji
  • [ ] Rozumiesz mechanizm name mangling
  • [ ] Potrafisz walidować dane w setterze

Analogia Safari: Enkapsulacja to system ochrony dla wrażliwych danych o gatunkach zagrożonych - nie każdy powinien mieć dostęp do lokalizacji rzadkich zwierząt! 🔒🦏

W następnej lekcji Darwin nauczy Cię magicznych metod - ukrytych zachowań, które sprawiają, że Twoje klasy działają jak wbudowane typy Pythona! ✨🐍

Ir a CodeWorlds