SZCZEGÓ Ł OWY SPIS TRE Ś
CI
PODZIĘKOWANIA
WPROWADZENIE
Kto i dlaczego powinien przeczytać tę książkę
O tej książce
ROZPOCZYNANIE PROJEKTU
Wersje Pythona
Układ projektu
Czego nie robić
Numerowanie wersji
Style programowania i automatyczne sprawdzanie
Narzędzia
Niestandardowe importery
Obiekty wyszukujące z listy
Przydatne standardowe biblioteki
Zewnętrzne biblioteki
Lista wymagań, jakie powinna spełniać bezpieczna biblioteka zewnętrzna
Chronienie kodu przy użyciu opakowania API
Instalacja pakietu: dodatkowe możliwości narzędzia pip
Stosowanie i wybieranie platform
Doug Hellmann, Python Core Developer, o bibliotekach Pythona
1
Rozpoczęcie pracy ze Sphinksem i reST
Pisanie rozszerzeń Sphinksa
ądzanie zmianami interfejsów API
Numerowanie wersji API
Dokumentowanie zmian
Oznaczanie przestarzałych funkcji przy użyciu modułu warnings
Podsumowanie
Christophe de Vienne o rozwijaniu interfejsów API
4
OBSŁUGA ZNACZNIKÓW CZASU I STREF CZASOWYCH
Problem brakujących stref czasowych
Budowanie domyślnych obiektów datetime
Rozpoznawanie stref czasowych przy użyciu dateutil
Serializowanie obiektów datetime rozpoznających strefę czasową
Rozwiązywanie problemu niejednoznacznych godzin
Podsumowanie
5
Krótka historia plików setup.py
Zarządzanie pakietami przy użyciu setup.cfg
Standardowy format dystrybucji wheel
Dzielenie się swoją pracą ze światem
Punkty wejścia
punktów wejścia
skryptów konsoli
Wykorzystywanie wtyczek i sterowników
Nick Coghlan o zarządzaniu pakietami
Podstawy testowania
Pomijanie testów
Uruchamianie wybranych testów
Równoległe uruchamianie testów
83
85
Tworzenie obiektów wykorzystywanych w testach przy użyciu fikstur . . 86
Uruchamianie scenariuszy testowych
Kontrolowane testy bazujące na atrapach obiektów
Odkrywanie nieprzetestowanego kodu przy użyciu coverage .
Środowiska wirtualne . . . . .
Konfigurowanie środowiska wirtualnego
Stosowanie virtualenv wraz z narzędziem tox
Odtwarzanie środowiska
87
88
93
95
96
98
Stosowanie różnych wersji Pythona
Integrowanie innych testów
Zasady dotyczące testowania
Robert Collins o testowaniu
7
METODY I DEKORATORY
Dekoratory i kiedy ich używać
Tworzenie dekoratorów
Pisanie dekoratorów
Stosowanie wielu dekoratorów
Pisanie dekoratorów klas
W jaki sposób działają metody w Pythonie
Metody statyczne
Metody klasy
Metody abstrakcyjne
Mieszanie metod statycznych, klasy i abstrakcyjnych
Umieszczanie implementacji w metodach abstrakcyjnych
Prawda o metodzie super
Podsumowanie
8
PROGRAMOWANIE FUNKCYJNE
Tworzenie czystych funkcji
Tworzenie generatora
Zwracanie i przekazywanie wartości przy użyciu instrukcji yield
Inspekcje generatorów
Wyrażenia listowe
Funkcjonowanie funkcji funkcyjnych
Stosowanie funkcji na elementach przy użyciu funkcji map()
Filtrowanie list przy użyciu funkcji filter()
Pobieranie indeksów przy użyciu funkcji enumerate()
Sortowanie listy przy użyciu funkcji sorted()
Wyszukiwanie przy użyciu any() i all() elementów spe
niających warunki
Łączenie list przy użyciu funkcji zip()
Rozwiązania typowych problemów
Przydatne funkcje itertools
Pisanie
Przechodzenie
Rozszerzanie narzędzia flake8 o analizy AST
Pisanie klasy
Pomijanie nieistotnego kodu
Sprawdzanie, czy istnieje odpowiedni dekorator
Poszukiwanie argumentu self
Szybkie wprowadzenie do Hy
Podsumowanie
Paul Tagliamonte o AST i Hy
10
WYDAJNO
ŚĆ I OPTYMALIZACJE
Struktury danych
Zrozumienie działania programu przez profilowanie
Dezasemblowanie przy użyciu modułu dis
Efektywne definiowanie funkcji
Listy uporządkowane i bisect
namedtuple i __slots__
Memoizacja .
Szybszy Python dzięki PyPy
Eliminacja kopiowania przy użyciu protokołu bufora
Podsumowanie
Victor Stinner o optymalizacji
SKALOWALNOŚĆ I ARCHITEKTURA
Wielowątkowość w Pythonie i jej ograniczenia
Przetwarzanie wieloprocesowe kontra wielowątkowe
Architektura sterowana zdarzeniami
Inne opcje i asyncio
Architektura zorientowana na usługi
Komunikacja między procesami przy użyciu ZeroMQ
Podsumowanie
ZARZĄDZANIE
Rozwiązania RDBMS, ORM i kiedy ich używa
Wewnętrzne bazy danych
Strumieniowe przesyłanie danych przy użyciu Flask i PostgreSQL
Pisanie aplikacji do strumieniowego przesyłania danych
Budowanie aplikacji
Dimitri Fontaine on Databases
Użycie modułu six do wspierania wersji Python 2 i 3
Ciągi a unicode
Rozwiązywanie problemu przeniesienia modułów Pythona
Moduł modernize
Wykorzystywanie Pythona jak języka Lisp do tworzenia jednego dyspozytora
Tworzenie metod generycznych w języku Lisp
Metody generyczne w Pythonie
Menedżery kontekstu
Redukowanie szablonowego kodu przy użyciu attr
Podsumowanie
INDEKS

pe nego potencja u programowania funkcyjnego, gdy ma si jedynie do wiadczenie w imperatywnym programowaniu obiektowym. Lisp nie jest czysto funkcyjny, ale k adzie wi kszy nacisk na programowanie funkcyjne ni j zyk Python.
Tworzenie czystych funkcji
Gdy piszemy kod w stylu funkcyjnym, nasze funkcje s zaprojektowane tak, aby nie mia y adnych efektów ubocznych. Powinny jedynie pobiera dane wej ciowe i zwraca wynik bez zachowywania stanu b d mody kowania czegokolwiek, co nie jest odzwierciedlone w warto ci zwrotnej. Funkcje zgodne z t koncepcj nazywane s czysto funkcyjnymi. Zacznijmy od przyk adu zwyk ej funkcji, która nie jest czysto funkcyjna i usuwa ostatni element z listy:
def remove_last_item(mylist): """Usuwa ostatni element z listy.""" mylist.pop(-1) # Modyfikuje mylist
A oto czysta wersja tej samej funkcji:
def butlast(mylist):
return mylist[:-1] # Zwraca kopi mylist
De niujemy funkcj butlast() w taki sposób, aby dzia a a ona jak butlast w Lispie, zwracaj c list bez ostatniego elementu, nie mody kuj c oryginalnej listy. Zamiast tego zwraca kopi listy, w której wprowadzone zosta y zmiany, umo liwiaj c zachowanie orygina u.
Praktyczne zalety programowania funkcyjnego to m.in.:
Modu owo Funkcyjny styl pisania kodu zmusza do wprowadzenia pewnego rozdzia u podczas rozwi zywania poszczególnych problemów i u atwia ponowne wykorzystywanie fragmentów kodu w innym kontek cie. Poniewa funkcja nie zale y od adnej zewn trznej zmiennej ani stanu, wywo anie jej z innego miejsca w kodzie jest bardzo proste. Zwi z o Programowanie funkcyjne jest zwykle mniej rozwlek e ni inne paradygmaty programowania.
Wspó bie no Czysto funkcyjne funkcje s bezpieczne w tkowo i mog by uruchamiane równolegle. Niektóre j zyki funkcyjne robi to w sposób automatyczny, co mo e by bardzo pomocne w przysz o ci, gdy pojawia si potrzeba skalowania aplikacji. Python nie oferuje jeszcze takiej mo liwo ci.
Testowalno Testowanie programu funkcyjnego jest niesamowicie proste: wystarczy zestaw danych wej ciowych i zestaw oczekiwanych
wyników. Funkcje s idempotentne, co oznacza, e ka de z wielu wywo a tej samej funkcji z tymi samymi argumentami zwróci zawsze ten sam wynik.
Generatory
Generator jest obiektem, który zachowuje si jak iterator pod tym wzgldem, e generuje i zwraca warto w ka dym wywo aniu metody next(), do momentu zg oszenia wyj tku StopIteration. Generatory, wprowadzone po raz pierwszy w PEP 255, oferuj prosty sposób tworzenia obiektów, które implementuj protokó iteratora. Cho zasadniczo generatorów nie trzeba implementowa w stylu funkcyjnym, u atwia to ich pisanie oraz debugowanie i jest powszechn praktyk .
Aby utworzy generator, wystarczy napisa zwyk funkcj Pythona, która zawiera instrukcj yield . Python wykryje u ycie instrukcji yield i oznaczy funkcj jako generator. Gdy nadchodzi czas wykonania instrukcji yield , funkcja zwraca warto jak w przypadku u ycia instrukcji return , z jedn istotn ró nic : interpreter zapisze odwo anie do stosu, które zostanie wykorzystane do wznowienia wykonania funkcji, gdy funkcja next() zostanie ponownie wywo ana.
Gdy funkcje s wykonywane, a cuch ich wykona tworzy stos – mówi si , e wywo ania funkcji s umieszczane na stosie. Gdy funkcja zwraca warto , zostaje usuni ta ze stosu i zwracana przez ni warto zostaje przekazana do funkcji wywo uj cej. W przypadku generatora funkcja tak naprawd nie zwraca warto ci, a jedynie j przekazuje (yield). Dlatego Python zapisuje stan funkcji w postaci odwo ania do stosu, wznawiaj c wykonanie generatora od zapisanego miejsca, gdy potrzebna jest nast pna iteracja generatora.
Tworzenie generatora
Jak wspomnieli my, generator tworzymy, pisz c normaln funkcj i umieszczaj c w niej instrukcj yield. Listing 8.1 s u y do utworzenia generatora o nazwie mygenerator(), który zawiera trzy instrukcje yield, co oznacza, e dokona iteracji podczas trzech kolejnych wywo a funkcji next()
>> def mygenerator():
... yield 1
... yield 2
yield 'a' ...
>>> mygenerator()
<generator object mygenerator at 0x10d77fa50>
>>> g = mygenerator()
>>> next(g)
1
wolniej lub szybciej. Zamiast spekulować, pomożemy zrozumieć, jak profilować aplikację, aby wiedzieć, która część programu spowalnia jego działanie i gdzie znajdują się wąskie gardła.
Struktury danych
Większość problemów programistycznych można rozwiązać w elegancki i prosty sposób przy użyciu odpowiednich struktur danych. Python oferuje wiele różnych struktur danych i umiejętność właściwego ich wykorzystywania pozwala na uzyskiwanie bardziej czytelnego i stabilnego rozwiązania niż programowanie własnych struktur danych. Na przykład wszyscy używają dict, ale ile razy widziałeś kod, który próbuje uzyskać dostęp do słownika, przechwytując wyjątek KeyError, jak poniżej:
def get_fruits(basket, fruit): try: return basket[fruit] except KeyError: return None
lub sprawdzając najpierw, czy klucz istnieje:
def get_fruits(basket, fruit): if fruit in basket: return basket[fruit]
Je ś li u ż yjemy metody get() dost ę pnej w klasie dict , to unikamy konieczności przechwytywania wyjątku bądź wstępnego sprawdzania, czy klucz istnieje:
def get_fruits(basket, fruit): return basket.get(fruit)
Metoda dict.get() może również zwracać domyślną wartość zamiast None – wystarczy dodać do jej wywołania drugi argument:
def get_fruits(basket, fruit): # Zwraca fruit lub Banana, jeśli nie znaleziono obiektu fruit. return basket.get(fruit, Banana())
Wielu programistów ma na sumieniu stosowanie podstawowych struktur danych Pythona bez poznania wszystkich oferowanych przez nie metod. Odnosi się to również do zbiorów: dostępne w nich metody mogą rozwiązywać wiele problemów, które w przeciwnym razie wymagałyby napisania zagnieżdżonych bloków for / if . Na przykład programiści często używają
pętli for/if do sprawdzania, czy istnieją elementy nienależące do listy, jak w tym przykładzie:
def has_invalid_fields(fields): for field in fields: if field not in ['foo', 'bar']: return True return False
Pętla przechodzi po wszystkich elementach listy i sprawdza, czy wszystkie z nich są równe foo albo bar. Jednak można osiągnąć ten cel w bardziej efektywny sposób, bez stosowania pętli:
def has_invalid_fields(fields): return bool(set(fields) - set(['foo', 'bar']))
Ten zmieniony kod przekształca fields na zbiór i wyznacza resztę zbioru, odejmując od niego set(['foo', 'bar']). Następnie przekształca zbiór na wartość logiczną, aby sprawdzić, czy pozostały jakieś elementy, które nie są równe ani foo, ani bar. Zastosowanie zbiorów eliminuje konieczność iteracji po liście i sprawdzania pojedynczych elementów. Prosta operacja na dwóch zbiorach, przeprowadzona wewnętrznie przez Pythona, jest szybsza. Python oferuje również bardziej zaawansowane struktury danych, które mog ą znacznie u ł atwia ć utrzymywanie kodu. Przeanalizujmy przyk ł ad przedstawiony na listingu 10.1.
def add_animal_in_family(species, animal, family): if family not in species: species[family] = set() species[family].add(animal)
species = {}
add_animal_in_family(species, 'cat', 'felidea')
Listing 10.1. Dodawanie wpisu do słownika zbiorów
Ten kod jest całkiem poprawny, ale ile razy będziemy potrzebować w naszym programie odmiany kodu z listingu 10.1? Dziesiątki? Setki? Python oferuje strukturę collections.defaultdict, która w elegancki sposób rozwiązuje ten problem:
import collections
def add_animal_in_family(species, animal, family): species[family].add(animal)
species = collections.defaultdict(set) add_animal_in_family(species, 'cat', 'felidea')
Memoizacja
Memoizacja to technika optymalizacji służąca do przyspieszania wywołań funkcji przez przechowywanie ich wyników w pamięci podręcznej. Wynik funkcji mo ż e zosta ć przechowany tylko wtedy, gdy funkcja jest czysta , czyli nie ma żadnych skutków ubocznych i nie zależy od żadnego globalnego stanu (dodatkowe informacje o funkcjach czystych znaleźć można w rozdziale 8).
Prosty przykład funkcji, którą można poddać memoizacji, stanowi funkcja sin(), przedstawiona na listingu 10.17.
>>> import math
>>> _SIN_MEMOIZED_VALUES = {}
>>> def memoized_sin(x):
... if x not in _SIN_MEMOIZED_VALUES:
... _SIN_MEMOIZED_VALUES[x] = math.sin(x)
return _SIN_MEMOIZED_VALUES[x]
>>> memoized_sin(1)
0.8414709848078965
>>> _SIN_MEMOIZED_VALUES
{1: 0.8414709848078965}
>>> memoized_sin(2)
0.9092974268256817
>>> memoized_sin(2)
0.9092974268256817
>>> _SIN_MEMOIZED_VALUES
{1: 0.8414709848078965, 2: 0.9092974268256817}
>>> memoized_sin(1)
0.8414709848078965
>>> _SIN_MEMOIZED_VALUES
{1: 0.8414709848078965, 2: 0.9092974268256817}
Listing 10.17. Funkcja sin() poddana memoizacji
Na listingu 10.17, gdy funkcja memoized_sin() jest wywoływana po raz pierwszy z argumentem, który nie znajduje się w słowniku _SIN_MEMOIZED_ VALUES, jej wartość jest wyliczana i przechowywana w tym słowniku. Jeśli ponownie wywołamy funkcję z tą samą wartością, to wynik zostanie pobrany ze słownika, zamiast wyliczony. Wartość funkcji sin() jest wyliczana bardzo szybko, jednak niektóre zaawansowane funkcje wymagające przeprowadzenia skomplikowanych obliczeń są bardziej czasochłonne i to w takiej sytuacji memoizacja przynosi najlepsze efekty.
Jeśli czytałeś już o dekoratorach (w przeciwnym razie zajrzyj do podrozdziału „Dekoratory i kiedy ich używać” na stronie 108), to możesz zauważyć, że jest to doskonała okazja do ich zastosowania. PyPI zawiera kilka implementacji memoizacji bazujących na dekoratorach, od bardzo prostych przypadków po bardzo złożone i kompleksowe.
Począwszy od wersji Python 3.3, moduł functools zawiera dekorator pamięci podręcznej LRU (least recently used, najdawniej użyty). Oferuje on tak ą sam ą funkcjonalno ść jak memoizacja, a dodatkowo ogranicza liczbę pozycji w pamięci podręcznej, usuwając najdawniej użyty element, gdy pamięć podręczna osiągnęła maksymalny rozmiar. Ten moduł oferuje również dane statystyczne m.in. liczby trafień i chybień w pamięci podręcznej (czy wartość była w niej dostępna, czy też nie). Moim zdaniem te dane statystyczne są niezbędne do implementowania tego typu pamięci podręcznej. Siła memoizacji, jak i każdej innej techniki przechowywania w pamięci podręcznej, leży w możliwości mierzenia jej użycia i użyteczności.
Listing 10.18 demonstruje, jak można wykorzystać metodę functools. lru_cache() do zaimplementowania memoizacji funkcji. Po udekorowaniu funkcja uzyskuje metodę cache_info(), którą można wywołać w celu sprawdzenia statystyk użycia pamięci podręcznej.
>>> import functools
>>> import math
>>> @functools.lru_cache(maxsize=2)
... def memoized_sin(x):
... return math.sin(x)
...
>>> memoized_sin(2)
0.9092974268256817
>>> memoized_sin.cache_info()
CacheInfo(hits=0, misses=1, maxsize=2, currsize=1)
>>> memoized_sin(2)
0.9092974268256817
>>> memoized_sin.cache_info()
CacheInfo(hits=1, misses=1, maxsize=2, currsize=1)
>>> memoized_sin(3)
0.1411200080598672
>>> memoized_sin.cache_info()
CacheInfo(hits=1, misses=2, maxsize=2, currsize=2)
>>> memoized_sin(4)
-0.7568024953079282
>>> memoized_sin.cache_info()
CacheInfo(hits=1, misses=3, maxsize=2, currsize=2)
>>> memoized_sin(3)
0.1411200080598672
>>> memoized_sin.cache_info()
CacheInfo(hits=2, misses=3, maxsize=2, currsize=2)
>>> memoized_sin.cache_clear()
>>> memoized_sin.cache_info()
CacheInfo(hits=0, misses=0, maxsize=2, currsize=0)
Listing 10.18. Sprawdzanie statystyk użycia pamięci podręcznej