Python dla Ekspertów

Kevin Clarkson

Spis treści

1. Zaawansowane aspekty składni Pythona

1.1. Dekoratory i metaklasy

1.2. Generatory i korutyny

1.3. Konteksty wykonania i menedżery kontekstu

1.4. Zaawansowane aspekty dziedziczenia

1.5. Protokoły i abstrakcyjne klasy bazowe

2. Metaprogramowanie i magia Pythona

2.1. Dynamiczne tworzenie klas i funkcji

2.2. Modyfikacja zachowania interpretera

2.3. Customowe deskryptory i atrybuty

2.4. Przeciążanie operatorów na poziomie zaawansowanym

2.5. Hooking i monkey patching

3. Optymalizacja wydajności kodu

3.1. Profilowanie i identyfikacja wąskich gardeł

3.2. Optymalizacja algorytmów i struktur danych

3.3. Wykorzystanie Cythona do przyspieszenia kodu

3.4. Paralelizacja i wykorzystanie wielu rdzeni

3.5. Optymalizacja pamięci i zarządzanie zasobami

4. Współbieżność i programowanie asynchroniczne

4.1. Wielowątkowość i GIL

4.2. Asyncio i programowanie oparte na zdarzeniach

4.4. Komunikacja między wątkami i procesami

4.5. Wzorce projektowe dla systemów współbieżnych

5. Zaawansowane wzorce projektowe

5.1. Wzorce kreacyjne w kontekście Pythona

5.2. Wzorce strukturalne i ich implementacja

5.3. Wzorce behawioralne i funkcyjne

5.4. Wzorce architektoniczne dla dużych systemów

5.5. Antywzorce i jak ich unikać

6. Architektura aplikacji w dużej skali

6.1. Projektowanie modułowe i pakietowe

6.2. Zarządzanie zależnościami i wersjonowanie

6.3. Skalowalność horyzontalna i wertykalna

6.4. Mikrousługi w Pythonie

6.5. Wzorce integracji systemów

7. Testowanie i debugowanie zaawansowanych systemów

7.1. Strategie testowania dla złożonych aplikacji

7.2. Mockowanie i stubbing zaawansowanych obiektów

7.3. Testy wydajnościowe i obciążeniowe

7.4. Debugowanie wielowątkowych i rozproszonych systemów

7.5. Continuous Integration i Deployment dla projektów Pythonowych

8. Python w analizie danych i uczeniu maszynowym

8.1. Zaawansowane techniki przetwarzania danych z NumPy i Pandas

8.2. Wizualizacja danych na poziomie eksperckim

8.3. Implementacja algorytmów uczenia maszynowego od podstaw

8.4. Optymalizacja modeli uczenia głębokiego

8.5. Przetwarzanie danych w czasie rzeczywistym

9. Bezpieczeństwo i kryptografia w Pythonie

9.1. Implementacja zaawansowanych algorytmów kryptograficznych

9.2. Bezpieczne przechowywanie i zarządzanie kluczami

9.3. Ataki i obrona w aplikacjach Pythonowych

9.4. Audyt bezpieczeństwa kodu Pythona

9.5. Zgodność z regulacjami dotyczącymi bezpieczeństwa danych

10. Integracja z systemami niskopoziomowymi

10.1. Interfejsy do systemów operacyjnych

10.2. Programowanie sterowników urządzeń

10.3. Integracja z kodem assemblerowym

10.4. Manipulacja pamięcią na niskim poziomie

10.5. Optymalizacja wydajności na poziomie sprzętowym

11. Tworzenie rozszerzeń w C/C++

11.1. Projektowanie API dla rozszerzeń

11.2. Zarządzanie pamięcią w rozszerzeniach

11.3. Wrappery do bibliotek C/C++

11.4. Debugowanie rozszerzeń

11.5. Optymalizacja wydajności rozszerzeń

12.Techniki przetwarzania tekstu i analizy języka naturalnego

12.1. Implementacja własnych algorytmów NLP

12.2. Tworzenie i trenowanie modeli językowych

12.3. Analiza sentymentu i klasyfikacja tekstu

12.4. Przetwarzanie i generowanie języka naturalnego

12.5. Integracja z zaawansowanymi bibliotekami NLP

13. Python w systemach rozproszonych

13.1. Projektowanie architektury systemów rozproszonych

13.2. Implementacja protokołów komunikacyjnych

13.3. Synchronizacja i spójność danych w systemach rozproszonych

13.4. Obsługa awarii i odporność na błędy

13.5. Skalowanie i zarządzanie klastrami Pythonowymi

14. Narzędzia do profilowania i analizy kodu

14.1. Zaawansowane techniki profilowania

14.2. Analiza statyczna kodu

14.3. Wykrywanie wycieków pamięci i zasobów

14.4. Optymalizacja wykorzystania CPU i GPU

14.5. Customowe narzędzia do analizy kodu

1. Zaawansowane aspekty składni Pythona

1.1. Dekoratory i metaklasy

Dekoratory można łączyć, stosując je jeden po drugim. Kolejność ma znaczenie - dekoratory są aplikowane od dołu do góry.

Copydef dekorator1(func):

def wrapper(*args, **kwargs):

print("Dekorator 1")

return func(*args, **kwargs)

return wrapper

def dekorator2(func):

def wrapper(*args, **kwargs):

print("Dekorator 2")

return func(*args, **kwargs)

return wrapper

@dekorator1

@dekorator2

def moja_funkcja():

print("Moja funkcja")

moja_funkcja()

# Wynik:

# Dekorator 1

# Dekorator 2

# Moja funkcja

Wbudowane dekoratory

Python oferuje kilka wbudowanych dekoratorów, które są często używane:

1. @property

Dekorator @property zamienia metodę klasy w atrybut tylko do odczytu:

Copyclass Osoba:

def __init__(self, imie, nazwisko):

self._imie = imie

self._nazwisko = nazwisko

@property

def pelne_imie(self):

return f"{self._imie} {self._nazwisko}"

osoba = Osoba("Jan", "Kowalski")

print(osoba.pelne_imie)  # Jan Kowalski

2. @classmethod

Dekorator @classmethod tworzy metodę klasy, która otrzymuje klasę jako pierwszy argument zamiast instancji:

Copyclass Osoba:

licznik = 0

def __init__(self, imie):

self.imie = imie

Osoba.licznik += 1

@classmethod

def liczba_osob(cls):

return cls.licznik

print(Osoba.liczba_osob())  # 0

osoba1 = Osoba("Jan")

osoba2 = Osoba("Anna")

print(Osoba.liczba_osob())  # 2

3. @staticmethod

Dekorator @staticmethod tworzy metodę statyczną, która nie otrzymuje ani instancji, ani klasy jako pierwszy argument:

Copyclass Matematyka:

@staticmethod

def dodaj(a, b):

return a + b

print(Matematyka.dodaj(3, 5))  # 8

Funkcje jako dekoratory klas

Funkcje mogą być również używane jako dekoratory klas. Pozwala to na modyfikację lub rozszerzenie funkcjonalności całej klasy:

Copydef dodaj_metode(cls):

def nowa_metoda(self):

return "To jest nowa metoda"

cls.nowa_metoda = nowa_metoda

return cls

@dodaj_metode

class MojaKlasa:

pass

obiekt = MojaKlasa()

print(obiekt.nowa_metoda())  # To jest nowa metoda

W tym przykładzie dekorator `dodaj_metode` dodaje nową metodę do dekorowanej klasy. Funkcja dekoratora przyjmuje klasę jako argument, modyfikuje ją i zwraca zmodyfikowaną klasę.

Metaklasy to "klasy klas" w Pythonie. Są one odpowiedzialne za tworzenie i kontrolowanie zachowania klas. Każda klasa w Pythonie jest instancją metaklasy, domyślnie type.

Podstawowa składnia definiowania klasy z użyciem metaklasy wygląda następująco:

Copyclass MojaMetaklasa(type):

def __new__(cls, name, bases, attrs):

# Modyfikacja lub dodanie atrybutów

return super().__new__(cls, name, bases, attrs)

class MojaKlasa(metaclass=MojaMetaklasa):

pass

Metoda `__new__` metaklasy jest wywoływana podczas tworzenia klasy i może modyfikować jej definicję przed utworzeniem.

Customowe metaklasy

Customowe metaklasy pozwalają na zaawansowaną kontrolę nad procesem tworzenia i zachowaniem klas. Oto przykład metaklasy, która automatycznie dodaje metodę do każdej klasy:

Copyclass DodajMetodeMetaklasa(type):

def __new__(cls, name, bases, attrs):

def nowa_metoda(self):

return f"Jestem instancją klasy {name}"

attrs['nowa_metoda'] = nowa_metoda

return super().__new__(cls, name, bases, attrs)

class MojaKlasa(metaclass=DodajMetodeMetaklasa):

pass

obiekt = MojaKlasa()

print(obiekt.nowa_metoda())  # Jestem instancją klasy MojaKlasa

Metaklasy mogą również implementować metody `__init__` i `__call__`, które są wywoływane odpowiednio po utworzeniu klasy i podczas tworzenia instancji klasy.

Dziedziczenie metaklas

Metaklasy mogą dziedziczyć po sobie, co pozwala na tworzenie hierarchii metaklas i dzielenie funkcjonalności:

Copyclass MetaklasaBazowa(type):

def __new__(cls, name, bases, attrs):

attrs['metoda_bazowa'] = lambda self: "Metoda z metaklasy bazowej"

return super().__new__(cls, name, bases, attrs)

class MetaklasaPochodna(MetaklasaBazowa):

def __new__(cls, name, bases, attrs):

attrs['metoda_pochodna'] = lambda self: "Metoda z metaklasy pochodnej"

return super().__new__(cls, name, bases, attrs)

class MojaKlasa(metaclass=MetaklasaPochodna):

pass

obiekt = MojaKlasa()

print(obiekt.metoda_bazowa())     # Metoda z metaklasy bazowej

print(obiekt.metoda_pochodna())   # Metoda z metaklasy pochodnej

W tym przykładzie `MetaklasaPochodna` dziedziczy po `MetaklasaBazowej`, co pozwala na dodanie metod z obu metaklas do klasy `MojaKlasa`.

Dziedziczenie metaklas jest szczególnie przydatne, gdy chcemy rozszerzyć funkcjonalność istniejącej metaklasy lub stworzyć hierarchię metaklas z różnymi poziomami modyfikacji klas.

1. Modyfikacja procesu tworzenia klasy

Metaklasy pozwalają na ingerencję w proces tworzenia klasy, umożliwiając modyfikację atrybutów, metod lub dodanie nowych elementów przed finalnym utworzeniem klasy.

Copyclass DodajPrefixMetaklasa(type):

def __new__(cls, name, bases, attrs):

prefixed_attrs = {f"my_{key}": value for key, value in attrs.items() if not key.startswith('__')}

return super().__new__(cls, name, bases, prefixed_attrs)

class MojaKlasa(metaclass=DodajPrefixMetaklasa):

x = 1

def metoda(self):

pass

print(MojaKlasa.my_x)  # 1

print(hasattr(MojaKlasa, 'my_metoda'))  # True

2. Automatyczne rejestrowanie klas

Metaklasy mogą być używane do automatycznego rejestrowania klas w centralnym rejestrze, co jest przydatne w przypadku systemów wtyczek lub mapowania klas.

Copyclass RejestrKlas:

klasy = {}

@classmethod

def zarejestruj(cls, nazwa, klasa):

cls.klasy[nazwa] = klasa

class AutoRejestrMetaklasa(type):

def __new__(cls, name, bases, attrs):

nowa_klasa = super().__new__(cls, name, bases, attrs)

RejestrKlas.zarejestruj(name, nowa_klasa)

return nowa_klasa

class KlasaA(metaclass=AutoRejestrMetaklasa):

pass

class KlasaB(metaclass=AutoRejestrMetaklasa):

pass

print(RejestrKlas.klasy)  # {'KlasaA': <class '__main__.KlasaA'>, 'KlasaB': <class '__main__.KlasaB'>}

3. Implementacja wzorców projektowych

Metaklasy mogą być wykorzystane do implementacji wzorców projektowych, takich jak Singleton.

Copyclass SingletonMetaklasa(type):

_instances = {}

def __call__(cls, *args, **kwargs):

if cls not in cls._instances:

cls._instances[cls] = super().__call__(*args, **kwargs)

return cls._instances[cls]

class Singleton(metaclass=SingletonMetaklasa):

pass

s1 = Singleton()

s2 = Singleton()

print(s1 is s2)  # True

Metaklasy w kontekście dziedziczenia wielokrotnego

W przypadku dziedziczenia wielokrotnego, gdzie klasy bazowe mają różne metaklasy, Python stosuje złożone reguły rozwiązywania konfliktów metaklas. Ogólna zasada jest taka, że metaklasa musi być podklasą wszystkich metaklas klas bazowych.

Copyclass MetaklasaA(type):

pass

class MetaklasaB(type):

pass

class MetaklasaC(MetaklasaA, MetaklasaB):

pass

class A(metaclass=MetaklasaA):

pass

class B(metaclass=MetaklasaB):

pass

class C(A, B, metaclass=MetaklasaC):

pass

W powyższym przykładzie, `MetaklasaC` dziedziczy po `MetaklasaA` i `MetaklasaB`, co pozwala na użycie jej jako metaklasy dla klasy `C`, która dziedziczy po `A` i `B`.

Jeśli nie zostanie dostarczona odpowiednia metaklasa, Python zgłosi błąd, informując o niemożliwości rozwiązania konfliktu metaklas.

Metaklasy w kontekście dziedziczenia wielokrotnego wymagają starannego planowania i zrozumienia hierarchii klas oraz metaklas, aby uniknąć konfliktów i zapewnić poprawne działanie kodu.

1.2. Generatory i korutyny

Generatory w Pythonie mogą nie tylko produkować wartości, ale także je przyjmować za pomocą metody `send()`. Pozwala to na dwukierunkową komunikację między kodem wywołującym a generatorem.

Copydef echo_generator():

while True:

value = yield

print(f"Otrzymano: {value}")

gen = echo_generator()

next(gen)  # Inicjalizacja generatora

gen.send("Hello")  # Wysyłanie wartości do generatora

gen.send("World")

# Wynik:

# Otrzymano: Hello

# Otrzymano: World

Metoda `send()` wznawia wykonanie generatora i wysyła wartość do ostatniego wyrażenia `yield`. Wartość ta staje się wynikiem wyrażenia `yield`.

Zamykanie generatorów (metody close() i throw())

Generatory można jawnie zamknąć za pomocą metody `close()`. Wywołanie tej metody powoduje zgłoszenie wyjątku `GeneratorExit` w miejscu, gdzie generator jest wstrzymany.

Copydef closable_generator():

try:

while True:

yield "Jestem aktywny"

finally:

print("Generator został zamknięty")

gen = closable_generator()

print(next(gen))

gen.close()

# Wynik:

# Jestem aktywny

# Generator został zamknięty

Metoda `throw()` pozwala na zgłoszenie dowolnego wyjątku w miejscu, gdzie generator jest wstrzymany:

Copydef exception_handler_generator():

try:

yield "Normalnie"

yield "Nadal normalnie"

except ValueError:

yield "Złapano ValueError"

gen = exception_handler_generator()

print(next(gen))

print(gen.throw(ValueError))

# Wynik:

# Normalnie

# Złapano ValueError

Delegowanie generatorów (yield from)

Wyrażenie `yield from` pozwala na delegowanie części lub całości generacji do innego generatora lub iterowalnego obiektu. Jest to szczególnie przydatne przy tworzeniu złożonych generatorów lub implementacji korutyn.

Copydef subgenerator():

yield 1

yield 2

yield 3

def main_generator():

yield "Start"

yield from subgenerator()

yield "Koniec"

for item in main_generator():

print(item)

# Wynik:

# Start

# 1

# 2

# 3

# Koniec

`yield from` nie tylko deleguje generację wartości, ale także obsługuje przesyłanie wartości i wyjątków między wywołującym a subdelegatem:

Copydef delegating_generator():

x = yield from subgenerator()

print(f"Subgenerator zwrócił: {x}")

def subgenerator():

while True:

x = yield

if x is None:

return "Zakończono"

print(f"Subgenerator otrzymał: {x}")

g = delegating_generator()

next(g)  # Inicjalizacja

g.send("Hello")

g.send("World")

g.send(None)  # Zakończenie subgeneratora

# Wynik:

# Subgenerator otrzymał: Hello

# Subgenerator otrzymał: World

# Subgenerator zwrócił: Zakończono

`yield from` upraszcza tworzenie złożonych generatorów i umożliwia bardziej modularne projektowanie kodu wykorzystującego generatory.

Korutyny to specjalne funkcje w Pythonie, które mogą być wstrzymywane i wznawiane w określonych punktach wykonania. Są one kluczowym elementem programowania asynchronicznego w Pythonie, umożliwiając efektywne zarządzanie współbieżnymi zadaniami bez konieczności używania wielu wątków.

Korutyny definiuje się za pomocą słowa kluczowego `async def`:

Copyasync def prosta_korutyna():

print("Rozpoczynam korutynę")

await asyncio.sleep(1)

print("Korutyna zakończona")

Słowo kluczowe `await` jest używane wewnątrz korutyny do wstrzymania jej wykonania do czasu zakończenia innej operacji asynchronicznej.

Asynchroniczne generatory

Asynchroniczne generatory łączą koncepcje generatorów i korutyn. Pozwalają na tworzenie asynchronicznych iteratorów, które mogą być używane w pętlach `async for`.

Składnia asynchronicznego generatora:

Copyasync def asynchroniczny_generator():

for i in range(3):

await asyncio.sleep(1)

yield i

async def main():

async for item in asynchroniczny_generator():

print(item)

asyncio.run(main())

# Wynik (z 1-sekundowymi przerwami):

# 0

# 1

# 2

Asynchroniczne generatory używają `async def` do definicji i `yield` do generowania wartości, ale mogą również używać `await` wewnątrz swojego ciała.

Słowa kluczowe async i await

`async` i `await` to kluczowe słowa wprowadzone w Pythonie 3.5 dla obsługi programowania asynchronicznego.

1. `async`:

* Używane do definiowania korutyn (`async def`)

* Używane do definiowania asynchronicznych menedżerów kontekstu (`async with`)

* Używane w asynchronicznych pętlach (`async for`)

2. `await`:

* Używane wewnątrz korutyn do wstrzymania wykonania i oczekiwania na zakończenie innej operacji asynchronicznej

* Może być używane tylko wewnątrz funkcji zdefiniowanych z `async def`

Przykład użycia `async` i `await`:

Copyimport asyncio

async def fetch_data():

print("Rozpoczynam pobieranie danych")

await asyncio.sleep(2)  # Symulacja operacji I/O

print("Dane pobrane")

return "Ważne dane"

async def process_data(data):

print("Rozpoczynam przetwarzanie")

await asyncio.sleep(1)  # Symulacja przetwarzania

print(f"Przetworzono: {data.upper()}")

async def main():

data = await fetch_data()

await process_data(data)

asyncio.run(main())

# Wynik (z odpowiednimi przerwami):

# Rozpoczynam pobieranie danych

# Dane pobrane

# Rozpoczynam przetwarzanie

# Przetworzono: WAŻNE DANE

W tym przykładzie, `async def` definiuje korutyny, a `await` jest używane do wstrzymania wykonania korutyny do czasu zakończenia asynchronicznej operacji. `asyncio.run()` jest używane do uruchomienia głównej korutyny i zarządzania pętlą zdarzeń.

Programowanie asynchroniczne z użyciem `async` i `await` pozwala na efektywne zarządzanie wieloma współbieżnymi zadaniami, szczególnie w aplikacjach intensywnie korzystających z operacji I/O.

1. Składnia:

* Generatory: definiowane za pomocą `def` i używają `yield`

* Korutyny: definiowane za pomocą `async def` i używają `await`

2. Cel:

* Generatory: głównie do tworzenia iteratorów i lazy evaluation

* Korutyny: do obsługi asynchronicznych operacji i współbieżności

3. Działanie:

* Generatory: wstrzymują się przy `yield`, zwracając wartość

* Korutyny: wstrzymują się przy `await`, oczekując na zakończenie innej operacji asynchronicznej

4. Wywołanie:

* Generatory: używane w pętlach `for` lub przez `next()`

* Korutyny: uruchamiane przez pętlę zdarzeń lub `await` w innej korutynie

Zastosowania korutyn w programowaniu asynchronicznym

1. Obsługa wielu połączeń sieciowych:

Copyasync def handle_connection(connection):

data = await connection.receive()

result = await process_data(data)

await connection.send(result)

2. Równoległe zapytania do bazy danych:

Copyasync def fetch_user_data(user_ids):

tasks = [db.fetch_user(user_id) for user_id in user_ids]

return await asyncio.gather(*tasks)

3. Asynchroniczne operacje na plikach:

Copyasync def process_large_file(filename):

async with aiofiles.open(filename, mode='r') as file:

content = await file.read()

return await process_content(content)

4. Obsługa długotrwałych zadań w tle:

Copyasync def background_task():

while True:

await perform_maintenance()

await asyncio.sleep(3600)  # co godzinę

Integracja korutyn z biblioteką asyncio

1. Uruchamianie korutyn:

Copyimport asyncio

async def main():

await asyncio.sleep(1)

print("Hello, World!")

asyncio.run(main())

2. Równoległe wykonywanie korutyn:

Copyasync def main():

task1 = asyncio.create_task(coroutine1())

task2 = asyncio.create_task(coroutine2())

await task1

await task2

3. Oczekiwanie na wiele korutyn:

Copyasync def main():

results = await asyncio.gather(

coroutine1(),

coroutine2(),

coroutine3()

)

4. Obsługa timeoutów:

Copyasync def main():

try:

result = await asyncio.wait_for(long_running_task(), timeout=10.0)

except asyncio.TimeoutError:

print("Operacja przekroczyła limit czasu")

5. Synchronizacja asynchroniczna:

Copylock = asyncio.Lock()

async def protected_resource():

async with lock:

# dostęp do współdzielonego zasobu

6. Asynchroniczne kolejki:

Copyqueue = asyncio.Queue()

async def producer():

await queue.put(item)

async def consumer():

item = await queue.get()

7. Asynchroniczne konteksty menedżera:

Copyasync with aiohttp.ClientSession() as session:

async with session.get('http://example.com') as response:

html = await response.text()

Korutyny i asyncio tworzą potężny ekosystem do programowania asynchronicznego w Pythonie, umożliwiając efektywne zarządzanie współbieżnymi zadaniami, szczególnie w aplikacjach intensywnie korzystających z I/O, takich jak serwery sieciowe, aplikacje webowe czy systemy przetwarzające duże ilości danych.

1.3. Konteksty wykonania i menedżery kontekstu

Menedżery kontekstu oferują elegancki sposób obsługi wyjątków poprzez metodę `__exit__`. Gdy wyjątek wystąpi wewnątrz bloku `with`, zostaje on przekazany do `__exit__` wraz z typem wyjątku, jego wartością i traceback.

Copyclass ExceptionHandler:

def __enter__(self):

print("Wejście do kontekstu")

def __exit__(self, exc_type, exc_value, traceback):

if exc_type is not None:

print(f"Złapano wyjątek: {exc_type.__name__}: {exc_value}")

return True  # Tłumi wyjątek

print("Wyjście z kontekstu")

return False  # Przepuszcza wyjątek (domyślne zachowanie)

with ExceptionHandler():

raise ValueError("Przykładowy błąd")

print("Kontynuacja programu")

Tworzenie własnych menedżerów kontekstu

Aby stworzyć własny menedżer kontekstu, należy zaimplementować metody `__enter__` i `__exit__`:

Copyclass DatabaseConnection:

def __init__(self, db_name):

self.db_name = db_name

def __enter__(self):

print(f"Łączenie z bazą danych {self.db_name}")

# Tu w rzeczywistości nastąpiłoby nawiązanie połączenia

return self

def __exit__(self, exc_type, exc_value, traceback):

print(f"Zamykanie połączenia z bazą danych {self.db_name}")

# Tu w rzeczywistości nastąpiłoby zamknięcie połączenia

if exc_type:

print(f"Wystąpił błąd: {exc_type.__name__}: {exc_value}")

return False

with DatabaseConnection("MyDB") as db:

print("Wykonywanie operacji na bazie danych")

# Symulacja błędu

raise Exception("Błąd bazy danych")

Funkcja contextlib.contextmanager

Moduł `contextlib` dostarcza dekorator `@contextmanager`, który pozwala na tworzenie menedżerów kontekstu za pomocą generatorów, upraszczając proces implementacji:

Copyfrom contextlib import contextmanager

@contextmanager

def file_manager(filename, mode):

try:

f = open(filename, mode)

yield f

finally:

f.close()

with file_manager('example.txt', 'w') as file:

file.write('Hello, World!')

W tym przykładzie:

1. Kod przed `yield` odpowiada metodzie `__enter__`.

2. `yield` zwraca obiekt, który będzie przypisany do zmiennej po `as`.

3. Kod po `yield` odpowiada metodzie `__exit__`.

`contextlib.contextmanager` automatycznie obsługuje wyjątki:

Copy@contextmanager

def error_handler():

try:

yield

except Exception as e:

print(f"Złapano wyjątek: {type(e).__name__}: {e}")

with error_handler():

raise ValueError("Przykładowy błąd")

Użycie `@contextmanager` znacznie upraszcza tworzenie menedżerów kontekstu, szczególnie gdy nie potrzebujemy pełnej kontroli nad procesem wejścia i wyjścia z kontekstu. Jest to szczególnie przydatne dla prostych przypadków użycia, gdzie głównym celem jest zapewnienie poprawnego zarządzania zasobami lub obsługa wyjątków.

1. Pliki: Wbudowany menedżer kontekstu dla plików zapewnia automatyczne zamknięcie pliku po zakończeniu operacji:

Copywith open('plik.txt', 'w') as f:

f.write('Przykładowy tekst')

# Plik zostaje automatycznie zamknięty po wyjściu z bloku with

2. Połączenia sieciowe: Dla połączeń sieciowych, np. z użyciem biblioteki `socket`:

Copyimport socket

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:

s.connect(('example.com', 80))

s.sendall(b'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')

data = s.recv(1024)

# Połączenie zostaje automatycznie zamknięte

3. Blokady: Menedżery kontekstu są przydatne do zarządzania blokadami, np. z modułu `threading`:

Copyimport threading

lock = threading.Lock()

with lock:

# Kod chroniony blokadą

pass

# Blokada zostaje automatycznie zwolniona

Zagnieżdżone menedżery kontekstu

Python pozwala na zagnieżdżanie menedżerów kontekstu, co jest przydatne, gdy potrzebujemy zarządzać wieloma zasobami jednocześnie:

Copywith open('plik1.txt', 'r') as f1, open('plik2.txt', 'w') as f2:

content = f1.read()

f2.write(content.upper())

Można również używać wielu linii dla lepszej czytelności:

Copywith (

open('plik1.txt', 'r') as f1,

open('plik2.txt', 'w') as f2,

open('plik3.txt', 'w') as f3

):

content = f1.read()

f2.write(content.upper())

f3.write(content.lower())

Aliasy w with (as)

Słowo kluczowe `as` w instrukcji `with` pozwala na przypisanie wyniku metody `__enter__` do zmiennej, która może być używana wewnątrz bloku kontekstowego:

Copyclass ContextExample:

def __enter__(self):

return "Wartość zwrócona przez __enter__"

def __exit__(self, exc_type, exc_value, traceback):

pass

with ContextExample() as ce:

print(ce)  # Wypisze: Wartość zwrócona przez __enter__

Można również używać wielu aliasów w jednej instrukcji `with`:

Copywith open('input.txt', 'r') as input_file, open('output.txt', 'w') as output_file:

for line in input_file:

output_file.write(line.upper())

W przypadku menedżerów kontekstu, które nie zwracają użytecznej wartości w `__enter__`, można pominąć alias:

Copywith open('log.txt', 'a'):

# Plik zostanie otwarty i zamknięty, ale nie potrzebujemy do niego odwoływać się

pass

Aliasy są szczególnie przydatne, gdy menedżer kontekstu zwraca obiekt, który ma być używany wewnątrz bloku `with`. Pozwalają na czytelne i zwięzłe zarządzanie zasobami, jednocześnie zapewniając dostęp do tych zasobów w kontrolowany sposób.

Asynchroniczne menedżery kontekstu są używane w programowaniu asynchronicznym i są definiowane za pomocą metod `__aenter__` i `__aexit__`. Używa się ich z instrukcją `async with`:

Copyimport asyncio

class AsyncContextManager:

async def __aenter__(self):

await asyncio.sleep(1)  # Symulacja asynchronicznej operacji

print("Wejście do kontekstu asynchronicznego")

return self

async def __aexit__(self, exc_type, exc_value, traceback):

await asyncio.sleep(1)  # Symulacja asynchronicznej operacji

print("Wyjście z kontekstu asynchronicznego")

async def main():

async with AsyncContextManager() as manager:

print("Wewnątrz kontekstu asynchronicznego")

asyncio.run(main())

Asynchroniczne menedżery kontekstu są szczególnie przydatne przy pracy z asynchronicznymi zasobami, takimi jak połączenia do baz danych czy sesje HTTP:

Copyimport aiohttp

import asyncio

async def fetch_data(url):

async with aiohttp.ClientSession() as session:

async with session.get(url) as response:

return await response.text()

asyncio.run(fetch_data('http://example.com'))

Konteksty wykonania w wielowątkowości

Menedżery kontekstu są przydatne w programowaniu wielowątkowym do zarządzania współdzielonymi zasobami i synchronizacją:

1. Blokady (Locks):

Copyimport threading

lock = threading.Lock()

def worker():

with lock:

# Kod chroniony blokadą

pass

threads = [threading.Thread(target=worker) for _ in range(5)]

for thread in threads:

thread.start()

2. Semafory:

Copyimport threading

semaphore = threading.Semaphore(2)  # Maksymalnie 2 wątki jednocześnie

def worker():

with semaphore:

# Kod ograniczony do wykonywania przez maksymalnie 2 wątki

pass

3. Lokalne dane wątku:

Copyimport threading

thread_local = threading.local()

def set_context():

thread_local.value = threading.current_thread().name

def worker():

with threading.local() as local:

set_context()

print(f"Wątek {local.value} wykonuje pracę")

threads = [threading.Thread(target=worker) for _ in range(3)]

for thread in threads:

thread.start()

Zastosowania menedżerów kontekstu w testowaniu

Menedżery kontekstu są niezwykle przydatne w testowaniu, szczególnie do:

1. Mockowania:

Copyfrom unittest.mock import patch

def test_function():

with patch('module.function') as mock_function:

mock_function.return_value = 'mocked value'

# Wykonaj test z podmienioną funkcją

2. Tymczasowych zmian środowiska:

Copyimport os

class EnvironmentVariable:

def __init__(self, key, value):

self.key = key

self.value = value

self.original = None

def __enter__(self):

self.original = os.environ.get(self.key)

os.environ[self.key] = self.value

def __exit__(self, exc_type, exc_value, traceback):

if self.original is None:

del os.environ[self.key]

else:

os.environ[self.key] = self.original

def test_environment_dependent():

with EnvironmentVariable('TEST_VAR', 'test_value'):

# Wykonaj test z ustawioną zmienną środowiskową

3. Zarządzanie zasobami testowymi:

Copyimport tempfile

import os

class TempDirectory:

def __enter__(self):

self.temp_dir = tempfile.mkdtemp()

return self.temp_dir

def __exit__(self, exc_type, exc_value, traceback):

os.rmdir(self.temp_dir)

def test_file_operations():

with TempDirectory() as temp_dir:

# Wykonaj testy operacji na plikach w tymczasowym katalogu

Menedżery kontekstu w testowaniu pomagają w izolowaniu testów, zarządzaniu zasobami testowymi i symulowaniu różnych warunków środowiskowych, co prowadzi do bardziej niezawodnych i powtarzalnych testów.

1.4. Zaawansowane aspekty dziedziczenia

Mieszaniny (mixins) to klasy zaprojektowane do rozszerzania funkcjonalności innych klas poprzez wielodziedziczenie. Nie są przeznaczone do samodzielnego użytku, ale do łączenia z innymi klasami.

Cechy mieszanin:

1. Implementują konkretne metody lub zestaw metod.

2. Nie mają własnego stanu (zazwyczaj nie mają **init**).

3. Służą do dzielenia funkcjonalności między niezwiązanymi klasami.

Przykład:

Copyclass JSONSerializableMixin:

def to_json(self):

import json

return json.dumps(self.__dict__)

class User(JSONSerializableMixin):

def __init__(self, name, email):

self.name = name

self.email = email

user = User("Jan", "jan@example.com")

print(user.to_json())  # {"name": "Jan", "email": "jan@example.com"}

Kompozycja vs dziedziczenie

Kompozycja i dziedziczenie to dwa różne podejścia do projektowania relacji między klasami.

Dziedziczenie:

* Tworzy relację "jest" (is-a).

* Pozwala na ponowne użycie kodu.

* Może prowadzić do sztywnych hierarchii.

Kompozycja:

* Tworzy relację "ma" (has-a).

* Oferuje większą elastyczność.

* Ułatwia zmianę zachowania w czasie wykonania.

Przykład kompozycji:

Copyclass Engine:

def start(self):

return "Engine started"

class Car:

def __init__(self):

self.engine = Engine()

def start(self):

return self.engine.start()

car = Car()

print(car.start())  # Engine started

Zaleca się preferowanie kompozycji nad dziedziczeniem, gdy to możliwe ("Composition over inheritance").

Dziedziczenie wielokrotne a wzorce projektowe

Dziedziczenie wielokrotne w Pythonie umożliwia implementację kilku ważnych wzorców projektowych:

1. Adapter:

Copyclass OldSystem:

def old_method(self):

return "Old method"

class NewInterface:

def new_method(self):

pass

class Adapter(OldSystem, NewInterface):

def new_method(self):

return self.old_method()

2. Dekorator:

Copyclass Component:

def operation(self):

return "Basic operation"

class Decorator(Component):

def __init__(self, component):

self.component = component

def operation(self):

return f"Decorated {self.component.operation()}"

# Użycie

basic = Component()

decorated = Decorator(basic)

print(decorated.operation())  # Decorated Basic operation

3. Strategia:

Copyclass DefaultStrategy:

def execute(self):

return "Default strategy"

class AlternativeStrategy:

def execute(self):

return "Alternative strategy"

class Context(DefaultStrategy, AlternativeStrategy):

def __init__(self, strategy=None):

self.strategy = strategy or self.execute

def execute_strategy(self):

return self.strategy()

# Użycie

context = Context(AlternativeStrategy().execute)

print(context.execute_strategy())  # Alternative strategy

4. Template Method:

Copyclass AbstractClass:

def template_method(self):

self.step1()

self.step2()

def step1(self):

pass

def step2(self):

pass

class ConcreteClass(AbstractClass):

def step1(self):

print("Implementacja kroku 1")

def step2(self):

print("Implementacja kroku 2")

Dziedziczenie wielokrotne w Pythonie pozwala na elastyczne implementacje tych wzorców, ale wymaga ostrożności, aby uniknąć konfliktów nazw i zapewnić czytelność kodu.

Metody klasowe i statyczne są dziedziczone w podobny sposób jak zwykłe metody instancji, ale z pewnymi różnicami:

Metody klasowe:

* Dziedziczone przez klasy pochodne.

* Pierwszy argument (cls) odnosi się do klasy, która wywołuje metodę, a nie do klasy, w której metoda jest zdefiniowana.

Copyclass Bazowa:

@classmethod

def metoda_klasowa(cls):

print(f"Metoda klasowa wywołana dla {cls.__name__}")

class Pochodna(Bazowa):

pass

Pochodna.metoda_klasowa()  # Wypisze: Metoda klasowa wywołana dla Pochodna

Metody statyczne:

* Również dziedziczone.

* Zachowują się tak samo w klasie bazowej i pochodnej, ponieważ nie mają dostępu do stanu klasy ani instancji.

Copyclass Bazowa:

@staticmethod

def metoda_statyczna():

print("Metoda statyczna")

class Pochodna(Bazowa):

pass

Pochodna.metoda_statyczna()  # Wypisze: Metoda statyczna

Abstrakcyjne klasy bazowe (abc module)

Moduł abc (Abstract Base Classes) w Pythonie umożliwia tworzenie abstrakcyjnych klas bazowych, które definiują interfejs dla klas pochodnych.

Główne cechy:

* Nie można tworzyć instancji abstrakcyjnej klasy bazowej.

* Klasy pochodne muszą implementować wszystkie abstrakcyjne metody.

Przykład:

Copyfrom abc import ABC, abstractmethod

class Ksztalt(ABC):

@abstractmethod

def oblicz_pole(self):

pass

class Kwadrat(Ksztalt):

def __init__(self, bok):

self.bok = bok

def oblicz_pole(self):

return self.bok ** 2

# Kwadrat().oblicz_pole()  # OK

# Ksztalt()  # Błąd: nie można utworzyć instancji klasy abstrakcyjnej

Interfejsy w Pythonie

Python nie ma wbudowanego konceptu interfejsów jak w niektórych innych językach, ale można je symulować za pomocą abstrakcyjnych klas bazowych:

Copyfrom abc import ABC, abstractmethod

class InterfejsBazy(ABC):

@abstractmethod

def polacz(self):

pass

@abstractmethod

def rozlacz(self):

pass

class BazaMySQL(InterfejsBazy):

def polacz(self):

print("Połączenie z MySQL")

def rozlacz(self):

print("Rozłączenie z MySQL")

class BazaPostgreSQL(InterfejsBazy):

def polacz(self):

print("Połączenie z PostgreSQL")

def rozlacz(self):

print("Rozłączenie z PostgreSQL")

Protokoły (Python 3.8+): Python 3.8 wprowadził koncepcję protokołów, które są bliższe tradycyjnym interfejsom:

Copyfrom typing import Protocol

class Drawable(Protocol):

def draw(self) -> None:

...

class Circle:

def draw(self) -> None:

print("Rysowanie koła")

def draw_shape(shape: Drawable):

shape.draw()

circle = Circle()

draw_shape(circle)  # OK, Circle implementuje protokół Drawable

Protokoły zapewniają statyczne sprawdzanie typów bez konieczności jawnego dziedziczenia.

Abstrakcyjne klasy bazowe i protokoły w Pythonie umożliwiają definiowanie kontraktów, które muszą być spełnione przez klasy implementujące, co prowadzi do bardziej przewidywalnego i łatwiejszego w utrzymaniu kodu.

W Pythonie konwencje nazewnicze definiują atrybuty prywatne i chronione:

1. Atrybuty prywatne (z przedrostkiem \_\_):

* Nie są bezpośrednio dostępne w klasach pochodnych.

* Python stosuje name mangling, zmieniając nazwę na \_NazwaKlasy\_\_atrybut.

Copyclass Bazowa:

def __init__(self):

self.__prywatny = "Prywatny"

self._chroniony = "Chroniony"

class Pochodna(Bazowa):

def __init__(self):

super().__init__()

print(self._chroniony)  # Dostęp do chronionego - OK

# print(self.__prywatny)  # Błąd - AttributeError

print(self._Bazowa__prywatny)  # Dostęp do prywatnego przez name mangling - OK

2. Atrybuty chronione (z przedrostkiem \_):

* Są dziedziczone i dostępne w klasach pochodnych.

* Konwencja sugeruje, że nie powinny być używane poza klasą i jej potomkami.

Dynamiczne dziedziczenie (zmiana **bases**)

Python pozwala na dynamiczną zmianę klas bazowych w czasie wykonania poprzez modyfikację atrybutu **bases**:

Copyclass A:

def metoda(self):

return "A"

class B:

def metoda(self):

return "B"

class C(A):

pass

print(C().metoda())  # Wypisze: A

# Zmiana klasy bazowej

C.__bases__ = (B,)

print(C().metoda())  # Wypisze: B

Należy używać tej techniki ostrożnie, ponieważ może prowadzić do trudnych do wykrycia błędów i nieoczekiwanych zachowań.

Rozwiązywanie konfliktów nazw w wielodziedziczeniu

Gdy klasa dziedziczy po wielu klasach bazowych, mogą pojawić się konflikty nazw. Python rozwiązuje je zgodnie z Method Resolution Order (MRO):

1. Kolejność dziedziczenia ma znaczenie:

Copyclass A:

def metoda(self):

return "A"

class B:

def metoda(self):

return "B"

class C(A, B):

pass

class D(B, A):

pass

print(C().metoda())  # Wypisze: A

print(D().metoda())  # Wypisze: B

2. Użycie super() do jawnego wywołania metod z klas bazowych:

Copyclass A:

def metoda(self):

return "A"

class B:

def metoda(self):

return "B"

class C(A, B):

def metoda(self):

return super().metoda() + " i C"

print(C().metoda())  # Wypisze: A i C

3. Jawne wywoływanie metod z konkretnych klas bazowych:

Copyclass A:

def metoda(self):

return "A"

class B:

def metoda(self):

return "B"

class C(A, B):

def metoda(self):

return A.metoda(self) + " i " + B.metoda(self)

print(C().metoda())  # Wypisze: A i B

4. Używanie mixin-ów do rozdzielenia funkcjonalności:

Copyclass LoggingMixin:

def log(self, message):

print(f"Log: {message}")

class A:

def metoda(self):

return "A"

class B(LoggingMixin, A):

def metoda(self):

result = super().metoda()

self.log(result)

return result

B().metoda()  # Wypisze: Log: A

Przy rozwiązywaniu konfliktów nazw kluczowe jest zrozumienie MRO i świadome projektowanie hierarchii klas. W złożonych przypadkach warto rozważyć użycie kompozycji zamiast wielodziedziczenia.

1.5. Protokoły i abstrakcyjne klasy bazowe

Moduł collections.abc (Abstract Base Classes) zawiera zestaw abstrakcyjnych klas bazowych, które definiują interfejsy dla różnych typów kolekcji i iteratorów w Pythonie. Główne klasy w tym module to:

1. Iterable: dla obiektów, które można iterować.

2. Container: dla obiektów, które obsługują operator in.

3. Sized: dla obiektów z metodą **len**.

4. Sequence: dla uporządkowanych, indeksowalnych kolekcji.

5. MutableSequence: dla modyfikowalnych sekwencji.

6. Mapping: dla obiektów typu słownikowego.

7. MutableMapping: dla modyfikowalnych mapowań.

8. Set: dla zbiorów.

9. MutableSet: dla modyfikowalnych zbiorów.

Przykład użycia:

Copyfrom collections.abc import Sequence

class CustomList(Sequence):

def __init__(self, data):

self._data = data

def __len__(self):

return len(self._data)

def __getitem__(self, index):

return self._data[index]

custom_list = CustomList([1, 2, 3])

print(isinstance(custom_list, Sequence))  # True

Tworzenie własnych abstrakcyjnych klas bazowych

Aby stworzyć własną abstrakcyjną klasę bazową, należy:

1. Zaimportować moduł abc.

2. Dziedziczyć po abc.ABC.

3. Użyć dekoratora @abstractmethod dla metod abstrakcyjnych.

Przykład:

Copyfrom abc import ABC, abstractmethod

class AbstractVehicle(ABC):

@abstractmethod

def start_engine(self):

pass

@abstractmethod

def stop_engine(self):

pass

def honk(self):

print("Beep beep!")

class Car(AbstractVehicle):

def start_engine(self):

print("Car engine started")

def stop_engine(self):

print("Car engine stopped")

# car = Car()  # OK

# vehicle = AbstractVehicle()  # Błąd: nie można utworzyć instancji klasy abstrakcyjnej

Dekorator @abstractmethod

Dekorator @abstractmethod służy do oznaczania metod jako abstrakcyjnych w klasach abstrakcyjnych. Główne cechy:

1. Metoda oznaczona jako @abstractmethod musi być zaimplementowana w klasach pochodnych.

2. Klasa zawierająca co najmniej jedną metodę abstrakcyjną staje się klasą abstrakcyjną.

3. Nie można tworzyć instancji klas abstrakcyjnych.

4. Klasy pochodne muszą implementować wszystkie metody abstrakcyjne, aby można było tworzyć ich instancje.

Przykład użycia:

Copyfrom abc import ABC, abstractmethod

class Shape(ABC):

@abstractmethod

def area(self):

pass

@abstractmethod

def perimeter(self):

pass

class Rectangle(Shape):

def __init__(self, width, height):

self.width = width

self.height = height

def area(self):

return self.width * self.height

def perimeter(self):

return 2 * (self.width + self.height)

# shape = Shape()  # Błąd: nie można utworzyć instancji klasy abstrakcyjnej

rect = Rectangle(5, 3)

print(rect.area())  # 15

print(rect.perimeter())  # 16

Abstrakcyjne klasy bazowe i dekorator @abstractmethod są narzędziami do definiowania interfejsów i wymuszania ich implementacji w klasach pochodnych. Pomagają w tworzeniu spójnego i przewidywalnego API, ułatwiając tym samym projektowanie i utrzymanie kodu.

Protokoły strukturalne, wprowadzone w PEP 544, to sposób definiowania interfejsów w Pythonie, który opiera się na strukturze obiektu, a nie na jawnym dziedziczeniu. Główne cechy:

1. Definicja poprzez klasę Protocol z modułu typing.

2. Obiekty są zgodne z protokołem, jeśli mają wymagane metody i atrybuty.

3. Brak konieczności jawnej deklaracji implementacji.

Przykład:

Copyfrom typing import Protocol

class Printable(Protocol):

def print(self) -> None:

...

class Document:

def print(self) -> None:

print("Drukuję dokument")

def print_object(obj: Printable) -> None:

obj.print()

doc = Document()

print_object(doc)  # Działa, mimo że Document nie dziedziczy po Printable

Moduł typing i protokoły w typowaniu statycznym

Moduł typing w Pythonie dostarcza narzędzi do statycznego typowania, w tym wsparcie dla protokołów:

1. Protocol: klasa bazowa dla definiowania protokołów.

2. runtime\_checkable: dekorator umożliwiający sprawdzanie zgodności z protokołem w czasie wykonania.

Przykład:

Copyfrom typing import Protocol, runtime_checkable

@runtime_checkable

class Sized(Protocol):

def __len__(self) -> int:

...

class CustomContainer:

def __len__(self) -> int:

return 42

container = CustomContainer()

print(isinstance(container, Sized))  # True

Protokoły w typowaniu statycznym pozwalają na:

1. Definiowanie interfejsów bez konieczności dziedziczenia.

2. Sprawdzanie typów w czasie kompilacji przez narzędzia takie jak mypy.

3. Opcjonalne sprawdzanie w czasie wykonania przy użyciu @runtime\_checkable.

Różnice między protokołami a abstrakcyjnymi klasami bazowymi

1. Definicja:

* Protokoły: definiowane jako klasy dziedziczące po typing.Protocol.

* ABC: definiowane jako klasy dziedziczące po abc.ABC.

2. Implementacja:

* Protokoły: implicitna, bazująca na strukturze obiektu.

* ABC: explicytna, wymaga jawnego dziedziczenia.

3. Sprawdzanie zgodności:

* Protokoły: głównie w czasie kompilacji, opcjonalnie w czasie wykonania.

* ABC: w czasie wykonania.

4. Elastyczność:

* Protokoły: bardziej elastyczne, umożliwiają częściową implementację.

* ABC: mniej elastyczne, wymagają pełnej implementacji metod abstrakcyjnych.

5. Użycie:

* Protokoły: idealne do definiowania interfejsów dla typowania statycznego.

* ABC: dobre do wymuszania implementacji konkretnych metod i tworzenia szkieletów klas.

Przykład różnicy:

Copyfrom typing import Protocol

from abc import ABC, abstractmethod

class PrintableProtocol(Protocol):

def print(self) -> None:

...

class PrintableABC(ABC):

@abstractmethod

def print(self) -> None:

pass

# Dla protokołu

class DocumentP:

def print(self) -> None:

print("Drukuję dokument")

# Dla ABC

class DocumentA(PrintableABC):

def print(self) -> None:

print("Drukuję dokument")

# Użycie

doc_p = DocumentP()  # OK

doc_a = DocumentA()  # OK

print(isinstance(doc_p, PrintableProtocol))  # True, jeśli użyto @runtime_checkable

print(isinstance(doc_a, PrintableABC))  # True

Protokoły oferują większą elastyczność i są bardziej zgodne z filozofią duck typing Pythona, podczas gdy abstrakcyjne klasy bazowe zapewniają silniejsze gwarancje dotyczące implementacji i są bardziej odpowiednie do definiowania szkieletów klas.

Protokoły można implementować dla istniejących klas bez modyfikacji ich kodu źródłowego. Można to osiągnąć poprzez:

1. Rejestrację klasy jako implementującej protokół:

Copyfrom typing import Protocol, runtime_checkable

@runtime_checkable

class Printable(Protocol):

def print(self) -> None:

...

class ExistingClass:

def print(self) -> None:

print("Drukuję")

# Rejestracja

Printable.register(ExistingClass)

obj = ExistingClass()

print(isinstance(obj, Printable))  # True

2. Użycie adaptera:

Copyclass LegacyPrinter:

def do_print(self):

print("Drukuję starą metodą")

class PrinterAdapter(Printable):

def __init__(self, legacy_printer):

self.legacy_printer = legacy_printer

def print(self) -> None:

self.legacy_printer.do_print()

legacy = LegacyPrinter()

adapter = PrinterAdapter(legacy)

Zastosowanie protokołów i abstrakcyjnych klas bazowych w projektowaniu API

1. Definiowanie kontraktów:

* Protokoły: dla luźno powiązanych interfejsów.

* ABC: dla ścisłych kontraktów z wymuszeniem implementacji.

Copyfrom typing import Protocol

from abc import ABC, abstractmethod

class DataSource(Protocol):

def get_data(self) -> str:

...

class DataProcessor(ABC):

@abstractmethod

def process(self, data: str) -> str:

pass

def process_data(source: DataSource, processor: DataProcessor) -> str:

data = source.get_data()

return processor.process(data)

2. Tworzenie rozszerzalnych systemów:

Copyclass Plugin(Protocol):

def execute(self) -> None:

...

class PluginManager:

def __init__(self):

self.plugins: list[Plugin] = []

def add_plugin(self, plugin: Plugin) -> None:

self.plugins.append(plugin)

def execute_all(self) -> None:

for plugin in self.plugins:

plugin.execute()

3. Definiowanie interfejsów dla bibliotek:

Copyclass Serializable(Protocol):

def to_json(self) -> str:

...

def from_json(self, json_str: str) -> None:

...

def save_to_file(obj: Serializable, filename: str) -> None:

with open(filename, 'w') as f:

f.write(obj.to_json())

Wpływ protokołów na wydajność i elastyczność kodu

Wydajność:

1. Protokoły nie wprowadzają dodatkowego narzutu w czasie wykonania, w przeciwieństwie do sprawdzania dziedziczenia ABC.

2. Statyczne sprawdzanie typów z użyciem protokołów może pomóc w wykryciu błędów na etapie kompilacji, co poprawia ogólną wydajność i niezawodność kodu.

Copyfrom typing import Protocol

class Drawable(Protocol):

def draw(self) -> None:

...

def render(objects: list[Drawable]) -> None:

for obj in objects:

obj.draw()  # Brak sprawdzania w runtime

Elastyczność:

1. Protokoły umożliwiają tworzenie bardziej modularnego kodu, ułatwiając wymianę komponentów.

2. Pozwalają na częściową implementację interfejsu, co jest przydatne w przypadku mock objects w testach.

Copyclass MockDrawable:

def draw(self) -> None:

pass  # Minimalna implementacja dla testów

def test_render():

mock = MockDrawable()

render([mock])  # Działa bez pełnej implementacji

3. Ułatwiają adaptację istniejącego kodu do nowych interfejsów bez konieczności modyfikacji oryginalnych klas.

Copyclass LegacyComponent:

def old_method(self):

print("Stara metoda")

class ComponentAdapter(Protocol):

def new_method(self):

...

def adapt(component: LegacyComponent) -> ComponentAdapter:

class Adapter:

def new_method(self):

component.old_method()

return Adapter()

adapted = adapt(LegacyComponent())

adapted.new_method()  # Wywołuje "Stara metoda"

Protokoły i abstrakcyjne klasy bazowe oferują różne podejścia do projektowania API, z protokołami faworyzującymi elastyczność i zgodność z duck typing, a ABC zapewniającymi silniejsze gwarancje implementacji. Wybór między nimi zależy od konkretnych wymagań projektu i preferowanego stylu programowania.

2. Metaprogramowanie i magia Pythona

Table of Contents

1. Zaawansowane aspekty składni Pythona

1.1. Dekoratory i metaklasy

1.2. Generatory i korutyny

1.3. Konteksty wykonania i menedżery kontekstu

1.4. Zaawansowane aspekty dziedziczenia

1.5. Protokoły i abstrakcyjne klasy bazowe

2. Metaprogramowanie i magia Pythona