Przedmowa . ...................................................................................................................11 1. Algorytmy i struktury danych . .................................................................................... 15 1.1. Wypakowywanie sekwencji do odrębnych zmiennych 1.2. Wypakowywanie elementów z obiektów iterowalnych o dowolnej długości 1.3. Zachowywanie ostatnich N elementów 1.4. Wyszukiwanie N największych lub najmniejszych elementów 1.5. Tworzenie kolejki priorytetowej 1.6. Odwzorowywanie kluczy na różne wartości ze słownika 1.7. Określanie uporządkowania w słownikach 1.8. Obliczenia na danych ze słowników 1.9. Wyszukiwanie identycznych danych w dwóch słownikach 1.10. Usuwanie powtórzeń z sekwencji przy zachowaniu kolejności elementów 1.11. Nazywanie wycinków 1.12. Określanie najczęściej występujących w sekwencji elementów 1.13. Sortowanie list słowników według wspólnych kluczy 1.14. Sortowanie obiektów bez wbudowanej obsługi porównań 1.15. Grupowanie rekordów na podstawie wartości pola 1.16. Filtrowanie elementów sekwencji 1.17. Pobieranie podzbioru słownika 1.18. Odwzorowywanie nazw na elementy sekwencji 1.19. Jednoczesne przekształcanie i redukowanie danych 1.20. Łączenie wielu odwzorowań w jedno
2. Łańcuchy znaków i tekst ..............................................................................................47 2.1. Podział łańcuchów znaków po wykryciu dowolnego z różnych ograniczników 2.2. Dopasowywanie tekstu do początkowej lub końcowej części łańcucha znaków 2.3. Dopasowywanie łańcuchów znaków za pomocą symboli wieloznacznych powłoki 2.4. Dopasowywanie i wyszukiwanie wzorców tekstowych
47 48 50 51
3
2.5. Wyszukiwanie i zastępowanie tekstu 2.6. Wyszukiwanie i zastępowanie tekstu bez uwzględniania wielkości liter 2.7. Tworzenie wyrażeń regularnych w celu uzyskania najkrótszego dopasowania 2.8. Tworzenie wyrażeń regularnych dopasowywanych do wielowierszowych wzorców 2.9. Przekształcanie tekstu w formacie Unicode na postać standardową 2.10. Używanie znaków Unicode w wyrażeniach regularnych 2.11. Usuwanie niepożądanych znaków z łańcuchów 2.12. Zapewnianie poprawności i porządkowanie tekstu 2.13. Wyrównywanie łańcuchów znaków 2.14. Łączenie łańcuchów znaków 2.15. Podstawianie wartości za zmienne w łańcuchach znaków 2.16. Formatowanie tekstu w celu uzyskania określonej liczby kolumn 2.17. Obsługiwanie encji HTML-a i XML-a w tekście 2.18. Podział tekstu na tokeny 2.19. Tworzenie prostego rekurencyjnego parsera zstępującego 2.20. Przeprowadzanie operacji tekstowych na łańcuchach bajtów
54 55 56 57 58 60 61 62 64 66 68 70 71 73 75 83
3. Liczby, daty i czas . ........................................................................................................87 3.1. Zaokrąglanie liczb 3.2. Przeprowadzanie dokładnych obliczeń na liczbach dziesiętnych 3.3. Formatowanie liczb w celu ich wyświetlenia 3.4. Stosowanie dwójkowych, ósemkowych i szesnastkowych liczb całkowitych 3.5. Pakowanie do bajtów i wypakowywanie z bajtów dużych liczb całkowitych 3.6. Przeprowadzanie obliczeń na liczbach zespolonych 3.7. Nieskończoność i wartości NaN 3.8. Obliczenia z wykorzystaniem ułamków 3.9. Obliczenia z wykorzystaniem dużych tablic liczbowych 3.10. Przeprowadzanie operacji na macierzach i z zakresu algebry liniowej 3.11. Losowe pobieranie elementów 3.12. Przekształcanie dni na sekundy i inne podstawowe konwersje związane z czasem 3.13. Określanie daty ostatniego piątku 3.14. Określanie przedziału dat odpowiadającego bieżącemu miesiącowi 3.15. Przekształcanie łańcuchów znaków na obiekty typu datetime 3.16. Manipulowanie datami z uwzględnieniem stref czasowych
4. Iteratory i generatory ..................................................................................................113 4.1. Ręczne korzystanie z iteratora 4.2. Delegowanie procesu iterowania 4.3. Tworzenie nowych wzorców iterowania z wykorzystaniem generatorów 4.4. Implementowanie protokołu iteratora 4.5. Iterowanie w odwrotnej kolejności
4
Spis treści
113 114 115 117 119
4.6. Definiowanie funkcji generatorów z dodatkowym stanem 4.7. Pobieranie wycinków danych zwracanych przez iterator 4.8. Pomijanie pierwszej części obiektu iterowalnego 4.9. Iterowanie po wszystkich możliwych kombinacjach lub permutacjach 4.10. Przechodzenie po parach indeks – wartość sekwencji 4.11. Jednoczesne przechodzenie po wielu sekwencjach 4.12. Przechodzenie po elementach z odrębnych kontenerów 4.13. Tworzenie potoków przetwarzania danych 4.14. Przekształcanie zagnieżdżonych sekwencji na postać jednowymiarową 4.15. Przechodzenie po scalonych posortowanych obiektach iterowalnych zgodnie z kolejnością sortowania 4.16. Zastępowanie nieskończonych pętli while iteratorem
120 121 122 124 125 127 129 130 133 134 135
5. Pliki i operacje wejścia-wyjścia . ................................................................................137 5.1. Odczyt i zapis danych tekstowych 5.2. Zapisywanie danych z funkcji print() do pliku 5.3. Stosowanie niestandardowych separatorów lub końca wiersza w funkcji print() 5.4. Odczyt i zapis danych binarnych 5.5. Zapis danych do pliku, który nie istnieje 5.6. Wykonywanie operacji wejścia-wyjścia na łańcuchach 5.7. Odczytywanie i zapisywanie skompresowanych plików z danymi 5.8. Przechodzenie po rekordach o stałej wielkości 5.9. Wczytywanie danych binarnych do zmiennego bufora 5.10. Odwzorowywanie plików binarnych w pamięci 5.11. Manipulowanie ścieżkami 5.12. Sprawdzanie, czy plik istnieje 5.13. Pobieranie listy zawartości katalogu 5.14. Nieuwzględnianie kodowania nazw plików 5.15. Wyświetlanie nieprawidłowych nazw plików 5.16. Dodawanie lub zmienianie kodowania otwartego pliku 5.17. Zapisywanie bajtów w pliku tekstowym 5.18. Umieszczanie deskryptora istniejącego pliku w obiekcie pliku 5.19. Tworzenie tymczasowych plików i katalogów 5.20. Komunikowanie z portami szeregowymi 5.21. Serializowanie obiektów Pythona
6. Kodowanie i przetwarzanie danych . ........................................................................ 167 6.1. Wczytywanie i zapisywanie danych CSV 6.2. Wczytywanie i zapisywanie danych w formacie JSON 6.3. Parsowanie prostych danych w XML-u 6.4. Stopniowe parsowanie bardzo dużych plików XML
167 170 174 176
Spis treści
5
6.5. Przekształcanie słowników na format XML 6.6. Parsowanie, modyfikowanie i ponowne zapisywanie dokumentów XML 6.7. Parsowanie dokumentów XML z przestrzeniami nazw 6.8. Komunikowanie się z relacyjnymi bazami danych 6.9. Dekodowanie i kodowanie cyfr w systemie szesnastkowym 6.10. Dekodowanie i kodowanie wartości w formacie Base64 6.11. Odczyt i zapis tablic binarnych zawierających struktury 6.12. Wczytywanie zagnieżdżonych struktur binarnych o zmiennej długości 6.13. Podsumowywanie danych i obliczanie statystyk
179 181 183 185 187 188 188 192 200
7. Funkcje ........................................................................................................................203 7.1. Pisanie funkcji przyjmujących dowolną liczbę argumentów 7.2. Tworzenie funkcji przyjmujących argumenty podawane wyłącznie za pomocą słów kluczowych 7.3. Dołączanie metadanych z informacjami do argumentów funkcji 7.4. Zwracanie wielu wartości przez funkcje 7.5. Definiowanie funkcji z argumentami domyślnymi 7.6. Definiowanie funkcji anonimowych (wewnątrzwierszowych) 7.7. Pobieranie wartości zmiennych w funkcjach anonimowych 7.8. Uruchamianie n-argumentowej jednostki wywoływalnej z mniejszą liczbą argumentów 7.9. Zastępowanie klas z jedną metodą funkcjami 7.10. Dodatkowy stan w funkcjach wywoływanych zwrotnie 7.11. Wewnątrzwierszowe zapisywanie wywoływanych zwrotnie funkcji 7.12. Dostęp do zmiennych zdefiniowanych w domknięciu
203 204 205 206 207 210 211 212 215 216 219 221
8. Klasy i obiekty .............................................................................................................225 8.1. Modyfikowanie tekstowej reprezentacji obiektów 8.2. Modyfikowanie formatowania łańcuchów znaków 8.3. Dodawanie do obiektów obsługi protokołu zarządzania kontekstem 8.4. Zmniejszanie zużycia pamięci przy tworzeniu dużej liczby obiektów 8.5. Hermetyzowanie nazw w klasie 8.6. Tworzenie atrybutów zarządzanych 8.7. Wywoływanie metod klasy bazowej 8.8. Rozszerzanie właściwości w klasie pochodnej 8.9. Tworzenie nowego rodzaju atrybutów klasy lub egzemplarza 8.10. Stosowanie właściwości obliczanych w leniwy sposób 8.11. Upraszczanie procesu inicjowania struktur danych 8.12. Definiowanie interfejsu lub abstrakcyjnej klasy bazowej 8.13. Tworzenie modelu danych lub systemu typów
8.14. Tworzenie niestandardowych kontenerów 8.15. Delegowanie obsługi dostępu do atrybutów 8.16. Definiowanie więcej niż jednego konstruktora w klasie 8.17. Tworzenie obiektów bez wywoływania metody __init__() 8.18. Rozszerzanie klas za pomocą klas mieszanych 8.19. Implementowanie obiektów ze stanem lub maszyn stanowych 8.20. Wywoływanie metod obiektu na podstawie nazwy w łańcuchu znaków 8.21. Implementowanie wzorca odwiedzający 8.22. Implementowanie wzorca odwiedzający bez stosowania rekurencji 8.23. Zarządzanie pamięcią w cyklicznych strukturach danych 8.24. Tworzenie klas z obsługą porównań 8.25. Tworzenie obiektów zapisywanych w pamięci podręcznej
259 262 266 267 269 273 278 279 283 288 291 293
9. Metaprogramowanie . ................................................................................................297 9.1. Tworzenie nakładek na funkcje 9.2. Zachowywanie metadanych funkcji przy pisaniu dekoratorów 9.3. Pobieranie pierwotnej funkcji z nakładki 9.4. Tworzenie dekoratorów przyjmujących argumenty 9.5. Definiowanie dekoratora z atrybutami dostosowywanymi przez użytkownika 9.6. Definiowanie dekoratorów przyjmujących opcjonalny argument 9.7. Wymuszanie sprawdzania typów w funkcji za pomocą dekoratora 9.8. Definiowanie dekoratorów jako elementów klasy 9.9. Definiowanie dekoratorów jako klas 9.10. Stosowanie dekoratorów do metod klasy i metod statycznych 9.11. Pisanie dekoratorów, które dodają argumenty do funkcji w nakładkach 9.12. Stosowanie dekoratorów do poprawiania definicji klas 9.13. Używanie metaklasy do kontrolowania tworzenia obiektów 9.14. Sprawdzanie kolejności definiowania atrybutów klasy 9.15. Definiowanie metaklas przyjmujących argumenty opcjonalne 9.16. Sprawdzanie sygnatury na podstawie argumentów *args i **kwargs 9.17. Wymuszanie przestrzegania konwencji pisania kodu w klasie 9.18. Programowe definiowanie klas 9.19. Inicjowanie składowych klasy w miejscu definicji klasy 9.20. Przeciążanie metod z wykorzystaniem uwag do funkcji 9.21. Unikanie powtarzających się metod właściwości 9.22. Definiowanie w łatwy sposób menedżerów kontekstu 9.23. Wykonywanie kodu powodującego lokalne efekty uboczne 9.24. Parsowanie i analizowanie kodu źródłowego Pythona 9.25. Dezasemblacja kodu bajtowego Pythona
10. Moduły i pakiety .........................................................................................................355 10.1. Tworzenie hierarchicznych pakietów z modułami 10.2. Kontrolowanie importowania wszystkich symboli 10.3. Importowanie modułów podrzędnych z pakietu za pomocą nazw względnych 10.4. Podział modułu na kilka plików 10.5. Tworzenie odrębnych katalogów z importowanym kodem z jednej przestrzeni nazw 10.6. Ponowne wczytywanie modułów 10.7. Umożliwianie wykonywania kodu z katalogu lub pliku zip jako głównego skryptu 10.8. Wczytywanie pliku z danymi z pakietu 10.9. Dodawanie katalogów do zmiennej sys.path 10.10. Importowanie modułów na podstawie nazwy z łańcucha znaków 10.11. Wczytywanie modułów ze zdalnego komputera z wykorzystaniem haków w poleceniu importu 10.12. Modyfikowanie modułów w trakcie importowania 10.13. Instalowanie pakietów tylko na własny użytek 10.14. Tworzenie nowego środowiska Pythona 10.15. Rozpowszechnianie pakietów
12. Współbieżność ............................................................................................................429 12.1. Uruchamianie i zatrzymywanie wątków 12.2. Ustalanie, czy wątek rozpoczął pracę 12.3. Komunikowanie się między wątkami 12.4. Blokowanie sekcji krytycznej 12.5. Blokowanie z unikaniem zakleszczenia 12.6. Zapisywanie stanu wątku 8
Spis treści
429 432 434 439 441 445
12.7. Tworzenie puli wątków 12.8. Proste programowanie równoległe 12.9. Jak radzić sobie z mechanizmem GIL (i przestać się nim martwić) 12.10. Definiowanie zadań działających jak aktory 12.11. Przesyłanie komunikatów w modelu publikuj-subskrybuj 12.12. Używanie generatorów zamiast wątków 12.13. Odpytywanie wielu kolejek wątków 12.14. Uruchamianie procesu demona w systemie Unix
446 449 453 456 459 462 468 471
13. Skrypty narzędziowe i zarządzanie systemem . .......................................................475 13.1. Przyjmowanie danych wejściowych skryptu za pomocą przekierowań, potoków lub plików wejściowych 13.2. Kończenie pracy programu wyświetleniem komunikatu o błędzie 13.3. Parsowanie opcji z wiersza poleceń 13.4. Prośba o podanie hasła w czasie wykonywania programu 13.5. Pobieranie rozmiarów terminala 13.6. Wywoływanie zewnętrznych poleceń i pobieranie danych wyjściowych 13.7. Kopiowanie lub przenoszenie plików i katalogów 13.8. Tworzenie i wypakowywanie archiwów 13.9. Wyszukiwanie plików na podstawie nazwy 13.10. Wczytywanie plików konfiguracyjnych 13.11. Dodawanie mechanizmu rejestrowania operacji do prostych skryptów 13.12. Dodawanie obsługi rejestrowania do bibliotek 13.13. Tworzenie stopera 13.14. Określanie limitów wykorzystania pamięci i procesora 13.15. Uruchamianie przeglądarki internetowej
14. Testowanie, debugowanie i wyjątki .........................................................................497 14.1. Testowanie danych wyjściowych wysyłanych do strumienia stdout 14.2. Podstawianie obiektów w testach jednostkowych 14.3. Sprawdzanie wystąpienia wyjątków w testach jednostkowych 14.4. Zapisywanie danych wyjściowych testu w pliku 14.5. Pomijanie testów lub przewidywanie ich niepowodzenia 14.6. Obsługa wielu wyjątków 14.7. Przechwytywanie wszystkich wyjątków 14.8. Tworzenie niestandardowych wyjątków 14.9. Zgłaszanie wyjątku w odpowiedzi na wystąpienie innego wyjątku 14.10. Ponowne zgłaszanie ostatniego wyjątku 14.11. Wyświetlanie komunikatów ostrzegawczych 14.12. Debugowanie prostych awarii programu 14.13. Profilowanie i pomiar czasu pracy programów 14.14. Przyspieszanie działania programów Spis treści
15. Rozszerzenia w języku C ............................................................................................525 15.1. Dostęp do kodu w języku C za pomocą modułu ctypes 15.2. Pisanie prostych modułów rozszerzeń w języku C 15.3. Pisanie funkcji rozszerzeń manipulujących tablicami 15.4. Zarządzanie nieprzejrzystymi wskaźnikami w modułach rozszerzeń w języku C 15.5. Definiowanie i eksportowanie interfejsów API języka C w modułach rozszerzeń 15.6. Wywoływanie kodu Pythona w kodzie w języku C 15.7. Zwalnianie blokady GIL w rozszerzeniach w języku C 15.8. Jednoczesne wykonywanie wątków z kodu w językach C i Python 15.9. Umieszczanie kodu w języku C w nakładkach opartych na narzędziu Swig 15.10. Używanie Cythona do tworzenia nakładek na istniejący kod w języku C 15.11. Używanie Cythona do pisania wydajnych operacji na tablicach 15.12. Przekształcanie wskaźnika do funkcji w jednostkę wywoływalną 15.13. Przekazywanie łańcuchów znaków zakończonych symbolem NULL do bibliotek języka C 15.14. Przekazywanie łańcuchów znaków Unicode do bibliotek języka C 15.15. Przekształcanie łańcuchów znaków z języka C na ich odpowiedniki z Pythona 15.16. Używanie łańcuchów znaków o nieznanym kodowaniu pobieranych z języka C 15.17. Przekazywanie nazw plików do rozszerzeń w języku C 15.18. Przekazywanie otwartych plików do rozszerzeń w języku C 15.19. Wczytywanie w języku C danych z obiektów podobnych do plików 15.20. Pobieranie obiektów iterowalnych w języku C 15.21. Diagnozowanie błędów segmentacji
A Dalsza lektura .............................................................................................................585 Skorowidz . ..................................................................................................................587
10
Spis treści
Przedmowa
Od 2008 w świecie Pythona można było obserwować powolną ewolucję Pythona 3. Od początku było wiadomo, że wprowadzenie tej wersji do powszechnego użytku zajmie dużo czasu. Nawet wtedy, gdy powstaje ta książka (czyli w 2013 roku), wielu zawodowych programistów Pythona w wersjach produkcyjnych kodu wciąż używa Pythona 2. Wiele mówi się o tym, że Python 3 nie jest zgodny ze starszymi wersjami języka. To prawda, zgodność wstecz jest problemem dla każdego, kto ma dostęp do gotowej bazy kodu. Jeśli jednak skupić się na przyszłości, okazuje się, że Python 3 ma do zaoferowania znacznie więcej, niż mogłoby się wydawać. Podobnie jak Python 3 jest językiem jutra, tak też i wydanie książki Python. Receptury zostało znacznie zmodyfikowane w porównaniu z wcześniejszymi edycjami. Przede wszystkim jest to pozycja mocno nastawiona na przyszłość. Wszystkie receptury napisano i przetestowano pod kątem Pythona 3.3, bez uwzględniania starszych wersji lub dawnych sposobów pracy. Wiele zaprezentowanych receptur działa tylko w wersjach 3.3 i nowszych Pythona. To podejście może być ryzykowne, jednak nadrzędnym celem jest opracowanie książki z rozwiązaniami opartymi na najnowszych narzędziach i idiomach. Mamy nadzieję, że przedstawione receptury pomogą zarówno programistom piszącym nowy kod w Pythonie 3, jak i osobom, które chcą zmodernizować istniejący kod. Oczywiste jest, że opracowanie książki w tym stylu jest wyzwaniem redakcyjnym. Gdy poszukasz receptur Pythona w internecie, w witrynach ActiveState (w sekcji poświęconej recepturom Pythona), Stack Overflow lub podobnych, znajdziesz dosłownie tysiące przydatnych rozwiązań. Jednak większość z nich oparta jest na dawnych narzędziach. Prawie wszystkie są napisane pod kątem Pythona 2, a ponadto często zawierają sztuczki związane z różnicami między starszymi wersjami Pythona (np. 2.3 i 2.4). Oprócz tego w takich rozwiązaniach nieraz wykorzystuje się przestarzałe techniki, które w Pythonie 3.3 są dostępne w formie wbudowanych funkcji. Wyszukiwanie receptur przeznaczonych dla Pythona 3 jest trudniejsze. Dlatego przy wyborze zagadnień omawianych w książce, zamiast szukać rozwiązań związanych z Pythonem 3, zainspirowaliśmy się istniejącym kodem i gotowymi technikami. Na podstawie tych pomysłów przygotowaliśmy nowe receptury, celowo napisane z wykorzystaniem najnowszych technik programowania w Pythonie. Dlatego książka ta jest źródłem wiedzy dla każdego, kto chce pisać kod w nowoczesnym stylu. Wybierając rozwiązania, doszliśmy do wniosku, że w książce nie da się uwzględnić wszystkich możliwych operacji obsługiwanych przez Pythona. Dlatego położyliśmy nacisk na rdzeń tego języka, a także na zadania wykonywane w różnych obszarach. Ponadto w wielu recepturach
11
staraliśmy się opisać funkcje wprowadzone w Pythonie 3, które prawdopodobnie nie są znane nawet doświadczonym programistom używającym starszych wersji tego języka. Zamiast kodu związanego z bardzo wąskimi praktycznymi problemami, preferujemy rozwiązania ilustrujące techniki programowania o ogólnym zastosowaniu (czyli wzorce programistyczne). Choć uwzględniliśmy kilka niezależnych pakietów, większość receptur oparta jest na rdzeniu języka i bibliotece standardowej.
Dla kogo przeznaczona jest ta książka Książka ta jest skierowana do doświadczonych programistów Pythona, którzy chcą lepiej zrozumieć ten język oraz nowoczesne idiomy programowania. Duża część materiału dotyczy zaawansowanych technik wykorzystywanych w bibliotekach, platformach i aplikacjach. W większości receptur przyjmujemy, że posiadasz wiedzę niezbędną do zrozumienia danego zagadnienia (np. ogólne informacje z zakresu nauk komputerowych, struktur danych, badania złożoności, programowania systemów, współbieżności, programowania w języku C itd.). Przedstawione rozwiązania to często tylko ramy, które mają zapewnić informacje niezbędne do rozpoczęcia pracy, natomiast uzupełnienie szczegółów wymaga dodatkowych samodzielnych poszukiwań. Dlatego zakładamy, że wiesz, jak korzystać z wyszukiwarek i doskonałej internetowej dokumentacji Pythona. Cierpliwość potrzebna na opanowanie wielu zaawansowanych receptur zostanie nagrodzona znacznie lepszym zrozumieniem działania Pythona. Dzięki temu poznasz nowe sztuczki i techniki, które możesz wykorzystać we własnym kodzie.
Dla kogo ta książka nie jest przeznaczona Nie jest to książka dla początkujących użytkowników, którzy chcą nauczyć się Pythona od podstaw. Zakładamy, że znasz już podstawy opisane w samouczkach Pythona lub pozycjach dla nowicjuszy. Książka ta nie ma też charakteru encyklopedii, w której można by np. szybko znaleźć opis funkcji z konkretnego modułu. Dotyczy określonych zagadnień programistycznych, zawiera możliwe rozwiązania i stanowi punkt wyjścia do bardziej zaawansowanego materiału, który można znaleźć w internecie lub w innych pozycjach.
Konwencje stosowane w tej książce W książce stosowane są następujące konwencje typograficzne: Kursywa Tak oznaczone są nowe pojęcia, adresy URL, adresy e-mail, nazwy plików i rozszerzenia plików. Czcionka o stałej szerokości
Jest używana w listingach programów, a także w akapitach do oznaczania elementów programów (np. nazw zmiennych lub funkcji, baz danych, typów danych, zmiennych środowiskowych, instrukcji i słów kluczowych).
12
Przedmowa
Pogrubiona czcionka o stałej szerokości
Tak oznaczone są polecenia i inny tekst, który należy wprowadzić w takiej postaci, w jakiej został podany. Stała szerokość i kursywa
Tak oznaczony jest tekst, który należy zastąpić wartościami określonymi przez siebie lub kontekst. Ta ikona oznacza wskazówkę, sugestię lub ogólną uwagę.
Ta ikona oznacza ostrzeżenie.
Przykładowy kod w internecie Prawie cały przykładowy kod z tej książki jest dostępny w internecie na stronie http://github.com/ dabeaz/python-cookbook (polską wersję znajdziesz w witrynie wydawnictwa Helion pod adresem www.helion.pl/ksiazki/pytre3.htm). Czekamy na poprawki błędów, usprawnienia i komentarze.
Korzystanie z przykładowego kodu Książka ta ma pomóc Ci w wykonywaniu zadań. Jeśli znajduje się w niej przykładowy kod, zwykle możesz wykorzystać go we własnych programach i dokumentacji. Nie musisz prosić nas o pozwolenie, chyba że kopiujesz duże części kodu. Np. napisanie programu z wykorzystaniem kilku fragmentów kodu z tej książki nie wymaga pozwolenia. Jednak wymaga go sprzedaż lub dystrybucja płyty CD-ROM z przykładami z książek wydawnictwa O’Reilly. Udzielenie odpowiedzi za pomocą cytatu tekstu i przykładowego kodu z książki nie wymaga pozwolenia, natomiast jest ono niezbędne przy umieszczaniu dużych fragmentów kodu w dokumentacji produktów. Będzie nam miło, gdy podasz tę książkę jako źródło informacji, nie jest to jednak konieczne. Przy podawaniu źródła zwykle określa się tytuł, autora, wydawnictwo i numer ISBN. Oto przykład: Python. Receptury. Wydanie III, David Beazley i Brian K. Jones, Helion, ISBN 978-83-246-8180-8. Jeśli sądzisz, że planowany przez Ciebie sposób wykorzystania kodu wykracza poza zasady dozwolonego użytku lub przedstawione tu uprawnienia, skontaktuj się z wydawnictwem O’Reilly. Jego adres to [email protected].
Przedmowa
13
Podziękowania Chcemy podziękować redaktorom technicznym, Jake’owi Vanderplasowi, Robertowi Kernowi i Andrei Crotti, za bardzo pomocne komentarze. Jesteśmy wdzięczni także skupionej wokół Pythona społeczności za wsparcie i słowa zachęty. Dziękujemy również redaktorom wcześniejszego wydania: Aleksowi Martellemu, Annie Ravenscroft i Davidowi Ascherowi. Choć to wydanie napisaliśmy od nowa, poprzednie zapewniło początkowe ramy pomocne przy wyborze zagadnień i receptur. Na koniec nie mniej ważne podziękowania składamy czytelnikom wstępnych wersji książki za komentarze i sugerowane poprawki.
Podziękowania od Davida Beazley’a Pisanie książki to poważne zadanie. Dlatego dziękuję mojej żonie Pauli i moim dwóm synkom za cierpliwość oraz wsparcie w czasie, gdy pracowałem nad tym projektem. Duża część tej książki pochodzi z materiałów, które opracowałem, prowadząc przez sześć lat szkolenia z Pythona. Dziękuję wszystkim osobom, które uczestniczyły w moich kursach i przyczyniły się do powstania tej książki. Dziękuję też Nedowi Batchelderowi, Travisovi Oliphantowi, Peterowi Wangowi, Brianowi Van de Henowi, Hugo Shi, Raymondowi Hettingerowi, Michaelowi Foordowi i Danielowi Kleinowi za podróżowanie po całym świecie i prowadzenie kursów w czasie, gdy ja pracowałem nad książką w moim domu w Chicago. Meghan Blanchette i Rachel Roumeliotis z wydawnictwa O’Reilly także bardzo przyczyniły się do ukończenia prac nad książką mimo kilku falstartów i nieprzewidzianych opóźnień. Na zakończenie nie mniej ważne podziękowania składam skupionej wokół Pythona społeczności za nieustające wsparcie i znoszenie moich zwariowanych pomysłów. David M. Beazley http://www.dabeaz.com https://twitter.com/dabeaz
Podziękowania od Briana Jonesa Dziękuję współautorowi tej książki, Davidowi Beazley’owi, a także Meghan Blanchette i Rachel Roumeliotis z wydawnictwa O’Reilly za pracę ze mną nad tym projektem. Dziękuję też mojej wspaniałej żonie Nataszy za cierpliwość i zachętę w czasie, gdy pisałem tę książkę, oraz wspieranie realizowania wszystkich moich ambicji. Jednak przede wszystkim dziękuję społeczności skupionej wokół Pythona. Choć uczestniczyłem w pracach nad różnymi projektami o otwartym dostępie do kodu źródłowego i językami oraz byłem członkiem kilku klubów, żadna praca nie była tak satysfakcjonująca jak ta wykonana w służbie tej społeczności. Brian K. Jones http://www.protocolostomy.com https://twitter.com/bkjones
14
Przedmowa
ROZDZIAŁ 1.
Algorytmy i struktury danych
Python udostępnia wiele przydatnych wbudowanych struktur danych, np. listy, zbiory i słow niki. Zazwyczaj korzystanie z tych struktur jest proste. Nieraz jednak pojawiają się wątpliwości dotyczące wyszukiwania, sortowania, porządkowania i filtrowani W tym rozdziale oma mi. Ponadto przedwiamy standardowe struktury danych oraz algorytmy związane z stawiamy różne struktury danych z modułu collections.
' ��
���ych zmiennych � Q
1.1. Wypakowywanie sekwencji d Problem Istnieje N-elementowa krotka lub sekwenc�N zmiennych.
�cr=•
Rozwiązanie
�
�
ą programista chce zapisać w kolekcji
0 �ickt) można zapisać w zmiennych za pomocą prostej
Dowolną sckwcncj� (lub itcro operacji przypisania. JedynV'......_ ymóg jest taki, że liczba i struktura zmiennych muszą od powiadać sekwencji. O o d:
�
� �
>>> p >>> X, >>> X 4 >>> Y
=
s)
(4'
Y= p
>>> »> data = [ >>> name,
'ACME',
s hares,
50,
9 1 .1,
=
price,
date
price,
( year,
( 2 012 ,
12 ,
2 1)
]
data
name
'ACME' >>> date (2 012,
12 ,
>>> name,
2 1) s hares,
mon,
day)
data
>>> name
'ACME' >>> year
2012
15
mon 12 >>> day
21 >>>
Jeśli liczba elementów jest niewłaściwa, wystąpi błąd. Oto przykład: >>> p = (4' 5) X, y, Z = p
Omówienie Wypakowywanie działa dla wszystkich iterowalnych obiektów, nie tylko dla krotek i list. Technikę tę można zastosować też dla łańcuchów znaków, plików,iteratorów i generatorów. Oto przykład: >>>
s
>>> a, >>> a 'W'
=
'Witaj' b,
c,
d,
e
s
>>> b 'i'
>>>
e
'j'
�
>>>
W trakcie wypakowywania programista chce c as usunąć niektóre wartości. Python nie udostępnia specjalnej składni do wykon nia eJ racji, można jednak zastosować pomijaną później zmienną o określonej nazwie. O ad: »> data = [ 'ACME', >>>
>>>
shares, shares
_,
price,
('> , "-/
50, _
9 1 .1, (20 = data
�
50 >>> price 9 1 .1
:::
Na y się jednak up scu kodu.
�� �
,
21 )]
•
�
, że wybrana nazwa zmiennej nie jest używana w innym miej
1.2. Wypakowywanie elementów z
obiektów iterowalnych o dowolnej długości
Problem Programista chce wypakować N elementów z obiektu iterowalnego,ale obiekt ten może za wierać więcej niż N elementów,co prowadzi do wyjątku too many values to unpack (czyli zbyt dużo wartości do wypakowania).
16
Rozdział 1. Algorytmy i struktury danych
Rozwiązanie Do rozwiązania tego problemu można zastosować wyrażenia z gwiazdką Pythona. Załóżmy, że prowadzisz kurs i pod koniec semestru chcesz odrzucić najlepszą i najgorszą ocenę z prac domowych oraz obliczyć średnią z pozostałych ocen. Jeśli oceny są tylko cztery, można wy pakować je wszystkie, co jednak zrobić, jeśli jest ich np. 24? Wyrażenie z gwiazdką pozwala łatwo wykonać zadanie: def dro p_first_last(grades): first,
*middle,
last
=
grades
return avgCmiddle)
Oto inny przykład. Załóżmy, że w rekordach z danymi użytkowników znajduje się nazwisko i adres e-mail, po których następuje dowolna liczba numerów telefonu. Rekordy można wy pakować w następujący sposób: >»
� u listą. Nie ma znaczenia, ile �le nie zawierać takich numerów).
Warto zauważyć, że zmienna phone_numbers zawsze numerów telefonu program wypakuje (rekord mo · e r Dlatego w kodzie, który korzysta ze zmiennej pho że lista nie istnieje, ani sprawdzać typu danych.
mbers,
nie trzeba uwzględniać tego,
o Zmienna z gwiazdką może też zajmować pie zą pozycję na liście. Załóżmy, że sekwencja wartości reprezentuje poziom sprzedaż�tatnich ośmiu kwartałach. Jeśli chcesz porów nać sprzedaż z ostatniego kwartału ze � � z siedmiu wcześniejszych, możesz zastosować następujący kod: :U *trailing_qtrs,
trailing_avg
=
�
§:
current_qtr
s um(trai i
es_record
l len(trailing_qtrs)
return avg_comparison(tr · ·ng_avg,
\ � [10,
current_qtr)
Oto przebieg tych opert;WJ wiL oczny w interpreterze Pythona: >>>
*trailing, cur >>> trailing [10, 8, 7, l, 9, 5, >>> current
8, 7,
l,
9,
5,
10,
3]
10]
Omówienie Zaawansowane wypakowywanie obiektów iterowalnych służy do wypakowywania obiek tów o nieznanej lub dowolnie dużej długości. Obiekty iterowalne często mają znany kompo nent lub występuje w nich wzorzec (np. wszystko po pierwszym elemencie to numer telefonu), a składnia z gwiazdką pozwala programistom łatwo wykorzystać te wzorce, zamiast wyko nywać skomplikowane operacje w celu pobrania potrzebnych elementów z obiektu.
1.2.
Wypakowywanie elementów z obiektów iterowalnych o dowolnej długości
17
Warto zauważyć, że składnia z gwiazdką jest przydatna zwłaszcza w trakcie przechodzenia po sekwencji krotek różnej długości, np. krotek z tagami: records
[
=
( 'foo ',
l,
( 'bar',
' hello'),
( 'foo',
3 , 4 ),
2),
def do_foo(x,
y):
print( 'foo',
x,
def do_bar( s): print( 'bar ',
s)
for tag,
y)
*args in records:
if tag
'foo':
==
do_foo( *args) ==
elif tag
'bar':
do_bar( *args)
� �
Wypakowywanie za pomocą gwiazdki jest też przydatne w połąc u z niektórymi opera cjami na łańcuchach znaków, np. przy ich dzieleniu. Oto przykład: �""""'>>> llne
=
�� � .�
' nobody:*:-2 :2 :Unprlvlleged User:/var/empty:
>>> uname,
*flelds,
homedlr,
sh
=
llne.spllt (' : ')
>>> uname
sr
1
false'
""'-·
'nabody' >>> homedir '/var/empty' >>> sh
»>
'/usr/bin/false'
�
��� � '-.L '-V
eo
e usunąć. Wtedy w trakcie wypakowywania Czasem programista chce wypakować wartoś nie można podać samej gwiazdki (*), - a atomiast zastosować ją w połączeniu z nazwą pomijanych zmiennych, np. _lub ign orować). Oto przykład: ·
»> record >>> name,
=
('ACME',
*_,
(*_,
>>> name
'ACME' >>> year 2012 >>>
50,
year)
123 . 4 5, =
�
r� d
� � �
•
,
18,
2012) )
Występują pewne podobieństwa między wypakowywaniem z wykorzystaniem gwiazdki a funkcjami przetwarzania list dostępnymi w niektórych językach funkcyjnych. Np. można łatwo rozdzielić listę na głowę i ogon: >>> items
=
>>> head,
*tail
[1,
10, 7,
=
4,
S,
9]
items
>>> head
l >>> tail [10, >>>
7,
4,
S,
9]
Można sobie wyobrazić funkcje rozdzielające w ten sposób listy w ramach ciekawego algo rytmu rekurencyjnego. Oto przykład: >>> def sum(items) : h ead,
*tail
return head
18
+
items sum(tail)
if tail else head
Rozdział 1. Algorytmy i struktury danych
>>> sum(items) 36
Warto jednak pamiętać, że rekurencja nie jest mocną stroną Pythona (z uwagi na wbudowane ograniczenie rekurencji). Dlatego ostatni przykład jest jedynie akademicką ciekawostką.
1.3.
Zachowywanie ostatnich N elementów
Problem Programista chce przechowywać w historii kilka ostatnich elementów używanych w trakcie iterowania lub wykonywania innych operacji.
Rozwiązanie
�
Do przechowywania niepehtej historii doskonale nadaje się obiek Poniższy kod dopasowuje tekst z sekwencji wierszy i zwraca p ąq (poprzednimi N wierszami): from collections import deque def search(lines,
pattern,
history= S) :
previous_lines = deque(maxlen= history) for line in lines: if pattern in yield line,
lin e: previous_lines
previous_lines.append(line)
� S$
# Przykład zastosowania obiektu typu deque do if
__
name__ == '__main__ ':
with open( 'somefile.txt' for line,
a
prevlin es in
for pline in p ev · print( pline,
�
print ( line,
e
ollections. deque.
� �ersz wraz z kontekstem '�
�� � ��
•
o �� o
p�
f:
�
e rc�( f,
'python',
5) :
.
d= ' )
=
print("-"
Omówienie W trakcie pisania kodu wyszukującego elementy często używa się funkcji generatora, np. yield, tak jak w tej recepturze. Pozwala to rozdzielić proces wyszukiwania od kodu wyko rzystującego wyniki. Jeśli nie znasz generatorów, zapoznaj się z recepturą 4.3. Instrukcja deque C maxlen=N) tworzy kolejkę o stałej długości. Jeśli program doda nowy element do pełnej kolejki, automatycznie usunięty zostanie najstarszy element. Oto przykład: >>> q = deque( maxlen=3) >>> q.append(l) >>> q.append(2) >>> q.append(3) >>> q deque([l,
2,
3] ,
maxle n = 3)
1.3. Zachowywanie ostatnich N elementów
19
>>> q.append(4) >>> q deque([2,
3, 4] ,
maxlen=3)
>>> q.append(S ) >>> q deque ( [3 , 4,
5],
maxlen =3)
Choć można ręcznie wykonywać takie operacje na liście (dołączać i usuwać elementy itd.), rozwiązanie oparte na kolejce jest bardziej eleganckie i działa znacznie szybciej. Obiekt typu deque można wykorzystać wszędzie tam, gdzie potrzebna jest prosta kolejka. Jeśli nie określisz maksymalnego rozmiaru takiego obiektu, otrzymasz nieograniczoną kolejkę, w której można dodawać i usuwać elementy z obu końców. Oto przykład: q = deque( ) >>>
q.append(l)
>>> q.append(2) >>> q.append(3) >>> q deque([l,
2,
3])
>>> q.appendleft(4)
�
>>> q deque([4 ,
l, 2 , 3] )
>>> q.pop()
�
: � u; ([4 , l, 2] ) �» q.popleft()
�V
�
� ..� 0
�l'b;: �
Dodawanie i usuwanie elementów z końców kol równaniu z listą, ponieważ wstawianie i usuw O(N).
1.4.
o
złożoność 0(1). Jest to różnica w po mentów z jej początku ma złożoność
��:��ększych � elementów
Wyszukiwa lub najmnie
�
�
Problem
Programista chce utworzyć listę z N największych lub najmniejszych elementów z kolekcji.
Rozwiązanie Moduł heapq ma dwie funkcje, nia. Oto przykład:
nlargest ( )
i
nsmallest (),które
import heapq n u ms = [1,
8, 2 , 23 ,
7,
pri n t ( h eapq.nlarge s t (3 , pri n t ( h eapq.nsmallest(3 ,
Obie funkcje przyjmują też klucz, co pozwala stosować je do bardziej skomplikowanych struktur danych. Oto przykład: portfolio = {'name': {'name': {'name': {'name': {'name': {'name': ]
Omówienie Jeśli szukasz N najmniejszych lub największych elementów, a N jest małe w porównaniu z wielkością całej kolekcji, opisane funkcje zapewniają dobrą wydajność. Najpierw przekształcają dane na listę, na której elementy są uporządkowane w kopiec. Oto przykład: >>> nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2] >>> import heapq >>> heap = list(nums) >>> heapq.heapify(heap) >>> heap [-4, 2, 1, 23, 7, 2, 18, 23, 42, 37, 8] >>>
Najważniejszą cechą zmiennej heap jest to, że pozycja heap[0] zawsze zawiera najmniejszy element. Ponadto kolejne elementy można łatwo znaleźć za pomocą metody heapq.heappop(), która pobiera pierwszy element i zastępuje go następnym najmniejszym elementem (wymaga to O(log N) operacji, gdzie N to wielkość kopca). Aby znaleźć trzy najmniejsze elementy, trzeba wykonać następujące instrukcje: >>> heapq.heappop(heap) -4 >>> heapq.heappop(heap) 1 >>> heapq.heappop(heap) 2
Funkcje nlargest() i nsmallest() sprawdzają się najlepiej, gdy liczba szukanych elementów jest stosunkowo niewielka. Aby znaleźć jeden najmniejszy lub największy element, wydajniej jest zastosować funkcję min() lub max(). Także jeśli N jest bliskie wielkości kolekcji, zwykle szybciej jest najpierw posortować dane, a następnie pobrać ich wycinek (czyli wywołać instrukcję sorted(items)[:N] lub sorted(items)[-N:]). Warto zauważyć, że funkcje nlargest() i nsmallest() mogą działać w różny sposób i przeprowadzać pewne optymalizacje (np. sortując dane, jeśli N jest bliskie wielkości wejściowej kolekcji). Choć nie trzeba korzystać z tej receptury, zastosowanie kopca to ciekawe i warte analizy rozwiązanie. Omówienie kopca znajdziesz w każdej dobrej książce na temat algorytmów i struktur danych. Szczegóły implementacji przedstawionych funkcji opisano także w dokumentacji modułu heapq.
1.4. Wyszukiwanie N największych lub najmniejszych elementów
21
1.5. Tworzenie kolejki priorytetowej Problem Programista chce utworzyć kolejkę, która sortuje elementy według określonych priorytetów i w każdej operacji pop zwraca element o najwyższym priorytecie.
Rozwiązanie W poniższej klasie wykorzystano moduł heapq do utworzenia prostej kolejki priorytetowej: import heapq class PriorityQueue: def __init__(self): self._queue = [] self._index = 0 def push(self, item, priority): heapq.heappush(self._queue, (-priority, self._index, item)) self._index += 1 def pop(self): return heapq.heappop(self._queue)[-1]
Zauważ, że pierwsza instrukcja pop() zwróciła element o najwyższym priorytecie. Ponadto dwa elementy o tym samym priorytecie (foo i grok) zostały zwrócone w tej samej kolejności, w jakiej wstawiono je do kolejki.
Omówienie Najważniejsze aspekty tej receptury związane są z wykorzystaniem modułu heapq. Funkcje heapq.heappush() i heapq.heappop() wstawiają elementy do listy _queue oraz usuwają je z niej w taki sposób, że pierwszy element na liście ma najmniejszy priorytet (tę technikę opisano
22
Rozdział 1. Algorytmy i struktury danych
w recepturze 1.4.). Metoda heappop zawsze zwraca „najmniejszy” element, co jest ważne ze względu na pobieranie z kolejki właściwych elementów. Ponadto ponieważ operacje push i pop mają złożoność O(log N) (gdzie N to liczba elementów kopca), są stosunkowo wydajne nawet dla dużych wartości N. W tej recepturze kolejka zawiera krotki w postaci (-priority, index, item). Wartość priority jest ujemna, dzięki czemu kolejka jest posortowana od najwyższych do najniższych priorytetów. Jest to kolejność odwrotna niż w standardowym kopcu, gdzie wartości są sortowane od najmniejszych do największych. Zmienna index pozwala odpowiednio uporządkować elementy o tym samym priorytecie. Stale zwiększany indeks sprawia, że elementy są sortowane według kolejności wstawiania. Indeks pełni też ważną funkcję przy porównywaniu elementów o tym samym priorytecie. Warto rozwinąć ten temat. Obiektów typu Item w tym przykładzie nie można uporządkować: >>> a = Item('foo') >>> b = Item('bar') >>> a < b Traceback (most recent call last): File "", line 1, in TypeError: unorderable types: Item() < Item() >>>
Krotki (priority, item) można porównywać, jeśli priorytety mają różną wartość. Natomiast przy porównywaniu krotek o identycznych priorytetach wystąpi ten sam błąd co wcześniej. Oto przykład: >>> a = (1, Item('foo')) >>> b = (5, Item('bar')) >>> a < b True >>> c = (1, Item('grok')) >>> a < c Traceback (most recent call last): File "", line 1, in TypeError: unorderable types: Item() < Item() >>>
Dodanie indeksu i utworzenie krotek (priority, index, item) pozwala całkowicie uniknąć problemu, ponieważ żadna z krotek nie ma tej samej wartości index (Python nie porównuje pozostałych wartości, gdy może wcześniej ustalić wynik porównania): >>> a >>> b >>> c >>> a True >>> a True >>>
= = = <
(1, 0, Item('foo')) (5, 1, Item('bar')) (1, 2, Item('grok')) b
< c
Jeśli chcesz wykorzystać taką kolejkę do komunikacji między wątkami, musisz dodać odpowiednie blokady i sygnały. W recepturze 12.3. pokazano jeden ze sposobów na uzyskanie pożądanych efektów. W dokumentacji modułu heapq znajdziesz dodatkowe przykłady i omówienie teorii oraz zastosowań kopców.
1.5. Tworzenie kolejki priorytetowej
23
1.6. Odwzorowywanie kluczy na różne wartości ze słownika Problem Programista chce utworzyć słownik, w którym klucze są odwzorowane na więcej niż jedną wartość (jest to tzw. wielosłownik).
Rozwiązanie Słownik to odwzorowanie, w którym każdy klucz odpowiada jednej wartości. Aby odwzorować klucz na więcej wartości, trzeba zapisać je w odrębnym kontenerze, np. na liście lub w zbiorze. Słownik można utworzyć w następujący sposób: d = { 'a' : [1, 2, 3], 'b' : [4, 5] } e = { 'a' : {1, 2, 3}, 'b' : {4, 5} }
Wybór dotyczący tego, czy stosować listy czy zbiory, zależy od przeznaczenia kodu. Listy pozwalają zachować elementy w kolejności ich wstawiania. Zbiory należy stosować, gdy chce się usunąć powtarzające się wartości, a kolejność elementów nie ma znaczenia. Aby łatwo utworzyć taki słownik, można wykorzystać obiekt typu defaultdict z modułu collections. Cechą tego obiektu jest to, że automatycznie inicjuje pierwszą wartość, dzięki czemu można skoncentrować się na dodawaniu dalszych elementów. Oto przykład: from collections import defaultdict d = defaultdict(list) d['a'].append(1) d['a'].append(2) d['b'].append(4) ... d = defaultdict(set) d['a'].add(1) d['a'].add(2) d['b'].add(4) ...
Należy pamiętać, że obiekt typu defaultdict automatycznie tworzy w słowniku wpisy dla szukanych później kluczy (nawet jeśli klucze te pierwotnie nie znajdują się w słowniku). Jeśli takie działanie jest niepożądane, można zastosować metodę setdefault() standardowego słownika. Oto przykład: d = {} # Zwykły słownik d.setdefault('a', []).append(1) d.setdefault('a', []).append(2) d.setdefault('b', []).append(4) ...
24
Rozdział 1. Algorytmy i struktury danych
Jednak dla wielu programistów stosowanie metody setdefault() jest nienaturalne. Ponadto instrukcja ta przy każdym wywołaniu tworzy nowy egzemplarz początkowej wartości (w przykładzie jest to pusta lista: []).
Omówienie Utworzenie wielosłownika jest proste, jednak samodzielne inicjowanie pierwszej wartości może okazać się skomplikowane. Możesz np. natrafić na następujący kod: d = {} for key, value in pairs: if key not in d: d[key] = [] d[key].append(value)
Zastosowanie obiektu typu defaultdict prowadzi do powstania dużo bardziej przejrzystego kodu: d = defaultdict(list) for key, value in pairs: d[key].append(value)
Receptura ta jest ściśle powiązana z problemem grupowania rekordów na potrzeby przetwarzania danych (zobacz recepturę 1.15).
1.7. Określanie uporządkowania w słownikach Problem Programista chce utworzyć słownik i zachować kontrolę nad kolejnością elementów w trakcie poruszania się po nich lub ich serializowania.
Rozwiązanie Do kontrolowania kolejności elementów w słowniku można wykorzystać obiekt typu OrderedDict z modułu collections. W trakcie iterowania zachowuje on kolejność, w jakiej dane zostały wstawione. Oto przykład: from collections import OrderedDict d = OrderedDict() d['foo'] = 1 d['bar'] = 2 d['spam'] = 3 d['grok'] = 4 # Zwraca "foo 1", "bar 2", "spam 3", "grok 4" for key in d: print(key, d[key])
1.7. Określanie uporządkowania w słownikach
25
Obiekt typu OrderedDict jest przydatny zwłaszcza do tworzenia odwzorowań, które potem zostaną zserializowane lub zakodowane do innego formatu. Jeśli np. chcesz precyzyjnie określić kolejność, w jakiej pola pojawiają się w danych w formacie JSON, wystarczy zapisać je za pomocą obiektu typu OrderedDict: >>> import json >>> json.dumps(d) '{"foo": 1, "bar": 2, "spam": 3, "grok": 4}' >>>
Omówienie Obiekt typu OrderedDict wewnętrznie przechowuje listę podwójnie wiązaną, na której klucze są uporządkowane zgodnie z kolejnością ich wstawiania. Gdy wstawiany jest nowy element, zostaje on umieszczony na końcu listy. Późniejsze modyfikacje kluczy nie zmieniają ich kolejności. Warto wiedzieć, że obiekt typu OrderedDict zajmuje dwa razy więcej miejsca niż zwykły słownik (z uwagi na dodatkową listę wiązaną). Dlatego jeśli chcesz utworzyć strukturę danych z dużą liczbą obiektów typu OrderedDict (np. wczytać 100 000 wierszy z pliku CSV do listy obiektów typu OrderedDict), powinieneś przeanalizować wymagania aplikacji i ustalić, czy korzyści z zastosowania obiektów typu OrderedDict przeważają nad kosztami związanymi z potrzebną dodatkową pamięcią.
1.8. Obliczenia na danych ze słowników Problem Programista chce wykonywać różne obliczenia (znaleźć wartość minimalną i maksymalną, posortować elementy itd.) na danych ze słownika.
Rozwiązanie Przyjrzyj się słownikowi, w którym symbole akcji są odwzorowane na ceny: prices = { 'ACME': 45.23, 'AAPL': 612.78, 'IBM': 205.55, 'HPQ': 37.20, 'FB': 10.75 }
Aby przeprowadzić przydatne obliczenia z wykorzystaniem zawartości słownika, często warto zamienić klucze z wartościami za pomocą instrukcji zip(). Poniżej pokazujemy, jak ustalić ceny minimalną i maksymalną oraz powiązane z nimi symbole akcji: min_price = min(zip(prices.values(), prices.keys())) # min_price to (10.75, 'FB') max_price = max(zip(prices.values(), prices.keys())) # max_price to (612.78, 'AAPL')
26
Rozdział 1. Algorytmy i struktury danych
Aby uporządkować dane, należy zastosować instrukcje zip() i sorted(), tak jak w poniższym kodzie: prices_sorted = sorted(zip(prices.values(), prices.keys())) # prices_sorted to [(10.75, 'FB'), (37.2, 'HPQ'), # (45.23, 'ACME'), (205.55, 'IBM'), # (612.78, 'AAPL')]
W trakcie przeprowadzania obliczeń warto pamiętać o tym, że instrukcja zip() tworzy iterator, który można wykorzystać tylko raz. Poniższy kod jest błędny: prices_and_names = zip(prices.values(), prices.keys()) print(min(prices_and_names)) # OK print(max(prices_and_names)) # ValueError: max() arg is an empty sequence
Omówienie Jeśli spróbujesz przeprowadzić standardową redukcję danych w słowniku, stwierdzisz, że przetwarzane są tylko klucze, a nie wartości. Oto przykład: min(prices) max(prices)
# Zwraca 'AAPL' # Zwraca 'IBM'
Prawdopodobnie nie na tym zależy autorowi kodu, jeśli chce się przeprowadzić obliczenia z wykorzystaniem wartości ze słownika. Można spróbować rozwiązać ten problem za pomocą metody values() słownika: min(prices.values()) max(prices.values())
# Zwraca 10.75 # Zwraca 612.78
Niestety, nie zawsze jest to pożądany efekt. Możliwe, że programista chce mieć dostęp do informacji na temat powiązanych kluczy (aby np. ustalić, które akcje są najtańsze). Klucz powiązany z minimalną i maksymalną wartością można uzyskać, podając w instrukcjach min() i max() funkcję zwracającą klucz. Oto przykład: min(prices, key=lambda k: prices[k]) max(prices, key=lambda k: prices[k])
# Zwraca 'FB' # Zwraca 'AAPL'
Aby jednak ustalić wartość minimalną, trzeba wykonać dodatkowy krok: min_value = prices[min(prices, key=lambda k: prices[k])]
Technika wykorzystująca metodę zip() rozwiązuje problem przekształcania słownika na sekwencję par (wartość, klucz). Przy porównywaniu krotek w takiej postaci najpierw uwzględniany jest element wartość, a dopiero potem klucz. Jest to dokładnie to, czego potrzeba. Dzięki temu można łatwo przeprowadzić redukcje i posortować dane ze słownika za pomocą jednej instrukcji. Warto zauważyć, że w obliczeniach przeprowadzanych na parach (wartość, klucz) klucz służy do określenia wyniku w sytuacji, gdy kilka pozycji ma tę samą wartość. Jeśli przy obliczeniach z wykorzystaniem metod min() i max() kilka elementów ma tę samą wartość, zwrócony zostanie ten o najmniejszym lub największym kluczu. Oto przykład: >>> prices = { 'AAA' : 45.23, 'ZZZ': 45.23 } >>> min(zip(prices.values(), prices.keys())) (45.23, 'AAA') >>> max(zip(prices.values(), prices.keys())) (45.23, 'ZZZ') >>>
1.8. Obliczenia na danych ze słowników
27
1.9. Wyszukiwanie identycznych danych w dwóch słownikach Problem Istnieją dwa słowniki i programista chce się dowiedzieć, jakie wspólne dane się w nich znajdują (identyczne klucze, wartości itd.).
Rozwiązanie Istnieją dwa słowniki: a = { 'x' 'y' 'z' } b = { 'w' 'x' 'y' }
: 1, : 2, : 3 : 10, : 11, : 2
Aby ustalić, jakie wspólne dane znajdują się w tych słownikach, wystarczy przeprowadzić standardowe operacje na zbiorach, używając metod keys() i items(). Oto przykład: # Wyszukiwanie wspólnych kluczy a.keys() & b.keys() # { 'x', 'y' } # Wyszukiwanie kluczy, które nie występują w słowniku b a.keys() - b.keys() # { 'z' } # Wyszukiwanie wspólnych par (klucz, wartość) a.items() & b.items() # { ('y', 2) }
Operacje tego rodzaju umożliwiają też modyfikowanie i filtrowanie zawartości słownika. Załóżmy, że programista chce utworzyć nowy słownik z usuniętymi wybranymi kluczami. Oto przykładowy kod, w którym wykorzystano wyrażenie słownikowe: # Tworzenie nowego słownika pozbawionego wybranych kluczy c = {key:a[key] for key in a.keys() - {'z', 'w'}} # c to {'x': 1, 'y': 2}
Omówienie Słownik to odwzorowanie między zbiorami kluczy i wartości. Metoda keys() słownika zwraca obiekt widoku kluczy, który udostępnia klucze. Mało znaną cechą widoków kluczy jest to, że obsługują standardowe operacje na zbiorach, np. wyznaczanie sumy, części wspólnej i różnicy. Dlatego jeśli potrzebne są takie operacje na kluczach słownika, często można bezpośrednio wykorzystać obiekty widoku kluczy bez wcześniejszego przekształcania ich na zbiory. Metoda items() słownika zwraca obiekt widoku elementów składający się z par (klucz, wartość). Obiekt ten obsługuje podobne operacje na zbiorach i może posłużyć do ustalenia, które pary klucz – wartość występują w obu słownikach.
28
Rozdział 1. Algorytmy i struktury danych
Metoda values() słownika jest podobna, jednak nie obsługuje operacji na zbiorach opisanych w tej recepturze. Po części wynika to z tego, że — w odróżnieniu od kluczy — wartości w ich widoku nie zawsze są unikatowe. Dlatego przydatność niektórych operacji na zbiorach jest wątpliwa. Jeśli jednak trzeba wykonać takie operacje, można to zrobić — wystarczy najpierw przekształcić wartości na zbiór.
1.10. Usuwanie powtórzeń z sekwencji przy zachowaniu kolejności elementów Problem Programista chce usunąć z sekwencji powtarzające się wartości, a jednocześnie zachować kolejność pozostawionych elementów.
Rozwiązanie Jeśli dla wartości z sekwencji można utworzyć skróty, rozwiązanie jest proste — można wykorzystać zbiór i generator. Oto przykład: def dedupe(items): seen = set() for item in items: if item not in seen: yield item seen.add(item)
Funkcję tę można wykorzystać w następujący sposób: >>> a = [1, 5, 2, 1, 9, 1, 5, 10] >>> list(dedupe(a)) [1, 5, 2, 9, 10] >>>
To rozwiązanie działa tylko wtedy, gdy dla elementów sekwencji można utworzyć skróty. Aby usunąć powtórzenia z sekwencji elementów innego rodzaju (np. słowników), należy wprowadzić w recepturze drobną zmianę: def dedupe(items, key=None): seen = set() for item in items: val = item if key is None else key(item) if val not in seen: yield item seen.add(val)
Za pomocą argumentu key można określić funkcję, która na potrzeby usuwania powtórzeń przekształca elementy sekwencji tak, aby można było utworzyć dla nich skróty. Działa to tak: >>> a = [ {'x':1, 'y':2}, {'x':1, 'y':3}, {'x':1, 'y':2}, {'x':2, 'y':4}] >>> list(dedupe(a, key=lambda d: (d['x'],d['y']))) [{'x': 1, 'y': 2}, {'x': 1, 'y': 3}, {'x': 2, 'y': 4}] >>> list(dedupe(a, key=lambda d: d['x'])) [{'x': 1, 'y': 2}, {'x': 2, 'y': 4}] >>>
To drugie rozwiązanie działa poprawnie także przy usuwaniu powtórzeń wartości jednego pola lub atrybutu albo większej struktury danych. 1.10. Usuwanie powtórzeń z sekwencji przy zachowaniu kolejności elementów
29
Omówienie Jeśli programista chce tylko usunąć powtórzenia, często wystarczy utworzyć zbiór. Oto przykład: >>> [1, >>> {1, >>
a 5, 2, 1, 9, 1, 5, 10] set(a) 2, 10, 5, 9}
Jednak w tym podejściu kolejność elementów nie jest zachowywana, dlatego dane zostają wymieszane. Przedstawione rozwiązanie pozwala tego uniknąć. Wykorzystanie w tej recepturze funkcji generatora wynika z tego, że programista może jej potrzebować do wykonywania bardzo ogólnych zadań, niekoniecznie związanych z przetwarzaniem list. Np. aby wczytać plik i usunąć powtarzające się wiersze, można zastosować następujący kod: with open(somefile,'r') as f: for line in dedupe(f): ...
Specyfikacja funkcji key jest taka sama jak funkcji wbudowanych sorted(), min(), max() i podobnych (zobacz receptury 1.8 i 1.13.).
1.11. Nazywanie wycinków Problem Program stał się nieczytelnym zbitkiem zapisanych na stałe indeksów wycinków i programista chce go uporządkować.
Rozwiązanie Załóżmy, że fragment kodu pobiera określone pola z danymi zapisane w łańcuchach znaków z rekordami o polach o stałej szerokości (dane zapisane są w prostych plikach lub podobnym formacie): ###### 0123456789012345678901234567890123456789012345678901234567890' record = '. ..............................100 .......513.25 ..........' cost = int(record[20:32]) * float(record[40:48])
Zamiast stosować to podejście, można nazwać wycinki w następujący sposób: SHARES = slice(20,32) PRICE = slice(40,48) cost = int(record[SHARES]) * float(record[PRICE])
Druga wersja pozwala uniknąć licznych niezrozumiałych, zapisanych na stałe indeksów, a działanie kodu staje się dużo bardziej zrozumiałe.
Omówienie Zgodnie z ogólną regułą pisanie kodu z wieloma zapisanymi na stałe wartościami indeksów prowadzi do problemów z czytelnością i konserwowaniem kodu. Jeśli wrócisz do kodu za rok, spojrzysz na niego i zaczniesz się zastanawiać, co miałeś na myśli, pisząc go. Przedstawione tu rozwiązanie to prosty sposób na jednoznaczne określenie działania kodu. 30
Rozdział 1. Algorytmy i struktury danych
Wbudowana metoda slice() tworzy obiekt wycinka, który można wykorzystać w dowolnym miejscu, gdzie stosowanie wycinków jest dopuszczalne. Oto przykład: >>> >>> >>> [2, >>> [2, >>> >>> [0, >>> >>> [0,
Jeśli istnieje obiekt s typu slice, można uzyskać więcej informacji na jego temat, sprawdzając atrybuty s.start, s.stop i s.step: >>> >>> 10 >>> 50 >>> 2 >>>
a = slice(10, 50, 2) a.start a.stop a.step
Ponadto można odwzorować wycinek na sekwencję określonej długości. W tym celu należy zastosować metodę indices(size) wycinka. Zwraca ona krotkę (start, stop, krok), w której wszystkie wartości są dostosowane do ograniczeń (pozwala to uniknąć wyjątków IndexError przy stosowaniu indeksów). Oto przykład: >>> >>> (5, >>> ... ... W r d >>>
s = 'HelloWorld' a.indices(len(s)) 10, 2) for i in range(*a.indices(len(s))): print(s[i])
1.12. Określanie najczęściej występujących w sekwencji elementów Problem Istnieje sekwencja elementów i programista chce określić, który z nich występuje najczęściej.
Rozwiązanie Klasa collections.Counter jest zaprojektowana do rozwiązywania takich właśnie problemów. Udostępnia nawet wygodną metodę most_common(), która zwraca potrzebną odpowiedź.
1.12. Określanie najczęściej występujących w sekwencji elementów
31
Załóżmy, że programista ma listę słów i chce sprawdzić, które z nich pojawia się najczęściej. Można to zrobić tak: words = [ 'look', 'into', 'my', 'eyes', 'look', 'into', 'my', 'eyes', 'the', 'eyes', 'the', 'eyes', 'the', 'eyes', 'not', 'around', 'the', 'eyes', "don't", 'look', 'around', 'the', 'eyes', 'look', 'into', 'my', 'eyes', "you're", 'under' ] from collections import Counter word_counts = Counter(words) top_three = word_counts.most_common(3) print(top_three) # Zwraca [('eyes', 8), ('the', 5), ('look', 4)]
Omówienie Akceptowalne dane wejściowe do obiektu Counter to dowolna sekwencja elementów, dla których można utworzyć skróty. Na zapleczu Counter to słownik, który odwzorowuje elementy na liczbę ich wystąpień. Oto przykład: >>> word_counts['not'] 1 >>> word_counts['eyes'] 8 >>>
Aby ręcznie zwiększyć liczbę wystąpień, można zastosować dodawanie: >>> morewords = ['why','are','you','not','looking','in','my','eyes'] >>> for word in morewords: ... word_counts[word] += 1 ... >>> word_counts['eyes'] 9 >>>
Inna możliwość to zastosowanie metody update(): >>> word_counts.update(morewords) >>>
Mało znaną cechą obiektów typu Counter jest to, że można je ze sobą łączyć za pomocą różnych operatorów matematycznych: >>> a = Counter(words) >>> b = Counter(morewords) >>> a Counter({'eyes': 8, 'the': 5, 'look': 4, 'into': 3, 'my': 3, 'around': 2, "you're": 1, "don't": 1, 'under': 1, 'not': 1}) >>> b Counter({'eyes': 1, 'looking': 1, 'are': 1, 'in': 1, 'not': 1, 'you': 1, 'my': 1, 'why': 1}) >>> # Łączenie liczb wystąpień >>> c = a + b >>> c Counter({'eyes': 9, 'the': 5, 'look': 4, 'my': 4, 'into': 3, 'not': 2, 'around': 2, "you're": 1, "don't": 1, 'in': 1, 'why': 1, 'looking': 1, 'are': 1, 'under': 1, 'you': 1})
32
Rozdział 1. Algorytmy i struktury danych
>>> # Odejmowanie liczb wystąpień >>> d = a - b >>> d Counter({'eyes': 7, 'the': 5, 'look': 4, 'into': 3, 'my': 2, 'around': 2, "you're": 1, "don't": 1, 'under': 1}) >>>
Nie trzeba tłumaczyć, że obiekty Counter to niezwykle przydatne narzędzie do rozwiązywania dowolnych problemów, w których trzeba dzielić i zliczać dane. Należy stosować te obiekty zamiast ręcznie pisanych rozwiązań opartych na słownikach.
1.13. Sortowanie list słowników według wspólnych kluczy Problem Istnieje lista słowników, a programista chce posortować je na podstawie jednej lub kilku wartości ze słowników.
Rozwiązanie Sortowanie struktur tego rodzaju jest łatwe. Wystarczy zastosować funkcję itemgetter modułu operator. Załóżmy, że programista uruchomił zapytanie do tabeli bazy danych, aby pobrać listę użytkowników witryny. Otrzymał następującą strukturę danych: rows = [ {'fname': {'fname': {'fname': {'fname': ]
Można stosunkowo łatwo wyświetlić te wiersze uporządkowane według pól występujących we wszystkich słownikach. Oto przykład: from operator import itemgetter rows_by_fname = sorted(rows, key=itemgetter('fname')) rows_by_uid = sorted(rows, key=itemgetter('uid')) print(rows_by_fname) print(rows_by_uid)
Ten kod zwróci następujące dane: [{'fname': {'fname': {'fname': {'fname':
Omówienie W tym przykładzie do wbudowanej funkcji sorted() przekazano listę rows. Wspomniana funkcja przyjmuje argument przekazywany za pomocą słowa kluczowego key. Argument ten powinien być jednostką wywoływalną, która jako dane wejściowe przyjmuje jeden element z listy rows i zwraca wartość używaną do sortowania. Funkcja itemgetter() to właśnie taka jednostka. Funkcja operator.itemgetter()jako argument przyjmuje indeksy, które pozwalają pobrać potrzebne wartości z rekordów z listy rows. Takim argumentem może być nazwa klucza ze słownika, numer elementu listy lub dowolna wartość, którą można przekazać do metody __getitem__() obiektu. Jeśli do funkcji itemgetter() przekazanych zostanie kilka indeksów, jednostka wywoływalna zwróci krotki ze wszystkimi wskazanymi elementami, a metoda sorted() uporządkuje dane wyjściowe na podstawie kolejności takich krotek. Jest to przydatne przy jednoczesnym sortowaniu danych według kilku pól (np. imienia i nazwiska w przykładzie). Funkcja itemgetter() w pewnym stopniu została zastąpiona przez wyrażenie lambda. Oto przykład: rows_by_fname = sorted(rows, key=lambda r: r['fname']) rows_by_lfname = sorted(rows, key=lambda r: (r['lname'],r['fname']))
To rozwiązanie działa poprawnie. Jednak kod z wykorzystaniem funkcji itemgetter() jest zazwyczaj nieco szybszy. Dlatego możesz wybrać tę funkcję, jeśli zależy Ci na wydajności. Nie należy też zapominać, że technikę przedstawioną w tej recepturze można zastosować również do takich funkcji jak min() i max(). Oto przykład: >>> min(rows, key=itemgetter('uid')) {'fname': 'John', 'lname': 'Cleese', 'uid': 1001} >>> max(rows, key=itemgetter('uid')) {'fname': 'Big', 'lname': 'Jones', 'uid': 1004} >>>
1.14. Sortowanie obiektów bez wbudowanej obsługi porównań Problem Programista chce sortować obiekty tej samej klasy, jednak nie mają one wbudowanej obsługi porównań. 34
Rozdział 1. Algorytmy i struktury danych
Rozwiązanie Wbudowana funkcja sorted() przyjmuje argument key, w którym można podać jednostkę wywoływalną zwracającą pewną wartość z obiektu, używaną przez funkcję sorted do porównywania obiektów określonego rodzaju. Jeśli w aplikacji używana jest sekwencja obiektów typu User i programista chce posortować je według atrybutu user_id, może podać jednostkę wywoływalną przyjmującą obiekt typu User jako dane wejściowe i zwracającą atrybut user_id. Oto przykład: >>> class User: ... def __init__(self, user_id): ... self.user_id = user_id ... def __repr__(self): ... return 'User({})'.format(self.user_id) ... >>> users = [User(23), User(3), User(99)] >>> users [User(23), User(3), User(99)] >>> sorted(users, key=lambda u: u.user_id) [User(3), User(23), User(99)] >>>
Zamiast stosować wyrażenie lambda, można wykorzystać funkcję operator.attrgetter(): >>> from operator import attrgetter >>> sorted(users, key=attrgetter('user_id')) [User(3), User(23), User(99)] >>>
Omówienie Wybór między wyrażeniem lambda a funkcją attrgetter() może zależeć od osobistych preferencji. Funkcja attrgetter() jest jednak nieco szybsza, a ponadto umożliwia jednoczesne pobranie wielu pól. Podobnie jest przy stosowaniu funkcji operator.itemgetter() do słowników (zobacz recepturę 1.13). Jeśli w obiektach typu User znajdują się atrybuty first_name i last_name, dane można posortować w następujący sposób: by_name = sorted(users, key=attrgetter('last_name', 'first_name'))
Ponadto warto zauważyć, że technikę przedstawioną w tej recepturze można zastosować do takich funkcji, jak min() i max(). Oto przykład: >>> min(users, key=attrgetter('user_id') User(3) >>> max(users, key=attrgetter('user_id') User(99) >>>
1.15. Grupowanie rekordów na podstawie wartości pola Problem Istnieje sekwencja słowników, a programista chce przejść po danych w grupach opartych na wartości konkretnego pola (np. z datą). 1.15. Grupowanie rekordów na podstawie wartości pola
35
Rozwiązanie Funkcja itertools.groupby() wyjątkowo dobrze nadaje się do grupowania danych w opisany sposób. Aby zilustrować jej działanie, załóżmy, że istnieje następująca lista słowników: rows = [ {'address': {'address': {'address': {'address': {'address': {'address': {'address': {'address': ]
Przyjmijmy, że programista chce przejść po porcjach danych pogrupowanych według daty. Aby uzyskać pożądany efekt, najpierw trzeba posortować dane według odpowiedniego pola (tu jest to pole z datą), a następnie wywołać funkcję itertools.groupby(): from operator import itemgetter from itertools import groupby # Najpierw sortowanie według odpowiedniego pola rows.sort(key=itemgetter('date')) # Przechodzenie po pogrupowanych danych for date, items in groupby(rows, key=itemgetter('date')): print(date) for i in items: print(' ', i)
Ten kod zwraca następujące dane wyjściowe: 07/01/2012 {'date': {'date': 07/02/2012 {'date': {'date': {'date': 07/03/2012 {'date': 07/04/2012 {'date': {'date':
'07/01/2012', 'address': '5412 N CLARK'} '07/01/2012', 'address': '4801 N BROADWAY'} '07/02/2012', 'address': '5800 E 58TH'} '07/02/2012', 'address': '5645 N RAVENSWOOD'} '07/02/2012', 'address': '1060 W ADDISON'} '07/03/2012', 'address': '2122 N CLARK'} '07/04/2012', 'address': '5148 N CLARK'} '07/04/2012', 'address': '1039 W GRANVILLE'}
Omówienie Funkcja groupby() najpierw bada sekwencję i znajduje kolejne serie identycznych wartości (lub wartości zwróconych przez określoną funkcję z argumentu key). W każdej iteracji funkcja zwraca wartość wraz z iteratorem, który zapewnia dostęp do wszystkich elementów z grupy o określonej wartości. Ważnym krokiem wstępnym jest posortowanie danych według określonego pola. Ponieważ funkcja groupby() sprawdza tylko przyległe elementy, pominięcie sortowania powoduje, że rekordy nie zostaną pogrupowane w pożądany sposób.
36
Rozdział 1. Algorytmy i struktury danych
Jeśli programista chce tylko pogrupować dane według dat w większą strukturę danych, która umożliwia dostęp bezpośredni, może zastosować funkcję defaultdict() i utworzyć wielosłownik (zobacz recepturę 1.6). Oto przykład: from collections import defaultdict rows_by_date = defaultdict(list) for row in rows: rows_by_date[row['date']].append(row)
To podejście pozwala na łatwy dostęp do rekordów o określonej dacie: >>> for r in rows_by_date['07/01/2012']: ... print(r) ... {'date': '07/01/2012', 'address': '5412 N CLARK'} {'date': '07/01/2012', 'address': '4801 N BROADWAY'} >>>
W tym ostatnim przykładzie sortowanie rekordów nie jest konieczne. Dlatego jeśli zajęcie większej ilości pamięci nie jest problemem, efekt można uzyskać szybciej niż przy wcześniejszym sortowaniu i późniejszym przechodzeniu po danych za pomocą funkcji groupby().
1.16. Filtrowanie elementów sekwencji Problem W sekwencji znajdują się dane. Programista chce pobrać wartości lub zredukować sekwencję na podstawie określonych kryteriów.
Rozwiązanie Najłatwiejszym sposobem na przefiltrowanie danych z sekwencji jest zastosowanie wyrażeń listowych: >>> mylist = [1, 4, -5, 10, -7, 2, 3, -1] >>> [n for n in mylist if n > 0] [1, 4, 10, 2, 3] >>> [n for n in mylist if n < 0] [-5, -7, -1] >>>
Wadą stosowania wyrażeń listowych jest to, że jeśli dane wejściowe są duże, także wynik zajmuje dużo miejsca. Jeżeli stanowi to problem, można wykorzystać wyrażenia z generatorem, aby iteracyjnie uzyskać przefiltrowane wartości. Oto przykład: >>> pos = (n for n in mylist if n > 0) >>> pos at 0x1006a0eb0> >>> for x in pos: ... print(x) ... 1 4 10 2 3 >>>
1.16. Filtrowanie elementów sekwencji
37
Czasem w wyrażeniu listowym lub wyrażeniu z generatorem nie da się łatwo przedstawić kryteriów filtrowania. Załóżmy, że proces filtrowania wymaga obsługi wyjątków lub innych skomplikowanych operacji. Wtedy kod filtrujący można umieścić w odrębnej funkcji i zastosować wbudowaną funkcję filter(). Oto przykład: values = ['1', '2', '-3', '-', '4', 'N/A', '5'] def is_int(val): try: x = int(val) return True except ValueError: return False ivals = list(filter(is_int, values)) print(ivals) # Zwracane dane ['1', '2', '-3', '4', '5']
Funkcja filter() tworzy iterator, dlatego jeśli chcesz uzyskać listę wyników, zastosuj funkcję list() w przedstawiony tu sposób.
Omówienie Wyrażenia listowe i wyrażenia z generatorami są często najłatwiejszym sposobem na przefiltrowanie prostych danych. Dodatkową zaletą jest możliwość przekształcania danych w trakcie filtrowania: >>> mylist = [1, 4, -5, 10, -7, 2, 3, -1] >>> import math >>> [math.sqrt(n) for n in mylist if n > 0] [1.0, 2.0, 3.1622776601683795, 1.4142135623730951, 1.7320508075688772] >>>
Jedną z modyfikacji filtrowania jest zastępowanie wartości, które nie spełniają kryteriów, nowymi danymi (zamiast usuwania wartości). Zamiast tylko wyszukiwać wartości dodatnie, można modyfikować nieodpowiednie dane i dostosowywać je do określonego przedziału. Często efekt ten można łatwo uzyskać, umieszczając kryterium filtrowania w wyrażeniu warunkowym: >>> >>> [1, >>> >>> [0, >>>
clip_neg = [n if n > 0 else 0 for n in mylist] clip_neg 4, 0, 10, 0, 2, 3, 0] clip_pos = [n if n < 0 else 0 for n in mylist] clip_pos 0, -5, 0, -7, 0, 0, -1]
Inne warte uwagi narzędzie do filtrowania to itertools.compress(). Przyjmuje ono iterowalny obiekt i powiązaną sekwencję wartości logicznych pełniących funkcję selektora. Dane wyjściowe to wszystkie elementy z obiektu iterowalnego, dla których powiązana wartość logiczna to True. Może to być przydatne, jeśli chcesz wykorzystać wynik przefiltrowania jednej sekwencji w innej, powiązanej sekwencji. Załóżmy, że istnieją dwie kolumny danych: addresses = [ '5412 N CLARK', '5148 N CLARK', '5800 E 58TH', '2122 N CLARK' '5645 N RAVENSWOOD',
38
Rozdział 1. Algorytmy i struktury danych
'1060 W ADDISON', '4801 N BROADWAY', '1039 W GRANVILLE', ] counts = [ 0, 3, 10, 4, 1, 7, 6, 1]
Teraz przyjmijmy, że programista chce utworzyć listę wszystkich adresów, dla których powiązana wartość jest większa niż pięć. Oto kod, który pozwala uzyskać taki efekt: >>> from itertools import compress >>> more5 = [n > 5 for n in counts] >>> more5 [False, False, True, False, False, True, True, False] >>> list(compress(addresses, more5)) ['5800 E 58TH', '4801 N BROADWAY', '1039 W GRANVILLE'] >>>
Rozwiązanie polega na tym, że kod najpierw tworzy sekwencję wartości logicznych określających, które elementy spełniają warunek. Funkcja compress() pobiera następnie elementy powiązane z wartościami True. Funkcja compress() (podobnie jak filter()) standardowo zwraca iterator. Dlatego trzeba wywołać funkcję list(), aby w razie potrzeby przekształcić wyniki na listę.
1.17. Pobieranie podzbioru słownika Problem Programista chce utworzyć słownik, który jest podzbiorem innego słownika.
Rozwiązanie Pożądany efekt można łatwo uzyskać za pomocą wyrażenia słownikowego. Oto przykład: prices = { 'ACME': 45.23, 'AAPL': 612.78, 'IBM': 205.55, 'HPQ': 37.20, 'FB': 10.75 } # Tworzenie słownika, w którym wszystkie ceny są wyższe niż 200 p1 = { key:value for key, value in prices.items() if value > 200 } # Tworzenie słownika akcji spółek technologicznych tech_names = { 'AAPL', 'IBM', 'HPQ', 'MSFT' } p2 = { key:value for key,value in prices.items() if key in tech_names }
Omówienie Wiele wyników, jakie można osiągnąć przy użyciu wyrażeń słownikowych, można uzyskać także dzięki utworzeniu sekwencji krotek i przekazaniu ich do funkcji dict(). Oto przykład: p1 = dict((key, value) for key, value in prices.items() if value > 200)
1.17. Pobieranie podzbioru słownika
39
Jednak rozwiązanie z wykorzystaniem wyrażenia słownikowego jest bardziej przejrzyste i działa wyraźnie szybciej (ponad dwukrotnie szybciej dla słownika prices z tego przykładu). Czasem to samo zadanie można wykonać na kilka sposobów. Drugi przykład można zmodyfikować tak: # Tworzenie słownika akcji spółek technologicznych tech_names = { 'AAPL', 'IBM', 'HPQ', 'MSFT' } p2 = { key:prices[key] for key in prices.keys() & tech_names
Jednak pomiary wykazały, że to podejście jest niemal 1,6 razy wolniejsze od pierwszego rozwiązania. Jeśli wydajność ma znaczenie, zwykle warto poświęcić czas na jej zbadanie. Konkretne informacje na temat pomiaru czasu i profilowania kodu znajdziesz w recepturze 14.13.
1.18. Odwzorowywanie nazw na elementy sekwencji Problem Programista korzysta z elementów listy lub krotki na podstawie ich pozycji, jednak to powoduje, że kod jest czasem nieczytelny. Ponadto programista chce być w mniejszym stopniu zależny od pozycji w strukturze i chciałby móc wskazywać elementy za pomocą nazw.
Rozwiązanie Metoda collections.namedtuple() pozwala uzyskać pożądany efekt, a koszty korzystania z niej są tylko minimalnie większe niż koszty używania zwykłych krotek. Metoda ta jest metodą fabryczną, która zwraca podklasę standardowego typu tuple Pythona. Należy podać nazwę typu i jego pola, a metoda zwróci klasę. Aby utworzyć obiekt tej klasy, należy podać wartości zdefiniowanych pól. Oto przykład: >>> from collections import namedtuple >>> Subscriber = namedtuple('Subscriber', ['addr', 'joined']) >>> sub = Subscriber('[email protected]', '2012-10-19') >>> sub Subscriber(addr='[email protected]', joined='2012-10-19') >>> sub.addr '[email protected]' >>> sub.joined '2012-10-19' >>>
Choć egzemplarz klasy namedtuple wygląda jak zwykły obiekt, obsługuje wszystkie standardowe operacje krotek (np. wypakowywanie i wskazywanie elementów za pomocą indeksu) i można go stosować zamiast krotki: >>> len(sub) 2 >>> addr, joined = sub >>> addr '[email protected]' >>> joined '2012-10-19' >>>
40
Rozdział 1. Algorytmy i struktury danych
Nazwane krotki służą przede wszystkim do oddzielania kodu od pozycji używanych elementów. Jeśli w wyniku wywołania skierowanego do bazy danych pobierasz długą listę krotek i manipulujesz nimi, wskazując elementy z określonych pozycji, kod przestanie działać, gdy dodasz do tabeli nową kolumnę. Jeżeli jednak najpierw zrzutujesz zwrócone krotki na krotki nazwane, problem ten nie wystąpi. Oto przykładowy kod, w którym zastosowano zwykłe krotki: def compute_cost(records): total = 0.0 for rec in records: total += rec[1] * rec[2] return total
Wskazywanie elementów na podstawie pozycji często sprawia, że kod jest mniej uniwersalny i w większym stopniu zależny od struktury rekordów. Oto wersja oparta na typie namedtuple: from collections import namedtuple Stock = namedtuple('Stock', ['name', 'shares', 'price']) def compute_cost(records): total = 0.0 for rec in records: s = Stock(*rec) total += s.shares * s.price return total
Oczywiście można uniknąć bezpośredniego przekształcania danych na krotki nazwane typu Stock, jeśli przykładowa sekwencja records zawiera już takie krotki.
Omówienie Krotki typu namedtuple można zastosować zamiast słownika (zajmuje on więcej pamięci). Dlatego jeśli tworzysz duże struktury danych obejmujące słowniki, wykorzystanie krotek typu namedtuple będzie wydajniejsze. Warto jednak pamiętać, że takie krotki — w odróżnieniu od słowników — są niemodyfikowalne. Oto przykład: >>> s = Stock('ACME', 100, 123.45) >>> s Stock(name='ACME', shares=100, price=123.45) >>> s.shares = 75 Traceback (most recent call last): File "", line 1, in AttributeError: can't set attribute >>>
Jeśli chcesz zmienić dowolny z atrybutów, możesz to zrobić za pomocą metody _replace() obiektu typu namedtuple. Powoduje ona powstanie nowego obiektu tego rodzaju ze zmodyfikowanymi wartościami: >>> s = s._replace(shares=75) >>> s Stock(name='ACME', shares=75, price=123.45) >>>
Metodę _replace() można wykorzystać do uzupełnienia krotek nazwanych z opcjonalnymi lub pustymi polami. W tym celu należy utworzyć krotkę prototypową z wartościami domyślnymi, a następnie zastosować metodę _replace() do tworzenia nowych obiektów ze zmodyfikowanymi wartościami:
1.18. Odwzorowywanie nazw na elementy sekwencji
41
from collections import namedtuple Stock = namedtuple('Stock', ['name', 'shares', 'price', 'date', 'time']) # Tworzenie obiektu prototypowego stock_prototype = Stock('', 0, 0.0, None, None) # Funkcja przekształcająca słownik na obiekt typu Stock def dict_to_stock(s): return stock_prototype._replace(**s)
Kod ten działa w następujący sposób: >>> a = {'name': 'ACME', 'shares': 100, 'price': 123.45} >>> dict_to_stock(a) Stock(name='ACME', shares=100, price=123.45, date=None, time=None) >>> b = {'name': 'ACME', 'shares': 100, 'price': 123.45, 'date': '12/17/2012'} >>> dict_to_stock(b) Stock(name='ACME', shares=100, price=123.45, date='12/17/2012', time=None) >>>
Warto również wspomnieć, że jeśli programista chce utworzyć wydajną strukturę danych, w której modyfikowane będą różne atrybuty, typ namedtuple nie jest najlepszym wyborem. Zamiast tego można zdefiniować klasę z wykorzystaniem zmiennych __slots__ (zobacz recepturę 8.4).
1.19. Jednoczesne przekształcanie i redukowanie danych Problem Programista chce wywołać funkcję redukcyjną (np. sum(), min() lub max()), ale najpierw musi przekształcić lub przefiltrować dane.
Rozwiązanie Bardzo eleganckim sposobem na połączenie redukcji i przekształcania danych jest zastosowanie argumentu w postaci wyrażenia z generatorem. Aby obliczyć sumę kwadratów, można zastosować następujący kod: nums = [1, 2, 3, 4, 5] s = sum(x * x for x in nums)
Oto kilka innych przykładów: # Sprawdzanie, czy w katalogu znajdują się pliki .py import os files = os.listdir('dirname') if any(name.endswith('.py') for name in files): print('Katalog z Pythonem!') else: print('Niestety, nie ma Pythona.') # Wyświetlanie krotki w formacie CSV s = ('ACME', 50, 123.45) print(','.join(str(x) for x in s))
42
Rozdział 1. Algorytmy i struktury danych
# Redukowanie danych z pól ze struktury danych portfolio = [ {'name':'GOOG', 'shares': 50}, {'name':'YHOO', 'shares': 75}, {'name':'AOL', 'shares': 20}, {'name':'SCOX', 'shares': 65} ] min_shares = min(s['shares'] for s in portfolio)
Omówienie W tym rozwiązaniu przedstawiono pewien aspekt wyrażeń z generatorem podawanych jako jedyny argument funkcji — nie wymagają one powtarzania nawiasów. Obie poniższe instrukcje działają tak samo: s = sum((x * x for x in nums)) s = sum(x * x for x in nums)
# Przekazywanie wyrażenia z generatorem jako argumentu # Bardziej elegancka składnia
Podawanie argumentu w postaci generatora to często wydajniejsze i bardziej eleganckie podejście niż tworzenie najpierw pomocniczej listy. Jeśli nie zastosujesz wyrażenia z generatorem, możesz napisać kod w inny sposób: nums = [1, 2, 3, 4, 5] s = sum([x * x for x in nums])
To rozwiązanie działa, jednak wymaga nowego kroku i utworzenia dodatkowej listy. Gdy lista jest mała, jest to nieistotne, gdyby jednak lista nums była długa, powstałaby duża pomocnicza struktura danych, używana tylko raz, a następnie usuwana. Rozwiązanie z generatorem pozwala iteracyjnie przekształcać dane, dlatego jest znacznie wydajniejsze ze względu na pamięć. Niektóre funkcje redukcyjne (np. min() i max()) przyjmują argument key. Może on okazać się przydatny w sytuacjach, gdy zamierzasz zastosować generator. Przykładowy kod z kolekcją portfolio można zastąpić następującym: # Pierwotna wersja zwraca 20 min_shares = min(s['shares'] for s in portfolio) # Alternatywna wersja zwraca {'name': 'AOL', 'shares': 20} min_shares = min(portfolio, key=lambda s: s['shares'])
1.20. Łączenie wielu odwzorowań w jedno Problem Istnieje kilka słowników lub odwzorowań, które programista chce logicznie scalić w jedno odwzorowanie w celu wykonania pewnych operacji — np. wyszukania wartości lub sprawdzenia, czy w danych występują określone klucze.
Rozwiązanie Załóżmy, że istnieją dwa słowniki: a = {'x': 1, 'z': 3 } b = {'y': 2, 'z': 4 }
1.20. Łączenie wielu odwzorowań w jedno
43
Teraz przyjmijmy, że programista chce wyszukiwać dane w obu słownikach (np. najpierw w słowniku a, a następnie — jeśli danych nie znaleziono — w b). Łatwym sposobem na wykonanie tego zadania jest wykorzystanie klasy ChainMap z modułu collections. Oto przykład: from collections import ChainMap c = ChainMap(a,b) print(c['x']) # Wyświetla 1 (z a) print(c['y']) # Wyświetla 2 (z b) print(c['z']) # Wyświetla 3 (z a)
Omówienie Obiekt typu ChainMap przyjmuje kilka odwzorowań i sprawia, że można ich używać jak jednego. Jednak odwzorowania te nie są scalane ze sobą. Obiekt typu ChainMap przechowuje listę odwzorowań i sprawia, że standardowe operacje słownikowe są wykonywane na tej liście. Pozwala to na wykonywanie większości zadań. Oto przykład: >>> len(c) 3 >>> list(c.keys()) ['x', 'y', 'z'] >>> list(c.values()) [1, 2, 3] >>>
Jeśli klucze się powtarzają, używane są wartości z pierwszego odwzorowania. Dlatego zapis c['z'] w przykładzie zawsze dotyczy wartości ze słownika a, a nie ze słownika b. Operacje modyfikujące dane zawsze dotyczą odwzorowania, które znajduje się wcześniej na liście. Oto przykład: >>> c['z'] = 10 >>> c['w'] = 40 >>> del c['x'] >>> a {'w': 40, 'z': 10} >>> del c['y'] Traceback (most recent call last): ... KeyError: "Key not found in the first mapping: 'y'" >>>
Klasa ChainMap jest przydatna zwłaszcza przy stosowaniu wartości dostępnych w określonym zasięgu, np. zmiennych w języku programowania (globalnych, lokalnych itd.). Istnieją metody, które ułatwiają wykonywanie potrzebnych w tym kontekście zadań: >>> values = ChainMap() >>> values['x'] = 1 >>> # Dodawanie nowego odwzorowania >>> values = values.new_child() >>> values['x'] = 2 >>> # Dodawanie nowego odwzorowania >>> values = values.new_child() >>> values['x'] = 3 >>> values ChainMap({'x': 3}, {'x': 2}, {'x': 1}) >>> values['x'] 3 >>> # Usuwanie ostatniego odwzorowania >>> values = values.parents >>> values['x']
To podejście działa, ale wymaga utworzenia odrębnego obiektu słownika (lub destrukcyjnej modyfikacji jednego z istniejących). Ponadto jeśli zawartość któregoś z pierwotnych słowników się zmieni, nie zostanie to odzwierciedlone w scalonym słowniku: >>> a['x'] = 13 >>> merged['x'] 1
Klasa ChainMap wykorzystuje pierwotne słowniki, dlatego opisane problemy nie występują. Oto przykład: >>> >>> >>> >>> 1 >>> >>> 42 >>>
a = {'x': 1, 'z': 3 } b = {'y': 2, 'z': 4 } merged = ChainMap(a, b) merged['x'] a['x'] = 42 merged['x']
# Zwróć uwagę na zmianę w scalonych słownikach
1.20. Łączenie wielu odwzorowań w jedno
45
46
Rozdział 1. Algorytmy i struktury danych
ROZDZIAŁ 2.
Łańcuchy znaków i tekst
W prawie każdym przydatnym programie potrzebne jest przetwarzanie tekstu — czy to przy parsowaniu informacji, czy to przy generowaniu danych wyjściowych. W tym rozdziale koncentrujemy się na standardowych problemach związanych z manipulowaniem tekstem, m.in. na rozdzielaniu, przeszukiwaniu, podstawianiu, analizie leksykalnej i parsowaniu łańcuchów znaków. Wiele z tego rodzaju zadań można łatwo wykonać za pomocą wbudowanych metod łańcuchów znaków. Jednak bardziej skomplikowane operacje wymagają wykorzystania wyrażeń regularnych lub tworzenia kompletnych parserów. Tu omówiono wszystkie te zagadnienia, a ponadto wyjaśniono pewne skomplikowane aspekty stosowania kodowania Unicode.
2.1. Podział łańcuchów znaków po wykryciu dowolnego z różnych ograniczników Problem Programista chce podzielić łańcuch znaków na pola, jednak poszczególne ograniczniki (i odstępy wokół nich) nie są identyczne.
Rozwiązanie Metoda split() łańcuchów znaków jest przeznaczona do stosowania w bardzo prostych sytuacjach i nie umożliwia obsługi różnych ograniczników ani nie uwzględnia odstępów wokół nich. Jeśli potrzebnych jest więcej możliwości, należy zastosować metodę re.split(): >>> line = 'asdf fjdk; afed, fjek,asdf, foo' >>> import re >>> re.split(r'[;,\s]\s*', line) ['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']
Omówienie Funkcja re.split() jest przydatna, ponieważ pozwala podać różne wzorce stosowane jako separator. W rozwiązaniu separatorem jest albo przecinek (,), albo średnik (;), albo odstęp. Po każdym z tych symboli może występować dowolna liczba odstępów. Jeśli program wykryje któryś z tych wzorców, cały dopasowany fragment jest uznawany za ogranicznik 47
rozdzielający pola leżące po obu stronach. W efekcie otrzymujemy listę pól (tak jak przy stosowaniu metody str.split()). Przy korzystaniu z metody re.split() trzeba zachować ostrożność, jeśli wzorzec z wyrażeniem regularnym obejmuje zapisaną w nawiasach grupę przechwytującą. Takie grupy powodują, że w wynikach pojawia się także dopasowywany tekst. Zobacz, jak zadziała poniższa instrukcja: >>> fields = re.split(r'(;|,|\s)\s*', line) >>> fields ['asdf', ' ', 'fjdk', ';', 'afed', ',', 'fjek', ',', 'asdf', ',', 'foo'] >>>
W niektórych sytuacjach pobieranie ograniczników może być przydatne. Mogą one pomóc np. w modyfikowaniu wynikowego łańcucha znaków: >>> values = fields[::2] >>> delimiters = fields[1::2] + [''] >>> values ['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo'] >>> delimiters [' ', ';', ',', ',', ',', ''] >>> # Przekształcanie wiersza za pomocą takich samych ograniczników >>> ''.join(v+d for v,d in zip(values, delimiters)) 'asdf fjdk;afed,fjek,asdf,foo' >>>
Jeśli nie chcesz umieszczać ograniczników w wynikach, ale potrzebujesz nawiasów do pogrupowania fragmentów wzorca wyrażenia regularnego, zastosuj grupę nieprzechwytującą w formie (?:…). Oto przykład: >>> re.split(r'(?:,|;|\s)\s*', line) ['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo'] >>>
2.2. Dopasowywanie tekstu do początkowej lub końcowej części łańcucha znaków Problem Programista chce sprawdzić, czy na początku lub końcu łańcucha znaków występuje określony wzorzec tekstowy, np. rozszerzenie pliku, adres URL o określonym schemacie itd.
Rozwiązanie Prostym sposobem na sprawdzenie początku lub końca łańcucha znaków jest zastosowanie metod str.startswith() i str.endswith(). Oto przykład: >>> filename = 'spam.txt' >>> filename.endswith('.txt') True >>> filename.startswith('file:') False >>> url = 'http://www.python.org'
48
Rozdział 2. Łańcuchy znaków i tekst
>>> url.startswith('http:') True >>>
Aby sprawdzić kilka wzorców, wystarczy krotkę z nimi przekazać do metody startswith() lub endswith(): >>> import os >>> filenames = os.listdir('.') >>> filenames [ 'Makefile', 'foo.c', 'bar.py', 'spam.c', 'spam.h' ] >>> [name for name in filenames if name.endswith(('.c', '.h')) ] ['foo.c', 'spam.c', 'spam.h' >>> any(name.endswith('.py') for name in filenames) True >>>
Oto następny przykład: from urllib.request import urlopen def read_data(name): if name.startswith(('http:', 'https:', 'ftp:')): return urlopen(name).read() else: with open(name) as f: return f.read()
Co ciekawe, jest to jedno z miejsc w Pythonie, w których jako dane wejściowe są wymagane krotki. Jeśli wzorce są zapisane na liście lub w zbiorze, najpierw trzeba koniecznie je przekształcić za pomocą metody tuple(): >>> choices = ['http:', 'ftp:'] >>> url = 'http://www.python.org' >>> url.startswith(choices) Traceback (most recent call last): File "", line 1, in TypeError: startswith first arg must be str or a tuple of str, not list >>> url.startswith(tuple(choices)) True >>>
Omówienie Metody startswith() i endswith() zapewniają bardzo wygodny sposób na proste sprawdzanie przedrostków i przyrostków. Podobne operacje można wykonać za pomocą wycinków, jednak w znacznie mniej elegancki sposób. Oto przykład: >>> filename = 'spam.txt' >>> filename[-4:] == '.txt' True >>> url = 'http://www.python.org' >>> url[:5] == 'http:' or url[:6] == 'https:' or url[:4] == 'ftp:' True >>>
Możesz też pomyśleć o wykorzystaniu wyrażeń regularnych, tak jak w poniższym kodzie: >>> import re >>> url = 'http://www.python.org' >>> re.match('http:|https:|ftp:', url) <_sre.SRE_Match object at 0x101253098> >>>
2.2. Dopasowywanie tekstu do początkowej lub końcowej części łańcucha znaków
49
To rozwiązanie działa, jednak przy prostym dopasowywaniu jest przesadą. Kod z receptury jest prostszy i wydajniejszy. Ponadto metody startswith() i endswith() dobrze łączą się z innymi operacjami, np. standardowymi redukcjami danych. Poniższa instrukcja sprawdza, czy w katalogu występują pliki o określonych rozszerzeniach: if any(name.endswith(('.c', '.h')) for name in listdir(dirname)): ..
2.3. Dopasowywanie łańcuchów znaków za pomocą symboli wieloznacznych powłoki Problem Programista chce dopasowywać tekst za pomocą symboli wieloznacznych używanych w powłoce Uniksa (np. *.py,Dat[0-9]*.csv itd.).
Rozwiązanie Moduł fnmatch udostępnia dwie funkcje, fnmatch() i fnmatchcase(), które można wykorzystać do dopasowywania tekstu w ten sposób. Stosowanie tych funkcji jest proste: >>> from fnmatch import fnmatch, fnmatchcase >>> fnmatch('foo.txt', '*.txt') True >>> fnmatch('foo.txt', '?oo.txt') True >>> fnmatch('Dat45.csv', 'Dat[0-9]*') True >>> names = ['Dat1.csv', 'Dat2.csv', 'config.ini', 'foo.py'] >>> [name for name in names if fnmatch(name, 'Dat*.csv')] ['Dat1.csv', 'Dat2.csv'] >>>
Funkcja fnmatch() przy dopasowywaniu uwzględnia (lub nie) wielkość liter w zależności od systemu operacyjnego (różne systemy działają w odmienny sposób). Oto przykład: >>> # W systemie OS X (komputery Mac) >>> fnmatch('foo.txt', '*.TXT') False >>> # W systemie Windows >>> fnmatch('foo.txt', '*.TXT') True >>>
Jeśli wielkość liter ma znaczenie, należy zastosować funkcję fnmatchcase(). Dopasowuje ona tekst z uwzględnieniem podanych dużych i małych liter: >>> fnmatchcase('foo.txt', '*.TXT') False >>>
50
Rozdział 2. Łańcuchy znaków i tekst
Często pomijaną cechą tych funkcji jest możliwość ich zastosowania do przetwarzania danych z łańcuchów znaków innych niż nazwy plików. Załóżmy, że używana jest poniższa lista adresów: addresses = [ '5412 N CLARK ST', '1060 W ADDISON ST', '1039 W GRANVILLE AVE', '2122 N CLARK ST', '4802 N BROADWAY', ]
Można napisać wyrażenie listowe w następującej postaci: >>> from fnmatch import fnmatchcase >>> [addr for addr in addresses if fnmatchcase(addr, '* ST')] ['5412 N CLARK ST', '1060 W ADDISON ST', '2122 N CLARK ST'] >>> [addr for addr in addresses if fnmatchcase(addr, '54[0-9][0-9] *CLARK*')] ['5412 N CLARK ST'] >>>
Omówienie Dopasowywanie z wykorzystaniem modułu fnmatch można umiejscowić między prostymi metodami łańcuchów znaków a rozbudowanymi możliwościami wyrażeń regularnych. Moduł ten często jest sensownym wyborem, gdy programista chce tylko udostępnić prosty mechanizm umożliwiający stosowanie symboli wieloznacznych przy przetwarzaniu danych. Jeśli chcesz napisać kod przeznaczony do dopasowywania nazw plików, wykorzystaj moduł glob (zobacz recepturę 5.13).
2.4. Dopasowywanie i wyszukiwanie wzorców tekstowych Problem Programista chce dopasować lub znaleźć tekst odpowiadający określonemu wzorcowi.
Rozwiązanie Jeśli szukany tekst to prosty literał, często wystarczą podstawowe metody łańcuchów znaków, np. str.find(), str.endswith(), str.startswith() itd. Oto przykład: >>> text = 'tak, ale nie, ale tak, ale nie, ale tak' >>> # Dokładne dopasowanie >>> text == 'tak' False >>> # Dopasowanie na początku lub na końcu >>> text.startswith('tak') True >>> text.endswith('nie') False
2.4. Dopasowywanie i wyszukiwanie wzorców tekstowych
51
>>> # Określanie miejsca pierwszego wystąpienia >>> text.find('nie') 9 >>>
Przy bardziej skomplikowanym dopasowywaniu zastosuj wyrażenia regularne i moduł re. Aby zilustrować podstawy stosowania wyrażeń regularnych, załóżmy, że programista chce znajdować daty podane za pomocą cyfr, np. „11/27/2012”. Oto przykładowe rozwiązanie: >>> >>> >>> >>> >>> >>> ... ... ... ... Tak >>> ... ... ... ... Nie >>>
text1 = '11/27/2012' text2 = 'Nov 27, 2012' import re # Proste dopasowywanie — \d+ oznacza dopasowanie jednej lub kilku cyfr if re.match(r'\d+/\d+/\d+', text1): print('Tak') else: print('Nie') if re.match(r'\d+/\d+/\d+', text2): print('Tak') else: print('Nie')
Jeśli zamierza się wielokrotnie dopasowywać tekst do tego samego wzorca, zwykle warto wstępnie skompilować wzorzec z wyrażeniem regularnym do obiektu wzorca: >>> >>> ... ... ... ... Tak >>> ... ... ... ... Nie >>>
datepat = re.compile(r'\d+/\d+/\d+') if datepat.match(text1): print('Tak') else: print('Nie') if datepat.match(text2): print('Tak') else: print('Nie')
Funkcja match() zawsze próbuje znaleźć dopasowanie na początku łańcucha znaków. Jeśli chcesz znaleźć w tekście wszystkie wystąpienia wzorca, zastosuj metodę findall(): >>> text = 'Dziś jest 11/27/2012. PyCon rozpoczyna się 3/13/2013.' >>> datepat.findall(text) ['11/27/2012', '3/13/2013'] >>>
W definicjach wyrażeń regularnych często stosuje się grupy przechwytujące. W tym celu część wzorca należy umieścić w nawiasach: >>> datepat = re.compile(r'(\d+)/(\d+)/(\d+)') >>>
Grupy przechwytujące często upraszczają późniejsze przetwarzanie dopasowanego tekstu, ponieważ można osobno pobrać zawartość każdej grupy. Oto przykład:
52
Rozdział 2. Łańcuchy znaków i tekst
>>> m = datepat.match('11/27/2012') >>> m <_sre.SRE_Match object at 0x1005d2750> >>> # Pobieranie zawartości każdej grupy >>> m.group(0) '11/27/2012' >>> m.group(1) '11' >>> m.group(2) '27' >>> m.group(3) '2012' >>> m.groups() ('11', '27', '2012') >>> month, day, year = m.groups() >>> >>> # Wyszukiwanie wszystkich pasujących fragmentów (zwróć uwagę na podział na krotki) >>> text 'Dziś jest 11/27/2012. PyCon rozpoczyna się 3/13/2013.' >>> datepat.findall(text) [('11', '27', '2012'), ('3', '13', '2013')] >>> for month, day, year in datepat.findall(text): ... print('{}-{}-{}'.format(year, month, day)) ... 2012-11-27 2013-3-13 >>>
Metoda findall() przeszukuje tekst i znajduje wszystkie pasujące fragmenty, po czym zwraca je w postaci listy. Jeśli chcesz iteracyjnie znaleźć wszystkie dopasowania, zastosuj metodę finditer(). Oto przykład: >>> for m in datepat.finditer(text): ... print(m.groups()) ... ('11', '27', '2012') ('3', '13', '2013') >>>
Omówienie Przedstawianie prostego samouczka z zakresu teorii wyrażeń regularnych wykracza poza zakres tej książki. Jednak w recepturze tej pokazano bardzo podstawowe techniki stosowania modułu re do dopasowywania i wyszukiwania tekstu. Mechanizm polega na początkowym skompilowaniu wzorca za pomocą metody re.compile() i późniejszym zastosowaniu takich metod jak match(), findall() i finditer(). W trakcie tworzenia wzorca stosunkowo często podaje się nieprzetworzone łańcuchy znaków, np. r'(\d+)/(\d+)/(\d+)'. W takich łańcuchach lewy ukośnik nie jest interpretowany, co może być przydatne w kontekście wyrażeń regularnych. W standardowych łańcuchach znaków trzeba podać dwa lewe ukośniki, np. '(\\d+)/(\\d+)/(\\d+)'. Warto pamiętać, że metoda match() sprawdza tylko początek łańcucha znaków. Możliwe, że doprowadzi to do dopasowania nieoczekiwanych danych: >>> m = datepat.match('11/27/2012abcdef') >>> m <_sre.SRE_Match object at 0x1005d27e8>
2.4. Dopasowywanie i wyszukiwanie wzorców tekstowych
53
>>> m.group() '11/27/2012' >>>
Jeśli chcesz znaleźć dokładne dopasowanie, upewnij się, że wzorzec obejmuje znacznik końca ($), tak jak w poniższym kodzie: >>> datepat = re.compile(r'(\d+)/(\d+)/(\d+)$') >>> datepat.match('11/27/2012abcdef') >>> datepat.match('11/27/2012') <_sre.SRE_Match object at 0x1005d2750> >>>
Jeśli wykonujesz tylko proste operacje dopasowywania lub wyszukiwania tekstu, często możesz pominąć etap kompilacji i zastosować funkcje z poziomu modułu re. Oto przykład: >>> re.findall(r'(\d+)/(\d+)/(\d+)', text) [('11', '27', '2012'), ('3', '13', '2013')] >>>
Warto przy tym pamiętać, że jeśli chcesz często dopasowywać lub wyszukiwać dany wzorzec, dobrze jest go najpierw skompilować. Następnie można z niego wielokrotnie korzystać. Funkcje z poziomu modułu przechowują pamięć podręczną ostatnio skompilowanych wzorców, dlatego ich ponowne kompilowanie nie jest dużym obciążeniem. Jednak stosując własny skompilowany wzorzec, można zmniejszyć liczbę operacji wyszukiwania i skrócić przetwarzanie.
2.5. Wyszukiwanie i zastępowanie tekstu Problem Programista chce wyszukiwać i zastępować wzorzec tekstowy w łańcuchu znaków.
Rozwiązanie Dla prostych wzorców w formie literału można wykorzystać metodę str.replace(). Oto przykład: >>> text = 'tak, ale nie, ale tak, ale nie, ale tak' >>> text.replace('tak', 'ta') 'ta, ale nie, ale ta, ale nie, ale ta' >>>
Gdy wzorce są bardziej skomplikowane, należy wykorzystać funkcje i metody sub() z modułu re. Załóżmy, że chcesz zastąpić dane w formacie „11/27/2012” datami w postaci „2012-11-27”. Oto przykładowe rozwiązanie: >>> text = 'Dziś jest 11/27/2012. PyCon rozpoczyna się 3/13/2013.' >>> import re >>> re.sub(r'(\d+)/(\d+)/(\d+)', r'\3-\1-\2', text) 'Dziś jest 2012-11-27. PyCon rozpoczyna się 2013-3-13.' >>>
Pierwszy argument funkcji sub() to dopasowywany wzorzec, a drugim jest wzorzec nowego tekstu. Cyfry z lewym ukośnikiem, np. \3, reprezentują numery grup przechwytujących z dopasowywanego wzorca.
54
Rozdział 2. Łańcuchy znaków i tekst
Jeśli zamierzasz wielokrotnie zastępować ten sam wzorzec, warto go skompilować, aby poprawić wydajność kodu: >>> import re >>> datepat = re.compile(r'(\d+)/(\d+)/(\d+)') >>> datepat.sub(r'\3-\1-\2', text) 'Dziś jest 2012-11-27. PyCon rozpoczyna się 2013-3-13.' >>>
Przy bardziej skomplikowanym podstawianiu można podać wywoływaną zwrotnie funkcję. Oto przykład: >>> from calendar import month_abbr >>> def change_date(m): ... mon_name = month_abbr[int(m.group(1))] ... return '{} {} {}'.format(m.group(2), mon_name, m.group(3)) ... >>> datepat.sub(change_date, text) 'Dziś jest 27 Nov 2012. PyCon rozpoczyna się 13 Mar 2013.' >>>
Argumentem dla wywoływanej zwrotnie funkcji zastępującej jest obiekt dopasowania zwrócony przez funkcję match() lub find(). Za pomocą metody .group() można pobrać z tego obiektu konkretne fragmenty dopasowania. Wywoływana zwrotnie funkcja powinna zwracać tekst zastępczy. Jeśli nie tylko pobierasz tekst zastępczy, ale chcesz się również dowiedzieć, ile zmian zostało wprowadzonych, zastosuj funkcję re.subn(): >>> newtext, n = datepat.subn(r'\3-\1-\2', text) >>> newtext 'Dziś jest 2012-11-27. PyCon rozpoczyna się 2013-3-13.' >>> n 2 >>>
Omówienie Wyszukiwanie i zastępowanie z wykorzystaniem wyrażeń regularnych opiera się głównie na przedstawionej metodzie sub(). Najbardziej skomplikowanym aspektem jest podawanie wzorca z wyrażeniem regularnym — to zadanie najlepiej zostawić jako ćwiczenie dla czytelników.
2.6. Wyszukiwanie i zastępowanie tekstu bez uwzględniania wielkości liter Problem Programista chce wyszukiwać i zastępować tekst bez uwzględniania wielkości liter.
Rozwiązanie Aby przeprowadzić operacje na tekście bez uwzględniania wielkości znaków, należy zastosować moduł re i użyć flagi re.IGNORECASE w odpowiednich operacjach:
2.6. Wyszukiwanie i zastępowanie tekstu bez uwzględniania wielkości liter
Przykład ten pokazuje pewne ograniczenie — wielkość liter w tekście zastępczym jest inna niż w pierwotnym. Jeśli chcesz to zmienić, możesz wykorzystać funkcję pomocniczą, taką jak poniższa: def matchcase(word): def replace(m): text = m.group() if text.isupper(): return word.upper() elif text.islower(): return word.lower() elif text[0].isupper(): return word.capitalize() else: return word return replace
Oto przykład wykorzystania tej funkcji: >>> re.sub('python', matchcase('wąż'), text, flags=re.IGNORECASE) 'WĄŻ WIELKIE, wąż małe, Wąż Mieszane' >>>
Omówienie W prostych sytuacjach zastosowanie opcji re.IGNORECASE wystarczy do dopasowywania tekstu bez uwzględniania wielkości liter. Warto jednak pamiętać, że technika ta może okazać się nieodpowiednia przy dopasowywaniu tekstu w formacie Unicode z ujednoliconą wielkością liter (zobacz recepturę 2.10).
2.7. Tworzenie wyrażeń regularnych w celu uzyskania najkrótszego dopasowania Problem Programista chce dopasować wzorzec tekstowy za pomocą wyrażeń regularnych, jednak otrzymuje najdłuższe możliwe dopasowania wzorca, a jemu zależy na tym, aby kod wyszukiwał najkrótsze dopasowanie.
Rozwiązanie Problem ten często występuje we wzorcach, które dopasowują tekst umieszczony między ogranicznikami (czyli tekst między cudzysłowami). Przyjrzyj się przykładowi: >>> str_pat = re.compile(r'\"(.*)\"') >>> text1 = 'Komputer mówi "nie."' >>> str_pat.findall(text1) ['nie.']
56
Rozdział 2. Łańcuchy znaków i tekst
>>> text2 = 'Komputer mówi "nie." Telefon mówi "tak."' >>> str_pat.findall(text2) ['nie." Telefon mówi "tak.'] >>>
W tym przykładzie wzorzec r'\"(.*)\"' dopasowuje tekst umieszczony między cudzysłowami. Jednak operator * w wyrażeniach regularnych działa w sposób zachłanny, dlatego kod wyszukuje najdłuższe możliwe dopasowanie. Z tego powodu w drugim wywołaniu (dla łańcucha text2) kod niepoprawnie dopasowuje dwa łańcuchy znaków umieszczone między cudzysłowami. Aby rozwiązać problem, po operatorze * należy we wzorcu dodać modyfikator ?: >>> str_pat = re.compile(r'\"(.*?)\"') >>> str_pat.findall(text2) ['nie.', 'tak.'] >>>
Dzięki temu dopasowywanie nie przebiega w sposób zachłanny, a kod zwraca najkrótsze dopasowanie.
Omówienie W tej recepturze opisano jeden z problemów często występujących przy pisaniu wyrażeń regularnych ze znakiem kropki (.). We wzorcach kropka pasuje do dowolnego znaku oprócz znaku nowego wiersza. Jeśli jednak kropka znajduje się między początkowym i końcowym tekstem (np. cudzysłowami), kod spróbuje znaleźć najdłuższy fragment pasujący do wzorca. To sprawia, że wiele wystąpień początkowego i końcowego tekstu jest przeskakiwanych i pojawia się w wynikach dłuższego dopasowania. Symbol ? po takich operatorach jak * lub + powoduje, że algorytm dopasowywania wyszukuje najkrótszy pasujący tekst.
2.8. Tworzenie wyrażeń regularnych dopasowywanych do wielowierszowych wzorców Problem Programista chce dopasować blok tekstu za pomocą wyrażenia regularnego, przy czym pasujący tekst znajduje się w kilku wierszach.
Rozwiązanie Problem ten często występuje we wzorcach, w których programista użył kropki (.) do dopasowania dowolnych znaków, ale zapomniał, że nie pasuje ona do znaku nowego wiersza. Załóżmy, że kod ma wyszukiwać komentarze z języka C: >>> >>> >>> ... ... >>> >>>
comment = re.compile(r'/\*(.*?)\*/') text1 = '/* To jest komentarz */' text2 = '''/* To jest komentarz wielowierszowy */ ''' comment.findall(text1)
2.8. Tworzenie wyrażeń regularnych dopasowywanych do wielowierszowych wzorców
57
[' To jest komentarz '] >>> comment.findall(text2) [] >>>
Aby rozwiązać problem, można uwzględnić znaki nowego wiersza: >>> comment = re.compile(r'/\*((?:.|\n)*?)\*/') >>> comment.findall(text2) [' To jest komentarz\n wielowierszowy '] >>>
W tym wzorcu człon (?:.|\n) to grupa nieprzechwytująca. Grupa ta jest dopasowywana, ale nie jest przechwytywana ani numerowana jako odrębna jednostka.
Omówienie Funkcja re.compile() przyjmuje przydatną tu opcję re.DOTALL. Dzięki temu symbol . w wyrażeniu regularnym pasuje do wszystkich znaków, w tym do znaku nowego wiersza. Oto przykład: >>> comment = re.compile(r'/\*(.*?)\*/', re.DOTALL) >>> comment.findall(text2) [' To jest komentarz\n wielowierszowy ']
Opcja re.DOTALL działa dobrze w prostych sytuacjach, jednak może okazać się problematyczna, gdy wzorce są bardzo skomplikowane lub gdy trzeba połączyć kilka odrębnych wyrażeń regularnych w celu podziału tekstu na tokeny (zobacz recepturę 2.18). Jeśli to możliwe, zwykle lepiej jest zdefiniować wzorzec z wyrażeniem regularnym, który działa poprawnie bez konieczności stosowania dodatkowych opcji.
2.9. Przekształcanie tekstu w formacie Unicode na postać standardową Problem Programista używa łańcuchów znaków w formacie Unicode, jednak musi mieć pewność, że wszystkie łańcuchy mają tę samą postać.
Rozwiązanie W formacie Unicode niektóre znaki są reprezentowane przez więcej niż jedną sekwencję wartości. Przyjrzyj się następującemu przykładowi: >>> s1 = 'Papryczka Jalape\u00f1o' >>> s2 = 'Papryczka Jalapen\u0303o' >>> s1 'Papryczka Jalapeño' >>> s2 'Papryczka Jalapeño' >>> s1 == s2 False >>> len(s1) 18 >>> len(s2) 19 >>>
58
Rozdział 2. Łańcuchy znaków i tekst
Tekst „Papryczka Jalapeño” przedstawiono tu w dwóch postaciach. W pierwszej występuje kompletny symbol „ñ” (U+00F1). W drugiej — litera „n” z alfabetu łacińskiego i znak łączony „~” (U+0303). Różne reprezentacje stanowią problem w programach porównujących łańcuchy znaków. Aby go rozwiązać, należy najpierw znormalizować tekst, przekształcając go na postać standardową za pomocą modułu unicodedata: >>> import unicodedata >>> t1 = unicodedata.normalize('NFC', s1) >>> t2 = unicodedata.normalize('NFC', s2) >>> t1 == t2 True >>> print(ascii(t1)) 'Papryczka Jalape\xf1o' >>> t3 = unicodedata.normalize('NFD', s1) >>> t4 = unicodedata.normalize('NFD', s2) >>> t3 == t4 True >>> print(ascii(t3)) 'Spicy Jalapen\u0303o' >>>
Pierwszy argument metody normalize() określa sposób normalizacji łańcucha znaków. Wartość NFC oznacza stosowanie kompletnych znaków (czyli pojedynczych kodów, jeśli istnieją). Wartość NFD powoduje łączenie znaków z ich części składowych. Python obsługuje też opcje NFKC i NFKD, które zapewniają większą kompatybilność potrzebną przy obsłudze niektórych rodzajów znaków. Oto przykład: >>> s = '\ufb01' # Pojedynczy znak >>> s ' ' >>> unicodedata.normalize('NFD', s) ' ' # Zwróć uwagę na to, że znaki łączone tutaj są rozdzielane >>> unicodedata.normalize('NFKD', s) 'fi' >>> unicodedata.normalize('NFKC', s) 'fi' >>>
Omówienie Normalizowanie to ważny aspekt w każdym kodzie, w którym trzeba zapewnić poprawne i spójne przetwarzanie tekstu w formacie Unicode. Dotyczy to zwłaszcza łańcuchów znaków pobranych jako dane wejściowe od użytkownika, ponieważ programista ma wtedy niewielką kontrolę nad zastosowanym kodowaniem. Normalizowanie może być też istotnym etapem zapewniania poprawności i filtrowania tekstu. Załóżmy, że programista chce usunąć z tekstu wszystkie znaki diakrytyczne (np. w celu wyszukiwania lub dopasowywania danych): >>> t1 = unicodedata.normalize('NFD', s1) >>> ''.join(c for c in t1 if not unicodedata.combining(c)) 'Papryczka Jalapeno' >>
2.9. Przekształcanie tekstu w formacie Unicode na postać standardową
59
W ostatnim fragmencie przedstawiono inny ważny aspekt modułu unicodedata — funkcje narzędziowe przeznaczone do sprawdzania klas znaków. Funkcja combining() sprawdza, czy dany symbol jest znakiem łączonym. W module istnieją też inne funkcje, które przeznaczone są do określania kategorii znaków, wykrywania cyfr itd. Kodowanie Unicode to rozległe zagadnienie. Szczegółowe informacje o normalizacji znajdziesz na stronie poświęconej temu tematowi w witrynie Unicode (http://www.unicode.org/faq/normalization.html). Ponadto Ned Batchelder udostępnił w swojej witrynie (http://nedbatchelder.com/text/unipain.html) doskonałą prezentację dotyczącą problemów z obsługą znaków Unicode w Pythonie.
2.10. Używanie znaków Unicode w wyrażeniach regularnych Problem Programista używa wyrażeń regularnych do przetwarzania tekstu, ale martwi się o obsługę znaków Unicode.
Rozwiązanie Moduł re domyślnie ma wbudowaną podstawową obsługę niektórych klas znaków Unicode. Np. ciąg \d pasuje do dowolnej cyfry w formacie Unicode: >>> import re >>> num = re.compile('\d+') >>> # Cyfry ASCII >>> num.match('123') <_sre.SRE_Match object at 0x1007d9ed0> >>> # Cyfry arabskie >>> num.match('\u0661\u0662\u0663') <_sre.SRE_Match object at 0x101234030> >>>
Jeśli chcesz umieścić we wzorcu konkretne znaki Unicode, możesz zastosować standardową sekwencję ucieczki (np. \uFFFF lub \UFFFFFFF). Poniżej znajduje się wyrażenie regularne, które pasuje do wszystkich znaków z kilku różnych stron kodowych dla alfabetu arabskiego: >>> arabic = re.compile('[\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff]+') >>>
Przy dopasowywaniu i wyszukiwaniu warto znormalizować cały tekst i zapewnić jego poprawność, przekształcając go najpierw na postać standardową (zobacz recepturę 2.9). Należy też jednak pamiętać o specjalnych przypadkach. Przyjrzyj się dopasowywaniu bez uwzględniania wielkości znaków w połączeniu z ujednolicaniem wielkości liter: >>> pat = re.compile('stra\u00dfe', re.IGNORECASE) >>> s = 'straße' >>> pat.match(s) # Pasuje <_sre.SRE_Match object at 0x10069d370> >>> pat.match(s.upper()) # Nie pasuje >>> s.upper() # Ujednolicanie wielkości znaków 'STRASSE' >>>
60
Rozdział 2. Łańcuchy znaków i tekst
Omówienie Łączenie znaków Unicode i wyrażeń regularnych to często dobry sposób na wpakowanie się w poważne kłopoty. Jeśli naprawdę chcesz stosować wyrażenia regularne z takimi znakami, pomyśl o zainstalowaniu niezależnej biblioteki wyrażeń regularnych (https://pypi.python.org/pypi/regex), która zapewnia pełną obsługę ujednolicania wielkości liter Unicode, a także udostępnia wiele innych funkcji (m.in. dopasowywanie przybliżone).
2.11. Usuwanie niepożądanych znaków z łańcuchów Problem Programista chce usunąć niepożądane znaki (np. odstępy) z początku, końca lub środkowej części łańcucha.
Rozwiązanie Do usunięcia znaków z początku lub końca łańcucha można zastosować metodę strip(). Wersje lstrip() i rstrip() usuwają znaki z lewej lub prawej części łańcucha. Metody te domyślnie usuwają odstępy, można jednak określić także inne znaki. Oto przykład: >>> # Usuwanie odstępów >>> s = ' Witaj, świecie >>> s.strip() 'Witaj, świecie' >>> s.lstrip() 'Witaj, świecie \n' >>> s.rstrip() ' Witaj, świecie' >>>
Omówienie Różne odmiany metody strip() są powszechnie stosowane przy odczycie i porządkowaniu danych na potrzeby późniejszego przetwarzania. Przy ich użyciu można usunąć odstępy i cudzysłowy, a także wykonać inne zadania. Warto pamiętać, że usuwanie nie dotyczy tekstu w środkowej części łańcucha znaków. Oto przykład: >>> s = ' Witaj, świecie >>> s = s.strip() >>> s 'Witaj, świecie' >>>
\n'
2.11. Usuwanie niepożądanych znaków z łańcuchów
61
Jeśli chcesz zmodyfikować środkową część tekstu, zastosuj inną technikę. Możesz wywołać metodę replace() lub zastąpić tekst, wykorzystując wyrażenia regularne: >>> s.replace(' ', '') 'Witaj,świecie' >>> import re >>> re.sub('\s+', ' ', s) 'Witaj, świecie' >>>
Usuwanie znaków z łańcucha często połączone jest z innego rodzaju iteracyjnymi operacjami, np. wczytywaniem wierszy danych z pliku. Jest to jeden z obszarów, w których przydatne są wyrażenia z generatorem: with open(filename) as f: lines = (line.strip() for line in f) for line in lines: ...
Wyrażenie lines = (line.strip() for line in f) w tym kodzie pozwala przekształcać dane. Jest to wydajne podejście, ponieważ nie wymaga uprzedniego zapisania danych na pomocniczej liście. Kod służy tylko do utworzenia iteratora, a niepożądane znaki usuwane są ze wszystkich generowanych wierszy. Bardziej zaawansowane usuwanie znaków umożliwia metoda translate(). Więcej szczegółów znajdziesz w następnej recepturze, poświęconej zapewnianiu poprawności łańcuchów znaków.
2.12. Zapewnianie poprawności i porządkowanie tekstu Problem Znudzony początkujący haker w formularzu na stronie wpisał tekst „pýtĥöñ”, a programista chce poprawić ten łańcuch.
Rozwiązanie Problem zapewniania poprawności i porządkowania tekstu dotyczy wielu zadań związanych z parsowaniem tekstu i obsługą danych. Do przekształcenia liter na standardową wielkość na bardzo podstawowym poziomie można wykorzystać proste funkcje łańcuchów znaków (np. str.uppter() i str.lower()). Ponadto proste operacje zastępowania, str.replace() lub re.sub(), pozwalają usunąć lub zmodyfikować określone sekwencje znaków. Można też znormalizować tekst, używając metody unicodedata.normalize(), co opisano w recepturze 2.9. Można jednak dodatkowo rozbudować proces zapewniania poprawności. Załóżmy, że programista chce usunąć znaki z określonego zakresu lub znaki diakrytyczne. W tym celu może zastosować często pomijaną metodę str.translate(). Przyjmijmy, że program otrzymał przedstawiony poniżej skomplikowany łańcuch znaków: >>> s = 'pýtĥöñ\fjest\tsuper\r\n' >>> s 'pýtĥöñ\x0cjest\tsuper\r\n' >>>
62
Rozdział 2. Łańcuchy znaków i tekst
Pierwszy krok polega na usunięciu odstępów. W tym celu należy przygotować krótką tablicę translacji i wywołać metodę translate(): >>> remap = { ... ord('\t') : ' ', ... ord('\f') : ' ', ... ord('\r') : None # Usuwane ... } >>> a = s.translate(remap) >>> a 'pýtĥöñ jest super\n' >>>
Jak widać, odstępy reprezentowane przez ciągi \t i \f odwzorowano na pojedynczy odstęp. Znak powrotu karetki (\r) program całkowicie usunął. Można zastosować odwzorowywanie na większą skalę i utworzyć dużo bardziej rozbudowane tablice. Usuńmy wszystkie znaki łączone: >>> import unicodedata >>> import sys >>> cmb_chrs = dict.fromkeys(c for c in range(sys.maxunicode) ... if unicodedata.combining(chr(c))) ... >>> b = unicodedata.normalize('NFD', a) >>> b 'pýtĥöñ jest super\n' >>> b.translate(cmb_chrs) 'python jest super\n' >>>
W tym przykładzie słownik odwzorowujący każdy znak łączony z kodowania Unicode na None jest tworzony za pomocą funkcji dict.fromkeys(). Następnie pierwotne dane wejściowe są normalizowane na standardowe znaki za pomocą metody unicodedata.normalize(). Potem program za pomocą funkcji translate usuwa wszystkie akcenty. Za pomocą podobnych technik można usunąć znaki innych rodzajów (np. znaki sterujące). Oto następny przykład — tu tablica translacji odwzorowuje wszystkie cyfry dziesiętne z formatu Unicode na ich odpowiedniki w formacie ASCII: >>> digitmap = { c: ord('0') + unicodedata.digit(chr(c)) ... for c in range(sys.maxunicode) ... if unicodedata.category(chr(c)) == 'Nd' } ... >>> len(digitmap) 460 >>> # Cyfry arabskie >>> x = '\u0661\u0662\u0663' >>> x.translate(digitmap) '123' >>>
Jeszcze innym sposobem na uporządkowanie tekstu jest zastosowanie funkcji dekodujących i kodujących wejścia-wyjścia. Technika ta polega na tym, aby najpierw przeprowadzić wstępne porządkowanie tekstu, a następnie wywołać kombinację operacji encode() i decode() w celu usunięcia lub zmodyfikowania wybranych znaków. Oto przykład: >>> a 'pýtĥöñ jest super\n' >>> b = unicodedata.normalize('NFD', a)
2.12. Zapewnianie poprawności i porządkowanie tekstu
63
>>> b.encode('ascii', 'ignore').decode('ascii') 'python jest super\n' >>>
Tu proces normalizacji powoduje rozłożenie pierwotnego tekstu na znaki i — zapisane osobno — znaki łączone. Późniejsze kodowanie i dekodowanie ASCII powoduje usunięcie znaków łączonych w jednym kroku. Oczywiście to rozwiązanie zadziała tylko wtedy, jeśli ostatecznym celem jest uzyskanie tekstu w formacie ASCII.
Omówienie Poważnym problemem związanym z zapewnianiem poprawności tekstu jest spadek wydajności. Ogólnie im prostsze są operacje, tym szybciej działają. Przy prostym zastępowaniu tekstu najszybszym rozwiązaniem jest zwykle metoda str.replace() i to nawet wtedy, gdy trzeba wywołać ją wielokrotnie. Aby usunąć odstępy, można zastosować następujący kod: def clean_spaces(s): s = s.replace('\r', '') s = s.replace('\t', ' ') s = s.replace('\f', ' ') return s
Kod ten jest wyraźnie szybszy od wersji z metodą translate() lub podejścia opartego na wyrażeniach regularnych. Metoda translate() okazuje się bardzo szybka, gdy trzeba przeprowadzić skomplikowane odwzorowania „znak na znak” lub w złożony sposób usunąć dane. Wydajność należy zwykle zbadać w kontekście konkretnej aplikacji. Niestety, nie można zaproponować jednej techniki, która jest optymalna we wszystkich sytuacjach. Dlatego należy wypróbować różne podejścia i sprawdzić ich wydajność. Choć w tej recepturze skoncentrowaliśmy się na tekście, podobne techniki można zastosować do bajtów, tak aby za pomocą wyrażeń regularnych wyszukiwać je, zastępować i przekształcać.
2.13. Wyrównywanie łańcuchów znaków Problem Programista chce wyrównać tekst w ramach jego formatowania.
Rozwiązanie Do prostego wyrównywania łańcuchów znaków można zastosować metody ljust(), rjust() i center() poniższych łańcuchów: >>> text = 'Witaj, świecie' >>> text.ljust(20) 'Witaj, świecie ' >>> text.rjust(20) ' Witaj, świecie' >>> text.center(20) ' Witaj, świecie ' >>>
64
Rozdział 2. Łańcuchy znaków i tekst
Wszystkie te metody przyjmują też opcjonalny znak dopełnienia. Oto przykład: >>> text.rjust(20,'=') '======Witaj, świecie' >>> text.center(20,'*') '***Witaj, świecie***' >>>
Także funkcja format() umożliwia łatwe wyrównywanie tekstu. Wystarczy zastosować znak <, > lub ^ i podać pożądaną szerokość: >>> format(text, '>20') 'Witaj, świecie ' >>> format(text, '<20') ' Witaj, świecie' >>> format(text, '^20') ' Witaj, świecie ' >>>
Jeśli chcesz zastosować znak dopełnienia inny niż odstęp, podaj go przed symbolem określającym sposób wyrównania: >>> format(text, '=>20s') '======Witaj, świecie' >>> format(text, '*^20s') '***Witaj, świecie***' >>>
Tego rodzaju kody formatowania można też zastosować w metodzie format() przy określaniu wyglądu wielu wartości. Oto przykład: >>> '{:>10s} {:>10s}'.format('Witaj,', 'świecie') ' Witaj, świecie' >>>
Zaletą metody format() jest to, że działa nie tylko dla łańcuchów znaków. Jest przeznaczona dla wartości dowolnego rodzaju, dzięki czemu jest bardziej uniwersalna. Można ją zastosować np. dla liczb: >>> >>> ' >>> ' >>>
x = 1.2345 format(x, '>10') 1.2345' format(x, '^10.2f') 1.23 '
Omówienie W starszym kodzie do formatowania tekstu stosowano też operator %: >>> '%-20s' % text 'Witaj, świecie ' >>> '%20s' % text ' Witaj, świecie' >>>
Jednak w nowym kodzie lepiej jest stosować funkcję format(). Daje ona znacznie więcej możliwości niż operator %, a ponadto jest bardziej uniwersalna niż metody ljust(), rjust() i center() łańcuchów znaków, ponieważ działa dla obiektów dowolnego rodzaju. Kompletną listę cech funkcji format() znajdziesz w internetowej dokumentacji Pythona (http://docs.python.org/3/library/string.html#formatspec).
2.13. Wyrównywanie łańcuchów znaków
65
2.14. Łączenie łańcuchów znaków Problem Programista chce połączyć wiele krótkich łańcuchów znaków w jeden długi.
Rozwiązanie Jeśli łączone łańcuchy znaków znajdują się w sekwencji lub obiekcie iterowalnym, najszybszym sposobem ich połączenia jest zastosowanie metody join(): >>> parts = ['Co', 'stolica', 'to', 'stolica'] >>> ' '.join(parts) 'Co stolica to stolica' >>> ','.join(parts) 'Co,stolica,to,stolica' >>> ''.join(parts) 'Costolicatostolica' >>>
Na pozór składnia ta wygląda dziwacznie, jednak operacja join() jest metodą łańcuchów znaków. Po części wynika to z tego, że złączane obiekty mogą pochodzić z rozmaitych sekwencji danych (list, krotek, słowników, plików, zbiorów lub generatorów) i dlatego implementowanie metody join() osobno dla każdego z tych obiektów byłoby nadmiarowe. Dlatego wystarczy podać pożądany separator i wywołać metodę join(), aby scalić fragmenty tekstu. Przy łączeniu niewielkiej liczby łańcuchów znaków zwykle wystarczy zastosować znak +: >>> >>> >>> 'Co >>>
a = 'Co stolica,' b = 'to stolica' a + ' ' + b stolica, to stolica'
Operator + działa dobrze także jako zastępnik dla bardziej skomplikowanych operacji formatowania łańcuchów znaków. Oto przykład: >>> print('{} Co stolica to >>> print(a + Co stolica to >>>
{}'.format(a,b)) stolica ' ' + b) stolica
Jeśli chcesz w kodzie źródłowym połączyć literały, wystarczy umieścić je obok siebie. Nie trzeba wtedy stosować operatora +: >>> a = 'Witaj' 'Świecie' >>> a 'WitajŚwiecie' >>>
Omówienie Może się wydawać, że złączanie łańcuchów znaków jest na tyle proste, iż nie warto poświęcać mu całej receptury. Jednak programiści często stosują w tym zakresie rozwiązania, które poważnie zmniejszają wydajność kodu.
66
Rozdział 2. Łańcuchy znaków i tekst
Najważniejsze jest to, aby pamiętać, że stosowanie operatora + do łączenia dużej liczby łańcuchów znaków jest bardzo niewydajne z uwagi na tworzenie kopii łańcuchów w pamięci i zachodzące przywracanie pamięci. Należy unikać zwłaszcza kodu, który złącza łańcuchy znaków w następujący sposób: s = '' for p in parts: s += p
Ten kod działa wyraźnie wolniej niż metoda join(). Wynika to przede wszystkim z tego, że każda operacja += wymaga utworzenia nowego obiektu łańcucha. Lepiej jest najpierw zapisać wszystkie części w kolekcji, a następnie je złączyć. Powiązana z tym (i całkiem elegancka) sztuczka polega na jednoczesnym przekształceniu danych na łańcuchy znaków i złączeniu ich za pomocą wyrażenia z generatorem (zobacz recepturę 1.19). Oto przykład: >>> data = ['ACME', 50, 91.1] >>> ','.join(str(d) for d in data) 'ACME,50,91.1' >>>
Warto też zwrócić uwagę na niepotrzebne operacje łączenia łańcuchów znaków. Czasem programiści stosują je w miejscach, gdzie tak naprawdę są zbędne, np. przy wyświetlaniu tekstu: print(a + ':' + b + ':' + c) print(':'.join([a, b, c]))
# Nieeleganckie # Także nieeleganckie
print(a, b, c, sep=':')
# Lepsze rozwiązanie
Łączenie operacji wejścia-wyjścia ze scalaniem łańcuchów znaków to rozwiązanie, które należy zbadać w konkretnych aplikacjach. Przyjrzyj się dwóm poniższym fragmentom kodu: # Wersja 1. (z łączeniem łańcuchów znaków) f.write(chunk1 + chunk2) # Wersja 2. (odrębne operacje wejścia-wyjścia) f.write(chunk1) f.write(chunk2)
Jeśli oba łańcuchy znaków są krótkie, pierwsza wersja może okazać się znacznie wydajniejsza z uwagi na koszty związane z wykonywaniem systemowych wywołań dotyczących wejściawyjścia. Jeżeli jednak łańcuchy są długie, wydajniejsza może być druga wersja, ponieważ nie wymaga tworzenia długiego łańcucha pomocniczego i kopiowania dużych bloków pamięci. Warto tu ponownie podkreślić, że w celu określenia najwydajniejszej techniki trzeba sprawdzić, jak poszczególne rozwiązania sprawdzają się dla konkretnych danych. Jeśli piszesz kod, który generuje dane wyjściowe na podstawie wielu krótkich łańcuchów znaków, możesz zastosować funkcję generatora i wykorzystać instrukcję yield do generowania fragmentów tekstu. Oto przykład: def sample(): yield 'Co' yield 'stolica' yield 'to' yield 'stolica'
Ciekawym aspektem tego rozwiązania jest to, że nie ma tu założeń dotyczących sposobu łączenia poszczególnych fragmentów. Można wykorzystać do tego np. metodę join(): text = ''.join(sample())
2.14. Łączenie łańcuchów znaków
67
Można też przekierować fragmenty do urządzenia wejścia-wyjścia: for part in sample(): f.write(part)
Można również zastosować rozwiązanie mieszane, które w inteligentny sposób określa, jak wykonywać operacje wejścia-wyjścia: def combine(source, maxsize): parts = [] size = 0 for part in source: parts.append(part) size += len(part) if size > maxsize: yield ''.join(parts) parts = [] size = 0 yield ''.join(parts) for part in combine(sample(), 32768): f.write(part)
Najważniejsze jest to, że w pierwotnej funkcji generatora nie są istotne szczegóły działania kodu. Służy ona jedynie do zwracania fragmentów tekstu.
2.15. Podstawianie wartości za zmienne w łańcuchach znaków Problem Programista chce utworzyć łańcuch znaków, w którym nazwy zmiennych są zastępowane łańcuchami reprezentującymi wartości poszczególnych zmiennych.
Rozwiązanie Python nie udostępnia funkcji umożliwiającej proste wstawianie wartości zmiennych w łańcuchach znaków. Jednak podobny efekt można uzyskać za pomocą metody format() łańcuchów znaków: >>> s = '{name} otrzymał {n} wiadomości.' >>> s.format(name='Robert', n=37) 'Robert otrzymał 37 wiadomości.' >>>
Jeśli wstawiane wartości rzeczywiście są zapisane w zmiennych, można połączyć instrukcje format_map() i vars(): >>> name = 'Robert' >>> n = 37 >>> s.format_map(vars()) 'Robert otrzymał 37 wiadomości.' >>>
68
Rozdział 2. Łańcuchy znaków i tekst
Ciekawą cechą metody vars() jest to, że działa także dla obiektów. Oto przykład: >>> class Info: ... def __init__(self, name, n): ... self.name = name ... self.n = n ... >>> a = Info('Robert',37) >>> s.format_map(vars(a)) 'Robert otrzymał 37 wiadomości.' >>>
Wadą metod format() i format_map() jest to, że nie obsługują w elegancki sposób brakujących wartości: >>> s.format(name='Robert') Traceback (most recent call last): File "", line 1, in KeyError: 'n' >>>
Jednym ze sposobów uniknięcia problemu jest napisanie zastępczej klasy słownika z metodą __missing__(): class safesub(dict): def __missing__(self, key): return '{' + key + '}'
Teraz można umieścić w tej klasie dane dla metody format_map(): >>> del n # n musi być niezdefiniowane >>> s.format_map(safesub(vars())) 'Robert otrzymał {n} wiadomości.' >>>
Jeśli często stosujesz w kodzie podobne rozwiązanie, możesz umieścić proces podstawiania zmiennych w prostej funkcji narzędziowej i wykorzystać w niej „sztuczkę z ramką”. Oto przykład: import sys def sub(text): return text.format_map(safesub(sys._getframe(1).f_locals))
Teraz można napisać następujący kod: >>> name = 'Robert' >>> n = 37 >>> print(sub('Cześć, {name}')) Cześć, Robert >>> print(sub('Otrzymałeś {n} wiadomości.')) Otrzymałeś 37 wiadomości. >>> print(sub('Twój ulubiony kolor to {color}')) Twój ulubiony kolor to {color} >>>
Omówienie Brak prawdziwego mechanizmu wstawiania w Pythonie wartości zmiennych doprowadził przez lata do powstania wielu technik. Zamiast rozwiązania przedstawionego w tej recepturze czasem stosuje się formatowanie łańcuchów znaków w następującej postaci:
2.15. Podstawianie wartości za zmienne w łańcuchach znaków
69
>>> name = 'Robert' >>> n = 37 >>> '%(name) otrzymał %(n) wiadomości.' % vars() 'Robert otrzymał 37 wiadomości.' >>>
Czasem stosuje się też szablonowe łańcuchy znaków: >>> import string >>> s = string.Template('$name otrzymał $n wiadomości.') >>> s.substitute(vars()) 'Robert otrzymał 37 wiadomości.' >>>
Jednak metody format() i format_map() są znacznie nowsze od pozostałych technik, dlatego warto z nich korzystać. Jedną z zalet metody format() jest to, że zapewnia wszystkie mechanizmy związane z formatowaniem łańcuchów znaków (wyrównywanie, dopełnianie, formatowanie liczb itd.), niedostępne w innych technikach (np. przy stosowaniu obiektów Template dla łańcuchów znaków). Niektóre aspekty tego rozwiązania ilustrują kilka interesujących zaawansowanych funkcji. Mało znana metoda __missing__() klas odwzorowań i słowników pozwala zdefiniować sposób obsługi brakujących wartości. W klasie safesub metoda ta zwraca brakującą wartość jako nazwę zmiennej. Zamiast wyjątku KeyError użytkownik widzi w wynikowym łańcuchu znaków nazwę brakującej zmiennej, co może być przydatne w trakcie debugowania. W funkcji sub() używane jest wywołanie sys._getframe(1) w celu zwrócenia ramki stosu do jednostki wywołującej. Z ramki pobierany jest atrybut f_locals, co pozwala uzyskać dostęp do zmiennych lokalnych. Nie trzeba tłumaczyć, że zwykle nie należy manipulować ramkami stosu. Jednak w funkcjach narzędziowych, np. przy wstawianiu wartości zmiennych w łańcuchach znaków, opisana możliwość bywa przydatna. Przy okazji warto zauważyć, że f_locals to słownik, który zawiera kopię zmiennych lokalnych z jednostki wywołującej. Choć można zmodyfikować zawartość tego słownika, zmiany nie są trwałe. Dlatego choć dostęp do ramki stosu z innej jednostki może wydawać się szkodliwy, nie da się przypadkowo zmienić ani zmiennych, ani lokalnego środowiska jednostki wywołującej.
2.16. Formatowanie tekstu w celu uzyskania określonej liczby kolumn Problem Programista używa długich łańcuchów znaków i chce zmienić ich formatowanie, tak aby zajmowały określoną liczbę kolumn.
Rozwiązanie Należy zastosować moduł textwrap w celu sformatowania wyświetlanego tekstu. Załóżmy, że w programie występuje następujący długi łańcuch znaków: s = "Look into my eyes, look into my eyes, the eyes, the eyes, \ the eyes, not around the eyes, don't look around the eyes, \ look into my eyes, you're under."
70
Rozdział 2. Łańcuchy znaków i tekst
Poniżej pokazano, jak za pomocą modułu textwrap można zmienić na różne sposoby formatowanie tego tekstu: >>> import textwrap >>> print(textwrap.fill(s, 70)) Look into my eyes, look into my eyes, the eyes, the eyes, the eyes, not around the eyes, don't look around the eyes, look into my eyes, you're under. >>> print(textwrap.fill(s, 40)) Look into my eyes, look into my eyes, the eyes, the eyes, the eyes, not around the eyes, don't look around the eyes, look into my eyes, you're under. >>> print(textwrap.fill(s, 40, initial_indent=' Look into my eyes, look into my eyes, the eyes, the eyes, the eyes, not around the eyes, don't look around the eyes, look into my eyes, you're under. >>> print(textwrap.fill(s, 40, subsequent_indent=' Look into my eyes, look into my eyes, the eyes, the eyes, the eyes, not around the eyes, don't look around the eyes, look into my eyes, you're under.
'))
'))
Omówienie Moduł textwrap jest prostym narzędziem do porządkowania tekstu w celu jego wyświetlenia — zwłaszcza gdy dane wyjściowe mają dobrze wyglądać na terminalu. Szerokość terminalu można uzyskać za pomocą wywołania os.get_terminal_size(): >>> import os >>> os.get_terminal_size().columns 80 >>>
Metoda fill() ma kilka dodatkowych opcji, które określają, jak ma ona obsługiwać znaki tabulacji, końce zdań itd. Więcej informacji znajdziesz w dokumentacji klasy textwrap.TextWrapper (http://docs.python.org/3.3/library/textwrap.html#textwrap.TextWrapper).
2.17. Obsługiwanie encji HTML-a i XML-a w tekście Problem Programista chce zastąpić encje HTML-a i XML-a (np. &entity; lub code;) odpowiadającym im tekstem. Chce też generować tekst, poprzedzając niektóre symbole (np. <, > lub &) znakiem ucieczki.
Rozwiązanie Jeśli generujesz tekst, możesz stosunkowo łatwo zastąpić znaki specjalne (takie jak < lub >) za pomocą funkcji html.escape():
2.17. Obsługiwanie encji HTML-a i XML-a w tekście
71
>>> s = 'Elementy są zapisane w postaci "tekst".' >>> import html >>> print(s) Elementy są zapisane w postaci "tekst". >>> print(html.escape(s)) Elementy są zapisane w postaci "tekst". >>> # Wyłączenie zastępowania cudzysłowów >>> print(html.escape(s, quote=False)) Elementy są zapisane w postaci "tekst". >>>
Jeśli chcesz wygenerować tekst w formacie ASCII i umieścić w nim kody znaków spoza tego formatu, możesz dodać argument errors='xmlcharrefreplace' do różnych funkcji wejścia-wyjścia: >>> s = 'Papryczka Jalapeño' >>> s.encode('ascii', errors='xmlcharrefreplace') b'Papryczka Jalapeño' >>>
Aby zastąpić encje w tekście, należy zastosować inne podejście. Jeśli przetwarzasz kod w HTML-u lub XML-u, spróbuj najpierw użyć parsera odpowiedniego języka. Takie narzędzia zwykle automatycznie zastępują wartości w trakcie parsowania, dzięki czemu nie trzeba tego robić samemu. Jeżeli jednak otrzymałeś sam tekst z encjami i chcesz je ręcznie zastąpić, możesz wykorzystać różne funkcje i metody narzędziowe parserów kodu w HTML-u i XML-u. Oto przykład: >>> s = 'Papryczka "Jalapeño".' >>> from html.parser import HTMLParser >>> p = HTMLParser() >>> p.unescape(s) 'Papryczka "Jalapeño".' >>> >>> t = 'Znak zachęty to >>>' >>> from xml.sax.saxutils import unescape >>> unescape(t) 'Znak zachęty to >>>' >>>
Omówienie Poprawne zastępowanie znaków specjalnych to łatwy do przeoczenia aspekt generowania kodu w HTML-u lub XML-u. Jest to prawdą zwłaszcza przy samodzielnym generowaniu danych wyjściowych przy użyciu funkcji print() lub innych podstawowych mechanizmów formatowania łańcuchów znaków. Łatwym rozwiązaniem jest zastosowanie funkcji narzędziowych, np. html.escape(). Jeśli chcesz przetwarzać tekst w drugim kierunku, pomocne będą różne funkcje narzędziowe, np. xml.sax.saxutils.unescape(). Jednak naprawdę warto zastanowić się nad zastosowaniem odpowiedniego parsera. Przy przetwarzaniu kodu w HTML-u lub XML-u moduł parsera, np. html.parser lub xml.etree.ElementTree, powinien zadbać o automatyczne zastępowanie encji w podanym tekście.
72
Rozdział 2. Łańcuchy znaków i tekst
2.18. Podział tekstu na tokeny Problem Programista chce parsować łańcuch znaków od lewej do prawej, aby uzyskać strumień tokenów.
Rozwiązanie Załóżmy, że w programie występuje łańcuch znaków w następującej postaci: text = 'foo = 23 + 42 * 10'
Aby podzielić łańcuch znaków na tokeny, nie wystarczy zastosować dopasowywania do wzorca. Dodatkowo potrzebny jest jeszcze sposób na identyfikowanie rodzaju wzorca. Można np. przekształcić łańcuch znaków na sekwencję par: tokens = [('NAME', 'foo'), ('EQ','='), ('NUM', '23'), ('PLUS','+'), ('NUM', '42'), ('TIMES', '*'), ('NUM', 10')]
Aby podzielić łańcuch znaków w ten sposób, najpierw trzeba zdefiniować wszystkie możliwe tokeny (w tym odstępy) za pomocą wzorców z wyrażeniami regularnymi. Należy przy tym zastosować nazwane grupy przechwytujące: import re NAME = r'(?P[a-zA-Z_][a-zA-Z_0-9]*)' NUM = r'(?P\d+)' PLUS = r'(?P\+)' TIMES = r'(?P\*)' EQ = r'(?P=)' WS = r'(?P\s+)' master_pat = re.compile('|'.join([NAME, NUM, PLUS, TIMES, EQ, WS]))
W tych wzorcach (zbudowanych za pomocą modułu re) wykorzystano konwencję ?P, aby przypisać nazwy do wzorców. Nazwy te będą potrzebne później. Następnie do podziału na tokeny można zastosować mało znaną metodę scanner() obiektów wzorców. Metoda ta tworzy obiekt skanera, w którym powtarzane wywołania match() powodują przejście przez podany tekst dopasowanie po dopasowaniu. Oto interaktywny przykład działania obiektu skanera: >>> scanner = master_pat.scanner('foo = 42') >>> scanner.match() <_sre.SRE_Match object at 0x100677738> >>> _.lastgroup, _.group() ('NAME', 'foo') >>> scanner.match() <_sre.SRE_Match object at 0x100677738> >>> _.lastgroup, _.group() ('WS', ' ') >>> scanner.match() <_sre.SRE_Match object at 0x100677738> >>> _.lastgroup, _.group() ('EQ', '=') >>> scanner.match() <_sre.SRE_Match object at 0x100677738> >>> _.lastgroup, _.group() ('WS', ' ') >>> scanner.match()
Aby wykorzystać tę technikę w kodzie, można ją uporządkować i zapisać w generatorze: from collections import namedtuple Token = namedtuple('Token', ['type','value']) def generate_tokens(pat, text): scanner = pat.scanner(text) for m in iter(scanner.match, None): yield Token(m.lastgroup, m.group()) # Przykład zastosowania for tok in generate_tokens(master_pat, 'foo = 42'): print(tok) # # # # # #
Jeśli chcesz przefiltrować strumień tokenów, możesz albo zdefiniować dodatkowe funkcje generatora, albo zastosować wyrażenie z generatorem. Poniżej pokazano, jak można odfiltrować wszystkie tokeny reprezentujące odstępy: tokens = (tok for tok in generate_tokens(master_pat, text) if tok.type != 'WS') for tok in tokens: print(tok)
Omówienie Podział na tokeny to często pierwszy etap bardziej zaawansowanych operacji parsowania i obsługi tekstu. Aby zastosować pokazaną tu technikę skanowania, należy pamiętać o kilku ważnych kwestiach. Przede wszystkim trzeba zidentyfikować każdą możliwą sekwencję tekstu, który może pojawiać się w danych wejściowych. Każdej takiej sekwencji powinien odpowiadać wzorzec oparty na module re. Jeśli program znajdzie niedopasowany tekst, zakończy skanowanie. Dlatego w przykładowym kodzie trzeba utworzyć token dla odstępów (WS). Ważna jest też kolejność tokenów w nadrzędnym wyrażeniu regularnym. W trakcie dopasowywania moduł re próbuje dopasować wzorce w podanej kolejności. Dlatego jeśli wzorzec odpowiada podłańcuchowi dłuższego wzorca, trzeba się upewnić, że dłuższy wzorzec został podany jako pierwszy. Oto przykład: LT = r'(?P<)' LE = r'(?P<=)' EQ = r'(?P=)' master_pat = re.compile('|'.join([LE, LT, EQ])) # master_pat = re.compile('|'.join([LT, LE, EQ]))
# Poprawnie # Błąd
Drugi wzorzec jest błędny, ponieważ uzna ciąg <= za token LT, po którym następuje token EQ, a nie za pojedynczy token LE, o co prawdopodobnie chodziło programiście.
74
Rozdział 2. Łańcuchy znaków i tekst
Ponadto trzeba zwrócić uwagę na wzorce odpowiadające podłańcuchom tekstu. Załóżmy, że istnieją dwa następujące wzorce: PRINT = r'(Pprint)' NAME = r'(P[a-zA-Z_][a-zA-Z_0-9]*)' master_pat = re.compile('|'.join([PRINT, NAME])) for tok in generate_tokens(master_pat, 'printer'): print(tok) # Zwracane dane: # Token(type='PRINT', value='print') # Token(type='NAME', value='er')
Przy bardziej zaawansowanym podziale na tokeny warto zainteresować się takimi pakietami jak PyParsing (http://pyparsing.wikispaces.com/) i PLY (http://www.dabeaz.com/ply/index.html). W następnej recepturze znajdziesz przykład wykorzystujący pakiet PLY.
2.19. Tworzenie prostego rekurencyjnego parsera zstępującego Problem Programista chce parsować tekst według zbioru zasad gramatycznych i wykonywać operacje lub tworzyć abstrakcyjne drzewo składniowe reprezentujące dane wejściowe. Gramatyka jest prosta, dlatego programista woli napisać parser samodzielnie, zamiast korzystać z platformy.
Rozwiązanie W tym problemie koncentrujemy się na parsowaniu tekstu według określonej gramatyki. Zwykle należy rozpocząć od opracowania formalnej specyfikacji gramatyki w notacji BNF lub EBNF. Gramatyka prostych wyrażeń arytmetycznych może wyglądać tak: expr ::= expr + term | expr - term | term term ::= term * factor | term / factor | factor factor ::= ( expr ) | NUM
A oto ta sama gramatyka w notacji EBNF: expr ::= term { (+|-) term }* term ::= factor { (*|/) factor }* factor ::= ( expr ) | NUM
W notacji EBNF fragmenty reguł umieszczone między znakami { … }* są opcjonalne. Gwiazdka (*) oznacza zero lub więcej powtórzeń (tak samo jak w wyrażeniach regularnych). 2.19. Tworzenie prostego rekurencyjnego parsera zstępującego
75
Jeśli nie wiesz, jak działa specyfikacja w notacji BNF, potraktuj ją jak spis zasad zastępowania, według których symbole umieszczone po lewej stronie można zastąpić symbolami podanymi po prawej (lub na odwrót). W trakcie parsowania kod próbuje dopasować wejściowy tekst do gramatyki, podstawiając elementy i rozwijając je na podstawie specyfikacji w notacji BNF. Załóżmy, że program parsuje wyrażenie 3 + 4 * 5. Wyrażenie to trzeba najpierw rozbić na strumień tokenów, używając technik przedstawionych w recepturze 2.18. W efekcie może powstać sekwencja tokenów w następującej postaci: NUM + NUM * NUM
Następnym etapem parsowania jest dopasowywanie gramatyki do wejściowych tokenów przez podstawianie elementów: expr expr expr expr expr expr expr expr expr expr expr expr
::= ::= ::= ::= ::= ::= ::= ::= ::= ::= ::=
term { (+|-) term }* factor { (*|/) factor }* { (+|-) term }* NUM { (*|/) factor }* { (+|-) term }* NUM { (+|-) term }* NUM + term { (+|-) term }* NUM + factor { (*|/) factor }* { (+|-) term }* NUM + NUM { (*|/) factor}* { (+|-) term }* NUM + NUM * factor { (*|/) factor }* { (+|-) term }* NUM + NUM * NUM { (*|/) factor }* { (+|-) term }* NUM + NUM * NUM { (+|-) term }* NUM + NUM * NUM
Aby prześledzić wszystkie podstawienia, warto zrobić sobie mocną kawę. Program sprawdza dane wejściowe i próbuje dopasować je do zasad gramatyki. Pierwszy wejściowy token to NUM. To ta część jest dopasowywana jako pierwsza (w wyniku podstawień). Następnie program przechodzi do kolejnego tokenu (+) itd. Niektóre fragmenty widoczne po prawej stronie (np. { (*/) factor }*) znikają po ustaleniu, że nie pasują do następnego tokenu. Przy udanym parsowaniu cała prawa strona jest rozwijana w wyniku dopasowania jej do strumienia wejściowych tokenów. Poniższa prosta receptura jest oparta na przedstawionych wcześniej informacjach. Utworzono tu rekurencyjny zstępujący ewaluator wyrażeń: import re import collections # Specyfikacje tokenów NUM = r'(?P\d+)' PLUS = r'(?P\+)' MINUS = r'(?P-)' TIMES = r'(?P\*)' DIVIDE = r'(?P/)' LPAREN = r'(?P\()' RPAREN = r'(?P\))' WS = r'(?P\s+)' master_pat = re.compile('|'.join([NUM, PLUS, MINUS, TIMES, DIVIDE, LPAREN, RPAREN, WS])) # Podział na tokeny Token = collections.namedtuple('Token', ['type','value']) def generate_tokens(text): scanner = master_pat.scanner(text) for m in iter(scanner.match, None): tok = Token(m.lastgroup, m.group()) if tok.type != 'WS':
76
Rozdział 2. Łańcuchy znaków i tekst
yield tok # Parser class ExpressionEvaluator: ''' Kod rekurencyjnego parsera zstępującego. Każda metoda obsługuje jedną zasadę gramatyki. Metoda ._accept() służy do testowania i akceptowania aktualnie „podglądanego” tokenu. Metoda ._expect() pozwala dokładnie dopasować i odrzucić następny token z danych wejściowych (jeśli token nie pasuje do wzorca, metoda zgłasza błąd SyntaxError). ''' def parse(self,text): self.tokens = generate_tokens(text) self.tok = None # Ostatni pobrany symbol self.nexttok = None # Następny symbol przekształcony na token self._advance() # Wczytywanie pierwszego „podglądanego” tokenu return self.expr() def _advance(self): 'Przejście do następnego tokenu' self.tok, self.nexttok = self.nexttok, next(self.tokens, None) def _accept(self,toktype): 'Testowanie i pobieranie następnego tokenu, jeśli pasuje do typu toktype' if self.nexttok and self.nexttok.type == toktype: self._advance() return True else: return False def _expect(self,toktype): 'Pobieranie następnego tokenu, jeśli pasuje do typu toktype, lub zgłaszanie błędu SyntaxError' if not self._accept(toktype): raise SyntaxError('Oczekiwano ' + toktype) # Zasady gramatyki def expr(self): "expression ::= term { ('+'|'-') term }*" exprval = self.term() while self._accept('PLUS') or self._accept('MINUS'): op = self.tok.type right = self.term() if op == 'PLUS': exprval += right elif op == 'MINUS': exprval -= right return exprval def term(self): "term ::= factor { ('*'|'/') factor }*" termval = self.factor() while self._accept('TIMES') or self._accept('DIVIDE'): op = self.tok.type right = self.factor() if op == 'TIMES': termval *= right elif op == 'DIVIDE': termval /= right return termval
def factor(self): "factor ::= NUM | ( expr )" if self._accept('NUM'): return int(self.tok.value) elif self._accept('LPAREN'): exprval = self.expr() self._expect('RPAREN') return exprval else: raise SyntaxError('Oczekiwano tokenu typu NUMBER lub LPAREN')
Oto przykład interaktywnego korzystania z klasy ExpressionEvaluator: >>> e = ExpressionEvaluator() >>> e.parse('2') 2 >>> e.parse('2 + 3') 5 >>> e.parse('2 + 3 * 4') 14 >>> e.parse('2 + (3 + 4) * 5') 37 >>> e.parse('2 + (3 + * 4)') Traceback (most recent call last): File "", line 1, in File "exprparse.py", line 40, in parse return self.expr() File "exprparse.py", line 67, in expr right = self.term() File "exprparse.py", line 77, in term termval = self.factor() File "exprparse.py", line 93, in factor exprval = self.expr() File "exprparse.py", line 67, in expr right = self.term() File "exprparse.py", line 77, in term termval = self.factor() File "exprparse.py", line 97, in factor raise SyntaxError("Oczekiwano tokenu typu NUMBER lub LPAREN") SyntaxError: Oczekiwano tokenu typu NUMBER lub LPAREN >>>
Jeśli interesuje Cię coś więcej niż sama ewaluacja, musisz zmodyfikować klasę Expression Evaluator. Oto inna implementacja. Ta klasa tworzy proste drzewo parsowania: class ExpressionTreeBuilder(ExpressionEvaluator): def expr(self): "expression ::= term { ('+'|'-') term }" exprval = self.term() while self._accept('PLUS') or self._accept('MINUS'): op = self.tok.type right = self.term() if op == 'PLUS': exprval = ('+', exprval, right) elif op == 'MINUS': exprval = ('-', exprval, right) return exprval def term(self): "term ::= factor { ('*'|'/') factor }" termval = self.factor()
78
Rozdział 2. Łańcuchy znaków i tekst
while self._accept('TIMES') or self._accept('DIVIDE'): op = self.tok.type right = self.factor() if op == 'TIMES': termval = ('*', termval, right) elif op == 'DIVIDE': termval = ('/', termval, right) return termval def factor(self): 'factor ::= NUM | ( expr )' if self._accept('NUM'): return int(self.tok.value) elif self._accept('LPAREN'): exprval = self.expr() self._expect('RPAREN') return exprval else: raise SyntaxError('Oczekiwano tokenu typu NUMBER lub LPAREN')
Omówienie Parsowanie to rozbudowane zagadnienie, któremu studenci poświęcają zwykle trzy pierwsze tygodnie na zajęciach dotyczących kompilatorów. Jeśli chcesz dowiedzieć się czegoś więcej o gramatykach, algorytmach parsowania i pokrewnych tematach, powinieneś zapoznać się z książką o kompilatorach (nie trzeba tłumaczyć, że nie da się zamieścić tu wszystkich informacji z tego zakresu). Ogólne reguły pisania rekurencyjnych parserów zstępujących są proste. Na początku należy przekształcić każdą zasadę gramatyki na funkcję lub metodę. Jeśli gramatyka wygląda tak: expr ::= term { ('+'|'-') term }* term ::= factor { ('*'|'/') factor }* factor ::= '(' expr ')' | NUM
należy zacząć od przekształcenia jej w zbiór metod: class ExpressionEvaluator: ... def expr(self): ... def term(self): ... def factor(self): ...
Zadanie każdej metody jest proste — ma ona przejść od lewej do prawej przez każdą część zasady gramatyki i pobrać przy tym tokeny. W pewnym sensie celem działania metody jest pobranie tokenu zgodnego z zasadą lub wygenerowanie błędu składni, jeśli nie można tego zrobić. Aby uzyskać ten efekt, należy zastosować następujące techniki: Jeśli następny symbol w regule to nazwa innej zasady gramatyki (np. term lub factor),
wystarczy wywołać metodę o tej nazwie. Na tym polega zstępujący charakter algorytmu — kontrola jest przekazywana w dół, do innej zasady gramatyki. Czasem reguły obejmują wywołania metod, które już są wykonywane (np. wywołanie expr w regule factor ::= '(' expr ')'). Jest to aspekt rekurencyjny algorytmu. Jeżeli następnym symbolem w regule musi być określony element (np. (), program anali-
zuje kolejny token i sprawdza, czy występuje dokładne dopasowanie. Jeśli element nie jest dopasowany, oznacza to błąd składni. W recepturze do wykonywania tych operacji służy metoda _expect(). Jeśli następnym symbolem w regule mogą być różne elementy (np. + lub -), trzeba
sprawdzić kolejny token pod kątem wszystkich możliwości i przejść dalej tylko wtedy, gdy znaleziono dopasowanie. W recepturze odpowiada za to metoda _accept(). Jest to pewnego rodzaju „mniej wymagająca” wersja metody _expect(), ponieważ przechodzi dalej tylko po znalezieniu dopasowania, jednak jeśli go nie znajdzie, cofa się bez zgłaszania błędu (co umożliwia sprawdzenie innych możliwości). W zasadach gramatyki obejmujących powtarzające się elementy (np. expr ::= term {
('+'|'-') term }*) powtórzenia są obsługiwane w pętli while. W ciele pętli zbierane
lub przetwarzane są wszystkie powtarzające się elementy do momentu znalezienia ich wszystkich. Po przetworzeniu całej zasady gramatyki każda metoda zwraca wynik do jednostki wy-
wołującej. W ten sposób wartości są przekazywane w trakcie parsowania. Np. zwracane wartości w ewaluatorze wyrażeń reprezentują częściowe wyniki parsowanego wyrażenia. Ostatecznie wszystkie elementy są łączone w metodzie nadrzędnej zasady gramatyki. Wprawdzie przedstawiony tu przykład jest prosty, ale rekurencyjne parsery zstępujące można wykorzystać również do tworzenia skomplikowanych parserów. Np. sam kod Pythona jest interpretowany przez rekurencyjny parser zstępujący. Jeśli chcesz, możesz przyjrzeć się tej gramatyce, analizując plik Grammar/Grammar w kodzie źródłowym Pythona. Jednak ręczne tworzenie parserów związane jest z licznymi pułapkami i ograniczeniami. Jednym z takich ograniczeń w rekurencyjnych parserach zstępujących jest to, że nie można przy ich użyciu przetwarzać zasad gramatyki z rekurencją lewostronną. Załóżmy, że programista chce przekształcić następującą regułę: items ::= items ',' item | item
W tym celu może on spróbować wykorzystać metodę items(): def items(self): itemsval = self.items() if itemsval and self._accept(','): itemsval.append(self.item()) else: itemsval = [ self.item() ]
Jedyny problem polega na tym, że to nie zadziała. Kod zgłosi błąd rekurencji nieskończonej.
80
Rozdział 2. Łańcuchy znaków i tekst
Czasem występują też pewne komplikacje związane z samymi zasadami gramatyki. Np. możesz się zastanawiać, czy wyrażenia można przedstawić przy użyciu prostszej gramatyki: expr ::= factor { ('+'|'-'|'*'|'/') factor }* factor ::= '(' expression ')' | NUM
Technicznie ta gramatyka działa, jednak nie uwzględnia standardowych reguł arytmetycznych dotyczących kolejności przetwarzania. Np. wyrażenie 3 + 4 * 5 da wartość 35 zamiast oczekiwanego wyniku 23. Odrębne reguły expr i term sprawiają, że gramatyka działa poprawnie. Jeśli gramatyki są naprawdę skomplikowane, często lepiej jest wykorzystać narzędzia do parsowania takie jak PyParsing (http://pyparsing.wikispaces.com/) lub PLY (http://www.dabeaz.com/ply/index.html). Tak wygląda kod ewaluatora wyrażeń opracowany za pomocą narzędzia PLY: from ply.lex import lex from ply.yacc import yacc # Lista tokenów tokens = [ 'NUM', 'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'LPAREN', 'RPAREN' ] # Ignorowane znaki t_ignore = ' \t\n' # Specyfikacje tokenów (w postaci wyrażeń regularnych) t_PLUS = r'\+' t_MINUS = r'-' t_TIMES = r'\*' t_DIVIDE = r'/' t_LPAREN = r'\(' t_RPAREN = r'\)' # Funkcje do przetwarzania tokenów def t_NUM(t): r'\d+' t.value = int(t.value) return t # Obsługa błędów def t_error(t): print('Błędny znak: {!r}'.format(t.value[0])) t.skip(1) # Tworzenie leksera lexer = lex() # Zasady gramatyki i funkcje obsługi błędów def p_expr(p): ''' expr : expr PLUS term | expr MINUS term ''' if p[2] == '+': p[0] = p[1] + p[3] elif p[2] == '-': p[0] = p[1] - p[3] def p_expr_term(p): '''
expr : term ''' p[0] = p[1] def p_term(p): ''' term : term TIMES factor | term DIVIDE factor ''' if p[2] == '*': p[0] = p[1] * p[3] elif p[2] == '/': p[0] = p[1] / p[3] def p_term_factor(p): ''' term : factor ''' p[0] = p[1] def p_factor(p): ''' factor : NUM ''' p[0] = p[1] def p_factor_group(p): ''' factor : LPAREN expr RPAREN ''' p[0] = p[2] def p_error(p): print('Błąd składni') parser = yacc()
W tym kodzie wszystko jest określone na znacznie wyższym poziomie. Wystarczy napisać dla tokenów wyrażenia regularne i wysokopoziomowe funkcje obsługi, wykonywane przy dopasowywaniu różnych zasad gramatyki. Za mechanizmy działania parsera, akceptowanie tokenów i inne aspekty odpowiada w całości biblioteka. Oto przykład pokazujący, jak wykorzystać wynikowy obiekt parsera: >>> parser.parse('2') 2 >>> parser.parse('2+3') 5 >>> parser.parse('2+(3+4)*5') 37 >>>
Jeśli szukasz więcej wyzwań programistycznych, ciekawym projektem może się okazać pisanie parserów i kompilatorów. W podręcznikach poświęconych kompilatorom znajdziesz wiele szczegółowych informacji z zakresu teorii. Także w internecie znajdziesz liczne cenne materiały. Warto też przyjrzeć się modułowi ast Pythona.
82
Rozdział 2. Łańcuchy znaków i tekst
2.20. Przeprowadzanie operacji tekstowych na łańcuchach bajtów Problem Programista chce wykonywać standardowe operacje tekstowe (np. usuwanie, wyszukiwanie i zastępowanie elementów) na łańcuchach bajtów (czyli na napisach w formacie ASCII).
Rozwiązanie Łańcuchy bajtów obsługują większość tych samych wbudowanych operacji co łańcuchy znaków. Oto przykład: >>> data = b'Witaj, Polsko' >>> data[0:5] b'Witaj' >>> data.startswith(b'Witaj') True >>> data.split() [b'Witaj,', b'Polsko'] >>> data.replace(b'Witaj,', b'Witaj, moja') b'Witaj, moja Polsko' >>>
Operacje te działają także dla tablic bajtów: >>> data = bytearray(b'Witaj, Polsko') >>> data[0:5] bytearray(b'Witaj') >>> data.startswith(b'Witaj') True >>> data.split() [bytearray(b'Witaj,'), bytearray(b'Polsko')] >>> data.replace(b'Witaj,', b'Witaj, moja') bytearray(b'Witaj, moja Polsko') >>>
Dla łańcuchów bajtów można zastosować dopasowywanie z wykorzystaniem wyrażeń regularnych, jednak same wzorce trzeba podać za pomocą bajtów. Oto przykład: >>> >>> data = b'FOO:BAR,SPAM' >>> import re >>> re.split('[:,]',data) Traceback (most recent call last): File "", line 1, in File "/usr/local/lib/python3.3/re.py", line 191, in split return _compile(pattern, flags).split(string, maxsplit) TypeError: can't use a string pattern on a bytes-like object >>> re.split(b'[:,]',data) [b'FOO', b'BAR', b'SPAM'] >>>
# Uwaga — wzorzec zapisany w formie bajtów
2.20. Przeprowadzanie operacji tekstowych na łańcuchach bajtów
83
Omówienie Zwykle prawie wszystkie operacje dotyczące łańcuchów znaków działają też dla łańcuchów bajtów. Trzeba jednak pamiętać o kilku ważnych różnicach. Po pierwsze, elementy łańcuchów bajtów pobierane za pomocą indeksu to liczby całkowite, a nie znaki: >>> >>> 'W' >>> 'i' >>> >>> 87 >>> 105 >>>
a = 'Witaj, Polsko' a[0]
# Łańcuch znaków
a[1] b = b'Witaj, Polsko' b[0]
# Łańcuch bajtów
b[1]
Ta różnica może wpływać na pracę programów, które próbują przetwarzać dane bajtowe znak po znaku. Po drugie, łańcuchy bajtów nie mają eleganckiej reprezentacji i nie są wyświetlane w przejrzysty sposób. Trzeba je najpierw przekształcić na łańcuch znaków: >>> s = b'Witaj, Polsko' >>> print(s) b'Witaj, Polsko' >>> print(s.decode('ascii')) Witaj, Polsko >>>
# Zwróć uwagę na b'...'
Dla łańcuchów bajtów nie można też stosować operacji formatujących: >>> b'%10s %10d %10.2f' % (b'ACME', 100, 490.1) Traceback (most recent call last): File "", line 1, in TypeError: unsupported operand type(s) for %: 'bytes' and 'tuple' >>> b'{} {} {}'.format(b'ACME', 100, 490.1) Traceback (most recent call last): File "", line 1, in AttributeError: 'bytes' object has no attribute 'format' >>>
Jeśli chce się zastosować formatowanie do łańcuchów bajtów, należy najpierw zmienić kodowanie danych i przekształcić je na zwykłe łańcuchy znaków. Oto przykład: >>> '{:10s} {:10d} {:10.2f}'.format('ACME', 100, 490.1).encode('ascii') b'ACME 100 490.10' >>>
Ponadto trzeba pamiętać, że stosowanie łańcuchów bajtów może zmieniać przebieg niektórych operacji — zwłaszcza tych związanych z systemem plików. Jeśli na przykład podasz nazwę pliku zakodowaną za pomocą bajtów, a nie w formie łańcucha znaków, kodowanie i dekodowanie nazwy będzie działać w nietypowy sposób. Oto przykład: >>> # Nazwa pliku w formacie UTF-8 >>> with open('jalape\xf1o.txt', 'w') as f: ... f.write('spicy') ... >>> # Pobieranie zawartości katalogu >>> import os
84
Rozdział 2. Łańcuchy znaków i tekst
>>> os.listdir('.') ['jalapeño.txt']
# Łańcuch znaków (nazwy są dekodowane)
>>> os.listdir(b'.') # Łańcuch bajtów (nazwy w postaci bajtów) [b'jalapen\xcc\x83o.txt'] >>>
Zauważ, że w ostatniej części przykładu podanie nazwy katalogu w formie łańcucha bajtów spowodowało zwrócenie nazwy pliku z nieodkodowanymi bajtami. Nazwa widoczna na liście zawartości katalogu zawiera nieprzetworzone kody w formacie UTF-8. Omówienie powiązanych problemów z nazwami plików znajdziesz w recepturze 5.15. Niektórzy programiści wolą stosować łańcuchy bajtów zamiast łańcuchów znaków ze względu na poprawę wydajności. Choć manipulowanie bajtami rzeczywiście jest wydajniejsze niż praca z tekstem (z uwagi na koszty związane ze stosowaniem formatu Unicode), zwykle prowadzi ono do powstawania zagmatwanego i nietypowego kodu. Często okazuje się, że łańcuchy bajtów źle współdziałają z wieloma innymi aspektami Pythona. Aby więc uzyskać pożądany efekt, trzeba ręcznie przeprowadzać operacje kodowania i dekodowania. Dlatego w trakcie pracy z tekstem lepiej jest stosować w programach standardowe łańcuchy znaków, a nie łańcuchy bajtów.
2.20. Przeprowadzanie operacji tekstowych na łańcuchach bajtów
85
86
Rozdział 2. Łańcuchy znaków i tekst
ROZDZIAŁ 3.
Liczby, daty i czas
Wykonywanie obliczeń matematycznych na liczbach całkowitych i zmiennoprzecinkowych w Pythonie jest proste. Jeśli jednak obliczenia dotyczą ułamków, tablic, dat lub czasu, wymagają więcej pracy. W tym rozdziale koncentrujemy się na zagadnieniach z tej drugiej kategorii.
3.1. Zaokrąglanie liczb Problem Programista chce zaokrąglić liczbę zmiennoprzecinkową do określonej liczby miejsc po przecinku.
Rozwiązanie Przy prostym zaokrąglaniu można wykorzystać wbudowaną funkcję round(value, ndigits): >>> round(1.23, 1) 1.2 >>> round(1.27, 1) 1.3 >>> round(-1.27, 1) -1.3 >>> round(1.25361,3) 1.254 >>>
Gdy wartość znajduje się dokładnie między dwiema innymi, jest zaokrąglana do najbliższej liczby parzystej. Dlatego wartości 1,5 i 2,5 są zaokrąglane do liczby 2. Liczba cyfr przekazywana do funkcji round() może być ujemna. Wtedy wartość jest zaokrąglana do pełnych dziesiątek, setek, tysięcy itd. Oto przykład: >>> a = 1627731 >>> round(a, -1) 1627730 >>> round(a, -2) 1627700 >>> round(a, -3) 1628000 >>>
87
Omówienie Nie należy mylić zaokrąglania z formatowaniem wyświetlanych wartości. Aby tylko wyświetlić wartość liczbową z określoną liczbą miejsc po przecinku, zwykle nie trzeba używać funkcji round(). Zamiast tego wystarczy określić precyzję przy formatowaniu. Oto przykład: >>> x = 1.23456 >>> format(x, '0.2f') '1.23' >>> format(x, '0.3f') '1.235' >>> 'Wartość to {:0.3f}'.format(x) 'Wartość to 1.235' >>>
Ponadto nie zaokrąglaj liczb zmiennoprzecinkowych, aby „rozwiązać” problemy z dokładnością obliczeń. Możesz np. pomyśleć o następującym podejściu: >>> a = 2.1 >>> b = 4.2 >>> c = a + b >>> c 6.300000000000001 >>> c = round(c, 2) >>> c 6.3 >>>
# „Naprawianie” wyniku
W większości zastosowań liczb zmiennoprzecinkowych nie jest to konieczne (ani zalecane). Choć w obliczeniach pojawiają się drobne błędy, są one znane i tolerowane. Jeśli ważne jest, aby uniknąć takich błędów (np. w aplikacji finansowej), warto rozważyć wykorzystanie opisanego w następnej recepturze modułu decimal.
3.2. Przeprowadzanie dokładnych obliczeń na liczbach dziesiętnych Problem Programista chce przeprowadzać dokładne obliczenia na liczbach dziesiętnych i uniknąć przy tym drobnych błędów, które pojawiają się przy stosowaniu liczb zmiennoprzecinkowych.
Rozwiązanie Dobrze znany problem dotyczący liczb zmiennoprzecinkowych związany jest z tym, że nie reprezentują one poprawnie wszystkich liczb o podstawie 10. Ponadto powodują, że nawet w prostych obliczeniach matematycznych mogą pojawiać się drobne błędy. Oto przykład: >>> a = 4.2 >>> b = 2.1 >>> a + b 6.300000000000001 >>> (a + b) == 6.3 False >>>
88
Rozdział 3. Liczby, daty i czas
Te błędy są „cechą” używanego procesora i obliczeń arytmetycznych zgodnych ze standardem IEEE 754 przeprowadzanych przez jednostkę zmiennoprzecinkową. Ponieważ typ zmiennoprzecinkowy w Pythonie przechowuje dane za pomocą reprezentacji natywnej, nie można nic zrobić, aby przy korzystaniu z obiektów typu float uniknąć przedstawionych błędów. Jeśli zależy Ci na większej precyzji (kosztem niższej wydajności), możesz zastosować moduł decimal: >>> from decimal import Decimal >>> a = Decimal('4.2') >>> b = Decimal('2.1') >>> a + b Decimal('6.3') >>> print(a + b) 6.3 >>> (a + b) == Decimal('6.3') True >>>
Początkowo kod ten może wydawać się dziwny — liczby są tu podawane jako łańcuchy znaków. Jednak obiekty typu Decimal działają w oczekiwany sposób, m.in. obsługują wszystkie standardowe operacje matematyczne. Gdy obiekty te wyświetla się lub stosuje w funkcjach formatujących łańcuchy znaków, wyglądają jak zwykłe liczby. Ważną cechą typu decimal jest to, że umożliwia kontrolowanie różnych aspektów obliczeń, w tym liczbę znaków i zaokrąglanie. Należy utworzyć lokalny kontekst, a następnie zmienić jego ustawienia. Oto przykład: >>> from decimal import localcontext >>> a = Decimal('1.3') >>> b = Decimal('1.7') >>> print(a / b) 0.7647058823529411764705882353 >>> with localcontext() as ctx: ... ctx.prec = 3 ... print(a / b) ... 0.765 >>> with localcontext() as ctx: ... ctx.prec = 50 ... print(a / b) ... 0.76470588235294117647058823529411764705882352941176 >>>
Omówienie Moduł decimal to implementacja specyfikacji „General Decimal Arithmetic Specification” IBM-u. Moduł oczywiście udostępnia dużą liczbę opcji konfiguracyjnych, których omawianie wykracza poza zakres tej książki. Osoby dopiero poznające Pythona mogą chcieć stosować moduł decimal do rozwiązania problemów z dokładnością typu danych float. Jednak bardzo ważne jest, aby zrozumieć dziedzinę aplikacji. W obszarze problemów naukowych lub inżynieryjnych, grafiki komputerowej oraz innych pokrewnych zagadnień częściej używa się standardowego typu zmiennoprzecinkowego. Po pierwsze, bardzo niewiele rzeczy mierzy się z dokładnością do 17 cyfr po przecinku zapewnianą przez liczby zmiennoprzecinkowe. Dlatego drobne błędy w obliczeniach nie mają znaczenia. Po drugie, natywne liczby zmiennoprzecinkowe są znacznie szybsze. Jest to ważne przy przeprowadzaniu wielu obliczeń. 3.2. Przeprowadzanie dokładnych obliczeń na liczbach dziesiętnych
89
Mimo to nie należy całkowicie ignorować opisanych błędów. Matematycy spędzili dużo czasu na analizowaniu różnych algorytmów. Niektóre z nich radzą sobie z obsługą błędów lepiej niż inne. Trzeba też uważać na uzyskane wyniki z uwagi na utratę cyfr znaczących w trakcie odejmowania oraz dodawania dużych i małych liczb. Oto przykład: >>> nums = [1.23e+18, 1, -1.23e+18] >>> sum(nums) # Zauważ, że jedynka znika 0.0 >>>
Aby rozwiązać ten ostatni błąd, można wykorzystać precyzyjniejszą implementację funkcji math.fsum(): >>> import math >>> math.fsum(nums) 1.0 >>>
Jednak w innych algorytmach trzeba je przeanalizować i zrozumieć efekty propagacji błędów. Moduł decimal stosuje się głównie w programach finansowych i podobnych. W takich aplikacjach bardzo irytujące są drobne błędy wkradające się do obliczeń. Moduł decimal pozwala ich uniknąć. Z obiektów Decimal często korzysta się też przy komunikowaniu się Pythona z bazami danych — zwłaszcza przy dostępie do danych finansowych.
3.3. Formatowanie liczb w celu ich wyświetlenia Problem Programista chce sformatować wyświetlane liczby i w tym celu chciałby określić liczbę cyfr, wyrównanie, separator tysięcy i inne szczegóły.
Rozwiązanie Aby sformatować jedną liczbę, można wykorzystać wbudowaną funkcję format(): >>> x = 1234.56789 >>> # Precyzja do dwóch miejsc po przecinku >>> format(x, '0.2f') '1234.57' >>> # Wyrównanie do prawej do długości 10 znaków; precyzja do pierwszej cyfry po przecinku >>> format(x, '>10.1f') ' 1234.6' >>> # Wyrównanie do lewej >>> format(x, '<10.1f') '1234.6 ' >>> # Wyśrodkowanie >>> format(x, '^10.1f') ' 1234.6 ' >>> # Dodanie separatora tysięcy >>> format(x, ',')
Jeśli chcesz zastosować notację wykładniczą, zmień f na e lub E (w zależności od pożądanej wielkości litery e określającej część wykładniczą): >>> format(x, 'e') '1.234568e+03' >>> format(x, '0.2E') '1.23E+03' >>>
Ogólna postać wyrażenia określająca szerokość i precyzję to w obu przypadkach '[<>^]?wi dth[,]?(.digits)?', gdzie width i digits to liczby całkowite, a ? oznacza część opcjonalną. Takie same kody formatujące są używane w metodzie .format() łańcuchów znaków. Oto przykład: >>> 'Wartość to {:0,.2f}'.format(x) 'Wartość to 1,234.57' >>>
Omówienie Formatowanie liczb w celu ich wyświetlenia jest zwykle proste. Przedstawiona tu technika działa zarówno dla liczb zmiennoprzecinkowych, jak i dla liczb typu Decimal z modułu decimal. Gdy liczba cyfr jest ograniczana, wartości są zaokrąglane według tych samych reguł co w funkcji round(): >>> x 1234.56789 >>> format(x, '0.1f') '1234.6' >>> format(-x, '0.1f') '-1234.6' >>>
Przy formatowaniu wartości z wykorzystaniem separatora tysięcy nie są uwzględniane ustawienia językowe. Jeśli trzeba je uwzględnić, warto zapoznać się z funkcjami z modułu locale. Można też zmodyfikować znak separatora za pomocą metody translate() łańcuchów znaków. Oto przykład: >>> swap_separators = { ord('.'):',', ord(','):'.' } >>> format(x, ',').translate(swap_separators) '1.234,56789' >>>
W kodzie napisanym w Pythonie liczby często są formatowane za pomocą operatora %: >>> '%0.2f' % x '1234.57' >>> '%10.1f' % x ' 1234.6' >>> '%-10.1f' % x '1234.6 ' >>>
Ten sposób formatowania jest akceptowalny, jednak daje mniej możliwości niż nowsza metoda format(). Operator % nie udostępnia pewnych mechanizmów, np. nie pozwala dodać separatora tysięcy.
3.3. Formatowanie liczb w celu ich wyświetlenia
91
3.4. Stosowanie dwójkowych, ósemkowych i szesnastkowych liczb całkowitych Problem Programista musi przekształcić lub wyświetlić liczby całkowite reprezentowane za pomocą cyfr w systemie dwójkowym, ósemkowym lub szesnastkowym.
Rozwiązanie Aby przekształcić liczbę całkowitą na łańcuch znaków z cyframi w systemie dwójkowym, ósemkowym lub szesnastkowym, należy zastosować funkcję bin(), oct() lub hex(): >>> x = 1234 >>> bin(x) '0b10011010010' >>> oct(x) '0o2322' >>> hex(x) '0x4d2' >>>
Jeśli nie chcesz, aby pojawiały się przedrostki 0b , 0o lub 0x , możesz zastosować funkcję format(). Oto przykład: >>> format(x, 'b') '10011010010' >>> format(x, 'o') '2322' >>> format(x, 'x') '4d2' >>>
W liczbach całkowitych uwzględniany jest znak, dlatego jeśli używasz liczb ujemnych, w danych wyjściowych pojawia się znak: >>> x = -1234 >>> format(x, 'b') '-10011010010' >>> format(x, 'x') '-4d2' >>>
Jeśli chcesz uzyskać wartość bez znaku, powinieneś do pierwotnej (ujemnej) wartości dodać wartość maksymalną dla liczb o danej długości bitowej. Np. aby wyświetlić wartość 32-bitową, należy zastosować następujący kod: >>> x = -1234 >>> format(2**32 + x, 'b') '11111111111111111111101100101110' >>> format(2**32 + x, 'x') 'fffffb2e' >>>
92
Rozdział 3. Liczby, daty i czas
Aby zmienić podstawę liczby całkowitej, wystarczy wywołać funkcję int() z odpowiednią podstawą: >>> int('4d2', 16) 1234 >>> int('10011010010', 2) 1234 >>>
Omówienie Korzystanie z dwójkowych, ósemkowych i szesnastkowych liczb całkowitych jest zwykle proste. Wystarczy pamiętać, że konwersje dotyczą tylko przekształcania wartości na postać tekstową i odwrotnie. Na zapleczu istnieje tylko jeden typ liczb całkowitych. Warto też ostrzec programistów stosujących liczby ósemkowe. Składnia Pythona przeznaczona do pracy z liczbami ósemkowymi jest nieco odmienna niż w wielu innych językach. Jeśli spróbujesz wywołać następujący kod, otrzymasz błąd składni: >>> import os >>> os.chmod('script.py', 0755) File "", line 1 os.chmod('script.py', 0755) ^ SyntaxError: invalid token >>>
Koniecznie należy dodać do wartości przedrostek 0o, tak jak w poniższym kodzie: >>> os.chmod('script.py', 0o755) >>>
3.5. Pakowanie do bajtów i wypakowywanie z bajtów dużych liczb całkowitych Problem Istnieje łańcuch bajtów i trzeba wypakować jego wartość do liczby całkowitej. Możliwe też, że programista musi przekształcić dużą liczbę całkowitą z powrotem do łańcucha bajtów.
Rozwiązanie Załóżmy, że w programie używane są 16-elementowe łańcuchy bajtów, w których zapisane są 128-bitowe liczby całkowite. Oto przykład: data = b'\x00\x124V\x00x\x90\xab\x00\xcd\xef\x01\x00#\x004'
Aby bajty były traktowane jak liczba całkowita, należy zastosować funkcję int.from_bytes() i określić porządek bitów: >>> len(data) 16 >>> int.from_bytes(data, 'little') 69120565665751139577663547927094891008 >>> int.from_bytes(data, 'big') 94522842520747284487117727783387188 >>>
3.5. Pakowanie do bajtów i wypakowywanie z bajtów dużych liczb całkowitych
93
Jeśli chcesz przekształcić dużą liczbę całkowitą z powrotem na łańcuchy bajtów, wywołaj metodę int.to_bytes() i określ liczbę bajtów oraz porządek bitów: >>> x = 94522842520747284487117727783387188 >>> x.to_bytes(16, 'big') b'\x00\x124V\x00x\x90\xab\x00\xcd\xef\x01\x00#\x004' >>> x.to_bytes(16, 'little') b'4\x00#\x00\x01\xef\xcd\x00\xab\x90x\x00V4\x12\x00' >>>
Omówienie Przekształcanie dużych liczb całkowitych na łańcuchy bajtów i odwrotnie nie jest często wykonywaną operacją. Jednak jest ona potrzebna w aplikacjach z niektórych dziedzin, np. w kryptografii i rozwiązaniach sieciowych. Np. adresy sieciowe IPv6 są reprezentowane jako 128-bitowe liczby całkowite. Jeśli piszesz kod, który pobiera takie wartości z rekordu z danymi, możesz potrzebować takich operacji. Zamiast techniki przedstawionej w recepturze można wypakować wartości za pomocą modułu struct (zobacz recepturę 6.11). To rozwiązanie działa, jednak wielkość liczb całkowitych, które można wypakować w ten sposób, jest ograniczona. Dlatego należy wypakować kilka wartości i połączyć je, aby uzyskać ostateczną liczbę. Oto przykład: >>> data b'\x00\x124V\x00x\x90\xab\x00\xcd\xef\x01\x00#\x004' >>> import struct >>> hi, lo = struct.unpack('>QQ', data) >>> (hi << 64) + lo 94522842520747284487117727783387188 >>>
Porządek bitów (little lub big) określa, czy bity wchodzące w skład liczby całkowitej są uporządkowane od najmniej do najbardziej istotnego lub odwrotnie. Kolejności bitów można się łatwo przyjrzeć, wykorzystując starannie dobraną wartość szesnastkową: >>> x = 0x01020304 >>> x.to_bytes(4, 'big') b'\x01\x02\x03\x04' >>> x.to_bytes(4, 'little') b'\x04\x03\x02\x01' >>>
Jeśli spróbujesz zapisać liczbę całkowitą w zbyt małym łańcuchu bajtów, wystąpi błąd. Do ustalenia, ile bitów potrzeba na zapisanie danej wartości, można wykorzystać metodę int.bit_length(): >>> x = 523 ** 23 >>> x 335381300113661875107536852714019056160355655333978849017944067 >>> x.to_bytes(16, 'little') Traceback (most recent call last): File "", line 1, in OverflowError: int too big to convert >>> x.bit_length() 208 >>> nbytes, rem = divmod(x.bit_length(), 8) >>> if rem: ... nbytes += 1 ... >>> >>> x.to_bytes(nbytes, 'little') b'\x03X\xf1\x82iT\x96\xac\xc7c\x16\xf3\xb9\xcf...\xd0' >>>
94
Rozdział 3. Liczby, daty i czas
3.6. Przeprowadzanie obliczeń na liczbach zespolonych Problem W kodzie komunikującym się z najnowszym internetowym systemem uwierzytelniania wystąpiła osobliwość i jedynym sposobem na poradzenie sobie z nią jest wykorzystanie płaszczyzny zespolonej. Możliwe też, że programista musi przeprowadzić pewne obliczenia z wykorzystaniem liczb zespolonych.
Rozwiązanie Liczby zespolone można podawać za pomocą funkcji complex(real, imag) lub przy użyciu liczb zmiennoprzecinkowych z przyrostkiem j. Oto przykład: >>> a = complex(2, 4) >>> b = 3 - 5j >>> a (2+4j) >>> b (3-5j) >>>
Części rzeczywistą i urojoną oraz liczbę sprzężoną można łatwo otrzymać w przedstawiony poniżej sposób: >>> a.real 2.0 >>> a.imag 4.0 >>> a.conjugate() (2-4j) >>>
Dla liczb zespolonych działają wszystkie standardowe operatory matematyczne: >>> a + b (5-1j) >>> a * b (26+2j) >>> a / b (-0.4117647058823529+0.6470588235294118j) >>> abs(a) 4.47213595499958 >>>
Do wykonywania dodatkowych operacji na liczbach zespolonych, np. obliczania sinusa, cosinusa lub pierwiastka kwadratowego, należy wykorzystać moduł cmath: >>> import cmath >>> cmath.sin(a) (24.83130584894638-11.356612711218174j) >>> cmath.cos(a) (-11.36423470640106-24.814651485634187j) >>> cmath.exp(a) (-4.829809383269385-5.5920560936409816j) >>>
3.6. Przeprowadzanie obliczeń na liczbach zespolonych
95
Omówienie Większość modułów matematycznych Pythona obsługuje liczby zespolone. Np. za pomocą modułu numpy można łatwo tworzyć liczby zespolone i przeprowadzać na nich operacje: >>> import numpy as np >>> a = np.array([2+3j, 4+5j, 6-7j, 8+9j]) >>> a array([ 2.+3.j, 4.+5.j, 6.-7.j, 8.+9.j]) >>> a + 2 array([ 4.+3.j, 6.+5.j, 8.-7.j, 10.+9.j]) >>> np.sin(a) array([ 9.15449915 -4.16890696j, -56.16227422 -48.50245524j, -153.20827755-526.47684926j, 4008.42651446-589.49948373j]) >>>
Standardowe funkcje matematyczne Pythona domyślnie nie generują liczb zespolonych, dlatego jest mało prawdopodobne, że taka liczba przypadkowo wystąpi w kodzie. Oto przykład: >>> import math >>> math.sqrt(-1) Traceback (most recent call last): File "", line 1, in ValueError: math domain error >>>
Jeśli wynikiem ma być liczba zespolona, trzeba bezpośrednio użyć modułu cmath lub zadeklarować typ liczb zespolonych za pomocą biblioteki, która je obsługuje. Oto przykład: >>> import cmath >>> cmath.sqrt(-1) 1j >>>
3.7. Nieskończoność i wartości NaN Problem Programista chce utworzyć wartości zmiennoprzecinkowe reprezentujące nieskończoność, nieskończoność ujemną lub NaN (ang. not a number, czyli nie liczba) lub sprawdzić, czy zmienna odpowiada jednej z nich.
Rozwiązanie Python nie udostępnia specjalnej składni do przedstawiania wymienionych nietypowych wartości zmiennoprzecinkowych, można je jednak utworzyć za pomocą funkcji float(): >>> a = float('inf') >>> b = float('-inf') >>> c = float('nan') >>> a inf >>> b -inf >>> c nan >>>
96
Rozdział 3. Liczby, daty i czas
Aby sprawdzić, czy zmienna ma jedną z tych wartości, można wykorzystać funkcje math.isinf() i math.isnan(): >>> math.isinf(a) True >>> math.isnan(c) True >>>
Omówienie Bardziej szczegółowe informacje na temat tych specjalnych wartości zmiennoprzecinkowych znajdziesz w specyfikacji standardu IEEE 754. Trzeba jednak pamiętać o kilku szczegółach. Wyjątkowo istotne są kwestie związane z porównaniami i operatorami. Wartości nieskończone są traktowane w obliczeniach zgodnie z regułami matematycznymi: >>> >>> inf >>> inf >>> 0.0 >>>
a = float('inf') a + 45 a * 10 10 / a
Jednak niektóre operacje są niezdefiniowane i dają w efekcie wartość NaN: >>> >>> nan >>> >>> nan >>>
a = float('inf') a/a b = float('-inf') a + b
Operacje z wartościami NaN dają wynik NaN i nie zgłaszają przy tym wyjątku. Oto przykład: >>> >>> nan >>> nan >>> nan >>> nan >>>
c = float('nan') c + 23 c / 2 c * 2 math.sqrt(c)
Ciekawą cechą wartości NaN jest to, że jeśli porównać je ze sobą, nie zostaną uznane za równe: >>> c >>> d >>> c False >>> c False >>>
= float('nan') = float('nan') == d is d
Dlatego jedynym pewnym sposobem na sprawdzenie, czy zmienna ma wartość NaN, jest użycie funkcji math.isnan() (to podejście zastosowano w recepturze).
3.7. Nieskończoność i wartości NaN
97
Czasem programista chce zmienić działanie Pythona, tak aby program zgłaszał wyjątek, gdy wynikiem operacji jest nieskończoność lub wartość NaN. Można wykorzystać do tego moduł fpectl, który jednak w standardowej kompilacji Pythona nie jest włączony, jest zależny od platformy i powinni z niego korzystać tylko doświadczeni programiści. Więcej informacji znajdziesz w internetowej dokumentacji Pythona (http://docs.python.org/3/library/fpectl.html).
3.8. Obliczenia z wykorzystaniem ułamków Problem Wsiadłeś do wehikułu czasu i okazało się, że masz do zrobienia zadanie domowe z zakresu ułamków. Możliwe też, że piszesz kod do przeprowadzania obliczeń dotyczących pomiarów produktów w sklepie z artykułami drewnianymi.
Rozwiązanie Do przeprowadzania obliczeń matematycznych z wykorzystaniem ułamków można zastosować moduł fractions. Oto przykład: >>> from fractions import Fraction >>> a = Fraction(5, 4) >>> b = Fraction(7, 16) >>> print(a + b) 27/16 >>> print(a * b) 35/64 >>> >>> >>> 35 >>> 64
# Określanie licznika i mianownika c = a * b c.numerator c.denominator
>>> # Przekształcanie na liczby zmiennoprzecinkowe >>> float(c) 0.546875 >>> # Ograniczanie mianownika >>> print(c.limit_denominator(8)) 4/7 >>> # Przekształcanie liczby zmiennoprzecinkowej na ułamek >>> x = 3.75 >>> y = Fraction(*x.as_integer_ratio()) >>> y Fraction(15, 4) >>>
Omówienie W większości programów obliczenia na ułamkach występują rzadko. Czasem jednak warto je zastosować. Np. umożliwianie w programie przyjmowania wartości ułamkowych wyrażonych w jednostkach miar i wykonywanie obliczeń na takich wartościach sprawia, że nie trzeba ręcznie przekształcać danych na liczby dziesiętne lub zmiennoprzecinkowe. 98
Rozdział 3. Liczby, daty i czas
3.9. Obliczenia z wykorzystaniem dużych tablic liczbowych Problem Programista musi przeprowadzać obliczenia na dużych zbiorach liczbowych, np. tablicach lub siatkach.
Rozwiązanie Do wykonywania wymagających obliczeń na tablicach należy stosować bibliotekę NumPy (http://www.numpy.org/). Główną jej cechą jest to, że udostępnia w Pythonie tablicę, która jest znacznie wydajniejsza i lepiej dostosowana do obliczeń matematycznych niż standardowe listy Pythona. Oto krótki przykład ilustrujący ważne różnice w działaniu list i tablic z biblioteki NumPy: >>> # Listy Pythona >>> x = [1, 2, 3, 4] >>> y = [5, 6, 7, 8] >>> x * 2 [1, 2, 3, 4, 1, 2, 3, 4] >>> x + 10 Traceback (most recent call last): File "", line 1, in TypeError: can only concatenate list (not "int") to list >>> x + y [1, 2, 3, 4, 5, 6, 7, 8] >>> # Tablice z biblioteki NumPy >>> import numpy as np >>> ax = np.array([1, 2, 3, 4]) >>> ay = np.array([5, 6, 7, 8]) >>> ax * 2 array([2, 4, 6, 8]) >>> ax + 10 array([11, 12, 13, 14]) >>> ax + ay array([ 6, 8, 10, 12]) >>> ax * ay array([ 5, 12, 21, 32]) >>>
Jak widać, podstawowe operacje matematyczne działają inaczej dla tablic. Operacje skalarne (np. ax * 2 lub ax + 10) dotyczą poszczególnych elementów. Ponadto jeśli oba operandy są tablicami, operacja jest stosowana dla wszystkich elementów, a w efekcie powstaje nowa tablica. Ponieważ operacje matematyczne są przeprowadzane jednocześnie na wszystkich elementach, można bardzo łatwo i szybko uzyskać dla całej tablicy wyniki wykonania funkcji — np. obliczyć wartość wielomianu: >>> def f(x): ... return 3*x**2 - 2*x + 7 ... >>> f(ax) array([ 8, 15, 28, 47]) >>>
3.9. Obliczenia z wykorzystaniem dużych tablic liczbowych
99
Biblioteka NumPy udostępnia kolekcję funkcji uniwersalnych, które umożliwiają wykonywanie operacji na tablicach. Funkcje te można stosować zamiast podobnych funkcji z modułu math. Oto przykład: >>> np.sqrt(ax) array([ 1. , 1.41421356, 1.73205081, 2. ]) >>> np.cos(ax) array([ 0.54030231, -0.41614684, -0.9899925 , -0.65364362]) >>>
Kod z funkcjami uniwersalnymi może być setki razy szybszy od kodu przechodzącego w pętli po poszczególnych elementach tablicy i wykonującego obliczenia przy użyciu funkcji z modułu math. Dlatego gdy to możliwe, należy korzystać z funkcji uniwersalnych. Na zapleczu tablice z biblioteki NumPy są alokowane w taki sam sposób jak w językach C i Fortran. Tablice te to duże ciągłe obszary pamięci zawierające elementy tego samego typu danych. Dzięki temu tablice mogą być znacznie większe niż listy Pythona. Można np. bez problemu utworzyć dwuwymiarową siatkę 10 000 na 10 000 liczb zmiennoprzecinkowych: >>> grid = np.zeros(shape=(10000,10000), dtype=float) >>> grid array([[ 0., 0., 0., ..., 0., 0., 0.], [ 0., 0., 0., ..., 0., 0., 0.], [ 0., 0., 0., ..., 0., 0., 0.], ..., [ 0., 0., 0., ..., 0., 0., 0.], [ 0., 0., 0., ..., 0., 0., 0.], [ 0., 0., 0., ..., 0., 0., 0.]]) >>>
Bardzo ważnym aspektem biblioteki NumPy jest rozszerzenie mechanizmu wskazywania elementów za pomocą indeksu na listach Pythona (dotyczy to zwłaszcza tablic wielowymiarowych). W ramach ilustracji utwórzmy prostą tablicę dwuwymiarową i spróbujmy przeprowadzić kilka eksperymentów:
Omówienie NumPy jest podstawowym elementem wielu bibliotek naukowych i inżynieryjnych z Pythona. Jest to także jeden z największych i najbardziej skomplikowanych spośród powszechnie używanych modułów. Można jednak wykonać przy jego użyciu przydatne zadania, zaczynając od prostych przykładów i samodzielnych prób. Warto zwrócić uwagę na pewną kwestię dotyczącą korzystania z tego modułu. Stosunkowo często stosuje się instrukcję import numpy as np, tak jak w przedstawionym rozwiązaniu. Pozwala to skrócić nazwę biblioteki do postaci, którą wygodniej jest wielokrotnie wprowadzać w programie. Aby znaleźć więcej informacji, należy odwiedzić stronę http://www.numpy.org.
3.9. Obliczenia z wykorzystaniem dużych tablic liczbowych
101
3.10. Przeprowadzanie operacji na macierzach i z zakresu algebry liniowej Problem Programista chce wykonywać operacje na macierzach i z zakresu algebry liniowej, np. mnożyć macierze, znajdować wyznaczniki, rozwiązywać równania liniowe itd.
Rozwiązanie Biblioteka NumPy (http://www.numpy.org/) udostępnia obiekt matrix, który pozwala wykonywać wspomniane zadania. Macierze przypominają nieco opisane w recepturze 3.9. obiekty tablic, ale przy obliczeniach uwzględniane są tu reguły algebry liniowej. Oto przykład ilustrujący kilka najważniejszych cech macierzy: >>> import numpy as np >>> m = np.matrix([[1,-2,3],[0,4,5],[7,8,-9]]) >>> m matrix([[ 1, -2, 3], [ 0, 4, 5], [ 7, 8, -9]]) >>> # Zwraca transpozycję >>> m.T matrix([[ 1, 0, 7], [-2, 4, 8], [ 3, 5, -9]]) >>> # Return inverse >>> m.I matrix([[ 0.33043478, -0.02608696, 0.09565217], [-0.15217391, 0.13043478, 0.02173913], [ 0.12173913, 0.09565217, -0.0173913 ]]) >>> # Tworzenie i mnożenie wektora >>> v = np.matrix([[2],[3],[4]]) >>> v matrix([[2], [3], [4]]) >>> m * v matrix([[ 8], [32], [ 2]]) >>>
W podpakiecie numpy.linalg znajdziesz więcej operacji. Oto przykład: >>> import numpy.linalg >>> # Wyznacznik >>> numpy.linalg.det(m) -229.99999999999983 >>> # Wartości własne >>> numpy.linalg.eigvals(m) array([-13.11474312, 2.75956154, 6.35518158])
102
Rozdział 3. Liczby, daty i czas
>>> # Wyznaczanie x z równania mx = v >>> x = numpy.linalg.solve(m, v) >>> x matrix([[ 0.96521739], [ 0.17391304], [ 0.46086957]]) >>> m * x matrix([[ 2.], [ 3.], [ 4.]]) >>> v matrix([[2], [3], [4]]) >>>
Omówienie Algebra liniowa to szerokie zagadnienie, znacznie wykraczające poza zakres tej książki. Jeśli jednak potrzebujesz manipulować macierzami i wektorami, biblioteka NumPy jest dobrym punktem wyjścia. Szczegółowe informacje znajdziesz na stronie http://www.numpy.org.
3.11. Losowe pobieranie elementów Problem Programista chce losowo pobierać elementy z tablicy lub generować liczby losowe.
Rozwiązanie Moduł random udostępnia różne funkcje związane z liczbami losowymi i losowym pobieraniem elementów. Np. aby losowo pobrać element z sekwencji, należy wywołać metodę random.choice(): >>> >>> >>> 2 >>> 3 >>> 1 >>> 4 >>> 6 >>>
Do generowania wartości zmiennoprzecinkowych z przedziału od 0 do 1 służy metoda random.random() : >>> random.random() 0.9406677561675867 >>> random.random() 0.133129581343897 >>> random.random() 0.4144991136919316 >>>
W celu wygenerowania N losowych bitów przedstawionych w postaci liczby całkowitej należy wywołać metodę random.getrandbits(): >>> random.getrandbits(200) 335837000776573622800628485064121869519521710558559406913275 >>>
Omówienie Moduł random generuje liczby losowe na podstawie algorytmu Mersenne Twister. Jest to algorytm deterministyczny, przy czym za pomocą funkcji random.seed()można zmienić używane ziarno: random.seed() # Ziarno oparte na czasie systemowym lub wywołaniu os.urandom() random.seed(12345) # Ziarno oparte na podanej liczbie całkowitej random.seed(b'bytedata') # Ziarno oparte na danych bajtowych
Oprócz przedstawionych mechanizmów moduł random() udostępnia metody do generowania rozkładu jednostajnego, rozkładu normalnego i innych rozkładów prawdopodobieństwa. Np. metoda random.uniform() generuje liczby na podstawie rozkładu jednostajnego, a random.gauss() — na podstawie rozkładu normalnego. Informacje o innych obsługiwanych rozkładach prawdopodobieństwa znajdziesz w dokumentacji.
104
Rozdział 3. Liczby, daty i czas
Metod z modułu random() nie należy używać w programach z obszaru kryptografii. Jeśli potrzebujesz metod do zastosowania w takim kontekście, pomyśl o metodach z modułu ssl. Np. za pomocą funkcji ssl.RAND_bytes() można wygenerować bezpieczną kryptograficznie sekwencję losowych bajtów.
3.12. Przekształcanie dni na sekundy i inne podstawowe konwersje związane z czasem Problem W kodzie trzeba wykonywać proste konwersje czasu, np. z dni na sekundy, z godzin na minuty itd.
Rozwiązanie Aby dokonywać przekształceń i przeprowadzać obliczenia arytmetyczne z wykorzystaniem różnych jednostek czasu, należy zastosować moduł datetime. Np. aby przedstawić przedział czasu, należy utworzyć obiekt typu timedelta: >>> from datetime import timedelta >>> a = timedelta(days=2, hours=6) >>> b = timedelta(hours=4.5) >>> c = a + b >>> c.days 2 >>> c.seconds 37800 >>> c.seconds / 3600 10.5 >>> c.total_seconds() / 3600 58.5 >>>
Jeśli chcesz zapisać określoną datę i czas, utwórz obiekty typu datetime. Następnie możesz manipulować nimi za pomocą standardowych operatorów matematycznych: >>> from datetime import datetime >>> a = datetime(2012, 9, 23) >>> print(a + timedelta(days=10)) 2012-10-03 00:00:00 >>> >>> b = datetime(2012, 12, 21) >>> d = b - a >>> d.days 89 >>> now = datetime.today() >>> print(now) 2012-12-21 14:54:43.094063 >>> print(now + timedelta(minutes=10)) 2012-12-21 15:04:43.094063 >>>
3.12. Przekształcanie dni na sekundy i inne podstawowe konwersje związane z czasem
105
Warto zauważyć, że przy przeprowadzaniu obliczeń z wykorzystaniem typu datetime uwzględniane są lata przestępne. Oto przykład: >>> a = datetime(2012, >>> b = datetime(2012, >>> a - b datetime.timedelta(2) >>> (a - b).days 2 >>> c = datetime(2013, >>> d = datetime(2013, >>> (c - d).days 1 >>>
3, 1) 2, 28)
3, 1) 2, 28)
Omówienie Do wykonywania najprostszych manipulacji datą i czasem moduł datetime jest wystarczający. Jeśli chcesz przeprowadzać bardziej złożone operacje (np. uwzględnić strefy czasowe, zapisywać nieprecyzyjne przedziały czasu, obliczać daty świąt itd.), pomyśl o zastosowaniu modułu dateutil (https://pypi.python.org/pypi/python-dateutil). Wiele standardowych obliczeń związanych z czasem można wykonać za pomocą funkcji dateutil.relativedelta(). Ważną cechą modułu dateutil jest to, że uzupełnia luki w zakresie obsługi miesięcy (i uwzględnia to, że mają różną liczbę dni). Oto przykład: >>> a = datetime(2012, 9, 23) >>> a + timedelta(months=1) Traceback (most recent call last): File "", line 1, in TypeError: 'months' is an invalid keyword argument for this function >>> >>> from dateutil.relativedelta import relativedelta >>> a + relativedelta(months=+1) datetime.datetime(2012, 10, 23, 0, 0) >>> a + relativedelta(months=+4) datetime.datetime(2013, 1, 23, 0, 0) >>> >>> # Czas między dwiema datami >>> b = datetime(2012, 12, 21) >>> d = b - a >>> d datetime.timedelta(89) >>> d = relativedelta(b, a) >>> d relativedelta(months=+2, days=+28) >>> d.months 2 >>> d.days 28 >>>
106
Rozdział 3. Liczby, daty i czas
3.13. Określanie daty ostatniego piątku Problem Programista szuka uniwersalnej techniki wyszukiwania daty ostatniego wystąpienia wybranego dnia tygodnia, np. ostatniego piątku.
Rozwiązanie Moduł datetime Pythona udostępnia funkcje narzędziowe i klasy pomocne przy wykonywaniu tego rodzaju obliczeń. Oto dobre uniwersalne rozwiązanie przedstawionego problemu: from datetime import datetime, timedelta weekdays = ['Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota', 'Niedziela'] def get_previous_byday(dayname, start_date=None): if start_date is None: start_date = datetime.today() day_num = start_date.weekday() day_num_target = weekdays.index(dayname) days_ago = (7 + day_num - day_num_target) % 7 if days_ago == 0: days_ago = 7 target_date = start_date - timedelta(days=days_ago) return target_date
To narzędzie można wykorzystać w sesji interpretera w następujący sposób: >>> datetime.today() # Ustalenie punktu odniesienia datetime.datetime(2012, 8, 28, 22, 4, 30, 263076) >>> get_previous_byday('Poniedziałek') datetime.datetime(2012, 8, 27, 22, 3, 57, 29045) >>> get_previous_byday('Wtorek') # Wtorek w poprzednim tygodniu (nie bieżący dzień) datetime.datetime(2012, 8, 21, 22, 4, 12, 629771) >>> get_previous_byday('Piątek') datetime.datetime(2012, 8, 24, 22, 5, 9, 911393) >>>
Za pomocą innego obiektu typu datetime można podać opcjonalny parametr start_date: >>> get_previous_byday('Niedziela', datetime(2012, 12, 21)) datetime.datetime(2012, 12, 16, 0, 0) >>>
Omówienie Ta receptura odwzorowuje datę początkową i docelową na numer odpowiadających im dni tygodnia (Poniedziałek to 0). Następnie za pomocą arytmetyki modulo kod określa, ile dni temu wypadła docelowa data. Potem wystarczy obliczyć pożądaną datę na podstawie daty początkowej, odejmując odpowiedni obiekt typu timedelta. Jeśli potrzebujesz wielu obliczeń tego rodzaju, lepsze efekty możesz uzyskać po zainstalowaniu pakietu python-dateutil (https://pypi.python.org/pypi/python-dateutil). Oto przykład pokazujący, jak wykonać te same obliczenia przy użyciu funkcji relativedelta() z pakietu dateutil:
3.13. Określanie daty ostatniego piątku
107
>>> from datetime import datetime >>> from dateutil.relativedelta import relativedelta >>> from dateutil.rrule import * >>> d = datetime.now() >>> print(d) 2012-12-23 16:31:52.718111 >>> # Następny piątek >>> print(d + relativedelta(weekday=FR)) 2012-12-28 16:31:52.718111 >>> >>> # Ostatni piątek >>> print(d + relativedelta(weekday=FR(-1))) 2012-12-21 16:31:52.718111 >>>
3.14. Określanie przedziału dat odpowiadającego bieżącemu miesiącowi Problem Programista chce przejść w pętli po wszystkich datach z bieżącego miesiąca i potrzebuje wydajnego sposobu na wyznaczenie przedziału czasu odpowiadającego temu okresowi.
Rozwiązanie Przechodzenie w pętli po datach nie wymaga uprzedniego tworzenia listy wszystkich dat. Wystarczy ustalić początkową i końcową datę przedziału, a następnie wykorzystać obiekt typu datetime.timedelta do zwiększania daty w każdym kroku. Poniższa funkcja przyjmuje obiekt typu datetime i zwraca krotkę zawierającą datę pierwszego dnia bieżącego miesiąca oraz datę pierwszego dnia następnego miesiąca: from datetime import datetime, date, timedelta import calendar def get_month_range(start_date=None): if start_date is None start_date = date.today().replace(day=1) _, days_in_month = calendar.monthrange(start_date.year, start_date.month) end_date = start_date + timedelta(days=days_in_month) return (start_date, end_date)
Następnie można stosunkowo łatwo przejść w pętli po przedziale dat: >>> a_day = timedelta(days=1) >>> first_day, last_day = get_month_range() >>> while first_day < last_day: ... print(first_day) ... first_day += a_day ... 2012-08-01 2012-08-02 2012-08-03 2012-08-04 2012-08-05
108
Rozdział 3. Liczby, daty i czas
2012-08-06 2012-08-07 2012-08-08 2012-08-09 # itd.
Omówienie Ta receptura oblicza najpierw datę pierwszego dnia bieżącego miesiąca. Szybkim sposobem na wykonanie tego zadania jest wykorzystanie metody replace() obiektów typu date lub datetime do ustawienia atrybutu days na 1. Wygodną cechą metody replace() jest to, że tworzy ona obiekt tego samego typu, jaki został do niej przekazany. Dlatego jeśli otrzymała obiekt typu date, zwraca obiekt typu date, natomiast jeśli otrzymała obiekt typu datetime, zwraca obiekt typu datetime. Dalej w kodzie wywoływana jest funkcja calendar.monthrange(). Pozwala to ustalić, ile dni ma dany miesiąc. Gdy potrzebne są podstawowe informacje na temat kalendarza, moduł calendar może okazać się pomocny. Funkcja monthrange() jest tylko jedną z wielu. Zwraca krotkę zawierającą dzień tygodnia wraz z liczbą dni w miesiącu. Po ustaleniu liczby dni w miesiącu należy określić datę końcową, dodając do daty początkowej odpowiedni obiekt typu timedelta. Drobnym, ale ważnym aspektem receptury jest to, że data końcowa nie jest uwzględniana w przedziale (jest to data pierwszego dnia następnego miesiąca). Jest to odpowiednik działania wycinków i operacji na przedziałach w Pythonie, gdzie końcowy element nigdy nie jest uwzględniany. Do przechodzenia w pętli po przedziale data służą standardowe operatory matematyczne i operatory porównywania. Do zwiększenia daty można wykorzystać obiekt typu timedelta, a operator < służy do sprawdzania, czy data wypada przed datą końcową. Najlepszym rozwiązaniem jest utworzenie funkcji, która działa podobnie jak wbudowana funkcja range(), ale jest przeznaczona do przetwarzania dat. Na szczęście efekt ten można bardzo łatwo uzyskać za pomocą generatora: def date_range(start, stop, step): while start < stop: yield start start += ste
Poniżej pokazano, jak zastosować tę funkcję: >>> for d in date_range(datetime(2012, 9, 1), datetime(2012,10,1), timedelta(hours=6)): ... print(d) ... 2012-09-01 00:00:00 2012-09-01 06:00:00 2012-09-01 12:00:00 2012-09-01 18:00:00 2012-09-02 00:00:00 2012-09-02 06:00:00 ... >>>
Napisanie tej funkcji jest tak proste głównie dlatego, że datami i czasem można manipulować za pomocą standardowych operatorów matematycznych i porównywania.
3.14. Określanie przedziału dat odpowiadającego bieżącemu miesiącowi
109
3.15. Przekształcanie łańcuchów znaków na obiekty typu datetime Problem Aplikacja pobiera określające czas dane jako łańcuchy znaków. Programista chce przekształcić je na obiekty typu datetime, aby wykonywać na nich operacje niezwiązane z łańcuchami znaków.
Rozwiązanie Problem ten można zwykle łatwo rozwiązać za pomocą standardowego modułu datetime Pythona. Oto przykład: >>> from datetime import datetime >>> text = '2012-09-20' >>> y = datetime.strptime(text, '%Y-%m-%d') >>> z = datetime.now() >>> diff = z - y >>> diff datetime.timedelta(3, 77824, 177393) >>
Omówienie Metoda datetime.strptime() obsługuje wiele kodów formatujących, w tym %Y odpowiadający latom w formacie czterocyfrowym i %m dla miesięcy w formacie dwucyfrowym. Warto zauważyć, że kody te działają też w drugą stronę, gdy trzeba zapisać obiekt typu datetime w formie łańcucha znaków i sprawić, aby dobrze wyglądał. Załóżmy, że kod generuje obiekt typu datetime, który trzeba przekształcić na zrozumiałą dla ludzi datę umieszczaną w nagłówku automatycznie generowanego listu lub raportu: >>> z datetime.datetime(2012, 9, 23, 21, 37, 4, 177393) >>> nice_z = datetime.strftime(z, '%A, %d %B %Y') >>> nice_z 'Sunday, 23 September 2012' >>>
Warto zauważyć, że wydajność metody strptime() jest często znacznie niższa od oczekiwanej. Wynika to z tego, że metodę tę napisano w czystym Pythonie, a ponadto trzeba w niej uwzględnić wszelkie ustawienia językowe systemu. Jeśli zamierzasz przetwarzać w kodzie dużą ilość dat i znasz ich format, prawdopodobnie lepszą wydajność uzyskasz dzięki opracowaniu niestandardowego rozwiązania. Jeżeli daty mają format „RRRR-MM-DD”, można napisać następującą funkcję: from datetime import datetime def parse_ymd(s): year_s, mon_s, day_s = s.split('-') return datetime(int(year_s), int(mon_s), int(day_s))
W trakcie testów okazało się, że funkcja ta działała ponad siedmiokrotnie szybciej niż funkcja datetime.strptime(). Warto to uwzględnić przy przetwarzaniu dużych ilości danych zawierających daty. 110
Rozdział 3. Liczby, daty i czas
3.16. Manipulowanie datami z uwzględnieniem stref czasowych Problem Telekonferencja jest zaplanowana na 21 grudnia 2012 roku na godzinę 9.30 rano czasu obowiązującego w Chicago. Która to będzie godzina dla rozmówcy mieszkającego w Bangalore w Indiach?
Rozwiązanie Przy rozwiązywaniu prawie każdego problemu związanego ze strefami czasowymi należy korzystać z modułu pytz (https://pypi.python.org/pypi/pytz) . Obejmuje on bazę danych stref czasowych Olsona, która jest standardową bazą informacji o strefach czasowych wykorzystywaną w wielu językach i systemach operacyjnych. Moduł pytz służy przede wszystkim do tworzenia lokalnych wersji prostych dat utworzonych za pomocą biblioteki datetime. Poniżej pokazano, jak można przedstawić datę według czasu obowiązującego w Chicago: >>> from datetime import datetime >>> from pytz import timezone >>> d = datetime(2012, 12, 21, 9, 30, 0) >>> print(d) 2012-12-21 09:30:00 >>> >>> # Tworzenie daty dla czasu obowiązującego w Chicago >>> central = timezone('US/Central') >>> loc_d = central.localize(d) >>> print(loc_d) 2012-12-21 09:30:00-06:00 >>>
Po utworzeniu lokalnej wersji daty można ją przekształcić na inne strefy czasowe. Aby ustalić, która godzina będzie w tym samym czasie w Bangalore, należy zastosować następujący kod: >>> # Przekształcanie na czas obowiązujący w Bangalore >>> bang_d = loc_d.astimezone(timezone('Asia/Kolkata')) >>> print(bang_d) 2012-12-21 21:00:00+05:30 >>>
Przy wykonywaniu operacji arytmetycznych na lokalnych datach trzeba pamiętać o czasie letnim i podobnych aspektach. Np. w 2013 roku czas letni w Stanach Zjednoczonych zaczął się 13 marca o godzinie 2.00 rano (kiedy to przestawiono zegarki o godzinę do przodu). Zastosowanie naiwnego rozwiązania spowoduje uzyskanie błędnych wyników: >>> d = datetime(2013, 3, 10, 1, 45) >>> loc_d = central.localize(d) >>> print(loc_d) 2013-03-10 01:45:00-06:00 >>> later = loc_d + timedelta(minutes=30) >>> print(later) 2013-03-10 02:15:00-06:00 # BŁĄD! >>>
3.16. Manipulowanie datami z uwzględnieniem stref czasowych
111
Wynik jest błędny, ponieważ nie uwzględniono przesunięcia czasu lokalnego o godzinę. Aby rozwiązać problem, należy wywołać metodę normalize() dla strefy czasowej: >>> from datetime import timedelta >>> later = central.normalize(loc_d + timedelta(minutes=30)) >>> print(later) 2013-03-10 03:15:00-05:00 >>>
Omówienie Aby ułatwić sobie zadanie, można zastosować popularną strategię zarządzania lokalnymi datami i na potrzeby przechowywania daty oraz manipulowania nią przekształcać je wszystkie na czas UTC. Oto przykład: >>> print(loc_d) 2013-03-10 01:45:00-06:00 >>> utc_d = loc_d.astimezone(pytz.utc) >>> print(utc_d) 2013-03-10 07:45:00+00:00 >>>
Po przekształceniu dat na czas UTC nie trzeba przejmować się kwestiami związanymi z czasem letnim i podobnymi problemami. Można wtedy przeprowadzać na datach normalne operacje arytmetyczne. Gdy trzeba zwrócić dane w czasie lokalnym, wystarczy przekształcić je na odpowiednią strefę czasową. Oto przykład: >>> later_utc = utc_d + timedelta(minutes=30) >>> print(later_utc.astimezone(central)) 2013-03-10 03:15:00-05:00 >>>
Jeden z problemów przy korzystaniu ze stref czasowych dotyczy ustalenia nazwy danej strefy. Skąd autor tej receptury wiedział, że nazwa strefy czasowej odpowiedniej dla Indii to „Asia/Kolkata”? Aby znaleźć właściwą nazwę, należy sprawdzić zawartość słownika pytz.country_timezones, podając jako klucz kod kraju zgodnie ze standardem ISO 3166: >>> pytz.country_timezones['IN'] ['Asia/Kolkata'] >>>
Możliwe, że gdy będziesz czytał tę książkę, moduł pytz będzie już uznawany za przestarzały, a zalecanym rozwiązaniem będą usprawnione mechanizmy obsługi stref czasowych, co opisano w dokumencie PEP 431 (http://www.python.org/dev/peps/pep-0431/). Jednak nawet wtedy wiele opisanych tu zagadnień będzie aktualnych (np. wskazówki dotyczące stosowania dat UTC).
112
Rozdział 3. Liczby, daty i czas
ROZDZIAŁ 4.
Iteratory i generatory
Obsługa iterowania (czyli przechodzenia po elementach) to jedna z najmocniejszych stron Pythona. Na ogólnym poziomie iterowanie można traktować jak sposób na przetwarzanie elementów sekwencji. Jednak możliwości są o wiele większe. Można np. tworzyć własne obiekty iteratorów, stosować przydatne wzorce iterowania z modułu itertools, tworzyć funkcje generatorów itd. W tym rozdziale omówiono standardowe problemy związane z iterowaniem.
4.1. Ręczne korzystanie z iteratora Problem Programista zamierza przetwarzać elementy obiektu iterowalnego, ale z pewnych powodów nie może lub nie chce korzystać z pętli for.
Rozwiązanie Aby ręcznie wykorzystać obiekt iterowalny, należy zastosować funkcję next() i napisać kod przechwytujący wyjątki StopIteration. Poniżej pokazano, jak ręcznie wczytywać wiersze z pliku: with open('/etc/passwd') as f: try: while True: line = next(f) print(line, end='') except StopIteration: pass
Wyjątek StopIteration standardowo służy jako sygnał zakończenia iteracji. Jeśli jednak programista ręcznie wywołuje metodę next() (tak jak w przedstawionym kodzie), można nakazać kodowi zwrócenie końcowej wartości, np. None. Oto przykład: with open('/etc/passwd') as f: while True: line = next(f, None) if line is None: break print(line, end='')
113
Omówienie W większości sytuacji z obiektów iterowalnych korzysta się za pomocą instrukcji for. Jednak od czasu do czasu niezbędna jest bardziej ścisła kontrola nad mechanizmem iterowania. Dlatego warto wiedzieć, co się dzieje w ramach procesu iterowania. W poniższym interaktywnym przykładzie pokazano podstawowe mechanizmy iterowania: >>> items = [1, 2, 3] >>> # Tworzenie iteratora >>> it = iter(items) # Wywołuje items.__iter__() >>> # Uruchamianie iteratora >>> next(it) # Wywołuje it.__next__() 1 >>> next(it) 2 >>> next(it) 3 >>> next(it) Traceback (most recent call last): File "", line 1, in StopIteration >>>
W dalszych recepturach rozwinięto techniki iterowania. Zakładamy tam, że znasz podstawowy protokół obsługi iteratorów. Koniecznie zapamiętaj tę pierwszą recepturę.
4.2. Delegowanie procesu iterowania Problem Programista utworzył niestandardowy obiekt kontenerowy, który wewnętrznie przechowuje listę, krotkę lub inny obiekt iterowalny. Teraz programista chce umożliwić iterowanie po nowym kontenerze.
Rozwiązanie Zwykle wystarczy zdefiniować metodę __iter__() i oddelegować w niej iterowanie do wewnętrznie przechowywanego kontenera. Oto przykład: class Node: def __init__(self, value): self._value = value self._children = [] def __repr__(self): return 'Node({!r})'.format(self._value) def add_child(self, node): self._children.append(node) def __iter__(self): return iter(self._children)
114
Rozdział 4. Iteratory i generatory
# Przykład if __name__ == '__main__': root = Node(0) child1 = Node(1) child2 = Node(2) root.add_child(child1) root.add_child(child2) for ch in root: print(ch) # Zwraca Node(1), Node(2)
W tym kodzie metoda __iter__() przekazuje żądanie iterowania do wewnętrznego atrybutu _children.
Omówienie Protokół obsługi iteratorów w Pythonie wymaga, aby metoda __iter__() zwracała specjalny obiekt iteratora udostępniający metodę __next__(), która odpowiada za iterowanie. Jeśli chcesz tylko przejść po zawartości innego kontenera, nie musisz interesować się działaniem tych metod. Wystarczy wtedy przekazać żądanie iterowania. Zastosowanie w tym miejscu funkcji iter() jest pewnym skrótem, który pozwala uprościć kod. Wywołanie iter(s) zwraca iterator w wyniku wywołania metody s.__iter__(), podobnie jak metoda len(s) wywołuje metodę s.__len__(s).
4.3. Tworzenie nowych wzorców iterowania z wykorzystaniem generatorów Problem Programista chce zastosować niestandardowy wzorzec iterowania, działający inaczej niż we wbudowanych funkcjach (takich jak range() lub reversed()).
Rozwiązanie Jeśli chcesz zastosować nowy wzorzec iterowania, zdefiniuj go przy użyciu funkcji generatora. Oto generator, który generuje przedział liczb zmiennoprzecinkowych: def frange(start, stop, increment): x = start while x < stop: yield x x += increment
Aby zastosować taką funkcję, należy wywoływać ją w pętli for lub w innej funkcji działającej dla obiektów iterowalnych (np. w funkcji sum() lub list()). Oto przykład: >>> for n in frange(0, 4, 0.5): ... print(n) ... 0
4.3. Tworzenie nowych wzorców iterowania z wykorzystaniem generatorów
Omówienie Samo umieszczenie wywołania yield w funkcji przekształca ją w generator. Generator (w odróżnieniu od standardowych funkcji) jest uruchamiany tylko w trakcie iterowania. Oto eksperyment, który warto przeprowadzić, aby przyjrzeć się mechanizmom działania takich funkcji: >>> def countdown(n): ... print('Rozpoczęcie odliczania od', n) ... while n > 0: ... yield n ... n -= 1 ... print('Gotowe!') ... >>> # Tworzenie generatora (zwróć uwagę na brak danych wyjściowych) >>> c = countdown(3) >>> c >>> # Przejście do pierwszego wywołania yield i zwrócenie wartości >>> next(c) Rozpoczęcie odliczania od 3 3 >>> # Przejście do następnego wywołania yield >>> next(c) 2 >>> # Przejście do następnego wywołania yield >>> next(c) 1 >>> # Przejście do następnego wywołania yield (zakończenie iterowania) >>> next(c) Gotowe! Traceback (most recent call last): File "", line 1, in StopIteration >>>
Najważniejszą cechą funkcji generatorów jest to, że działają tylko w odpowiedzi na operację next wykonaną w trakcie iterowania. Po zwróceniu wyniku przez funkcję generatora iterowanie zatrzymuje się. Jednak przy iterowaniu zwykle używana jest instrukcja for, która odpowiada za wszystkie operacje, dlatego nie trzeba się nimi martwić.
116
Rozdział 4. Iteratory i generatory
4.4. Implementowanie protokołu iteratora Problem Programista tworzy niestandardowe obiekty i chce dodać do nich obsługę iterowania, dlatego szuka łatwego sposobu na zaimplementowanie protokołu iteratora.
Rozwiązanie Zdecydowanie najłatwiejszym sposobem na dodanie obsługi iterowania dla obiektu jest wykorzystanie funkcji generatora. W recepturze 4.2. pokazano klasę Node służącą do przedstawiania struktur drzewiastych. Możliwe, że chcesz zaimplementować iterator, który przechodzi po węzłach zgodnie z algorytmem przeszukiwania w głąb. Oto możliwe rozwiązanie: class Node: def __init__(self, value): self._value = value self._children = [] def __repr__(self): return 'Node({!r})'.format(self._value) def add_child(self, node): self._children.append(node) def __iter__(self): return iter(self._children) def depth_first(self): yield self for c in self: yield from c.depth_first() # Przykład if __name__ == '__main__': root = Node(0) child1 = Node(1) child2 = Node(2) root.add_child(child1) root.add_child(child2) child1.add_child(Node(3)) child1.add_child(Node(4)) child2.add_child(Node(5)) for ch in root.depth_first(): print(ch) # Zwraca Node(0), Node(1), Node(3), Node(4), Node(2), Node(5)
W tym kodzie metodę depth_first() łatwo jest odczytać i opisać. Najpierw wywołuje ona funkcję yield dla bieżącego obiektu, a następnie przechodzi po wszystkich elementach podrzędnych, wywołując metodę yield dla obiektów uzyskanych przez wywołanie metody depth_first() elementów podrzędnych (wywołanie yield from).
Omówienie Protokół obsługi iteratorów Pythona wymaga, aby metoda __iter__() zwracała specjalny obiekt iteratora z implementacją operacji __next__() i używała wyjątku StopIteration do sygnalizowania zakończenia iterowania. Jednak pisanie takich obiektów jest często 4.4. Implementowanie protokołu iteratora
117
skomplikowane. Poniżej pokazano inną implementację metody depth_first(). Tym razem wykorzystano powiązaną klasę iteratora: class Node: def __init__(self, value): self._value = value self._children = [] def __repr__(self): return 'Node({!r})'.format(self._value) def add_child(self, other_node): self._children.append(other_node) def __iter__(self): return iter(self._children) def depth_first(self): return DepthFirstIterator(self) class DepthFirstIterator(object): ''' Przechodzenie w głąb ''' def __init__(self, start_node): self._node = start_node self._children_iter = None self._child_iter = None def __iter__(self): return self def __next__(self): # Zwracanie bieżącego obiektu, jeśli to pierwsze wywołanie, # i tworzenie iteratorów dla elementów podrzędnych if self._children_iter is None: self._children_iter = iter(self._node) return self._node # Jeśli przetwarzany jest element podrzędny, # należy zwrócić jego następny element elif self._child_iter: try: nextchild = next(self._child_iter) return nextchild except StopIteration: self._child_iter = None return next(self) # Przejście do następnego elementu podrzędnego i rozpoczęcie iterowania po nim else: self._child_iter = next(self._children_iter).depth_first() return next(self)
Klasa DepthFirstIterator działa tak samo jak wersja z generatorem, jest jednak bardziej złożona, ponieważ iterator musi zarządzać skomplikowanym stanem określającym miejsce w procesie iterowania. W praktyce nikt nie lubi pisać tak zagmatwanego kodu. Dlatego lepiej jest zdefiniować iterator w formie generatora i przejść do innych zadań.
118
Rozdział 4. Iteratory i generatory
4.5. Iterowanie w odwrotnej kolejności Problem Programista chce iterować po sekwencji w odwrotnej kolejności.
Rozwiązanie Należy wykorzystać wbudowaną funkcję reversed(). Oto przykład: >>> a = [1, 2, 3, 4] >>> for x in reversed(a): ... print(x) ... 4 3 2 1
Iterowanie w odwrotnym kierunku działa tylko wtedy, gdy można ustalić wielkość obiektu lub gdy udostępnia on specjalną metodę __reversed__(). Jeśli żaden z tych warunków nie jest spełniony, trzeba najpierw przekształcić obiekt na listę: # Wyświetlanie zawartości pliku od tyłu f = open('somefile') for line in reversed(list(f)): print(line, end='')
Warto wiedzieć, że przekształcanie obiektu iterowalnego na listę w przedstawiony tu sposób może wymagać dużej ilości pamięci (jeśli obiekt jest duży).
Omówienie Wielu programistów nie uświadamia sobie, że w klasach definiowanych przez użytkownika można dostosować iterowanie w odwrotnym kierunku do potrzeb. Należy w tym celu zaimplementować metodę __reversed__(): class Countdown: def __init__(self, start): self.start = start # Iterator przechodzący do przodu def __iter__(self): n = self.start while n > 0: yield n n -= 1 # Iterator przechodzący do tyłu def __reversed__(self): n = 1 while n <= self.start: yield n n += 1
Zdefiniowanie odwrotnego iteratora pozwala znacznie zwiększyć wydajność kodu, ponieważ nie trzeba zapisywać danych na liście i przechodzić po niej w odwrotnym kierunku.
4.5. Iterowanie w odwrotnej kolejności
119
4.6. Definiowanie funkcji generatorów z dodatkowym stanem Problem Programista chce zdefiniować funkcję generatora, przy czym ma ona obejmować dodatkowy stan udostępniany użytkownikom.
Rozwiązanie Jeśli generator ma udostępniać dodatkowy stan użytkownikom, pożądany efekt można łatwo uzyskać, tworząc klasę i umieszczając kod funkcji generatora w metodzie __iter__(). Oto przykład: from collections import deque class linehistory: def __init__(self, lines, histlen=3): self.lines = lines self.history = deque(maxlen=histlen) def __iter__(self): for lineno, line in enumerate(self.lines,1): self.history.append((lineno, line)) yield line def clear(self): self.history.clear()
W trakcie korzystania z tej klasy należy ją traktować jak zwykłą funkcję generatora. Ponieważ jednak tworzy ona obiekt, można uzyskać dostęp do jej wewnętrznych atrybutów, np. atrybutu history i metody clear(): with open('somefile.txt') as f: lines = linehistory(f) for line in lines: if 'python' in line: for lineno, hline in lines.history: print('{}:{}'.format(lineno, hline), end='')
Omówienie W trakcie stosowania generatorów łatwo wpaść w pułapkę i próbować wykonać wszystkie zadania za pomocą samych funkcji. Może to prowadzić do powstawania skomplikowanego kodu, jeśli funkcja generatora ma wchodzić w nietypowe interakcje z innymi częściami programu (udostępniać atrybuty, umożliwiać kontrolowanie wykonania kodu przez wywołania metod itd.). W takiej sytuacji lepiej jest zdefiniować klasę, tak jak w przedstawionym rozwiązaniu. Zdefiniowanie generatora w metodzie __iter__() nie wymaga zmian w algorytmie, a ponieważ generator jest częścią klasy, można łatwo udostępnić atrybuty i metody dla użytkowników.
120
Rozdział 4. Iteratory i generatory
Przedstawiona tu technika może wymagać dodatkowej operacji — wywołania metody iter(), jeśli do sterowania iterowaniem służy inny mechanizm niż pętla for. Oto przykład: >>> f = open('somefile.txt') >>> lines = linehistory(f) >>> next(lines) Traceback (most recent call last): File "", line 1, in TypeError: 'linehistory' object is not an iterator >>> # Najpierw wywołanie metody iter(), potem rozpoczęcie iterowania >>> it = iter(lines) >>> next(it) 'Witaj, świecie\n' >>> next(it) 'To tylko test\n' >>>
4.7. Pobieranie wycinków danych zwracanych przez iterator Problem Programista chce pobrać wycinek danych zwracanych przez iterator, ale standardowy operator pobierania wycinków nie działa.
Rozwiązanie Do pobierania wycinków danych z iteratorów i generatorów doskonale nadaje się funkcja itertools.isslice(). Oto przykład: >>> def count(n): ... while True: ... yield n ... n += 1 ... >>> c = count(0) >>> c[10:20] Traceback (most recent call last): File "", line 1, in TypeError: 'generator' object is not subscriptable >>> # Teraz wywołanie islice() >>> import itertools >>> for x in itertools.islice(c, 10, 20): ... print(x) ... 10 11 12 13 14 15 16 17 18 19 >>>
4.7. Pobieranie wycinków danych zwracanych przez iterator
121
Omówienie Standardowo nie można pobierać wycinków z iteratorów i generatorów, ponieważ nie jest znana ich długość (a mechanizmy te nie udostępniają indeksów). Wynikiem wywołania islice() jest iterator, który generuje pożądane elementy z wycinka, ale w tym celu pobiera i usuwa wszystkie elementy aż do początkowego indeksu wycinka. Następnie obiekt isslice zwraca dalsze obiekty do momentu dotarcia do końcowego indeksu. Należy podkreślić, że islice() przechodzi przez dane z określonego iteratora. Warto o tym pamiętać, ponieważ iteratory nie umożliwiają cofnięcia się. Jeśli ważna jest możliwość powrotu do wcześniejszych danych, lepszym rozwiązaniem może okazać się przekształcenie danych na listę.
4.8. Pomijanie pierwszej części obiektu iterowalnego Problem Programista chce przejść przez elementy z obiektu iterowalnego, ale kilka pierwszych z nich go nie interesuje i program ma je pominąć.
Rozwiązanie Moduł itertools udostępnia kilka funkcji, które można wykorzystać do wykonania tego zadania. Pierwszą z nich jest funkcja itertools.dropwhile(). Aby ją zastosować, należy podać funkcję i obiekt iterowalny. Zwrócony iterator usuwa pierwsze elementy sekwencji dopóty, dopóki podana funkcja zwraca wartość True. Następnie zwracane są wszystkie pozostałe elementy sekwencji. Załóżmy, że program wczytuje plik rozpoczynający się od wierszy komentarza: >>> with open('/etc/passwd') as f: ... for line in f: ... print(line, end='') ... ## # Baza użytkowników # # Plik ten jest sprawdzany bezpośrednio tylko wtedy, gdy system działa w trybie obsługi # jednego użytkownika. W innych sytuacjach informacje są pobierane z katalogu # Open Directory. ... ## nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false root:*:0:0:System Administrator:/var/root:/bin/sh ... >>>
Oto jeden ze sposobów na to, aby pominąć wszystkie początkowe wiersze z komentarzami: >>> from itertools import dropwhile >>> with open('/etc/passwd') as f: ... for line in dropwhile(lambda line: line.startswith('#'), f): ... print(line, end='') ...
Przykładowy kod pomija pierwsze elementy na podstawie wyniku funkcji testującej. Jeśli znasz liczbę elementów do pominięcia, możesz zastosować inne podejście — wykorzystać metodę itertools.islice(): >>> from itertools import islice >>> items = ['a', 'b', 'c', 1, 4, 10, 15] >>> for x in islice(items, 3, None): ... print(x) ... 1 4 10 15 >>>
W tym kodzie ostatni argument None metody islice() jest potrzebny do określenia, że program ma pobrać wszystko po trzech pierwszych elementach, a nie tylko trzy pierwsze elementy (czyli wycinek [3:] a nie [:3]).
Omówienie Funkcje dropwhile() i islice() mają charakter pomocniczy i pomagają uniknąć skomplikowanego kodu, takiego jak poniższy: with open('/etc/passwd') as f: # Pomijanie początkowych komentarzy while True: line = next(f, '') if not line.startswith('#'): break # Przetwarzanie pozostałych wierszy while line: # Do zastąpienia przydatnym przetwarzaniem print(line, end='') line = next(f, None)
Pominięcie pierwszej części obiektu iterowalnego różni się od odfiltrowania wszystkich danych określonego rodzaju. Np. pierwszą część receptury można zmodyfikować w następujący sposób: with open('/etc/passwd') as f: lines = (line for line in f if not line.startswith('#')) for line in lines: print(line, end='')
Ten kod usuwa wiersze komentarza z początku, ale ponadto pomija wszystkie takie wiersze w całym pliku. Rozwiązanie przedstawione wcześniej usuwa wiersze tylko dopóty, dopóki spełniony jest warunek. Później wszystkie pozostałe wiersze są zwracane bez filtrowania. Warto też podkreślić, że ta receptura działa dla wszystkich obiektów iterowalnych — także dla tych, których wielkości nie można z góry określić. Dotyczy to generatorów, plików i podobnych obiektów.
4.8. Pomijanie pierwszej części obiektu iterowalnego
123
4.9. Iterowanie po wszystkich możliwych kombinacjach lub permutacjach Problem Programista chce przejść po wszystkich możliwych kombinacjach lub permutacjach kolekcji elementów.
Rozwiązanie Moduł itertools udostępnia trzy funkcje przydatne przy rozwiązywaniu tego problemu. Pierwsza z nich, itertools.permutations(), pobiera kolekcję elementów i generuje sekwencję krotek ze wszystkimi możliwymi permutacjami (czyli układa elementy we wszystkie możliwe konfiguracje). Oto przykład: >>> items = ['a', 'b', 'c'] >>> from itertools import permutations >>> for p in permutations(items): ... print(p) ... ('a', 'b', 'c') ('a', 'c', 'b') ('b', 'a', 'c') ('b', 'c', 'a') ('c', 'a', 'b') ('c', 'b', 'a') >>>
Jeśli chcesz wygenerować wszystkie permutacje o mniejszej wielkości, możesz podać opcjonalny argument określający długość permutacji: >>> for p in permutations(items, 2): ... print(p) ... ('a', 'b') ('a', 'c') ('b', 'a') ('b', 'c') ('c', 'a') ('c', 'b') >>>
Za pomocą funkcji itertools.combinations() można wygenerować sekwencję kombinacji elementów pobranych z wejścia. Oto przykład: >>> from itertools import combinations >>> for c in combinations(items, 3): ... print(c) ... ('a', 'b', 'c') >>> for c in combinations(items, 2): ... print(c) ... ('a', 'b') ('a', 'c') ('b', 'c') >>> for c in combinations(items, 1): ... print(c)
124
Rozdział 4. Iteratory i generatory
... ('a',) ('b',) ('c',) >>>
Funkcja combinations() nie uwzględnia kolejności elementów. Kombinacja ('a', 'b') jest uznawana za identyczną z ('b', 'a') (której funkcja nie generuje). W tym procesie wybrane już elementy są usuwane z kolekcji możliwych kombinacji. Np. jeśli funkcja wybrała już 'a', nie uwzględnia później tego elementu. W funkcji itertools.comb inations_with_replacement() jest inaczej — ten sam element może pojawić się w kombinacji więcej niż raz. Oto przykład: >>> for c in combinations_with_replacement(items, 3): ... print(c) ... ('a', 'a', 'a') ('a', 'a', 'b') ('a', 'a', 'c') ('a', 'b', 'b') ('a', 'b', 'c') ('a', 'c', 'c') ('b', 'b', 'b') ('b', 'b', 'c') ('b', 'c', 'c') ('c', 'c', 'c') >>>
Omówienie W tej recepturze pokazano tylko część możliwości modułu itertools. Choć oczywiście można samodzielnie napisać kod do generowania permutacji i kombinacji, wymaga to dużo pracy. Gdy zetkniesz się ze skomplikowanymi problemami z iterowaniem, zawsze warto najpierw pomyśleć o module itertools. Jeśli problem występuje często, możliwe, że jego rozwiązanie jest już gotowe.
4.10. Przechodzenie po parach indeks – wartość sekwencji Problem Programista chce przejść po sekwencji, a jednocześnie śledzić, który jej element jest aktualnie przetwarzany.
Rozwiązanie Wbudowana funkcja enumerate() dobrze wykonuje to zadanie: >>> my_list = ['a', 'b', 'c'] >>> for idx, val in enumerate(my_list): ... print(idx, val) ... 0 a 1 b 2 c
4.10. Przechodzenie po parach indeks – wartość sekwencji
125
Aby wyświetlać dane przy użyciu standardowej numeracji (rozpoczynającej się od 1, a nie od 0), należy przekazać argument start: >>> my_list = ['a', 'b', 'c'] >>> for idx, val in enumerate(my_list, 1): ... print(idx, val) ... 1 a 2 b 3 c
Jest to przydatne zwłaszcza do śledzenia numerów wierszy pliku na potrzeby wyświetlania ich w komunikatach o błędach: def parse_data(filename): with open(filename, 'rt') as f: for lineno, line in enumerate(f, 1): fields = line.split() try: count = int(fields[1]) ... except ValueError as e: print('Wiersz {}: Błąd parsowania: {}'.format(lineno, e))
Funkcja enumerate() przydaje się np. do śledzenia miejsc wystąpień określonych wartości na liście. Jeśli chcesz odwzorować słowa z pliku na wiersze, w których występują, można to łatwo zrobić, używając funkcji enumerate() do przyporządkowania każdemu słowu odpowiedniego wiersza: word_summary = defaultdict(list) with open('myfile.txt', 'r') as f: lines = f.readlines() for idx, line in enumerate(lines): # Tworzenie listy słów z bieżącego wiersza words = [w.strip().lower() for w in line.split()] for word in words: word_summary[word].append(idx)
Jeśli wyświetlisz kolekcję word_summary po przetworzeniu pliku, zobaczysz, że jest to słownik (typu default dict) z kluczami reprezentującymi wszystkie słowa. Wartość powiązana z każdym kluczem (słowem) to lista numerów wierszy, w których dane słowo występuje. Jeśli słowo pojawia się w danym wierszu dwukrotnie, numer tego wiersza znajdzie się na liście dwa razy. Pozwala to obliczyć różne proste wskaźniki dotyczące tekstu.
Omówienie Funkcja enumerate() jest wygodnym skrótem, który możesz zastosować zamiast własnej zmiennej licznika. Możesz napisać kod podobny do tego: lineno = 1 for line in f: # Przetwarzanie wiersza ... lineno += 1
Jednak dużo bardziej eleganckim (i mniej narażonym na błędy) rozwiązaniem jest zastosowanie funkcji enumerate():
126
Rozdział 4. Iteratory i generatory
for lineno, line in enumerate(f): # Przetwarzanie wiersza ...
Wartość zwrócona przez funkcję enumerate() to obiekt typu enumerate. Jest to iterator, który zwraca kolejne krotki zawierające wartość licznika i wartość zwróconą przez wywołanie next() dla przekazanej sekwencji. Choć nie jest to poważny problem, warto wspomnieć o tym, że łatwo jest się pomylić przy stosowaniu funkcji enumerate() do sekwencji wypakowywanych krotek. Aby uzyskać pożądany efekt, należy zastosować następujące podejście: data = [ (1, 2), (3, 4), (5, 6), (7, 8) ] # Poprawny kod for n, (x, y) in enumerate(data): ... # Błąd! for n, x, y in enumerate(data): ...
4.11. Jednoczesne przechodzenie po wielu sekwencjach Problem Programista chce przechodzić jednocześnie po elementach więcej niż jednej sekwencji.
Rozwiązanie Aby przechodzić jednocześnie po więcej niż jednej sekwencji, należy zastosować funkcję zip(). Oto przykład: >>> xpts = [1, 5, 4, 2, 10, 7] >>> ypts = [101, 78, 37, 15, 62, 99] >>> for x, y in zip(xpts, ypts): ... print(x,y) ... 1 101 5 78 4 37 2 15 10 62 7 99 >>>
Wywołanie zip(a, b) tworzy iterator, który generuje krotki (x, y), gdzie x pochodzi z a, a y jest pobierane z b. Iterowanie kończy się, gdy funkcja pobierze wszystkie elementy z jednej sekwencji. Dlatego czas iterowania zależy od długości najkrótszej sekwencji wejściowej. Oto przykład: >>> >>> >>> ... ... (1, (2, (3, >>>
a = [1, 2, 3] b = ['w', 'x', 'y', 'z'] for i in zip(a,b): print(i) 'w') 'x') 'y')
4.11. Jednoczesne przechodzenie po wielu sekwencjach
127
Jeśli takie rozwiązanie jest niepożądane, należy zastosować funkcję itertools.zip_longest(). Oto przykład: >>> from itertools import zip_longest >>> for i in zip_longest(a,b): ... print(i) ... (1, 'w') (2, 'x') (3, 'y') (None, 'z') >>> for i in zip_longest(a, b, fillvalue=0): ... print(i) ... (1, 'w') (2, 'x') (3, 'y') (0, 'z') >>>
Omówienie Funkcja zip() jest często używana do łączenia danych w pary. Załóżmy, że istnieje lista nagłówków i wartości kolumn: headers = ['name', 'shares', 'price'] values = ['ACME', 100, 490.1]
Za pomocą funkcji zip() można połączyć wartości w pary i utworzyć słownik: s = dict(zip(headers,values))
Jeżeli chcesz wygenerować dane wyjściowe, możesz napisać następujący kod: for name, val in zip(headers, values): print(name, '=', val)
Do funkcji zip() można przekazać więcej niż dwie sekwencje wejściowe, choć robi się to rzadko. Wtedy w wynikowych krotkach znajduje się tyle elementów, ile jest wejściowych sekwencji. Oto przykład: >>> >>> >>> >>> ... ... (1, (2, (3, >>>
a = b = c = for
[1, 2, 3] [10, 11, 12] ['x','y','z'] i in zip(a, b, c): print(i)
10, 'x') 11, 'y') 12, 'z')
Ponadto warto zauważyć, że wynikiem działania funkcji zip() jest iterator. Jeśli chcesz zapisać pary wartości na liście, zastosuj funkcję list(): >>> zip(a, b) >>> list(zip(a, b)) [(1, 10), (2, 11), (3, 12)] >>>
128
Rozdział 4. Iteratory i generatory
4.12. Przechodzenie po elementach z odrębnych kontenerów Problem Programista chce wykonać tę samą operację na wielu obiektach, które jednak znajdują się w różnych kontenerach. Ponadto programista chce uniknąć stosowania pętli zagnieżdżonych, a jednocześnie zachować czytelność kodu.
Rozwiązanie Aby uprościć zadanie, można zastosować metodę itertools.chain(). Przyjmuje ona listę obiektów iterowalnych i zwraca iterator, który ukrywa to, że kod działa na wielu kontenerach. Zastanów się nad następującym przykładowym kodem: >>> >>> >>> >>> ... ... 1 2 3 4 x y z >>>
from itertools import chain a = [1, 2, 3, 4] b = ['x', 'y', 'z'] for x in chain(a, b): print(x)
Metoda chain() często znajduje zastosowanie w programach, w których określone operacje mają być wykonywane jednocześnie na wszystkich elementach pochodzących z różnych zbiorów roboczych. Oto przykład: # Różne zbiory robocze elementów active_items = set() inactive_items = set() # Przechodzenie po wszystkich elementach for item in chain(active_items, inactive_items): # Przetwarzanie elementu ...
To rozwiązanie jest dużo bardziej eleganckie niż stosowanie dwóch odrębnych pętli, tak jak w poniższym kodzie: for item in active_items: # Przetwarzanie elementów ... for item in inactive_items: # Przetwarzanie elementów ...
4.12. Przechodzenie po elementach z odrębnych kontenerów
129
Omówienie Metoda itertools.chain() przyjmuje jako argumenty obiekty iterowalne (jeden lub więcej). Następnie tworzy iterator, który po kolei pobiera i zwraca elementy wygenerowane przez każdy z podanych obiektów iterowalnych. Metoda chain() jest wydajniejsza niż kod, który najpierw łączy sekwencje, a następnie po nich przechodzi: # Niewydajne for x in a + b: ... # Lepsze for x in chain(a, b): ...
W pierwszej wersji operacja a + b tworzy zupełnie nową sekwencję, a ponadto a i b muszą być tego samego typu. Metoda chain() nie wykonuje takiej operacji, dlatego znacznie wydajniej wykorzystuje pamięć, gdy wejściowe sekwencje są duże. Ponadto metodę tę można łatwo zastosować do obiektów iterowalnych różnych typów.
4.13. Tworzenie potoków przetwarzania danych Problem Programista chce przetwarzać dane iteracyjnie, podobnie jak robią to potoki przetwarzania danych (przypominające potoki Uniksa). Możliwe, że ma do przetworzenia bardzo dużą ilość danych, których nie można w całości umieścić w pamięci.
Rozwiązanie Dobrym sposobem na zaimplementowanie potoków przetwarzania jest wykorzystanie funkcji generatorów. Załóżmy, że programista chce przetworzyć bardzo duży katalog z plikami dziennika: foo/ access-log-012007.gz access-log-022007.gz access-log-032007.gz ... access-log-012008 bar/ access-log-092007.bz2 ... access-log-022008
Przyjmijmy, że każdy plik zawiera wiersze danych w następującej postaci: 124.115.6.12 210.212.209.67 210.212.209.67 61.135.216.105 ...
Na potrzeby przetwarzania takich plików można zdefiniować kolekcję prostych funkcji generatorów, wykonujących określone niezależne zadania. Oto przykład: import import import import import
os fnmatch gzip bz2 rozwiązanie
def gen_find(filepat, top): ''' Wyszukiwanie w drzewie katalogów wszystkich nazw plików pasujących do wzorca wieloznacznego powłoki ''' for path, dirlist, filelist in os.walk(top): for name in fnmatch.filter(filelist, filepat): yield os.path.join(path,name) def gen_opener(filenames): ''' Otwieranie plików z sekwencji jeden po drugim i tworzenie obiektów plikowych. Plik jest zamykany natychmiast przy przejściu do następnego. ''' for filename in filenames: if filename.endswith('.gz'): f = gzip.open(filename, 'rt') elif filename.endswith('.bz2'): f = bz2.open(filename, 'rt') else: f = open(filename, 'rt') yield f f.close() def gen_concatenate(iterators): ''' Łączenie iteratorów w jedną sekwencję ''' for it in iterators: yield from it def gen_grep(pattern, lines): ''' Wyszukiwanie w sekwencji wierszy wzorca z wyrażenia regularnego ''' pat = re.compile(pattern) for line in lines: if pat.search(line): yield line
Teraz można łatwo połączyć funkcje, aby utworzyć potok przetwarzania. Np. aby znaleźć wszystkie wiersze dziennika zawierające słowo python, wystarczy zastosować następujący kod: lognames = gen_find('access-log*', 'www') files = gen_opener(lognames) lines = gen_concatenate(files) pylines = gen_grep('(?i)python', lines) for line in pylines: print(line)
4.13. Tworzenie potoków przetwarzania danych
131
Jeśli chcesz rozbudować potok, możesz nawet przekazać dane do wyrażeń z generatorem. Poniższy kod znajduje liczby przesłanych bajtów i oblicza ich sumę: lognames = gen_find('access-log*', 'www') files = gen_opener(lognames) lines = gen_concatenate(files) pylines = gen_grep('(?i)python', lines) bytecolumn = (line.rsplit(None,1)[1] for line in pylines) bytes = (int(x) for x in bytecolumn if x != '-') print('Total', sum(bytes))
Omówienie Przetwarzanie danych w potokach sprawdza się bardzo dobrze w wielu rodzajach problemów — np. w kontekście parsowania, odczytu ze źródeł danych generowanych w czasie rzeczywistym, okresowego sprawdzania danych itd. Aby zrozumieć przedstawiony kod, należy wiedzieć, że wywołanie yield działa jak pewnego rodzaju producent danych, natomiast pętla for — jak ich konsument. Gdy generatory są połączone ze sobą, każde wywołanie yield przekazuje jeden element danych do następnej części potoku, która iteracyjnie pobiera te elementy. W ostatnim przykładzie całym programem steruje funkcja sum(), pobierająca po jednym elemencie z potoku generatorów. Wygodną cechą opisanego podejścia jest to, że każda funkcja generatora jest zwykle krótka i niezależna. Dlatego pisanie i konserwowanie takich funkcji jest proste. Często funkcje te są na tyle ogólne, że można je ponownie wykorzystać w innym kontekście. Kod wynikowy, który łączy wszystkie komponenty, także często można czytać jak prostą, łatwą do zrozumienia recepturę. Ponadto wydajność tego podejścia ze względu na pamięć jest bardzo wysoka. Przedstawiony kod działa nawet dla dużych katalogów plików. Z uwagi na iteracyjny charakter przetwarzania kod ten zużywa bardzo niewiele pamięci. Z funkcją gen_concatenate() związany jest pewien szczegół. Funkcja ta służy do złączania wejściowych sekwencji w jedną długą sekwencję wierszy. Funkcja itertools.chain() ma podobne zadanie, jednak wymaga podania wszystkich złączanych obiektów iterowalnych jako argumentów. W przedstawionej tu recepturze wymagałoby to zastosowania instrukcji lines = itertools.chain(*files), co doprowadziłoby do pobrania wszystkich danych z generatora gen_opener(). Ponieważ generator ten zwraca sekwencję otwartych plików, które są natychmiast zamykane w następnym kroku iteracji, metody chain() nie można tu użyć. Przedstawione rozwiązanie pozwala uniknąć tego problemu. W funkcji gen_concatenate() występuje wywołanie yield from, które przekazuje zadanie do podgeneratora. Wywołanie yield from it powoduje, że funkcja gen_concatenate() zwraca wszystkie wartości wygenerowane przez generator it. Zagadnienie to opisano dokładnie w recepturze 4.14. Ponadto warto zauważyć, że podejście oparte na potokach nie pozwala rozwiązać każdego problemu z zakresu obsługi danych. Czasem trzeba pracować na wszystkich danych jednocześnie. Jednak nawet wtedy potoki generatorów mogą pozwolić na logiczny podział problemu i utworzenie procesu przebiegu pracy. Te techniki dokładnie opisał David Beazley w samouczku Generator Tricks for Systems Programmers (http://www.dabeaz.com/generators/). Znajdziesz tam jeszcze więcej przykładów.
132
Rozdział 4. Iteratory i generatory
4.14. Przekształcanie zagnieżdżonych sekwencji na postać jednowymiarową Problem Programista chce przekształcić zagnieżdżoną sekwencję na postać jednowymiarową — listę wartości.
Rozwiązanie Problem ten można łatwo rozwiązać, pisząc rekurencyjną funkcję generatora z wywołaniem yield from. Oto przykład: from collections import Iterable def flatten(items, ignore_types=(str, bytes)): for x in items: if isinstance(x, Iterable) and not isinstance(x, ignore_types): yield from flatten(x) else: yield x items = [1, 2, [3, 4, [5, 6], 7], 8] # Generuje wartości 1 2 3 4 5 6 7 8 for x in flatten(items): print(x)
Wywołanie isinstance(x, Iterable) w kodzie sprawdza, czy dany element jest obiektem iterowalnym. Jeśli tak jest, kod wywołuje instrukcję yield from, aby wygenerować wszystkie wartości w pewnego rodzaju procedurze. Efekt końcowy to jedna sekwencja niezagnieżdżonych wartości. Dodatkowy argument ignore_types i warunek not isinstance(x, ignore_types) zapobiegają traktowaniu łańcuchów znaków i bajtów jak obiektów iterowalnych oraz rozwijaniu ich do pojedynczych znaków. Dzięki temu kod działa w oczekiwany sposób dla zagnieżdżonych list łańcuchów znaków. Oto przykład: >>> items = ['Dawid', 'Paulina', ['Tomasz', 'Leon']] >>> for x in flatten(items): ... print(x) ... Dawid Paulina Tomasz Leon >>>
Omówienie Wywołanie yield from to wygodny skrót, który można zastosować przy pisaniu generatorów wywołujących inne generatory w podobny sposób jak procedury. Bez tego wywołania trzeba napisać kod z wykorzystaniem dodatkowej pętli for:
4.14. Przekształcanie zagnieżdżonych sekwencji na postać jednowymiarową
133
def flatten(items, ignore_types=(str, bytes)): for x in items: if isinstance(x, Iterable) and not isinstance(x, ignore_types): for i in flatten(x): yield i else: yield x
Choć zmiana jest niewielka, wywołanie yield from wygląda lepiej i prowadzi do powstawania bardziej przejrzystego kodu. Jak wspomniano, dodatkowy warunek dotyczący łańcuchów znaków i bajtów pozwala zapobiec ich rozwijaniu do pojedynczych znaków. Jeśli chcesz zapobiec rozwijaniu także innych typów, zmień wartość argumentu ignore_types. Warto też zauważyć, że wywołanie yield from odgrywa ważną rolę w złożonych programach z wykorzystaniem współprogramów i współbieżności opartej na generatorach.
4.15. Przechodzenie po scalonych posortowanych obiektach iterowalnych zgodnie z kolejnością sortowania Problem Programista ma kolekcję posortowanych sekwencji i chce przejść po ich scalonej wersji w kolejności zgodnej z porządkiem sortowania.
import heapq a = [1, 4, 7, 10] b = [2, 5, 6, 11] for c in heapq.merge(a, b): print(c)
Omówienie Iteracyjny charakter funkcji heapq.merge sprawia, że nigdy nie wczytuje ona podanych sekwencji w całości. Oznacza to, że można zastosować ją bardzo małym kosztem do bardzo długich sekwencji. Poniżej pokazano, jak scalić dwa posortowane pliki:
134
Rozdział 4. Iteratory i generatory
import heapq with open('sorted_file_1', 'rt') as file1,\ open('sorted_file_2') 'rt' as file2,\ open('merged_file', 'wt') as outf: for line in heapq.merge(file1, file2): outf.write(line)
Należy zwrócić uwagę, że funkcja heapq.merge() wymaga, aby wszystkie wejściowe sekwencje były już posortowane. Funkcja ta nie wczytuje wstępnie wszystkich danych do kopca ani nie przeprowadza wstępnego sortowania. Nie sprawdza też poprawności danych pod kątem wymogów związanych z sortowaniem. Jej działanie polega na sprawdzeniu zbioru elementów z początku każdej sekwencji wejściowej i zwróceniu najmniejszego z nich. Następnie wczytywany jest nowy element z wybranej sekwencji i cały proces powtarza się do momentu pobrania wszystkich danych z każdej wejściowej sekwencji.
4.16. Zastępowanie nieskończonych pętli while iteratorem Problem Programista używa kodu z pętlą while do iteracyjnego przetwarzania danych, ponieważ potrzebna jest funkcja lub nietypowy warunek niezgodny ze standardowym wzorcem iteracji.
Rozwiązanie W programach z operacjami wejścia-wyjścia stosunkowo często pojawia się następujący kod: CHUNKSIZE = 8192 def reader(s): while True: data = s.recv(CHUNKSIZE) if data == b'': break process_data(data)
Taki kod często można zastąpić wywołaniem iter(): def reader(s): for chunk in iter(lambda: s.recv(CHUNKSIZE), b''): process_data(data)
Jeśli masz wątpliwości, czy to zadziała, możesz wypróbować podobny kod dotyczący plików: >>> import sys >>> f = open('/etc/passwd') >>> for chunk in iter(lambda: f.read(10), ''): ... n = sys.stdout.write(chunk) ... nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false root:*:0:0:System Administrator:/var/root:/bin/sh daemon:*:1:1:System Services:/var/root:/usr/bin/false _uucp:*:4:4:Unix to Unix Copy Protocol:/var/spool/uucp:/usr/sbin/uucico ... >>>
4.16. Zastępowanie nieskończonych pętli while iteratorem
135
Omówienie Mało znaną cechą wbudowanej funkcji iter() jest to, że opcjonalnie przyjmuje jako dane wejściowe bezargumentową jednostkę wywoływalną oraz wartownika (wartość kończącą). Wtedy funkcja tworzy iterator, który wielokrotnie wywołuje podaną jednostkę wywoływalną do czasu zwrócenia przez nią wartości kończącej. To podejście działa dobrze dla pewnych typów wielokrotnie wywoływanych funkcji, np. związanych z operacjami wejścia-wyjścia. Aby wczytywać dane w porcjach z gniazd lub plików, zwykle trzeba wielokrotnie wywoływać metody read() lub recv(), a następnie sprawdzać, czy program nie doszedł do końca pliku. Przedstawiona receptura wykorzystuje oba mechanizmy i łączy je w jednym wywołaniu iter(). W rozwiązaniu trzeba zastosować wyrażenie lambda, aby utworzyć bezargumentową jednostkę wywoływalną i jednocześnie przekazać odpowiedni określający wielkość argument do metody recv() lub read().
136
Rozdział 4. Iteratory i generatory
ROZDZIAŁ 5.
Pliki i operacje wejścia-wyjścia
Wszystkie programy muszą przyjmować i zwracać dane. W tym rozdziale omówiono typowe idiomy dotyczące pracy z różnego rodzaju plikami (w tym plikami tekstowymi i binarnymi), kodowania plików i powiązanych kwestii. Znajdziesz tu także opis manipulowania nazwami plików i katalogami.
5.1. Odczyt i zapis danych tekstowych Problem Programista chce wczytywać lub zapisywać dane tekstowe. Możliwe, że są one w różnych formatach, np. ASCII, UTF-8 lub UTF-16.
Rozwiązanie Do wczytywania plików tekstowych należy stosować funkcję open() w trybie rt: # Wczytywanie całego pliku jako jednego łańcucha znaków with open('somefile.txt', 'rt') as f: data = f.read() # Przechodzenie po wierszach pliku with open('somefile.txt', 'rt') as f: for line in f: # Przetwarzanie wiersza ...
Aby zapisać plik tekstowy, należy wywołać funkcję open() w trybie wt. Powoduje to usunięcie poprzedniej zawartości pliku (jeśli istniała) i jej zastąpienie. Oto przykład: # Zapisywanie porcji danych tekstowych with open('somefile.txt', 'wt') as f: f.write(text1) f.write(text2) ... # Przekierowanie wywołania print with open('somefile.txt', 'wt') as f: print(line1, file=f) print(line2, file=f) ...
137
Aby dodać dane na koniec istniejącego pliku, należy wywołać funkcję open() w trybie at. Standardowo pliki są wczytywane i zapisywane przy użyciu domyślnego kodowania systemowego (można je określić za pomocą wywołania sys.getdefaultencoding()). W większości komputerów jest to kodowanie utf-8. Jeśli wiesz, że wczytywany lub zapisywany tekst ma inny format, należy podać w funkcji open() opcjonalny parametr encoding: with open('somefile.txt', 'rt', encoding='latin-1') as f: ...
Python rozpoznaje kilkaset możliwych kodowań tekstu. Do najbardziej popularnych należą ascii, latin-1, utf-8 i utf-16. Kodowanie utf-8 jest zwykle bezpiecznym rozwiązaniem w aplikacjach sieciowych. W kodowaniu ascii używane są znaki 7-bitowe z przedziału od U+0000 do U+007F. Kodowanie latin-1 to bezpośrednie odwzorowanie bajtów od 0 do 255 na znaki Unicode z zakresu od U+0000 do U+00FF. Wartą uwagi cechą tego kodowania jest to, że nigdy nie zgłasza błędu przy odczycie tekstu o nieznanym kodowaniu. Odczyt pliku przy użyciu tego kodowania czasem nie pozwala w pełni poprawnie odkodować tekstu, jednak może wystarczyć do wyodrębnienia z niego przydatnych danych. Ponadto jeśli później zapiszesz dane z powrotem do pliku, zachowane zostaną pierwotne dane wejściowe.
Omówienie Wczytywanie i zapisywanie plików tekstowych jest zwykle bardzo proste. Warto jednak pamiętać o pewnych szczegółach. Przede wszystkim instrukcja with w przykładach wyznacza kontekst, w którym można używać pliku. Gdy sterowanie wychodzi poza blok with, plik jest automatycznie zamykany. Nie musisz stosować instrukcji with, jednak jeśli z niej nie korzystasz, pamiętaj o zamknięciu pliku: f = open('somefile.txt', 'rt') data = f.read() f.close()
Inne drobne utrudnienie dotyczy wykrywania znaków nowego wiersza, które są różne w systemach Unix i Windows (\n oraz \r\n). Python domyślnie stosuje tryb uniwersalnych znaków nowego wiersza. W tym trybie wykrywane są wszystkie powszechnie stosowane znaki nowego wiersza i są one przekształcane przy odczycie na pojedynczy znak \n. Na wyjściu znak ten jest przekształcany na domyślny znak systemowy nowego wiersza. Jeśli nie chcesz stosować tego rodzaju przekształceń, podaj w funkcji open() argument newline='': # Odczyt z wyłączonym przekształcaniem znaków nowego wiersza with open('somefile.txt', 'rt', newline='') as f: ...
Aby zilustrować różnice między wersjami z przekształcaniem i bez przekształcania, poniżej pokazano, co pojawia się na komputerze z systemem Unix po wczytaniu pliku tekstowego z kodowaniem z systemu Windows. Plik ten zawiera nieprzetworzony tekst Witaj, świecie!\r\n. >>> # Z włączonym przekształcaniem znaków nowego wiersza (ustawienie domyślne) >>> f = open('hello.txt', 'rt') >>> f.read() 'Witaj, świecie!\n' >>> # Z wyłączonym przekształcaniem znaków nowego wiersza >>> g = open('hello.txt', 'rt', newline='') >>> g.read() 'Witaj, świecie!\r\n' >>>
138
Rozdział 5. Pliki i operacje wejścia-wyjścia
Ostatni problem dotyczy możliwych błędów kodowania w plikach tekstowych. W trakcie odczytu lub zapisu pliku tekstowego można natrafić na błędy kodowania lub dekodowania. Oto przykład: >>> f = open('sample.txt', 'rt', encoding='ascii') >>> f.read() Traceback (most recent call last): File "", line 1, in File "/usr/local/lib/python3.3/encodings/ascii.py", line 26, in decode return codecs.ascii_decode(input, self.errors)[0] UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 12: ordinal not in range(128) >>>
Błąd ten zwykle oznacza, że do odczytu pliku wykorzystano niewłaściwe kodowanie. Należy starannie zapoznać się ze specyfikacją danych i sprawdzić, czy program odczytuje je w prawidłowy sposób (np. czy nie używa kodowania UTF-8 zamiast Latin-1 lub innego). Jeśli błędy kodowania nadal występują, można przekazać do funkcji open() opcjonalny argument errors, aby określić sposób obsługi błędów. Oto kilka przykładów standardowych sposobów obsługi błędów: >>> # Zastępowanie błędnych znaków symbolem zastępczym Unicode U+fffd >>> f = open('sample.txt', 'rt', encoding='ascii', errors='replace') >>> f.read() 'Papryczka Jalape?o!' >>> # Pomijanie błędnych znaków >>> g = open('sample.txt', 'rt', encoding='ascii', errors='ignore') >>> g.read() 'Papryczka Jalapeo!' >>>
Jeśli często musisz ustawiać argumenty encoding i errors funkcji open() oraz stosować wiele sztuczek, prawdopodobnie niepotrzebnie komplikujesz kod. Podstawową regułą przy pracy z tekstem jest upewnienie się, że zastosowano odpowiednie kodowanie. Gdy masz wątpliwości, użyj ustawienia domyślnego (zwykle jest to UTF-8).
5.2. Zapisywanie danych z funkcji print() do pliku Problem Programista chce przekierować dane wyjściowe z funkcji print() do pliku.
Rozwiązanie Należy w funkcji print()podać argument za pomocą słowa kluczowego file, tak jak poniżej: with open('somefile.txt', 'rt') as f: print('Witaj, świecie!', file=f)
Omówienie Zapisywanie danych z funkcji print() do pliku nie wymaga dalszych wyjaśnień. Należy tylko pamiętać o tym, aby plik był otwarty w trybie tekstowym. Jeśli jest otwarty w trybie binarnym, zapis się nie powiedzie.
5.2. Zapisywanie danych z funkcji print() do pliku
139
5.3. Stosowanie niestandardowych separatorów lub końca wiersza w funkcji print() Problem Programista zamierza udostępniać dane za pomocą funkcji print(), przy czym chce zmienić separator lub znak końca wiersza.
Rozwiązanie Aby zmodyfikować dane wyjściowe w pożądany sposób, należy przekazać do funkcji print() argumenty za pomocą słów kluczowych sep i end. Oto przykład: >>> print('ACME', 50, 91.5) ACME 50 91.5 >>> print('ACME', 50, 91.5, sep=',') ACME,50,91.5 >>> print('ACME', 50, 91.5, sep=',', end='!!\n') ACME,50,91.5!! >>>
Za pomocą argumentu end można też usunąć znaki nowego wiersza w danych wyjściowych: >>> for i in range(5): ... print(i) ... 0 1 2 3 4 >>> for i in range(5): ... print(i, end=' ') ... 0 1 2 3 4 >>>
Omówienie Zastosowanie funkcji print() z nowym separatorem elementów to często najłatwiejszy sposób wyświetlania danych, gdy elementy ma rozdzielać znak inny niż odstęp. Czasem programiści stosują funkcję str.join(), aby uzyskać ten sam efekt: >>> print(','.join('ACME','50','91.5')) ACME,50,91.5 >>>
Problem z funkcją str.join() polega na tym, że działa ona tylko dla łańcuchów znaków. Oznacza to, że często trzeba stosować różne sztuczki, aby wykorzystać tę funkcję. Oto przykład: >>> row = ('ACME', 50, 91.5) >>> print(','.join(row)) Traceback (most recent call last): File "", line 1, in TypeError: sequence item 1: expected str instance, int found >>> print(','.join(str(x) for x in row)) ACME,50,91.5 >>>
140
Rozdział 5. Pliki i operacje wejścia-wyjścia
A wystarczy tylko napisać następujący kod: >>> print(*row, sep=',') ACME,50,91.5 >>>
5.4. Odczyt i zapis danych binarnych Problem Programista chce odczytać lub zapisać dane binarne, np. z pliku graficznego lub dźwiękowego.
Rozwiązanie Do odczytu i zapisu danych binarnych służy funkcja open() w trybie rb lub wb. Oto przykład: # Odczyt całego pliku jako jednego łańcucha bajtów with open('somefile.bin', 'rb') as f: data = f.read() # Zapis danych binarnych do pliku with open('somefile.bin', 'wb') as f: f.write(b'Witaj, Polsko')
Należy zauważyć, że przy odczycie danych binarnych wszystkie zwracane dane mają postać łańcuchów bajtów, a nie łańcuchów znaków. Także przy zapisie trzeba podawać dane w formie obiektów udostępniających bajty (np. w formie łańcuchów bajtów, obiektów bytearray itd.).
Omówienie Przy wczytywaniu danych binarnych drobne różnice w działaniu łańcuchów bajtów i znaków mogą prowadzić do problemów. Warto pamiętać zwłaszcza o tym, że w czasie pobierania elementów za pomocą indeksów i iterowania zwracane są całkowitoliczbowe wartości bajtów, a nie łańcuchy bajtów. Oto przykład: >>> >>> >>> 'W' >>> ... ... W i t a j ... >>> >>> >>> 87 >>> ... ... 87 105
# Łańcuch znaków t = 'Witaj, Polsko' t[0] for c in t: print(c)
# Łańcuch bajtów b = b'Witaj, Polsko' b[0] for c in b: print(c)
5.4. Odczyt i zapis danych binarnych
141
116 97 106 ... >>>
Jeśli zamierzasz wczytywać lub zapisywać tekst w plikach w trybie binarnym, pamiętaj o zakodowaniu lub odkodowaniu tekstu: with open('somefile.bin', 'rb') as f: data = f.read(16) text = data.decode('utf-8') with open('somefile.bin', 'wb') as f: text = 'Witaj, Polsko' f.write(text.encode('utf-8'))
Mało znaną cechą binarnych operacji wejścia-wyjścia jest to, że zapis do niektórych obiektów (np. tablic lub struktur języka C) nie wymaga pośredniego przekształcania danych na obiekty typu bytes. Oto przykład: import array nums = array.array('i', [1, 2, 3, 4]) with open('data.bin','wb') as f: f.write(nums)
Dotyczy to każdego obiektu z implementacją interfejsu bufora. Takie obiekty bezpośrednio udostępniają bufor pamięci operacjom, które potrafią z niego korzystać. Zapisywanie danych binarnych to jedna z takich operacji. Ponadto wiele obiektów umożliwia bezpośredni odczyt danych binarnych z pamięci za pomocą metody readinto() plików: >>> import array >>> a = array.array('i', [0, 0, 0, 0, 0, 0, 0, 0]) >>> with open('data.bin', 'rb') as f: ... f.readinto(a) ... 16 >>> a array('i', [1, 2, 3, 4, 0, 0, 0, 0]) >>>
Jednak przy stosowaniu tej techniki należy zachować daleko posuniętą ostrożność, ponieważ często działa ona inaczej w różnych systemach i jest zależna od długości słowa oraz porządku bajtów (bajty mogą być uporządkowane od najmniej lub od najbardziej znaczącego). W recepturze 5.9. znajdziesz inny przykład ilustrujący wczytywanie danych binarnych do zmiennego bufora.
5.5. Zapis danych do pliku, który nie istnieje Problem Programista chce zapisywać dane do pliku, ale tylko wtedy, jeśli dany plik jeszcze nie istnieje w systemie.
142
Rozdział 5. Pliki i operacje wejścia-wyjścia
Rozwiązanie Problem ten można łatwo rozwiązać za pomocą mało znanego trybu x funkcji open() (należy go zastosować zamiast standardowego trybu w). Oto przykład: >>> with open('somefile', 'wt') as f: ... f.write('Witaj\n') ... >>> with open('somefile', 'xt') as f: ... f.write('Witaj\n') ... Traceback (most recent call last): File "", line 1, in FileExistsError: [Errno 17] File exists: 'somefile' >>>
Dla plików binarnych należy zastosować tryb xb zamiast xt.
Omówienie Ta receptura to bardzo eleganckie rozwiązanie problemu, który czasem występuje przy zapisie plików (i prowadzi np. do przypadkowego zastąpienia istniejącego pliku). Inne rozwiązanie polega na wcześniejszym sprawdzeniu, czy plik istnieje: >>> import os >>> if not os.path.exists('somefile'): ... with open('somefile', 'wt') as f: ... f.write('Witaj\n') ... else: ... print('Plik już istnieje!') ... Plik już istnieje! >>>
Wyraźnie widać, że zastosowanie trybu x jest znacznie prostsze. Należy zauważyć, że tryb x to charakterystyczne dla Pythona 3 rozszerzenie funkcji open(). Tryb ten nie istnieje w starszych wersjach Pythona ani w bibliotekach języka C używanych w implementacji Pythona.
5.6. Wykonywanie operacji wejścia-wyjścia na łańcuchach Problem Programista chce przesłać łańcuch znaków lub bajtów do kodu, który napisano w celu manipulowania obiektami podobnymi do plików.
Rozwiązanie Należy wykorzystać klasy io.StringIO() i io.BytesIO() do utworzenia podobnych do plików obiektów manipulujących danymi łańcuchowymi. Oto przykład: >>> s = io.StringIO() >>> s.write('Witaj, świecie\n') 15
5.6. Wykonywanie operacji wejścia-wyjścia na łańcuchach
143
>>> print('To tylko test', file=s) >>> # Pobieranie wszystkich danych zapisanych do tego miejsca >>> s.getvalue() 'Witaj, świecie\nTo tylko test\n' >>> >>> # Dodawanie interfejsu plikowego do istniejącego łańcucha >>> s = io.StringIO('Witaj\nświecie\n') >>> s.read(4) 'Wita' >>> s.read() 'j\nświecie\n' >>>
Klasę io.StringIO należy stosować tylko dla tekstu. Jeśli pracujesz z danymi binarnymi, użyj klasy io.BytesIO: >>> s = io.BytesIO() >>> s.write(b'dane binarne') >>> s.getvalue() b'dane binarne' >>>
Omówienie Klasy StringIO i BytesIO są najbardziej przydatne w sytuacji, gdy z pewnych przyczyn trzeba odzwierciedlić działanie normalnego pliku. Np. w testach jednostkowych można wykorzystać klasę StringIO do utworzenia podobnego do pliku obiektu, który zawiera dane testowe przekazywane do funkcji działającej standardowo dla zwykłych plików. Warto pamiętać, że obiekty typu StringIO i BytesIO nie mają poprawnych całkowitoliczbowych deskryptorów plików. Dlatego nie działają w kodzie, który wymaga rzeczywistych plików z poziomu systemu (np. zwykłych plików, potoków lub gniazd).
5.7. Odczytywanie i zapisywanie skompresowanych plików z danymi Problem Programista chce wczytać lub zapisać dane w pliku skompresowanym za pomocą algorytmu gzip lub bz2.
Rozwiązanie Moduły gzip i bz2 umożliwiają łatwą pracę z takimi plikami. Oba moduły udostępniają specjalną implementację metody open(), którą można wykorzystać w tym celu. Aby wczytać skompresowane pliki jako tekst, należy napisać następujący kod: # Kompresja gzip import gzip with gzip.open('somefile.gz', 'rt') as f: text = f.read()
144
Rozdział 5. Pliki i operacje wejścia-wyjścia
# Kompresja bz2 import bz2 with bz2.open('somefile.bz2', 'rt') as f: text = f.read()
W celu zapisania skompresowanych danych zastosuj następujący kod: # Kompresja gzip import gzip with gzip.open('somefile.gz', 'wt') as f: f.write(text) # Kompresja bz2 import bz2 with bz2.open('somefile.bz2', 'wt') as f: f.write(text)
Wszystkie pokazane operacje wejścia-wyjścia są oparte na tekście oraz kodują i dekodują dane w formacie Unicode. Jeśli chcesz pracować z danymi binarnymi, zastosuj tryb rb lub wb.
Omówienie Wczytywanie i zapisywanie skompresowanych danych jest zazwyczaj łatwe. Warto jednak pamiętać, że bardzo ważny jest wybór odpowiedniego trybu. Jeśli go nie podasz, zostanie domyślnie zastosowany tryb binarny, przez co programy oczekujące na tekst nie będą działać poprawnie. Funkcje gzip.open() i bz2.open() przyjmują te same parametry co wbudowana funkcja open(), w tym encoding, errors, newline itd. W trakcie zapisywania skompresowanych danych można opcjonalnie ustawić poziom kompresji, podając argument za pomocą słowa kluczowego compresslevel: with gzip.open('somefile.gz', 'wt', compresslevel=5) as f: f.write(text)
Poziom domyślny to 9 (zapewnia on najwyższy poziom kompresji). Niższe poziomy pozwalają zwiększyć wydajność kosztem mniejszej kompresji danych. Mało znaną cechą funkcji gzip.open() i bz2.open() jest to, że można je wywołać dla istniejącego pliku otwartego w trybie binarnym. Poprawne jest np. następujące rozwiązanie: import gzip f = open('somefile.gz', 'rb') with gzip.open(f, 'rt') as g: text = g.read()
Dzięki temu można korzystać z modułów gzip i bz2 do pracy z różnymi obiektami podobnymi do plików, np. z gniazdami, potokami i plikami przechowywanymi w pamięci.
5.8. Przechodzenie po rekordach o stałej wielkości Problem Zamiast przechodzić po pliku wiersz po wierszu, programista chce przejść po kolekcji rekordów lub porcji danych o stałej wielkości.
5.8. Przechodzenie po rekordach o stałej wielkości
145
Rozwiązanie Należy zastosować funkcję iter() i wywołanie functools.partial() za pomocą eleganckiej sztuczki: from functools import partial RECORD_SIZE = 32 with open('somefile.data', 'rb') as f: records = iter(partial(f.read, RECORD_SIZE), b'') for r in records: ...
Obiekt records z tego przykładu to obiekt iterowalny, który do momentu dotarcia do końca pliku generuje porcje danych o stałej wielkości. Warto przy tym pamiętać, że jeśli długość pliku nie jest dokładnie wielokrotnością wielkości rekordu, ostatni element może mieć mniej bajtów.
Omówienie Mało znaną cechą funkcji iter() jest to, że pozwala utworzyć iterator, jeśli przekaże się do niej jednostkę wywoływalną i wartość wartownika. Uzyskany iterator wielokrotnie wywołuje jednostkę wywoływalną do czasu zwrócenia przez nią wartownika. W tym momencie iterowanie zostaje zakończone. W przedstawionym rozwiązaniu funkcja functools.partial służy do utworzenia jednostki wywoływalnej, która przy każdym wywołaniu wczytuje z pliku określoną liczbę bajtów. Wartość wartownika, czyli b'', jest zwracana po dotarciu do końca pliku. Ponadto w rozwiązaniu plik jest otwierany w trybie binarnym. Przy wczytywaniu rekordów o stałej wielkości jest to najczęściej stosowane podejście. W przypadku plików tekstowych częściej wczytuje się dane wiersz po wierszu (jest to domyślny przebieg iterowania).
5.9. Wczytywanie danych binarnych do zmiennego bufora Problem Programista chce wczytywać dane binarne bezpośrednio do zmiennego bufora bez pośredniej operacji kopiowania. Możliwe, że chce zmodyfikować dane w miejscu i zapisać je z powrotem do pliku.
Rozwiązanie Aby wczytać dane do zmiennej tablicy, zastosuj metodę readinto() plików. Oto przykład: import os.path def read_into_buffer(filename): buf = bytearray(os.path.getsize(filename)) with open(filename, 'rb') as f: f.readinto(buf) return buf
146
Rozdział 5. Pliki i operacje wejścia-wyjścia
Oto przykład ilustrujący korzystanie z powyższego kodu: >>> # Zapisywanie przykładowego pliku >>> with open('sample.bin', 'wb') as f: ... f.write(b'Witaj, Polsko') ... >>> buf = read_into_buffer('sample.bin') >>> buf bytearray(b'Witaj, Polsko') >>> buf[0:5] = b'Witaj' >>> buf bytearray(b'Witaj, Polsko') >>> with open('newsample.bin', 'wb') as f: ... f.write(buf) ... 11 >>>
Omówienie Metodę readinto() plików można wykorzystać do zapełnienia danymi przygotowanej wcześniej tablicy. Może to być tablica utworzona za pomocą modułu array lub biblioteki numpy i podobnych narzędzi. Metoda readinto() — w odróżnieniu od normalnej metody read() — zapełnia istniejący bufor, zamiast alokować nowe obiekty i zwracać je. Dlatego pozwala uniknąć dodatkowych alokacji pamięci. Jeśli chcesz na przykład wczytać plik binarny z rekordami o stałej długości, możesz zastosować następujący kod: record_size = 32
# Wielkość każdego rekordu (można zmienić tę wartość)
buf = bytearray(record_size) with open('somefile', 'rb') as f: while True: n = f.readinto(buf) if n < record_size: break # Korzystanie z zawartości bufora ...
Inną ciekawą cechą jest widok pamięci, który pozwala bez kopiowania tworzyć wycinki istniejącego bufora, a nawet zmieniać jego zawartość. Oto przykład: >>> buf bytearray(b'Witaj, Polsko') >>> m1 = memoryview(buf) >>> m2 = m1[-6:] >>> m2 >>> m2[:] = b'POLSKO' >>> buf bytearray(b'Witaj, POLSKO') >>>
Przy stosowaniu metody f.readinto() trzeba zawsze sprawdzać zwracany kod. Określa on liczbę wczytanych bajtów. Jeśli liczba bajtów jest mniejsza niż wielkość bufora, może to oznaczać, że dane są obcięte lub uszkodzone (jeśli programista oczekiwał, że program wczyta określoną liczbę bajtów).
5.9. Wczytywanie danych binarnych do zmiennego bufora
147
Warto też zwrócić uwagę na inne powiązane funkcje z rodziny into z różnych modułów bibliotecznych (np. funkcje recv_into(), pack_into() itd.). Wiele innych elementów Pythona także obsługuje bezpośrednie operacje wejścia-wyjścia i dostępu do danych, które można wykorzystać do zapełnienia lub zmiany zawartości tablic i buforów. W recepturze 6.12 znajdziesz dużo bardziej zaawansowany przykład interpretowania struktur binarnych i korzystania z widoków pamięci.
5.10. Odwzorowywanie plików binarnych w pamięci Problem Programista chce odwzorować w pamięci plik binarny na zmienną tablicę bajtów, np. w celu uzyskania dostępu bezpośredniego do jego zawartości lub wprowadzania zmian w miejscu.
Rozwiązanie Do odwzorowywania plików w pamięci służy moduł mmap. Poniżej przedstawiono funkcję narzędziową, która pokazuje, jak otworzyć plik i odwzorować go w pamięci w sposób działający w różnych systemach: import os import mmap def memory_map(filename, access=mmap.ACCESS_WRITE): size = os.path.getsize(filename) fd = os.open(filename, os.O_RDWR) return mmap.mmap(fd, size, access=access)
Aby zastosować tę funkcję, trzeba wcześniej utworzyć plik i zapisać w nim dane. Oto przykład pokazujący, jak utworzyć plik i uzupełnić go danymi do pożądanego rozmiaru: >>> size = 1000000 >>> with open('data', 'wb') as f: ... f.seek(size-1) ... f.write(b'\x00') ... >>>
Poniżej pokazano, jak odwzorować zawartość tego pliku w pamięci za pomocą funkcji memory_map() : >>> m = memory_map('data') >>> len(m) 1000000 >>> m[0:12] b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' >>> m[0] 0 >>> # Modyfikacja wycinka >>> m[0:13] = b'Witaj, Polsko' >>> m.close() >>> # Sprawdzanie, czy zmiany zostały wprowadzone >>> with open('data', 'rb') as f: ... print(f.read(13)) ... b'Witaj, Polsko' >>>
148
Rozdział 5. Pliki i operacje wejścia-wyjścia
Obiekt typu mmap zwrócony przez funkcję mmap() można wykorzystać także jako menedżera kontekstu. Wtedy powiązany plik jest automatycznie zamykany. Oto przykład: >>> with memory_map('data') as m: ... print(len(m)) ... print(m[0:12]) ... 1000000 b'Witaj, Polsko' >>> m.closed True >>>
Przedstawiona tu funkcja memory_map() domyślnie otwiera plik do odczytu i zapisu. Wszystkie zmiany wprowadzone w danych są kopiowane z powrotem do pierwotnego pliku. Jeśli potrzebny jest dostęp tylko do odczytu, należy jako wartość argumentu access ustawić mmap.ACCESS_READ: m = memory_map(filename, mmap.ACCESS_READ)
Jeśli chcesz lokalnie modyfikować dane, ale bez zapisywania zmian w pierwotnym pliku, zastosuj argument mmap.ACCESS_COPY: m = memory_map(filename, mmap.ACCESS_COPY)
Omówienie Wykorzystanie modułu mmap do odwzorowania plików w pamięci może być wydajnym i eleganckim sposobem na uzyskanie dostępu bezpośredniego do zawartości pliku. Zamiast otwierać plik i wywoływać w różnych kombinacjach funkcje seek(), read() i write(), można odwzorować plik i uzyskać dostęp do danych za pomocą wycinków. Pamięć udostępniana przez funkcję mmap() standardowo wygląda jak obiekt typu bytearray. Jednak dane można traktować w inny sposób, używając widoku pamięci. Oto przykład: >>> m = memory_map('data') >>> # Widok pamięci dla liczb całkowitych bez znaku >>> v = memoryview(m).cast('I') >>> v[0] = 7 >>> m[0:4] b'\x07\x00\x00\x00' >>> m[0:4] = b'\x07\x01\x00\x00' >>> v[0] 263 >>>
Należy podkreślić, że odwzorowanie pliku w pamięci nie powoduje wczytania do niej całej zawartości pliku. Plik nie jest kopiowany ani do bufora, ani do tablicy w pamięci. Zamiast tego system operacyjny rezerwuje fragment pamięci wirtualnej na zawartość pliku. Przy dostępie do różnych obszarów pliku odpowiednie jego części są wczytywane i odwzorowywane na dany fragment pamięci. Jednak nieużywane części pliku pozostają na dysku. Wszystko to dzieje się na zapleczu w sposób niezauważalny dla użytkownika. Jeśli w danym momencie więcej niż jeden interpreter Pythona odwzoruje w pamięci ten sam plik, uzyskany obiekt typu mmap można wykorzystać do wymiany danych między interpreterami. Oznacza to, że wszystkie interpretery mogą jednocześnie wczytywać i zapisywać dane, a modyfikacje wprowadzone przez jeden interpreter są automatycznie dostępne w innych. Oczywiście wymaga to dodatkowej staranności ze względu na synchronizowanie operacji, jednak podejście to stosuje się czasem zamiast przesyłania danych w komunikatach za pomocą potoków lub gniazd. 5.10. Odwzorowywanie plików binarnych w pamięci
149
Receptura ta jest tak uniwersalna, jak tylko to możliwe. Działa w systemach Unix i Windows. Warto wiedzieć, że obsługa wywołań funkcji mmap() na zapleczu przebiega w poszczególnych systemach nieco inaczej. Ponadto można tworzyć w pamięci anonimowe obszary z odwzorowanymi danymi. Jeśli te zagadnienia Cię interesują, dokładnie zapoznaj się z poświęconą im dokumentacją Pythona (http://docs.python.org/3/library/mmap.html).
5.11. Manipulowanie ścieżkami Problem Programista chce manipulować ścieżkami, aby ustalić nazwę pliku, nazwę katalogu, ścieżkę bezwzględną itd.
Rozwiązanie Do manipulowania ścieżkami służy moduł os.path. Oto interaktywny przykład ilustrujący kilka najważniejszych aspektów tego modułu: >>> import os >>> path = '/Users/beazley/Data/data.csv' >>> # Pobieranie ostatniego komponentu ścieżki >>> os.path.basename(path) 'data.csv' >>> # Pobieranie nazwy katalogu >>> os.path.dirname(path) '/Users/beazley/Data' >>> # Złączanie komponentów ścieżki >>> os.path.join('tmp', 'data', os.path.basename(path)) 'tmp/data/data.csv' >>> # Rozwijanie ścieżki do katalogu głównego użytkownika >>> path = '~/Data/data.csv' >>> os.path.expanduser(path) '/Users/beazley/Data/data.csv' >>> # Wyodrębnianie rozszerzenia pliku >>> os.path.splitext(path) ('~/Data/data', '.csv') >>>
Omówienie Do manipulowania nazwami plików należy używać modułu os.path, zamiast próbować pisać własny kod oparty na standardowych operacjach na łańcuchach znaków. Po części wynika to z przenośności kodu. Moduł os.path obsługuje różnice między systemami Unix i Windows oraz potrafi radzić sobie z nazwami w postaci Data/data.csv i Data\data.csv. Ponadto nie warto marnować czasu na wymyślanie istniejących już rozwiązań. Zwykle najlepiej jest wykorzystać dostępne mechanizmy.
150
Rozdział 5. Pliki i operacje wejścia-wyjścia
Warto zauważyć, że moduł os.path udostępnia wiele funkcji, których nie pokazano w tej recepturze. W dokumentacji znajdziesz więcej funkcji związanych ze sprawdzaniem plików, dowiązaniami symbolicznymi itd.
5.12. Sprawdzanie, czy plik istnieje Problem Programista chce sprawdzić, czy plik lub katalog istnieje.
Rozwiązanie Do sprawdzania, czy plik lub katalog istnieje, należy wykorzystać moduł os.path. Oto przykład: >>> import os >>> os.path.exists('/etc/passwd') True >>> os.path.exists('/tmp/spam') False >>>
Można także sprawdzić, jakiego rodzaju jest dany plik. Jeśli plik nie istnieje, wynikiem tego testu będzie wartość False: >>> # Czy jest to zwykły plik? >>> os.path.isfile('/etc/passwd') True >>> # Czy jest to katalog? >>> os.path.isdir('/etc/passwd') False >>> # Czy jest to dowiązanie symboliczne? >>> os.path.islink('/usr/local/bin/python3') True >>> # Pobieranie powiązanego pliku >>> os.path.realpath('/usr/local/bin/python3') '/usr/local/bin/python3.3' >>>
Jeśli potrzebne są metadane (np. rozmiar pliku lub data ostatniej modyfikacji), również można je pobrać za pomocą modułu os.path: >>> os.path.getsize('/etc/passwd') 3669 >>> os.path.getmtime('/etc/passwd') 1272478234.0 >>> import time >>> time.ctime(os.path.getmtime('/etc/passwd')) 'Wed Apr 28 13:10:34 2010' >>>
5.12. Sprawdzanie, czy plik istnieje
151
Omówienie Sprawdzanie informacji o plikach za pomocą modułu os.path jest proste. Jedyne, o czym trzeba pamiętać w trakcie pisania skryptów, to kwestia uprawnień. Dotyczy to zwłaszcza operacji pobierających metadane. Oto przykład: >>> os.path.getsize('/Users/guido/Desktop/foo.txt') Traceback (most recent call last): File "", line 1, in File "/usr/local/lib/python3.3/genericpath.py", line 49, in getsize return os.stat(filename).st_size PermissionError: [Errno 13] Permission denied: '/Users/guido/Desktop/foo.txt' >>>
5.13. Pobieranie listy zawartości katalogu Problem Programista chce pobrać listę plików z katalogu z systemu plików.
Rozwiązanie Do pobierania listy plików z katalogu służy funkcja os.listdir(): import os names = os.listdir('somedir')
W ten sposób można uzyskać nieprzetworzoną listę całej zawartości katalogu. Lista ta obejmuje wszystkie pliki, podkatalogi, dowiązania symboliczne itd. Jeśli chcesz przefiltrować te dane, pomyśl o zastosowaniu wyrażenia listowego w połączeniu z różnymi funkcjami z biblioteki os.path. Oto przykład: import os.path # Pobieranie wszystkich zwykłych plików names = [name for name in os.listdir('somedir') if os.path.isfile(os.path.join('somedir', name))] # Pobieranie wszystkich katalogów dirnames = [name for name in os.listdir('somedir') if os.path.isdir(os.path.join('somedir', name))]
Także metody startswith() i endswith() łańcuchów znaków mogą być przydatne do filtrowania zawartości katalogów: pyfiles = [name for name in os.listdir('somedir') if name.endswith('.py')]
Do dopasowywania nazw plików można wykorzystać moduł glob lub fnmatch: import glob pyfiles = glob.glob('somedir/*.py') from fnmatch import fnmatch pyfiles = [name for name in os.listdir('somedir') if fnmatch(name, '*.py')]
152
Rozdział 5. Pliki i operacje wejścia-wyjścia
Omówienie Pobieranie listy zawartości katalogu jest proste, jednak pozwala uzyskać tylko nazwy elementów z katalogu. Aby pobrać dodatkowe metadane, np. wielkość pliku, datę ostatniej modyfikacji itd., trzeba zastosować albo inne funkcje z modułu os.path, albo funkcję os.stat() do pobrania danych. Oto przykład: # Pobieranie listy zawartości katalogu import os import os.path import glob pyfiles = glob.glob('*.py') # Pobieranie rozmiarów plików i dat ostatniej modyfikacji name_sz_date = [(name, os.path.getsize(name), os.path.getmtime(name)) for name in pyfiles] for name, size, mtime in name_sz_date: print(name, size, mtime) # Inne rozwiązanie — pobieranie metadanych plików file_metadata = [(name, os.stat(name)) for name in pyfiles] for name, meta in file_metadata: print(name, meta.st_size, meta.st_mtime)
Ponadto warto pamiętać, że w kontekście nazw plików mogą wystąpić pewne problemy związane z kodowaniem. Dane zwracane przez funkcję os.listdir() i podobne standardowo są dekodowane na podstawie domyślnego systemowego kodowania nazw plików. Jednak czasem można natrafić na nazwy niemożliwe do odkodowania. Więcej informacji na temat obsługi takich nazw znajdziesz w recepturach 5.14. i 5.15.
5.14. Nieuwzględnianie kodowania nazw plików Problem Programista chce wykonywać operacje wejścia-wyjścia na plikach, używając nieprzetworzonych nazw plików (nie są one kodowane ani dekodowane według domyślnego kodowania nazw plików).
Rozwiązanie Domyślnie wszystkie nazwy plików są kodowane i dekodowane według kodowania tekstu zwracanego przez funkcję sys.getfilesystemencoding(): >>> sys.getfilesystemencoding() 'utf-8' >>>
Jeśli z pewnych przyczyn nie chcesz uwzględniać kodowania, podaj nazwę pliku w postaci nieprzetworzonego łańcucha bajtów. Oto przykład: >>> # Zapis pliku o nazwie w formacie Unicode >>> with open('jalape\xf1o.txt', 'w') as f: ... f.write('Pikantna papryczka!')
5.14. Nieuwzględnianie kodowania nazw plików
153
... 6 >>> # Lista zawartości katalogu (odkodowana) >>> import os >>> os.listdir('.') ['jalapeño.txt'] >>> # Lista zawartości katalogu (nieprzetworzona) >>> os.listdir(b'.') # Uwaga — łańcuch bajtów [b'jalapen\xcc\x83o.txt'] >>> # Otwieranie pliku przy użyciu nieprzetworzonej nazwy >>> with open(b'jalapen\xcc\x83o.txt') as f: ... print(f.read()) ... Pikantna papryczka! >>>
Jak widać w dwóch ostatnich operacjach, sposób podawania nazwy pliku zmienia się, gdy w funkcjach związanych z plikami (np. open() i os.listdir()) podawane są łańcuchy bajtów
Omówienie W standardowych warunkach nie trzeba przejmować się kodowaniem i dekodowaniem nazw plików — standardowe operacje wymagające takich nazw działają poprawnie. Jednak wiele systemów operacyjnych pozwala użytkownikom na przypadkowe lub złośliwe tworzenie plików o nazwach niezgodnych z oczekiwanymi regułami kodowania. Takie nazwy mogą sprawić, że programy Pythona pracujące na wielu plikach przestaną z tajemniczych przyczyn działać. Odczytywanie zawartości katalogów i stosowanie nazw plików w postaci nieprzetworzonych, niedekodowanych bajtów pozwala uniknąć takich problemów, choć dzieje się to kosztem pewnych niedogodności w trakcie programowania. Z receptury 5.15. dowiesz się, jak wyświetlać nazwy plików niemożliwe do odkodowania.
5.15. Wyświetlanie nieprawidłowych nazw plików Problem Program pobrał listę zawartości katalogu, jednak gdy próbował wyświetlić nazwy plików, przestał działać. Pojawiła się informacja o wyjątku UnicodeEncodeError i niezrozumiały komunikat surrogates not allowed.
Rozwiązanie W trakcie wyświetlania nazw plików o nieznanym pochodzeniu warto stosować przedstawione poniżej podejście, tak aby uniknąć błędów: def bad_filename(filename): return repr(filename)[1:-1] try: print(filename) except UnicodeEncodeError: print(bad_filename(filename))
154
Rozdział 5. Pliki i operacje wejścia-wyjścia
Omówienie Ta receptura dotyczy rzadkiego, ale bardzo irytującego problemu związanego z programami, które manipulują systemem plików. Python domyślnie przyjmuje, że wszystkie nazwy plików są zakodowane zgodnie z ustawieniem zwracanym przez funkcję sys.getfilesystemencoding(). Jednak niektóre systemy plików nie wymuszają kodowania, dlatego nazwy plików mogą być zakodowane w niewłaściwy sposób. Nie jest to częsty problem, jednak istnieje ryzyko, że użytkownik zrobi coś niemądrego i przypadkowo utworzy nieprawidłowy plik (np. w wyniku przekazania nieprawidłowej nazwy pliku do funkcji open() w błędnym kodzie). Gdy wykonywane są polecenia takie jak os.listdir(), błędne nazwy plików są dla Pythona problemem. Z jednej strony Python nie może po prostu odrzucić błędnych nazw. Z drugiej strony nie potrafi przekształcić nazwy pliku na poprawny łańcuch znaków. Rozwiązanie zastosowane w Pythonie polega na odwzorowaniu niemożliwej do odkodowania wartości bajta \xhh z nazwy pliku na tzw. kodowanie zastępcze, reprezentowane przez znak Unicode \udchh. Poniżej pokazano, jak może wyglądać błędna lista zawartości katalogu, jeśli zawiera plik o nazwie bäd.txt o kodowaniu Latin-1 zamiast UTF-8: >>> import os >>> files = os.listdir('.') >>> files ['spam.py', 'b\udce4d.txt', 'foo.txt'] >>>
Jeśli kod manipuluje nazwami plików, a nawet przekazuje je do funkcji (takich jak open()), wszystko działa prawidłowo. Problemy pojawiają się jedynie w kontekście zapisu nazw plików (np. wyświetlania na ekranie, zapisywania w dzienniku itd.). Jeśli spróbujesz wyświetlić przedstawioną wcześniej listę, program zakończy działanie: >>> for name in files: ... print(name) ... spam.py Traceback (most recent call last): File "", line 2, in UnicodeEncodeError: 'utf-8' codec can't encode character '\udce4' in position 1: surrogates not allowed >>>
Wynika to z tego, że technicznie znak \udce4 nie jest poprawny w kodowaniu Unicode. Jest to druga połowa dwuznakowej kombinacji nazywanej parą zastępczą. Ponieważ jednak brakuje tu pierwszej połowy, znak jest nieprawidłowy. Dlatego jedyny sposób na udane wygenerowanie danych wyjściowych to podjęcie działań naprawczych po wykryciu błędnej nazwy pliku. Po zmodyfikowaniu kodu na wersję przedstawioną w recepturze uzyskasz następujący efekt: >>> for name in files: ... try: ... print(name) ... except UnicodeEncodeError: ... print(bad_filename(name)) ... spam.py b\udce4d.txt foo.txt >>>
5.15. Wyświetlanie nieprawidłowych nazw plików
155
To, jakie operacje będzie wykonywać funkcja bad_filename(), zależy od programisty. Inne rozwiązanie polega na ponownym zakodowaniu wartości. Oto przykład: def bad_filename(filename): temp = filename.encode(sys.getfilesystemencoding(), errors='surrogateescape') return temp.decode('latin-1')
Ta wersja zwraca następujące dane wyjściowe: >>> for name in files: ... try: ... print(name) ... except UnicodeEncodeError: ... print(bad_filename(name)) ... spam.py bäd.txt foo.txt >>>
Dla większości czytelników receptura ta nie będzie ciekawa. Jeśli jednak piszesz ważne skrypty, które muszą działać niezawodnie dla nazw plików z systemu, warto pomyśleć o opisanych tu zagadnieniach. W przeciwnym razie może się okazać, że będziesz musiał w weekend dzwonić do biura w celu zdiagnozowania tajemniczego błędu.
5.16. Dodawanie lub zmienianie kodowania otwartego pliku Problem Programista chce dodać lub zmienić kodowanie Unicode otwartego pliku bez wcześniejszego zamykania go.
Rozwiązanie Aby dodać kodowanie Unicode do istniejącego pliku otwartego w trybie binarnym, należy umieścić go w obiekcie io.TextIOWrapper(): import urllib.request import io u = urllib.request.urlopen('http://www.python.org') f = io.TextIOWrapper(u,encoding='utf-8') text = f.read()
Jeśli chcesz zmienić kodowanie pliku otwartego w trybie tekstowym, zastosuj metodę detach() pliku, aby usunąć obecną warstwę kodowania tekstu przed zastąpieniem jej nową. Oto przykład ilustrujący, jak zmienić kodowanie dla obiektu sys.stdout: >>> import sys >>> sys.stdout.encoding 'UTF-8' >>> sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding='latin-1') >>> sys.stdout.encoding 'latin-1' >>>
156
Rozdział 5. Pliki i operacje wejścia-wyjścia
To rozwiązanie może spowodować, że dane wyświetlane w terminalu staną się nieczytelne. Kod ten tylko ilustruje technikę.
Omówienie System wejścia-wyjścia jest oparty na warstwach. Aby je zobaczyć, uruchom poniższy prosty przykład wykorzystujący plik tekstowy: >>> f = open('sample.txt','w') >>> f <_io.TextIOWrapper name='sample.txt' mode='w' encoding='UTF-8'> >>> f.buffer <_io.BufferedWriter name='sample.txt'> >>> f.buffer.raw <_io.FileIO name='sample.txt' mode='wb'> >>>
W tym przykładzie io.TextIOWrapper to warstwa obsługi tekstu, która koduje i dekoduje dane w formacie Unicode, io.BufferedWriter to buforowana warstwa wejścia-wyjścia obsługująca dane binarne, a io.FileIO to nieprzetworzony plik reprezentujący używany przez system operacyjny niskopoziomowy deskryptor pliku. Dodawanie i zmienianie kodowania tekstu polega na dodawaniu lub modyfikowaniu górnej warstwy io.TextIOWrapper. Zwykle manipulowanie różnymi warstwami za pomocą przedstawionych atrybutów jest niebezpieczne. Zobacz, co się stanie, gdy spróbujesz zmienić kodowanie przy użyciu tej techniki: >>> f <_io.TextIOWrapper name='sample.txt' mode='w' encoding='UTF-8'> >>> f = io.TextIOWrapper(f.buffer, encoding='latin-1') >>> f <_io.TextIOWrapper name='sample.txt' encoding='latin-1'> >>> f.write('Witaj') Traceback (most recent call last): File "", line 1, in ValueError: I/O operation on closed file. >>>
Ten kod nie działa, ponieważ pierwotna wartość zmiennej f jest usuwana, a używany plik zostaje zamknięty. Metoda detach() powoduje odłączenie górnej warstwy pliku i zwrócenie następnej warstwy. Nie można wtedy korzystać z górnej warstwy. Oto przykład: >>> f = open('sample.txt', 'w') >>> f <_io.TextIOWrapper name='sample.txt' mode='w' encoding='UTF-8'> >>> b = f.detach() >>> b <_io.BufferedWriter name='sample.txt'> >>> f.write('Witaj') Traceback (most recent call last): File "", line 1, in ValueError: underlying buffer has been detached >>>
5.16. Dodawanie lub zmienianie kodowania otwartego pliku
157
Jednak po odłączeniu górnej warstwy można do zwróconego obiektu dołączyć nową warstwę: >>> f = io.TextIOWrapper(b, encoding='latin-1') >>> f <_io.TextIOWrapper name='sample.txt' encoding='latin-1'> >>>
Choć pokazano tu zmienianie kodowania, opisaną technikę można też zastosować do zmiany obsługi wierszy, błędów oraz innych aspektów korzystania z plików. Oto przykład: >>> sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding='ascii', ... errors='xmlcharrefreplace') >>> print('Jalape\u00f1o') Jalapeño >>>
Warto zauważyć, że niewystępujący w kodowaniu ASCII znak ñ został zastąpiony w danych wyjściowych ciągiem ñ.
5.17. Zapisywanie bajtów w pliku tekstowym Problem Programista chce zapisać nieprzetworzone bajty do pliku otwartego w trybie tekstowym.
Rozwiązanie Wystarczy zapisać bajty do atrybutu buffer danego pliku. Oto przykład: >>> import sys >>> sys.stdout.write(b'Witaj\n') Traceback (most recent call last): File "", line 1, in TypeError: must be str, not bytes >>> sys.stdout.buffer.write(b'Witaj\n') Witaj 5 >>>
Podobnie przebiega wczytywanie danych binarnych z pliku tekstowego — należy wczytać dane z atrybutu buffer.
Omówienie System wejścia-wyjścia ma budowę warstwową. Pliki tekstowe powstają przez dodanie warstwy kodowania i dekodowania Unicode do buforowanego pliku w trybie binarnym. Atrybut buffer prowadzi do podstawowego pliku. Dostęp do niego pozwala pominąć warstwę kodowania i dekodowania tekstu. Obiekt sys.stdout można potraktować jako przypadek specjalny. Obiekt ten domyślnie jest otwierany w trybie tekstowym. Jeśli jednak piszesz skrypt, który ma przekazywać dane binarne do standardowego wyjścia, możesz wykorzystać przedstawioną tu technikę do pominięcia kodowania tekstu.
158
Rozdział 5. Pliki i operacje wejścia-wyjścia
5.18. Umieszczanie deskryptora istniejącego pliku w obiekcie pliku Problem Programista ma całkowitoliczbowy deskryptor pliku odpowiadający otwartemu kanałowi wejścia-wyjścia z systemu operacyjnego (np. plikowi, potokowi, gniazdu itd.) i chce umieścić ten deskryptor w obiekcie wyższego poziomu — w obiekcie pliku Pythona.
Rozwiązanie Deskryptor pliku różni się od zwykłych otwartych plików, ponieważ jest tylko całkowitoliczbowym uchwytem przypisanym przez system operacyjny do jednego z systemowych kanałów wejścia-wyjścia. Jeśli masz taki deskryptor, możesz za pomocą funkcji open() umieścić go w obiekcie pliku Pythona. Jako pierwszy argument zamiast nazwy pliku należy podać właśnie całkowitoliczbowy deskryptor. Oto przykład: # Otwieranie niskopoziomowego deskryptora pliku import os fd = os.open('somefile.txt', os.O_WRONLY | os.O_CREAT) # Przekształcanie go w poprawny plik f = open(fd, 'wt') f.write('Witaj, świecie\n') f.close()
Gdy wysokopoziomowy obiekt pliku jest zamykany lub usuwany, zamykany jest też powiązany deskryptor pliku. Jeśli to rozwiązanie jest niepożądane, należy przekazać do funkcji open() opcjonalny argument closefd=False. Oto przykład: # Tworzenie obiektu pliku, przy czym jego zamknięcie nie prowadzi do zamknięcia deskryptora f = open(fd, 'wt', closefd=False) ...
Omówienie W systemach uniksowych przedstawiona tu technika umieszczania deskryptora pliku w obiekcie może być wygodnym sposobem na dodanie interfejsu plikowego do istniejącego kanału wejścia-wyjścia (np. potoku lub gniazda), który został otwarty w inny sposób. Oto przykład, w którym zastosowano gniazda: from socket import socket, AF_INET, SOCK_STREAM def echo_client(client_sock, addr): print('Połączenie z adresem', addr) # Tworzenie nakładek w postaci plików w trybie tekstowym na potrzeby odczytu i zapisu gniazd client_in = open(client_sock.fileno(), 'rt', encoding='latin-1', closefd=False) client_out = open(client_sock.fileno(), 'wt', encoding='latin-1', closefd=False) # Zwracanie wierszy do klienta za pomocą plikowych operacji wejścia-wyjścia for line in client_in: client_out.write(line)
5.18. Umieszczanie deskryptora istniejącego pliku w obiekcie pliku
Należy podkreślić, że przykład ten ma jedynie ilustrować pewną cechę wbudowanej funkcji open() i działa tylko w systemach uniksowych. Jeśli chcesz dodać interfejs plikowy dla gniazd, a kod ma działać w różnych systemach, zastosuj metodę makefile() gniazd. Jednak jeżeli przenośność kodu nie ma znaczenia, przedstawione tu rozwiązanie zapewnia znacznie wyższą wydajność niż korzystanie z metody makefile(). Za pomocą pokazanej techniki można też utworzyć alias, który pozwala używać otwartego już pliku w nieco inny sposób niż określony przy jego otwieraniu. Poniżej przedstawiono, jak utworzyć obiekt pliku pozwalający przekazywać dane binarne do obiektu stdout (zwykle obiekt ten jest otwierany w trybie tekstowym): import sys # Tworzenie pliku w trybie binarnym dla obiektu stdout bstdout = open(sys.stdout.fileno(), 'wb', closefd=False) bstdout.write(b'Witaj, Polsko\n') bstdout.flush()
Choć można zapisać istniejący deskryptor pliku jako zwykły plik, warto pamiętać, że nie zawsze obsługiwane są wszystkie tryby plików. Ponadto niektóre deskryptory mogą powodować ciekawe efekty uboczne, zwłaszcza w kontekście obsługi błędów, sprawdzania końca pliku itd. Działanie przedstawionej techniki zależy od systemu operacyjnego. Żaden z przykładów nie zadziała w systemach nieuniksowych. Na zakończenie warto więc dodać, że trzeba starannie przetestować kod, aby się upewnić, że działa w oczekiwany sposób.
5.19. Tworzenie tymczasowych plików i katalogów Problem Programista chce utworzyć tymczasowy plik lub katalog używany w czasie działania programu. Następnie dany plik lub katalog ma zostać usunięty.
Rozwiązanie Moduł tempfile udostępnia wiele funkcji umożliwiających wykonanie tego zadania. Aby utworzyć anonimowy tymczasowy plik, należy wywołać funkcję tempfile.TemporaryFile: from tempfile import TemporaryFile with TemporaryFile('w+t') as f: # Odczytywanie danych i zapisywanie ich w pliku f.write('Witaj, świecie\n') f.write('Test\n')
160
Rozdział 5. Pliki i operacje wejścia-wyjścia
# Wracanie do początku i odczytywanie danych f.seek(0) data = f.read() # Tymczasowy plik jest usuwany
Z pliku można też korzystać w następujący sposób: f = TemporaryFile('w+t') # Korzystanie z tymczasowego pliku ... f.close() # Plik jest usuwany
Pierwszym argumentem funkcji TemporaryFile() jest tryb pliku. Zwykle jest to w+t dla plików tekstowych i w+b dla binarnych. Tryb ten obsługuje odczyt i zapis. Jest to przydatne, ponieważ plik zostanie usunięty, gdy programista zamknie go w celu zmiany trybu. Funkcja TemporaryFile() przyjmuje też te same argumenty co wbudowana funkcja open(). Oto przykład: with TemporaryFile('w+t', encoding='utf-8', errors='ignore') as f: ...
W większości systemów uniksowych plik utworzony przez funkcję TemporaryFile() jest anonimowy i nie odpowiada mu nawet wpis w katalogu. Jeśli chcesz to zmienić, zastosuj funkcję NamedTemporaryFile(): from tempfile import NamedTemporaryFile with NamedTemporaryFile('w+t') as f: print('filename is:', f.name) ... # Plik jest automatycznie usuwany
Tu atrybut f.name otwartego pliku zawiera nazwę pliku tymczasowego. Może to być przydatne, jeśli plik trzeba przekazać do innego kodu, który ma otworzyć dany plik. Podobnie jak przy korzystaniu z funkcji TemporaryFile(), tak i tu uzyskany plik jest automatycznie usuwany w momencie zamknięcia. Jeśli chcesz to zmienić, podaj argument delete=False: with NamedTemporaryFile('w+t', delete=False) as f: print('filename is:', f.name) ...
Aby utworzyć katalog tymczasowy, zastosuj funkcję tempfile.TemporaryDirectory(): from tempfile import TemporaryDirectory with TemporaryDirectory() as dirname: print('dirname is:', dirname) # Korzystanie z katalogu ... # Katalog i cała jego zawartość są usuwane
Omówienie Funkcje TemporaryFile(), NamedTemporaryFile() i TemporaryDirectory() to prawdopodobnie najwygodniejsze narzędzia do pracy z tymczasowymi plikami i katalogami, ponieważ automatycznie obsługują wszystkie etapy tworzenia i późniejszego usuwania takich obiektów. Na niższym poziomie do tworzenia tymczasowych plików i katalogów można też wykorzystać funkcje mkstemp() i mkdtemp():
Jednak funkcje te nie zarządzają tworzonymi obiektami. Np. funkcja mkstemp() zwraca nieprzetworzony deskryptor pliku systemu operacyjnego, a programista musi przekształcić ten deskryptor na poprawny plik. Ponadto to programista odpowiada za operacje porządkujące związane z tworzonymi plikami. Pliki tymczasowe zwykle tworzone są w domyślnej lokalizacji systemowej, np. w katalogu /var/tmp lub podobnym. Aby ustalić tę lokalizację, należy wywołać funkcję tempfile.gettempdir(): >>> tempfile.gettempdir() '/var/folders/7W/7WZl5sfZEF0pljrEB1UMWE+++TI/-Tmp-' >>>
Wszystkie funkcje związane z plikami tymczasowymi pozwalają zmienić katalog na takie pliki, a także sposób tworzenia ich nazw. Służą do tego argumenty podawane za pomocą słów kluczowych prefix, suffix i dir. Oto przykład: >>> f = NamedTemporaryFile(prefix='mytemp', suffix='.txt', dir='/tmp') >>> f.name '/tmp/mytemp8ee899.txt' >>>
Ponadto moduł tempfile() tworzy pliki tymczasowe w najbezpieczniejszy możliwy sposób. Dlatego uprawnienia dostępu do nich przyznaje tylko bieżącemu użytkownikowi, a także podejmuje działania zapobiegające wystąpieniu warunku wyścigu przy tworzeniu plików. Warto pamiętać, że w poszczególnych systemach moduł ten może działać inaczej. Dlatego szczegóły należy sprawdzić w oficjalnej dokumentacji (http://pyserial.sourceforge.net/).
5.20. Komunikowanie z portami szeregowymi Problem Programista chce odczytywać i zapisywać dane przez port szeregowy (zwykle robi się to w celu interakcji ze sprzętem, np. robotami lub czujnikami).
Rozwiązanie Choć pożądany efekt można uzyskać bezpośrednio, używając wbudowanych prostych operacji wejścia-wyjścia Pythona, najlepszym narzędziem do komunikacji przez porty szeregowe jest pakiet pySerial (http://pyserial.sourceforge.net/). Rozpoczęcie pracy z tym pakietem jest bardzo proste. Wystarczy otworzyć port szeregowy za pomocą kodu podobnego do poniższego: import serial ser = serial.Serial('/dev/tty.usbmodem641', baudrate=9600, bytesize=8, parity='N', stopbits=1)
162
Rozdział 5. Pliki i operacje wejścia-wyjścia
# Urządzenia mają różne nazwy
Nazwa urządzenia zależy od jego rodzaju i systemu operacyjnego. Np. w systemie Windows do otwierania portów komunikacyjnych takich jak COM0 i COM1 można używać nazw 0, 1 itd. Po otwarciu portu można odczytywać i zapisywać dane za pomocą wywołań read(), readline() i write(). Oto przykład: ser.write(b'G1 X50 Y50\r\n') resp = ser.readline()
Od tego momentu prosta komunikacja przez porty szeregowe jest zwykle łatwa w obsłudze.
Omówienie Choć komunikacja przez porty szeregowe wydaje się prosta, czasem okazuje się dość skomplikowana. Jednym z powodów, dla których należy stosować pakiety takie jak pySerial, jest obsługa przez nie zaawansowanych mechanizmów (limitów czasu, przepływu sterowania, opróżniania buforów, wymiany potwierdzeń itd.). Jeśli na przykład chcesz włączyć wymianę potwierdzeń RTS-CTS, wystarczy podać argument rtscts=True w metodzie Serial(). Dokumentacja omawianego pakietu jest doskonała, dlatego nie warto kopiować jej w tym miejscu. Warto pamiętać, że wszystkie operacje wejścia-wyjścia dla portów szeregowych są binarne. Dlatego w kodzie należy stosować bajty zamiast tekstu (można też w razie potrzeby przeprowadzić odpowiednie kodowanie lub dekodowanie tekstu). Jeśli potrzebne są binarne polecenia lub pakiety, przydatny może okazać się też moduł struct.
5.21. Serializowanie obiektów Pythona Problem Programista chce zserializować obiekt Pythona na strumień bajtów, aby móc zapisać dany obiekt do pliku, zachować go w bazie danych lub przesłać przez sieć.
Rozwiązanie Najczęściej stosowanym narzędziem do serializowania danych jest moduł pickle. Aby zapisać obiekt w pliku, należy użyć poniższego kodu: import pickle data = ... # Obiekt Pythona f = open('somefile', 'wb') pickle.dump(data, f)
Do zapisywania obiektu w łańcuchu znaków służy funkcja pickle.dumps(): s = pickle.dumps(data)
Aby odtworzyć obiekt ze strumienia bajtów, można zastosować funkcję pickle.load() lub pickle.loads(): # Odtwarzanie z pliku f = open('somefile', 'rb') data = pickle.load(f) # Odtwarzanie z łańcucha znaków data = pickle.loads(s)
5.21. Serializowanie obiektów Pythona
163
Omówienie W większości programów funkcje dump() i load() wystarczą do skutecznego korzystania z modułu pickle. Rozwiązanie to działa dla większości typów danych Pythona i klas zdefiniowanych przez użytkowników. Jeśli korzystasz z biblioteki, która umożliwia zapisywanie i odtwarzanie obiektów Pythona w bazach danych lub przesyłanie obiektów przez sieć, bardzo możliwe, że używa ona modułu pickle. Moduł pickle odpowiada za charakterystyczne dla Pythona samoopisowe kodowanie danych. Dzięki temu, że jest samoopisowe, serializowane dane zawierają informacje o początku i końcu każdego obiektu oraz o jego typie. Dlatego nie trzeba martwić się o definiowanie rekordów — kod działa i bez tego. Np. jeśli serializujesz grupę obiektów, możesz zastosować następujący kod: >>> import pickle >>> f = open('somedata', 'wb') >>> pickle.dump([1, 2, 3, 4], f) >>> pickle.dump('Witaj', f) >>> pickle.dump({'Jabłko', 'Gruszka', 'Banan'}, f) >>> f.close() >>> f = open('somedata', 'rb') >>> pickle.load(f) [1, 2, 3, 4] >>> pickle.load(f) 'Witaj' >>> pickle.load(f) {'Jabłko', 'Gruszka', 'Banan'} >>>
W ten sposób można serializować funkcje, klasy i obiekty, przy czym w wygenerowanych danych zakodowane są tylko referencje do powiązanych obiektów z kodu. Oto przykład: >>> import math >>> import pickle. >>> pickle.dumps(math.cos) b'\x80\x03cmath\ncos\nq\x00.' >>>
W momencie deserializacji program przyjmuje, że cały potrzebny kod źródłowy jest dostępny. Moduły, klasy i funkcje są w razie potrzeby automatycznie importowane. Gdy dane Pythona są współużytkowane przez interpretery z różnych komputerów, może to utrudniać konserwację kodu, ponieważ wszystkie komputery muszą mieć dostęp do tego samego kodu źródłowego. Funkcji pickle.load() nigdy nie należy używać do niezaufanych danych. W ramach wczytywania kodu moduł pickle automatycznie pobiera moduły i tworzy obiekty na ich podstawie. Napastnik, który wie, jak działa moduł pickle, może przygotować specjalnie spreparowane dane powodujące, że Python wykona określone polecenia systemowe. Dlatego moduł pickle należy stosować tylko wewnętrznie w interpreterach, które potrafią uwierzytelniać siebie nawzajem.
Niektórych obiektów nie można zserializować w ten sposób. Są to zwykle obiekty mające zewnętrzny stan w systemie, takie jak otwarte pliki, otwarte połączenia sieciowe, wątki, procesy, ramki stosu itd. W klasach zdefiniowanych przez użytkownika można czasem obejść to ograniczenie, udostępniając metody __getstate__() i __setstate__(). Wtedy funkcja pickle.dump() wywołuje metodę __getstate__(), aby pobrać serializowany obiekt, a przy 164
Rozdział 5. Pliki i operacje wejścia-wyjścia
deserializacji wywoływana jest metoda __setstate__(). Aby zilustrować możliwości tego podejścia, poniżej przedstawiono klasę ze zdefiniowanym wewnętrznie wątkiem, którą jednak można zarówno serializować, jak i deserializować: # countdown.py import time import threading class Countdown: def __init__(self, n): self.n = n self.thr = threading.Thread(target=self.run) self.thr.daemon = True self.thr.start() def run(self): while self.n > 0: print('T-minus', self.n) self.n -= 1 time.sleep(5) def __getstate__(self): return self.n def __setstate__(self, n): self.__init__(n)
# Po pewnym czasie f = open('cstate.p', 'wb') import pickle pickle.dump(c, f) f.close()
Teraz wyjdź z Pythona i po ponownym jego uruchomieniu wywołaj następujący kod: >>> f = open('cstate.p', 'rb') >>> pickle.load(f) countdown.Countdown object at 0x10069e2d0> T-minus 19 T-minus 18 ...
Powinieneś zobaczyć, jak wątek w magiczny sposób ponownie zaczyna działać i wznawia pracę od miejsca, w którym zakończył ją w momencie serializowania. Moduł pickle nie zapewnia wysokiej wydajności kodowania dużych struktur danych, np. tablic binarnych tworzonych przez takie biblioteki jak moduł array lub numpy. Jeśli chcesz przenosić duże ilości danych tablicowych, lepszym rozwiązaniem może być zapisanie ich w pliku lub zastosowanie standardowego kodowania, np. HDF5 (obsługiwanego przez niestandardowe biblioteki).
5.21. Serializowanie obiektów Pythona
165
Ponieważ moduł pickle działa tylko w Pythonie i wymaga kodu źródłowego, zwykle nie należy go używać do długoterminowego przechowywania danych. Jeśli kod źródłowy zostanie zmodyfikowany, wszystkie przechowywane dane mogą stać się nieczytelne. Przy przechowywaniu danych w bazach danych lub archiwach zwykle lepiej jest stosować bardziej standardowe kodowania, np. XML, CSV lub JSON. Są one w większym stopniu ustandaryzowane, obsługuje je wiele języków i są lepiej dostosowane do zmian w kodzie źródłowym. Ponadto warto pamiętać, że moduł pickle udostępnia wiele różnych opcji i ma skomplikowane przypadki brzegowe. Przy wykonywaniu typowych zadań nie trzeba się nimi przejmować. Jeśli jednak pracujesz nad rozbudowaną aplikacją, która do serializacji używa modułu pickle, należy zapoznać się z jego oficjalną dokumentacją (http://docs.python.org/3/library/pickle.html).
166
Rozdział 5. Pliki i operacje wejścia-wyjścia
ROZDZIAŁ 6.
Kodowanie i przetwarzanie danych
Rozdział ten poświęcony jest przede wszystkim używaniu Pythona do przetwarzania danych zapisanych za pomocą różnego rodzaju popularnych kodowań, np. w plikach CSV, w formacie JSON albo XML lub w rekordach w postaci binarnej. W rozdziale tym (w odróżnieniu od rozdziału o strukturach danych) najważniejsze są nie konkretne algorytmy, ale problem pobierania i zapisywania danych w programach.
6.1. Wczytywanie i zapisywanie danych CSV Problem Programista chce wczytać lub zapisać dane w formacie CSV.
Rozwiązanie Podczas pracy z większością rodzajów danych CSV należy korzystać z biblioteki csv. Załóżmy, że w pliku stocks.csv znajdują się dane na temat akcji spółek giełdowych: Symbol,Price,Date,Time,Change,Volume "AA",39.48,"6/11/2007","9:36am",-0.18,181800 "AIG",71.38,"6/11/2007","9:36am",-0.15,195500 "AXP",62.58,"6/11/2007","9:36am",-0.46,935000 "BA",98.31,"6/11/2007","9:36am",+0.12,104800 "C",53.08,"6/11/2007","9:36am",-0.25,360900 "CAT",78.29,"6/11/2007","9:36am",-0.23,225400
Dane te można wczytać jako sekwencję krotek: import csv with open('stocks.csv') as f: f_csv = csv.reader(f) headers = next(f_csv) for row in f_csv: # Przetwarzanie obiektu row ...
W tym kodzie row to krotka. Dlatego aby uzyskać dostęp do konkretnych pól, trzeba podać indeks, np. row[0] (pole Symbol) lub row[4] (pole Change).
167
Ponieważ stosowanie indeksów może prowadzić do problemów, można zastanowić się nad użyciem krotek nazwanych. Oto przykład: from collections import namedtuple with open('stock.csv') as f: f_csv = csv.reader(f) headings = next(f_csv) Row = namedtuple('Row', headings) for r in f_csv: row = Row(*r) # Przetwarzanie obiektu row ...
Dzięki temu można podawać nagłówki kolumn (np. row.Symbol i row.Change) zamiast indeksów. Warto zauważyć, że rozwiązanie to działa tylko wtedy, gdy nagłówki kolumn to poprawne identyfikatory Pythona. W przeciwnym razie trzeba zmodyfikować pierwotne nagłówki (np. symbole niedozwolone w identyfikatorach zastąpić podkreśleniem lub podobnym znakiem). Inna możliwość to wczytywanie danych jako sekwencji słowników. Tak działa poniższy kod: import csv with open('stocks.csv') as f: f_csv = csv.DictReader(f) for row in f_csv: # Przetwarzanie obiektu row ...
W tej wersji dostęp do każdego wiersza można uzyskać za pomocą nagłówków, np. row['Symbol'] lub row['Change']. Przy zapisywaniu danych CSV także należy wykorzystać moduł csv, przy czym trzeba utworzyć obiekt writer: headers = ['Symbol','Price','Date','Time','Change','Volume'] rows = [('AA', 39.48, '6/11/2007', '9:36am', -0.18, 181800), ('AIG', 71.38, '6/11/2007', '9:36am', -0.15, 195500), ('AXP', 62.58, '6/11/2007', '9:36am', -0.46, 935000), ] with open('stocks.csv','w') as f: f_csv = csv.writer(f) f_csv.writerow(headers) f_csv.writerows(rows)
Jeśli dane mają postać sekwencji słowników, należy zastosować następujący kod: headers = ['Symbol', 'Price', 'Date', 'Time', 'Change', 'Volume'] rows = [{'Symbol':'AA', 'Price':39.48, 'Date':'6/11/2007', 'Time':'9:36am', 'Change':-0.18, 'Volume':181800}, {'Symbol':'AIG', 'Price': 71.38, 'Date':'6/11/2007', 'Time':'9:36am', 'Change':-0.15, 'Volume': 195500}, {'Symbol':'AXP', 'Price': 62.58, 'Date':'6/11/2007', 'Time':'9:36am', 'Change':-0.46, 'Volume': 935000}, ] with open('stocks.csv','w') as f: f_csv = csv.DictWriter(f, headers) f_csv.writeheader() f_csv.writerows(rows)
168
Rozdział 6. Kodowanie i przetwarzanie danych
Omówienie Prawie zawsze lepiej jest zastosować moduł csv, zamiast próbować ręcznie rozdzielać i parsować dane CSV. Możliwe, że wpadniesz na pomysł napisania następującego kodu: with open('stocks.csv') as f: for line in f: row = line.split(',') # Przetwarzanie obiektu row ...
Problem z tym podejściem polega na tym, że trzeba poradzić sobie z wieloma niewygodnymi szczegółami. Np. jeśli któreś z pól znajduje się w cudzysłowie, trzeba usunąć ten znak. Ponadto gdy w polu z cudzysłowem występuje przecinek, kod nie będzie działał prawidłowo, ponieważ utworzy wiersz o niewłaściwej długości. Biblioteka csv napisana jest na podstawie reguł kodowania formatu CSV obowiązujących w programie Microsoft Excel. Jest to prawdopodobnie najczęściej stosowana odmiana, zapewniająca największą zgodność z innymi rozwiązaniami. Jednak w dokumentacji modułu csv znajdziesz informacje o kilku technikach umożliwiających zmianę kodowania (można np. zmienić znak separatora). Jeśli chcesz wczytać dane rozdzielone znakami tabulacji, zastosuj następujący kod: # Przykładowy kod wczytujący wartości oddzielone znakami tabulacji with open('stock.tsv') as f: f_tsv = csv.reader(f, delimiter='\t') for row in f_tsv: # Przetwarzanie obiektu row ...
Jeśli wczytujesz dane CSV i przekształcasz je na nazwane krotki, zachowaj staranność przy sprawdzaniu poprawności nazw kolumn. W wierszu z kolumnami w pliku CSV mogą znajdować się znaki niedozwolone w identyfikatorach: Street Address,Num-Premises,Latitude,Longitude 5412 N CLARK,10,41.980262,-87.668452
To sprawia, że próba utworzenia obiektu namedtuple się nie powiedzie (program zgłosi wyjątek ValueError). Aby rozwiązać ten problem, konieczne może być wcześniejsze zmodyfikowanie nagłówków. Można np. zastąpić niedozwolone znaki za pomocą wyrażenia regularnego: import re with open('stock.csv') as f: f_csv = csv.reader(f) headers = [ re.sub('[^a-zA-Z_]', '_', h) for h in next(f_csv) ] Row = namedtuple('Row', headers) for r in f_csv: row = Row(*r) # Przetwarzanie obiektu row ...
Warto też podkreślić, że moduł csv nie próbuje interpretować danych ani przekształcać ich na typ inny niż łańcuch znaków. Jeśli przekształcenie jest potrzebne, trzeba je przeprowadzić samodzielnie. Oto przykładowy kod, który dodatkowo przekształca typ wczytywanych danych CSV: col_types = [str, float, str, str, float, int] with open('stocks.csv') as f: f_csv = csv.reader(f) headers = next(f_csv)
6.1. Wczytywanie i zapisywanie danych CSV
169
for row in f_csv: # Przekształcanie elementów obiektu row row = tuple(convert(value) for convert, value in zip(col_types, row)) ...
Inna możliwość to przekształcenie wybranych pól słowników: print('Odczyt danych jako słowników z konwersją typu') field_types = [ ('Price', float), ('Change', float), ('Volume', int) ] with open('stocks.csv') as f: for row in csv.DictReader(f): row.update((key, conversion(row[key])) for key, conversion in field_types) print(row)
Zwykle jednak warto zachować ostrożność przy tego rodzaju przekształceniach. W praktyce w plikach CSV często brakuje wartości, dane są uszkodzone lub występują inne problemy, które mogą uniemożliwić poprawne przekształcenie danych. Warto to uwzględnić (i dodać odpowiednią obsługę wyjątków), jeśli nie masz pewności, że dane są bezbłędne. Jeżeli chcesz wczytać dane CSV w celu przeprowadzenia analiz danych i obliczenia statystyk, zapoznaj się z pakietem Pandas (http://pandas.pydata.org/). Zawiera on wygodną funkcję pandas.read_csv(), która wczytuje dane CSV do obiektu DataFrame. Następnie można wygenerować różne statystyki, przefiltrować dane i wykonać wiele innych wysokopoziomowych operacji. Przykład znajdziesz w recepturze 6.13.
6.2. Wczytywanie i zapisywanie danych w formacie JSON Problem Programista chce wczytywać lub zapisywać dane w formacie JSON (ang. JavaScript Object Notation).
Rozwiązanie Moduł json umożliwia łatwe kodowanie i dekodowanie danych w formacie JSON. Dwie podstawowe funkcje tego modułu to json.dumps() i json.loads(). Odpowiadają one interfejsowi używanemu w innych bibliotekach do obsługi serializacji, np. w pickle. Poniżej pokazano, jak przekształcić strukturę danych z Pythona na format JSON: import json data = { 'name' : 'ACME', 'shares' : 100, 'price' : 542.23 } json_str = json.dumps(data)
170
Rozdział 6. Kodowanie i przetwarzanie danych
Poniższy kod przekształca łańcuch znaków w formacie JSON z powrotem na strukturę danych z Pythona: data = json.loads(json_str)
Jeśli pracujesz z plikami, a nie z łańcuchami znaków, możesz też kodować i dekodować dane w formacie JSON za pomocą funkcji json.dump() i json.load(). Oto przykład: # Zapisywanie danych w formacie JSON with open('data.json', 'w') as f: json.dump(data, f) # Wczytywanie danych with open('data.json', 'r') as f: data = json.load(f)
Omówienie Format JSON obsługuje typy podstawowe None, bool, int, float i str, a także listy, krotki i słowniki zawierające dane tych typów. Przy przetwarzaniu słowników przyjmuje się, że klucze to łańcuchy znaków (klucze innego typu są przekształcane na łańcuchy znaków w momencie kodowania). Aby zachować zgodność ze specyfikacją formatu JSON, należy kodować tylko listy i słowniki Pythona. Ponadto w aplikacjach sieciowych standardowo obiektem najwyższego poziomu jest słownik. Format JSON jest niemal identyczny jak składnia Pythona. Występuje tylko kilka drobnych różnic — np. wartość True to true, False to false, a None to null. Oto przykładowe dane po zakodowaniu: >>> json.dumps(False) 'false' >>> d = {'a': True, ... 'b': 'Hello', ... 'c': None} >>> json.dumps(d) '{"b": "Hello", "c": null, "a": true}' >>>
Podczas sprawdzania danych odkodowanych z formatu JSON często trudno jest ustalić ich strukturę przez samo ich wyświetlenie (zwłaszcza gdy dane zawierają głęboko zagnieżdżone struktury lub wiele pól). Pomóc może funkcja pprint() z modułu pprint. Porządkuje ona klucze w kolejności alfabetycznej i wyświetla słownik w dużo bardziej zrozumiałej postaci. Poniżej pokazano, jak w elegancki sposób wyświetlić wyniki wyszukiwania informacji na Twitterze: >>> from urllib.request import urlopen >>> import json >>> u = urlopen('http://search.twitter.com/search.json?q=python&rpp=5') >>> resp = json.loads(u.read().decode('utf-8')) >>> from pprint import pprint >>> pprint(resp) {'completed_in': 0.074, 'max_id': 264043230692245504, 'max_id_str': '264043230692245504', 'next_page': '?page=2&max_id=264043230692245504&q=python&rpp=5', 'page': 1, 'query': 'python', 'refresh_url': '?since_id=264043230692245504&q=python', 'results': [{'created_at': 'Thu, 01 Nov 2012 16:36:26 +0000', 'from_user': ... },
6.2. Wczytywanie i zapisywanie danych w formacie JSON
01 Nov 2012 16:36:14 +0000', 01 Nov 2012 16:36:13 +0000', 01 Nov 2012 16:36:07 +0000', 01 Nov 2012 16:36:04 +0000',
>>>
W trakcie kodowania do formatu JSON na podstawie podanych danych standardowo tworzone są słowniki i listy. Aby utworzyć obiekt innego rodzaju, należy do funkcji json.loads() przekazać argument object_pairs_hook lub object_hook. Poniżej pokazano, jak odkodować dane z formatu JSON do obiektu OrderedDict, co pozwala zachować ich kolejność: >>> s = '{"name": "ACME", "shares": 50, "price": 490.1}' >>> from collections import OrderedDict >>> data = json.loads(s, object_pairs_hook=OrderedDict) >>> data OrderedDict([('name', 'ACME'), ('shares', 50), ('price', 490.1)]) >>>
Poniższy kod przekształca słownik z formatu JSON na obiekt Pythona: >>> class JSONObject: ... def __init__(self, d): ... self.__dict__ = d ... >>> >>> data = json.loads(s, object_hook=JSONObject) >>> data.name 'ACME' >>> data.shares 50 >>> data.price 490.1 >>>
W tym kodzie słownik utworzony przez odkodowanie danych z formatu JSON jest przekazywany jako jedyny argument do funkcji __init__(). Następnie można używać danych w dowolny sposób (np. bezpośrednio) jako obiektu słownika. Istnieje kilka opcji, które mogą okazać się przydatne w trakcie kodowania danych w formacie JSON. Jeśli chcesz, aby dane były elegancko sformatowane, możesz zastosować argument indent funkcji json.dumps(). Spowoduje to, że dane będą dobrze wyglądać (przyjmą podobny format jak przy stosowaniu funkcji pprint()). Oto przykład: >>> print(json.dumps(data)) {"price": 542.23, "name": "ACME", "shares": 100} >>> print(json.dumps(data, indent=4)) { "price": 542.23, "name": "ACME", "shares": 100 } >>>
172
Rozdział 6. Kodowanie i przetwarzanie danych
Jeśli chcesz posortować klucze w danych wyjściowych, zastosuj argument sort_keys: >>> print(json.dumps(data, sort_keys=True)) {"name": "ACME", "price": 542.23, "shares": 100} >>>
Obiektów zwykle nie można serializować do formatu JSON. Oto przykład: >>> class Point: ... def __init__(self, x, y): ... self.x = x ... self.y = y ... >>> p = Point(2, 3) >>> json.dumps(p) Traceback (most recent call last): File "", line 1, in File "/usr/local/lib/python3.3/json/__init__.py", line 226, in dumps return _default_encoder.encode(obj) File "/usr/local/lib/python3.3/json/encoder.py", line 187, in encode chunks = self.iterencode(o, _one_shot=True) File "/usr/local/lib/python3.3/json/encoder.py", line 245, in iterencode return _iterencode(o, 0) File "/usr/local/lib/python3.3/json/encoder.py", line 169, in default raise TypeError(repr(o) + " is not JSON serializable") TypeError: <__main__.Point object at 0x1006f2650> is not JSON serializable >>>
Jeśli chcesz serializować obiekty, możesz utworzyć funkcję, która przyjmuje obiekt i zwraca możliwy do zserializowania słownik: def serialize_instance(obj): d = { '__classname__' : type(obj).__name__ } d.update(vars(obj)) return d
Na potrzeby odtwarzania obiektów możesz napisać następujący kod: # Słownik odwzorowujący nazwy na znane klasy classes = { 'Point' : Point } def unserialize_object(d): clsname = d.pop('__classname__', None) if clsname: cls = classes[clsname] obj = cls.__new__(cls) # Tworzenie obiektu bez wywołania __init__ for key, value in d.items(): setattr(obj, key, value) return obj else: return d
Oto przykład ilustrujący, jak stosować te funkcje: >>> p = Point(2,3) >>> s = json.dumps(p, default=serialize_instance) >>> s '{"__classname__": "Point", "y": 3, "x": 2}' >>> a = json.loads(s, object_hook=unserialize_object) >>> a <__main__.Point object at 0x1017577d0> >>> a.x 2 >>> a.y 3 >>>
6.2. Wczytywanie i zapisywanie danych w formacie JSON
173
Moduł json udostępnia też wiele innych opcji pozwalających kontrolować niskopoziomowe interpretowanie liczb, wartości specjalnych (takich jak NaN) itd. Więcej informacji znajdziesz w dokumentacji (http://docs.python.org/3/library/json.html).
6.3. Parsowanie prostych danych w XML-u Problem Programista chce pobrać dane z prostego dokumentu XML.
Rozwiązanie Do pobierania danych z prostych dokumentów XML można wykorzystać moduł xml.etree. ElementTree. Załóżmy, że chcesz przeprowadzić parsowanie wiadomości z kanału RSS Planet Python (http://planet.python.org/) i przygotować ich streszczenie. Zadanie to wykonuje poniższy skrypt: from urllib.request import urlopen from xml.etree.ElementTree import parse # Pobieranie wiadomości z kanału RSS i parsowanie ich u = urlopen('http://planet.python.org/rss20.xml') doc = parse(u) # Pobieranie i wyświetlanie interesujących elementów for item in doc.iterfind('channel/item'): title = item.findtext('title') date = item.findtext('pubDate') link = item.findtext('link') print(title) print(date) print(link) print()
Gdy uruchomisz ten skrypt, uzyskasz dane podobne do poniższych: Steve Holden: Python for Data Analysis Mon, 19 Nov 2012 02:13:51 +0000 http://holdenweb.blogspot.com/2012/11/python-for-data-analysis.html Vasudev Ram: The Python Data model (for v2 and v3) Sun, 18 Nov 2012 22:06:47 +0000 http://jugad2.blogspot.com/2012/11/the-python-data-model.html Python Diary: Been playing around with Object Databases Sun, 18 Nov 2012 20:40:29 +0000 http://www.pythondiary.com/blog/Nov.18,2012/been-...-object-databases.html Vasudev Ram: Wakari, Scientific Python in the cloud Sun, 18 Nov 2012 20:19:41 +0000 http://jugad2.blogspot.com/2012/11/wakari-scientific-python-in-cloud.html Jesse Jiryu Davis: Toro: synchronization primitives for Tornado coroutines Sun, 18 Nov 2012 20:17:49 +0000 http://feedproxy.google.com/~r/EmptysquarePython/~3/_DOZT2Kd0hQ/
Omówienie Wiele aplikacji korzysta z danych w formacie XML. Format ten jest popularny nie tylko w zakresie wymiany danych w internecie, ale też jako narzędzie do przechowywania danych aplikacji (np. plików tekstowych lub bibliotek utworów muzycznych). W dalszej części omówienia zakładamy, że znasz podstawy działania formatu XML. Gdy XML służy tylko do przechowywania danych, struktura dokumentu jest często zwięzła i prosta. Np. plik kanału RSS wykorzystanego w przykładzie wygląda tak: Planet Python http://planet.python.org/ enPlanet Python - http://planet.python.org/Steve Holden: Python for Data Analysishttp://holdenweb.blogspot.com/...-data-analysis.html http://holdenweb.blogspot.com/...-data-analysis.html ...Mon, 19 Nov 2012 02:13:51 +0000Vasudev Ram: The Python Data model (for v2 and v3)http://jugad2.blogspot.com/...-data-model.html http://jugad2.blogspot.com/...-data-model.html ...Sun, 18 Nov 2012 22:06:47 +0000Python Diary: Been playing around with Object Databaseshttp://www.pythondiary.com/...-object-databases.html http://www.pythondiary.com/...-object-databases.html ...Sun, 18 Nov 2012 20:40:29 +0000 ...
Funkcja xml.etree.ElementTree.parse() parsuje cały dokument XML i przekształca go na obiekt dokumentu. Następnie można za pomocą metod find(), iterfind(), findtext() i podobnych wyszukiwać konkretne XML-owe elementy. Argumentami tych funkcji są nazwy konkretnych znaczników, np. channel/item lub title. Przy podawaniu znaczników trzeba uwzględnić ogólną strukturę dokumentu. Każda operacja wyszukiwania odbywa się względem początkowego elementu. Także nazwa znacznika podana w każdej operacji jest wyszukiwana względem takiego elementu. W przykładzie wywołanie doc.iterfind('channel/item') wyszukuje elementy item w elemencie channel. Element doc reprezentuje główny element dokumentu (element rss na najwyższym poziomie hierarchii). Późniejsze wywołania item.findtext() są przetwarzane względem znalezionych elementów item.
6.3. Parsowanie prostych danych w XML-u
175
Każdy element reprezentowany przez moduł ElementTree ma kilka ważnych atrybutów i metod przydatnych w trakcie parsowania dokumentu. Atrybut tag zawiera nazwę znacznika, atrybut text obejmuje tekst elementu, a metoda get() pozwala pobrać atrybuty (jeśli istnieją). Oto przykład: >>> doc >>> e = doc.find('channel/title') >>> e >>> e.tag 'title' >>> e.text 'Planet Python' >>> e.get('some_attribute') >>>
Warto zauważyć, że xml.etree.ElementTree nie jest jedynym narzędziem do parsowania XML-owych danych. Do wykonywania bardziej zaawansowanych operacji można zastosować bibliotekę lxml (https://pypi.python.org/pypi/lxml). Ma ona ten sam interfejs programowania co moduł ElementTree, dlatego przykłady przedstawione w tej recepturze będą działać w taki sam sposób. Wystarczy zmienić drugą instrukcję import na lxml.etree import parse. Biblioteka lxml ma tę zaletę, że jest w pełni zgodna ze standardami XML-a. Jest też bardzo szybka i udostępnia różne dodatkowe mechanizmy, np. sprawdzanie poprawności danych oraz obsługę języków XSLT i XPath.
6.4. Stopniowe parsowanie bardzo dużych plików XML Problem Programista chce pobrać dane z bardzo dużego dokumentu XML, zajmując jak najmniej pamięci.
Rozwiązanie Za każdym razem gdy natrafiasz na problem stopniowego przetwarzania danych, powinieneś pomyśleć o iteratorach i generatorach. Oto prosta funkcja, którą można wykorzystać do stopniowego przetwarzania bardzo dużych plików XML przy użyciu niewielkiej ilości pamięci: from xml.etree.ElementTree import iterparse def parse_and_remove(filename, path): path_parts = path.split('/') doc = iterparse(filename, ('start', 'end')) # Pomijanie elementu nadrzędnego next(doc) tag_stack = [] elem_stack = [] for event, elem in doc: if event == 'start': tag_stack.append(elem.tag) elem_stack.append(elem)
Aby sprawdzić działanie tej funkcji, znajdź duży plik XML. Pliki tego rodzaju często można znaleźć w witrynach rządowych i w serwisach z ogólnodostępnymi danymi. Możesz np. pobrać w formacie XML chicagowską bazę danych uszkodzeń dróg (https://data.cityofchicago.org/ Service-Requests/311-Service-Requests-Pot-Holes-Reported/7as2-ds3y). W czasie gdy powstawała ta książka, plik zawierał ponad 100 000 wierszy danych w następującym formacie: 2012-11-18T00:00:00Completed2012-11-18T00:00:0012-01906549Pot Hole in StreetFinal OutcomeCDOT Street Cut ... Outcome4714 S TALMAN AVE606321159494.686188561873313.835033841495841.808090232127896-87.690536847113052012-11-18T00:00:00Completed2012-11-18T00:00:0012-01906695Pot Hole in StreetFinal OutcomeCDOT Street Cut ... Outcome3510 W NORTH AVE606471152732.141276961910409.3897907526142341.91002084292946-87.71435952353961
6.4. Stopniowe parsowanie bardzo dużych plików XML
177
Załóżmy, że chcesz napisać skrypt, który porządkuje kody pocztowe według liczby powiązanych z nimi raportów o uszkodzeniach drogi. Do wykonania tego zadania można spróbować wykorzystać następujący kod: from xml.etree.ElementTree import parse from collections import Counter potholes_by_zip = Counter() doc = parse('potholes.xml') for pothole in doc.iterfind('row/row'): potholes_by_zip[pothole.findtext('zip')] += 1 for zipcode, num in potholes_by_zip.most_common(): print(zipcode, num)
Jedyny problem z tym skryptem polega na tym, że wczytuje do pamięci cały plik XML, aby przeprowadzić jego parsowanie. Na naszym komputerze skrypt ten potrzebował około 450 megabajtów pamięci. Zastosowanie kodu z receptury wymaga tylko niewielkiej zmiany w programie: from collections import Counter potholes_by_zip = Counter() data = parse_and_remove('potholes.xml', 'row/row') for pothole in data: potholes_by_zip[pothole.findtext('zip')] += 1 for zipcode, num in potholes_by_zip.most_common(): print(zipcode, num)
Ta wersja kodu zajmuje tylko około 7 megabajtów pamięci, zysk jest więc bardzo duży!
Omówienie W tej recepturze wykorzystano dwie podstawowe cechy modułu ElementTree. Po pierwsze, metoda iterparse() umożliwia stopniowe przetwarzanie dokumentów XML. Aby ją zastosować, należy podać nazwę pliku, a także listę zdarzeń, która zawiera jedną lub więcej spośród pozycji start, end, start-ns i end-ns. Iterator tworzony przez metodę iterparse() tworzy krotki w postaci (event, elem), gdzie event to jedno z wymienionych zdarzeń, a elem to wynikowy element XML. Oto przykład: >>> data = iterparse('potholes.xml',('start','end')) >>> next(data) ('start', ) >>> next(data) ('start', ) >>> next(data) ('start', ) >>> next(data) ('start', ) >>> next(data) ('end', ) >>> next(data) ('start', ) >>> next(data) ('end', ) >>>
178
Rozdział 6. Kodowanie i przetwarzanie danych
Zdarzenia start zachodzą, gdy element jest tworzony, ale jeszcze nie jest zapełniony żadnymi danymi (np. elementami podrzędnymi). Zdarzenia end mają miejsce, gdy element jest kompletny. Choć w recepturze tego nie pokazano, zdarzenia start-ns i end-ns służą do obsługi deklaracji przestrzeni nazw XML-a. W tej recepturze zdarzenia start i end służą do zarządzania stosem elementów oraz znaczników. Stos ten reprezentuje aktualną hierarchiczną strukturę parsowanego dokumentu i pozwala ustalić, czy dany element pasuje do ścieżki podanej w funkcji parse_end_remove(). Jeśli tak jest, element za pomocą wywołania yield jest zwracany do jednostki wywołującej. Poniższa instrukcja (występująca po wywołaniu yield) to najważniejsza funkcja modułu ElementTree ze względu na zmniejszenie ilości zajmowanej pamięci: elem_stack[-2].remove(elem)
Ta instrukcja powoduje, że wcześniej zwrócone elementy są usuwane z węzła nadrzędnego. Jeśli w innym miejscu nie istnieją żadne referencje do danego elementu, jest on usuwany i można przywrócić pamięć. Stopniowe parsowanie i usuwanie węzłów prowadzi do bardzo wydajnego stopniowego przejścia przez dokument. W żadnym momencie nie powstaje kompletne drzewo dokumentu. Mimo to można napisać kod, który w prosty sposób przetwarza XML-owe dane. Główną wadą tej receptury jest czas jej działania. Wersja kodu wczytująca najpierw do pamięci cały dokument działa mniej więcej dwukrotnie szybciej niż wersja przetwarzająca dane stopniowo. Zajmuje jednak ponad 60 razy więcej pamięci, dlatego jeśli zużycie pamięci ma znaczenie, wersja działająca stopniowo zapewnia znaczne korzyści.
6.5. Przekształcanie słowników na format XML Problem Programista chce przekształcić dane ze słownika Pythona na format XML.
Rozwiązanie Choć biblioteka xml.etree.ElementTree przeważnie służy do parsowania danych, można ją wykorzystać także do tworzenia dokumentów XML. Przyjrzyj się następującej funkcji: from xml.etree.ElementTree import Element def dict_to_xml(tag, d): ''' Przekształcanie prostego słownika par klucz – wartość na format XML ''' elem = Element(tag) for key, val in d.items(): child = Element(key) child.text = str(val) elem.append(child) return elem
6.5. Przekształcanie słowników na format XML
179
Oto przykład: >>> s = { 'name': 'GOOG', 'shares': 100, 'price':490.1 } >>> e = dict_to_xml('stock', s) >>> e >>>
Efektem przekształcania jest obiekt typu Element. W operacjach wejścia-wyjścia można łatwo przekształcić go na łańcuch bajtów. Służy do tego funkcja tostring() modułu xml. etree.ElementTree: >>> from xml.etree.ElementTree import tostring >>> tostring(e) b'490.1100GOOG' >>>
Jeśli chcesz do elementu dołączyć atrybuty, zastosuj metodę set(): >>> e.set('_id','1234') >>> tostring(e) b'490.1100GOOG' >>>
Jeżeli kolejność elementów ma znaczenie, pomyśl o utworzeniu obiektu OrderedDict zamiast zwykłego słownika (zobacz recepturę 1.7).
Omówienie Przy tworzeniu dokumentu XML możesz stwierdzić, że utworzysz zwykłe łańcuchy znaków. Oto przykład: def dict_to_xml_str(tag, d): ''' Przekształcanie prostego słownika par klucz – wartość na format XML ''' parts = ['<{}>'.format(tag)] for key, val in d.items(): parts.append('<{0}>{1}{0}>'.format(key,val)) parts.append('{}>'.format(tag)) return ''.join(parts)
Problem polega na tym, że próba ręcznego przetwarzania danych bardzo utrudnia pracę. Co się stanie, gdy w wartościach słownika znajdą się znaki specjalne takie jak poniżej? >>> d = { 'name' : '' } >>> # Tworzenie łańcucha znaków >>> dict_to_xml_str('item',d) '' >>> # Poprawne tworzenie XML-owych danych >>> e = dict_to_xml('item',d) >>> tostring(e) b'' >>>
Zauważ, że w drugim fragmencie znaki < i > są zastępowane kodami < i >.
180
Rozdział 6. Kodowanie i przetwarzanie danych
Warto wiedzieć, że jeśli znaki specjalne trzeba ręcznie zastąpić kodami lub na odwrót, można wywołać funkcje escape() i unescape() modułu xml.sax.saxutils. Oto przykład: >>> from xml.sax.saxutils import escape, unescape >>> escape('') '' >>> unescape(_) '' >>>
Stosowanie obiektów typu Element zamiast łańcuchów znaków pozwala nie tylko tworzyć poprawne dane wyjściowe. Łatwiej jest też łączyć je w większe dokumenty. Uzyskane obiekty typu Element można też przetwarzać na różne sposoby bez konieczności parsowania tekstu w formacie XML. Dzięki temu można przetwarzać dane na wyższym poziomie, a na końcu wyświetlić je jako łańcuchy znaków.
6.6. Parsowanie, modyfikowanie i ponowne zapisywanie dokumentów XML Problem Programista chce wczytać dokument XML, wprowadzić w nim zmiany, a następnie ponownie zapisać w formacie XML.
Rozwiązanie Wykonywanie takich zadań w łatwy sposób umożliwia moduł xml.etree.ElementTree. Należy zacząć od parsowania dokumentu w standardowy sposób. Załóżmy, że używany jest dokument pred.xml o następującej zawartości: 14791Clark &BalmoralNorth Bound
North Bound
22
5 MINHoward137822
15 MINHoward186722
6.6. Parsowanie, modyfikowanie i ponowne zapisywanie dokumentów XML
181
Oto przykład ilustrujący, jak za pomocą modułu ElementTree wczytać taki dokument i wprowadzić zmiany w jego strukturze: >>> from xml.etree.ElementTree import parse, Element >>> doc = parse('pred.xml') >>> root = doc.getroot() >>> root >>> # Usuwanie kilku elementów >>> root.remove(root.find('sri')) >>> root.remove(root.find('cr')) >>> >>> 1 >>> >>> >>>
# Wstawianie nowego elementu po ... root.getchildren().index(root.find('nm')) e = Element('spam') e.text = 'To test' root.insert(2, e)
>>> # Zapis danych z powrotem do pliku >>> doc.write('newpred.xml', xml_declaration=True) >>>
W wyniku wykonania tych operacji powstaje nowy plik XML: 14791Clark &BalmoralThis is a test
5 MINHoward137822
15 MINHoward186722
Omówienie Modyfikowanie struktury dokumentu XML jest proste, trzeba jednak pamiętać, że wszystkie zmiany są zwykle wprowadzane w elemencie nadrzędnym traktowanym jak lista. Aby na przykład skasować element, trzeba usunąć go z jego bezpośredniego węzła nadrzędnego, używając metody remove() tego węzła. Przy wstawianiu lub dołączaniu nowych elementów należy wywołać metodę insert() lub append() węzła nadrzędnego. Elementami można też manipulować za pomocą indeksów i wycinków, np. element[i] lub element[i:j]. Do tworzenia nowych elementów należy wykorzystać klasę Element w sposób pokazany w rozwiązaniu z tej receptury. Zagadnienie to opisano dokładnie w recepturze 6.5.
182
Rozdział 6. Kodowanie i przetwarzanie danych
6.7. Parsowanie dokumentów XML z przestrzeniami nazw Problem Programista chce parsować dokument XML, w którym jednak używane są XML-owe przestrzenie nazw.
Rozwiązanie Przyjrzyj się dokumentowi, w którym występują przestrzenie nazw: David BeazleyHello World
Hello World!
Gdy w trakcie parsowania dokumentu zechcesz uruchomić standardowe zapytania, okaże się, że nie jest to takie proste, ponieważ kod staje się bardzo rozwlekły: >>> # Kilka działających zapytań >>> doc.findtext('author') 'David Beazley' >>> doc.find('content') >>> # Zapytanie z przestrzenią nazw (nie działa) >>> doc.find('content/html') >>> # Zadziała, gdy podasz pełną nazwę >>> doc.find('content/{http://www.w3.org/1999/xhtml}html') >>> # Nie działa >>> doc.findtext('content/{http://www.w3.org/1999/xhtml}html/head/title') >>> # Z pełną nazwą >>> doc.findtext('content/{http://www.w3.org/1999/xhtml}html/' ... '{http://www.w3.org/1999/xhtml}head/{http://www.w3.org/1999/xhtml}title') 'Hello World' >>>
6.7. Parsowanie dokumentów XML z przestrzeniami nazw
183
Często można uprościć kod, umieszczając obsługę przestrzeni nazw w klasie narzędziowej: class XMLNamespaces: def __init__(self, **kwargs): self.namespaces = {} for name, uri in kwargs.items(): self.register(name, uri) def register(self, name, uri): self.namespaces[name] = '{'+uri+'}' def __call__(self, path): return path.format_map(self.namespaces)
Klasę tę można wykorzystać w następujący sposób: >>> ns = XMLNamespaces(html='http://www.w3.org/1999/xhtml') >>> doc.find(ns('content/{html}html')) >>> doc.findtext(ns('content/{html}html/{html}head/{html}title')) 'Hello World' >>>
Omówienie Parsowanie dokumentów XML z przestrzeniami nazw może być skomplikowane. Klasa XMLNamespaces pozwala tylko w pewnym stopniu uporządkować kod, ponieważ umożliwia stosowanie w operacjach skróconych określeń przestrzeni nazw zamiast pełnych identyfikatorów URI. Niestety, podstawowy parser z modułu ElementTree nie udostępnia mechanizmu do pobierania informacji na temat przestrzeni nazw. Jednak za pomocą funkcji iterparse() można pobrać pewne dane na temat zasięgu danej przestrzeni. Oto przykład: >>> from xml.etree.ElementTree import iterparse >>> for evt, elem in iterparse('ns2.xml', ('end', 'start-ns', 'end-ns')): ... print(evt, elem) ... end start-ns ('', 'http://www.w3.org/1999/xhtml') end end end end end end-ns None end end >>> elem # To element nadrzędny >>>
Na koniec uwaga — jeśli w parsowanym tekście występują przestrzenie nazw oprócz innych zaawansowanych mechanizmów XML-a, lepiej jest zastosować bibliotekę lxml (http://lxml.de/) zamiast modułu ElementTree. Biblioteka ta zapewnia lepsze sprawdzanie poprawności dokumentów na podstawie specyfikacji DTD, kompletniejszą obsługę języka XPath i inne zaawansowane funkcje XML-a. Przedstawiona tu receptura to tylko proste rozwiązanie, dzięki któremu parsowanie jest łatwiejsze.
184
Rozdział 6. Kodowanie i przetwarzanie danych
6.8. Komunikowanie się z relacyjnymi bazami danych Problem Programista chce pobierać, wstawiać lub usuwać wiersze w relacyjnej bazie danych.
Rozwiązanie Standardowym sposobem na przedstawianie wierszy danych w Pythonie jest użycie sekwencji krotek. Oto przykład: stocks = [ ('GOOG', 100, 490.1), ('AAPL', 50, 545.75), ('FB', 150, 7.45), ('HPQ', 75, 33.2), ]
Dane w tej postaci umożliwiają stosunkowo łatwe komunikowanie się z relacyjną bazą danych z wykorzystaniem dostępnego w Pythonie standardowego interfejsu API dla baz, opisanego w dokumencie PEP 249 (http://www.python.org/dev/peps/pep-0249/). Najważniejszą cechą tego interfejsu jest to, że wszystkie operacje na bazie danych są wykonywane za pomocą zapytań SQL-a. Każdy wiersz danych wejściowych lub wyjściowych jest reprezentowany przez krotkę. Aby zobaczyć, jak działa to podejście, można zastosować moduł sqlite3 Pythona. Jeśli korzystasz z innej bazy danych (takiej jak MySQL, Postgres lub ODBC), musisz zainstalować niezależny moduł. Jednak udostępniany przez niego interfejs programowania będzie identyczny lub bardzo podobny. Pierwszy krok polega na nawiązaniu połączenia z bazą danych. Zwykle należy wywołać funkcję connect() i przekazać parametry — nazwę bazy danych, nazwę serwera, nazwę użytkownika, hasło i w razie potrzeby inne dane. Oto przykład: >>> import sqlite3 >>> db = sqlite3.connect('database.db') >>>
Aby wykonać na danych jakieś operacje, trzeba utworzyć kursor. Po przygotowaniu kursora można zacząć wykonywanie zapytań SQL-a: >>> c = db.cursor() >>> c.execute('create table portfolio (symbol text, shares integer, price real)') >>> db.commit() >>>
W celu wstawienia do bazy sekwencji wierszy należy wywołać następujące polecenie: >>> c.executemany('insert into portfolio values (?,?,?)', stocks) >>> db.commit() >>>
6.8. Komunikowanie się z relacyjnymi bazami danych
185
Do wykonywania zapytań służą polecenia podobne do poniższego: >>> for row in db.execute('select * from portfolio'): ... print(row) ... ('GOOG', 100, 490.1) ('AAPL', 50, 545.75) ('FB', 150, 7.45) ('HPQ', 75, 33.2) >>>
Jeśli chcesz wykonywać zapytania przyjmujące parametry wejściowe od użytkowników, koniecznie poprzedź parametry znakiem ?: >>> min_price = 100 >>> for row in db.execute('select * from portfolio where price >= ?', (min_price,)): ... print(row) ... ('GOOG', 100, 490.1) ('AAPL', 50, 545.75) >>>
Omówienie Na niskim poziomie interakcja z bazą danych jest niezwykle łatwa. Wystarczy utworzyć polecenia SQL-a i przekazać je do odpowiedniego modułu, aby zaktualizować bazę lub pobrać dane. Występują jednak pewne skomplikowane przypadki, które wymagają specjalnych rozwiązań. Jedną z trudności jest odwzorowywanie danych z bazy na typy Pythona. Dla dat najczęściej stosuje się obiekty typu datetime z modułu datetime lub systemowe znaczniki czasu używane w module time. Liczby (zwłaszcza z danych finansowych, obejmujące części dziesiętne) można przedstawić jako obiekty typu Decimal z modułu decimal. Niestety, dokładne odwzorowania zależą od bazy używanej na zapleczu, dlatego należy zapoznać się z jej dokumentacją. Inny bardzo ważny problem dotyczy tworzenia łańcuchów znaków z poleceniami SQL-a. Do budowania takich łańcuchów nigdy nie należy stosować operatorów formatowania z Pythona (np. %) ani metody .format(). Jeśli wartości przekazane do operatorów formatowania pochodzą od użytkowników, program jest narażony na ataki przez wstrzyknięcie kodu w SQL-u (zobacz stronę http://xkcd.com/327). Specjalny symbol wieloznaczny ? w zapytaniach to informacja dla używanej na zapleczu bazy, że ma zastosować własny mechanizm podstawiania łańcuchów znaków, który (miejmy nadzieję) potrafi bezpiecznie wykonać tę operację. Niestety, bazy używane na zapleczu w niespójny sposób obsługują symbole wieloznaczne. W wielu modułach używane są symbole ? lub %s, natomiast w innych parametry są wskazywane za pomocą odmiennych znaków, takich jak :0 lub :1. Aby to ustalić, trzeba sprawdzić dokumentację modułu odpowiedniego dla używanej bazy. Atrybut paramstyle takich modułów zawiera informacje na temat sposobu podawania parametrów. Interfejs API bazy danych pozwala zwykle na łatwe przesyłanie danych do bazy i pobieranie ich z niej. Jeśli chcesz wykonywać bardziej skomplikowane zadania, warto zastosować interfejs wyższego poziomu, udostępniany np. w mapperze obiektowo-relacyjnym. SQLAlchemy (http://www.sqlalchemy.org/) i podobne biblioteki umożliwiają przedstawianie tabel bazy danych jako klas Pythona oraz przeprowadzanie operacji na bazie bez stosowania kodu w SQL-u.
186
Rozdział 6. Kodowanie i przetwarzanie danych
6.9. Dekodowanie i kodowanie cyfr w systemie szesnastkowym Problem Programista chce odkodować łańcuch cyfr szesnastkowych i przekształcić go na łańcuch bajtów lub zakodować łańcuch bajtów jako łańcuch cyfr szesnastkowych.
Rozwiązanie Jeśli chcesz tylko odkodować lub zakodować nieprzetworzony łańcuch cyfr szesnastkowych, zastosuj moduł binascii: >>> # Początkowy łańcuch bajtów >>> s = b'hello' >>> # Kodowanie w systemie szesnastkowym >>> import binascii >>> h = binascii.b2a_hex(s) >>> h b'68656c6c6f' >>> # Dekodowanie z powrotem na bajty >>> binascii.a2b_hex(h) b'hello' >>>
Podobne możliwości daje moduł base64. Oto przykład: >>> import base64 >>> h = base64.b16encode(s) >>> h b'68656C6C6F' >>> base64.b16decode(h) b'hello' >>>
Omówienie Przekształcanie danych na system szesnastkowy i odwrotnie za pomocą przedstawionych funkcji jest zwykle proste. Główną różnicą między pokazanymi technikami jest obsługa wielkości znaków. Funkcje base64.b16decode() i base64.b16encode() działają tylko dla dużych liter przedstawionych w systemie szesnastkowym, natomiast funkcje z modułu binascii obsługują zarówno duże, jak i małe litery. Warto też zauważyć, że dane wyjściowe funkcji kodujących to zawsze łańcuch bajtów. Aby przekształcić go na format Unicode w celu wyświetlenia, trzeba zastosować dodatkową operację dekodowania: >>> h = base64.b16encode(s) >>> print(h) b'68656C6C6F' >>> print(h.decode('ascii')) 68656C6C6F >>>
Przy dekodowaniu cyfr szesnastkowych funkcje b16decode() i a2b_hex() przyjmują łańcuchy bajtów i znaków w formacie Unicode. Łańcuchy te mogą jednak zawierać tylko cyfry szesnastkowe zakodowane w formacie ASCII. 6.9. Dekodowanie i kodowanie cyfr w systemie szesnastkowym
187
6.10. Dekodowanie i kodowanie wartości w formacie Base64 Problem Programista chce zakodować lub odkodować dane binarne w formacie Base64.
Rozwiązanie Moduł base64 udostępnia dwie funkcje (b64encode() i b64decode()) wykonujące potrzebne zadania. Oto przykład: >>> # Dane bajtowe >>> s = b'hello' >>> import base64 >>> # Kodowanie do formatu Base64 >>> a = base64.b64encode(s) >>> a b'aGVsbG8=' >>> # Dekodowanie z formatu Base64 >>> base64.b64decode(a) b'hello' >>>
Omówienie Kodowanie Base64 jest przeznaczone tylko dla danych bajtowych, np. dla łańcuchów i tablic bajtów. Ponadto dane wyjściowe w procesie kodowania to zawsze łańcuch bajtów. Jeśli chcesz łączyć dane w formacie Base64 z tekstem w formacie Unicode, powinieneś wykonać dodatkowy etap dekodowania. Oto przykład: >>> a = base64.b64encode(s).decode('ascii') >>> a 'aGVsbG8=' >>>
Przy dekodowaniu danych w formacie Base64 można podawać łańcuchy bajtów i łańcuchy znaków w formacie Unicode, przy czym te ostatnie mogą zawierać tylko znaki ASCII.
6.11. Odczyt i zapis tablic binarnych zawierających struktury Problem Programista chce wczytywać i zapisywać do krotek Pythona dane zakodowane jako tablica binarna z jednorodnymi strukturami.
188
Rozdział 6. Kodowanie i przetwarzanie danych
Rozwiązanie Do pracy z danymi binarnymi służy moduł struct. Oto przykładowy kod, który zapisuje listę krotek Pythona do pliku binarnego i koduje każdą krotkę jako strukturę, używając polecenia struct: from struct import Struct def write_records(records, format, f): ''' Zapisywanie sekwencji krotek do pliku binarnego ze strukturami. ''' record_struct = Struct(format) for r in records: f.write(record_struct.pack(*r)) # Przykład if __name__ == '__main__': records = [ (1, 2.3, 4.5), (6, 7.8, 9.0), (12, 13.4, 56.7) ] with open('data.b', 'wb') as f: write_records(records, '
Wczytać taki plik z powrotem do listy krotek można na kilka sposobów. Jeśli chcesz wczytywać plik stopniowo (w porcjach), możesz napisać następujący kod: from struct import Struct def read_records(format, f): record_struct = Struct(format) chunks = iter(lambda: f.read(record_struct.size), b'') return (record_struct.unpack(chunk) for chunk in chunks) # Przykład if __name__ == '__main__': with open('data.b','rb') as f: for rec in read_records('
Jeśli chcesz wczytać cały plik do łańcucha bajtów w jednej operacji odczytu i przekształcić dane fragment po fragmencie, możesz zastosować następujący kod: from struct import Struct def unpack_records(format, data): record_struct = Struct(format) return (record_struct.unpack_from(data, offset) for offset in range(0, len(data), record_struct.size)) # Przykład if __name__ == '__main__': with open('data.b', 'rb') as f: data = f.read() for rec in unpack_records('
W obu sytuacjach powstaje obiekt iterowalny, który zwraca krotki zapisane w pliku w momencie jego tworzenia. 6.11. Odczyt i zapis tablic binarnych zawierających struktury
189
Omówienie W programach, które muszą kodować i dekodować dane binarne, często wykorzystuje się moduł struct. Aby zadeklarować nową strukturę, wystarczy utworzyć obiekt typu Struct: # 32-bitowa liczba całkowita z rosnącym porządkiem bitów i dwie liczby zmiennoprzecinkowe podwójnej precyzji record_struct = Struct('
Struktury zawsze definiuje się za pomocą kodów (i, d, f itd.; więcej informacji znajdziesz w dokumentacji Pythona — http://docs.python.org/3/library/struct.html). Kody te odpowiadają określonym binarnym typom danych — 32-bitowym liczbom całkowitym, 64-bitowym liczbom zmiennoprzecinkowym, 32-bitowym liczbom zmiennoprzecinkowym itd. Początkowy znak < określa porządek bitów. Tu zastosowano porządek rosnący. Znak > oznacza porządek malejący, a symbol ! odpowiada porządkowi sieciowemu. Wynikowy obiekt typu Struct ma różne atrybuty i metody przeznaczone do manipulowania strukturami danego rodzaju. Atrybut size zawiera rozmiar struktury w bajtach. Jest to przydatne przy wykonywaniu operacji wejścia-wyjścia. Metody pack() i unpack() służą do pakowania i wypakowywania danych. Oto przykład: >>> from struct import Struct >>> record_struct = Struct('>> record_struct.size 20 >>> record_struct.pack(1, 2.0, 3.0) b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08@' >>> record_struct.unpack(_) (1, 2.0, 3.0) >>>
Czasem operacje pack() i unpack() są wywoływane jako funkcje z poziomu modułu: >>> import struct >>> struct.pack('>> struct.unpack('>>
To rozwiązanie działa, ale jest mniej eleganckie niż tworzenie jednego obiektu typu Struct (zwłaszcza gdy dana struktura występuje w wielu miejscach kodu). Przy tworzeniu obiektu typu Struct kod formatujący wystarczy podać raz, a wszystkie przydatne operacje zostaną wygodnie zgrupowane. Ułatwia to konserwację kodu, jeśli trzeba zmodyfikować strukturę (wtedy zmianę wystarczy wprowadzić w jednym miejscu). Kod do wczytywania struktur binarnych obejmuje liczne ciekawe, a przy tym eleganckie idiomy programowania. W funkcji read_records() za pomocą wywołania iter() tworzony jest iterator, który zwraca porcje danych o stałej wielkości (zobacz recepturę 5.8). Iterator ten wielokrotnie wywołuje podaną przez użytkownika jednostkę wywoływalną (np. lambda: f.read(record_struct.size)) do momentu zwrócenia przez nią określonej wartości (np. b). Wtedy iterowanie zostaje zakończone. Oto przykład: >>> f = open('data.b', 'rb') >>> chunks = iter(lambda: f.read(20), b'') >>> chunks >>> for chk in chunks: ... print(chk) ...
Jednym z powodów tworzenia obiektów iterowalnych jest to, że pozwalają one na tworzenie rekordów za pomocą wyrażeń z generatorem (tak jak w tym rozwiązaniu). Kod bez tej techniki wyglądałby tak: def read_records(format, f): record_struct = Struct(format) while True: chk = f.read(record_struct.size) if chk == b'': break yield record_struct.unpack(chk) return records
W funkcji unpack_records() wykorzystano inne podejście, oparte na metodzie unpack_from(). Jest to przydatna metoda do pobierania danych binarnych z dużych tablic binarnych. Metoda ta robi to zarówno bez tworzenia obiektów tymczasowych, jak i bez kopiowania danych w pamięci. Wystarczy podać łańcuch bajtów (lub dowolną tablicę) wraz z pozycją, a metoda pobierze pola bezpośrednio z tego miejsca. Gdyby zamiast metody unpack_from() zastosować metodę unpack(), trzeba by było zmodyfikować kod i pobierać wiele wycinków oraz obliczać pozycje. Oto przykład: def unpack_records(format, data): record_struct = Struct(format) return (record_struct.unpack(data[offset:offset + record_struct.size]) for offset in range(0, len(data), record_struct.size))
Wersja ta jest nie tylko mniej czytelna, ale też wymaga znacznie więcej pracy — obliczania pozycji, kopiowania danych i tworzenia małych wycinków. Jeśli chcesz wypakowywać dużą liczbę struktur z długiego i wczytanego już łańcucha bajtów, bardziej eleganckim rozwiązaniem jest zastosowanie metody unpack_from(). Wypakowywanie rekordów to jedna z sytuacji, w których można wykorzystać obiekty typu namedtuple z modułu collections. Pozwala to ustawić nazwy atrybutów w zwracanej krotce: from collections import namedtuple Record = namedtuple('Record', ['kind','x','y']) with open('data.p', 'rb') as f: records = (Record(*r) for r in read_records('
Jeśli piszesz program, który manipuluje dużą ilością danych binarnych, czasem lepiej jest zastosować bibliotekę, np. numpy. Zamiast wczytywać dane binarne na listę krotek, można wczytać je do ustrukturyzowanej tablicy, tak jak poniżej: >>> import numpy as np >>> f = open('data.b', 'rb') >>> records = np.fromfile(f, dtype='>> records array([(1, 2.3, 4.5), (6, 7.8, 9.0), (12, 13.4, 56.7)], dtype=[('f0', '>> records[0]
6.11. Odczyt i zapis tablic binarnych zawierających struktury
191
(1, 2.3, 4.5) >>> records[1] (6, 7.8, 9.0) >>>
Ponadto jeśli chcesz wczytywać dane binarne z plików o znanym formacie (np. plików graficznych, shape, GDF5 itd.), sprawdź, czy nie istnieje przeznaczony do tego moduł Pythona. Nie ma sensu wymyślać od nowa istniejących rozwiązań.
6.12. Wczytywanie zagnieżdżonych struktur binarnych o zmiennej długości Problem Programista chce wczytać skomplikowane dane binarne, które zawierają kolekcję rekordów zagnieżdżonych i (lub) o zmiennej długości. Takie dane mogą zawierać grafikę, filmy wideo, pliki shape itd.
Rozwiązanie Moduł struct można wykorzystać do odkodowania i zakodowania niemal dowolnej struktury z danych binarnych. Oto przykład ilustrujący, jakich danych dotyczy ta receptura. Załóżmy, że istnieje struktura danych Pythona reprezentująca kolekcję punktów tworzących różne wielokąty: polys = [ [ (1.0, 2.5), (3.5, 4.0), (2.5, 1.5) ], [ (7.0, 1.2), (5.1, 3.0), (0.5, 7.5), (0.8, 9.0) ], [ (3.4, 6.3), (1.2, 0.5), (4.6, 9.2) ], ]
Teraz załóżmy, że dane należy zapisać w pliku binarnym, który zaczyna się od następującego nagłówka: Bajt
Typ
Opis
0
int
Kod pliku (0x1234, rosnący porządek bitów)
4
double
Minimalna wartość x (rosnący porządek bitów)
12
double
Minimalna wartość y (rosnący porządek bitów)
20
double
Maksymalna wartość x (rosnący porządek bitów)
28
double
Maksymalna wartość y (rosnący porządek bitów)
36
int
Liczba wielokątów (rosnący porządek bitów)
Po nagłówku znajduje się zbiór rekordów reprezentujących wielokąty. Rekordy zakodowane są w następujący sposób: Bajt
Typ
Opis
0
int
Długość rekordu włącznie z wartością określającą długość (N bajtów)
4-N
Punkty
Pary współrzędnych (X, Y) w postaci liczb zmiennoprzecinkowych podwójnej precyzji
192
Rozdział 6. Kodowanie i przetwarzanie danych
Do zapisania takiego pliku można wykorzystać następujący kod w Pythonie: import struct import itertools def write_polys(filename, polys): # Określanie pola ograniczającego wielokąt flattened = list(itertools.chain(*polys)) min_x = min(x for x, y in flattened) max_x = max(x for x, y in flattened) min_y = min(y for x, y in flattened) max_y = max(y for x, y in flattened) with open(filename, 'wb') as f: f.write(struct.pack('
Aby ponownie wczytać zapisane dane, można wykorzystać bardzo podobny kod oparty na funkcji struct.unpack(). W kodzie tym należy odwrócić kolejność operacji wykonywanych w trakcie zapisu danych: import struct def read_polys(filename): with open(filename, 'rb') as f: # Wczytywanie nagłówka header = f.read(40) file_code, min_x, min_y, max_x, max_y, num_polys =\ struct.unpack('
Choć ten kod działa, jest stosunkowo skomplikowanym zbitkiem odczytów, wypakowywania struktur i podobnych operacji. Jeśli kod tego rodzaju służy do przetwarzania rzeczywistych plików z danymi, może być jeszcze bardziej złożony. Dlatego warto poszukać innego rozwiązania, które pozwoli programiście uprościć niektóre kroki i skoncentrować się na ważniejszych zadaniach. W dalszej części tej receptury zobaczysz, jak stopniowo opracowywać zaawansowany kod do interpretowania danych binarnych. Kod ten umożliwia programiście określenie wysokopoziomowej specyfikacji formatu pliku i na zapleczu odpowiada za szczegóły związane z odczytem i wypakowywaniem wszystkich danych. Warto zauważyć, że jest to jeden z najbardziej
6.12. Wczytywanie zagnieżdżonych struktur binarnych o zmiennej długości
193
zaawansowanych przykładów w całej książce. Wykorzystano tu różne techniki programowania obiektowego i metaprogramowania. Koniecznie przeczytaj omówienie, a także zapoznaj się z powiązanymi recepturami. Przy wczytywaniu danych binarnych pliki często zawierają nagłówki i inne struktury danych. Choć moduł struct potrafi wypakować takie dane do krotki, można też zapisać je za pomocą klasy. Oto kod, który to umożliwia: import struct class StructField: ''' Deskryptor reprezentujący pole prostej struktury ''' def __init__(self, format, offset): self.format = format self.offset = offset def __get__(self, instance, cls): if instance is None: return self else: r = struct.unpack_from(self.format, instance._buffer, self.offset) return r[0] if len(r) == 1 else r class Structure: def __init__(self, bytedata): self._buffer = memoryview(bytedata)
W tym kodzie deskryptor reprezentuje każde pole struktury. Każdy deskryptor obejmuje kod formatujący zgodny z obiektami typu struct oraz pozycję bajta w powiązanym buforze pamięci. W metodzie __get__() wykorzystano funkcję struct.unpack_from() do pobrania wartości z bufora bez konieczności tworzenia dodatkowych wycinków lub kopii. Klasa Structure to klasa bazowa przyjmująca dane bajtowe i zapisująca je w powiązanym buforze pamięci używanym w deskryptorze StructField. Przeznaczenie wywołania memoryview() w tej klasie stanie się zrozumiałe później. Za pomocą przedstawionego kodu można zdefiniować strukturę jako klasę wysokiego poziomu, która jest zgodna ze specyfikacją z przedstawionych wcześniej tabel opisujących oczekiwany format pliku. Oto przykład: class PolyHeader(Structure): file_code = StructField('
Poniżej pokazano, jak za pomocą tej klasy wczytać nagłówek z zapisanych wcześniej informacji o wielokącie: >>> f = open('polys.bin', 'rb') >>> phead = PolyHeader(f.read(40)) >>> phead.file_code == 0x1234 True >>> phead.min_x 0.5 >>> phead.min_y 0.5
Jest to ciekawe rozwiązanie, jednak ma kilka irytujących aspektów. Choć otrzymujemy wygodny interfejs klasy, kod jest stosunkowo rozwlekły i wymaga od użytkownika podawania wielu niskopoziomowych szczegółów (np. wielokrotnego używania polecenia StructField, podawania pozycji itd.). W klasie nie ma też standardowych udogodnień. Nie można np. obliczyć łącznej wielkości struktury. Gdy natrafisz na klasę, której definiowanie wymaga zbyt wiele kodu, pomyśl o zastosowaniu dekoratora lub metaklasy. Metaklasy można wykorzystać między innymi do wykonywania wielu niskopoziomowych zadań, dzięki czemu użytkownicy nie muszą się nimi zajmować. Przyjrzyj się poniższej przykładowej metaklasie i nieco zmodyfikowanej wersji klasy Structure: class StructureMeta(type): ''' Metaklasa automatycznie tworząca deskryptory StructField ''' def __init__(self, clsname, bases, clsdict): fields = getattr(self, '_fields_', []) byte_order = '' offset = 0 for format, fieldname in fields: if format.startswith(('<','>','!','@')): byte_order = format[0] format = format[1:] format = byte_order + format setattr(self, fieldname, StructField(format, offset)) offset += struct.calcsize(format) setattr(self, 'struct_size', offset) class Structure(metaclass=StructureMeta): def __init__(self, bytedata): self._buffer = bytedata @classmethod def from_file(cls, f): return cls(f.read(cls.struct_size))
Za pomocą nowej klasy Structure można zdefiniować strukturę w następujący sposób: class PolyHeader(Structure): _fields_ = [ ('
Jak widać, tym razem specyfikacja jest znacznie zwięźlejsza. Nowa metoda from_file() klasy ułatwia odczytywanie i zapisywanie danych w plikach, przy czym nie trzeba znać wielkości ani struktury tych danych. Oto przykład:
6.12. Wczytywanie zagnieżdżonych struktur binarnych o zmiennej długości
Po dodaniu metaklasy można wbudować w nią inteligentne mechanizmy. Załóżmy, że chcesz dodać obsługę zagnieżdżonych struktur binarnych. Oto nowa wersja metaklasy z nowym deskryptorem, który umożliwia obsługę takich struktur: class NestedStruct: ''' Deskryptor reprezentujący strukturę zagnieżdżoną ''' def __init__(self, name, struct_type, offset): self.name = name self.struct_type = struct_type self.offset = offset def __get__(self, instance, cls): if instance is None: return self else: data = instance._buffer[self.offset: self.offset+self.struct_type.struct_size] result = self.struct_type(data) # Zapisywanie uzyskanej struktury ponownie w obiekcie, aby # uniknąć dalszych obliczeń na tym etapie setattr(instance, self.name, result) return result class StructureMeta(type): ''' Metaklasa automatycznie tworząca deskryptory StructField ''' def __init__(self, clsname, bases, clsdict): fields = getattr(self, '_fields_', []) byte_order = '' offset = 0 for format, fieldname in fields: if isinstance(format, StructureMeta): setattr(self, fieldname, NestedStruct(fieldname, format, offset)) offset += format.struct_size else: if format.startswith(('<','>','!','@')): byte_order = format[0] format = format[1:] format = byte_order + format setattr(self, fieldname, StructField(format, offset)) offset += struct.calcsize(format) setattr(self, 'struct_size', offset)
196
Rozdział 6. Kodowanie i przetwarzanie danych
W tym kodzie deskryptor NestedStruct pozwala powiązać definicję struktury z obszarem pamięci. W tym celu należy z pamięci pobrać wycinek pierwotnego bufora i wykorzystać go do utworzenia obiektu o typie danej struktury. Ponieważ używany bufor pamięci jest tworzony jako widok pamięci, utworzenie wycinka nie powoduje umieszczenia jego kopii w pamięci. Wycinek jest tu tylko nakładką powiązaną z pierwotną pamięcią. Ponadto, aby uniknąć wielokrotnego tworzenia obiektów, deskryptor zapisuje uzyskany wewnętrzny obiekt struktury za pomocą techniki opisanej w recepturze 8.10. Nowa wersja rozwiązania pozwala napisać następujący kod: class Point(Structure): _fields_ = [ ('
Zaskakujące jest to, że kod nadal działa w oczekiwany sposób: >>> f = open('polys.bin', 'rb') >>> phead = PolyHeader.from_file(f) >>> phead.file_code == 0x1234 True >>> phead.min # Struktura zagnieżdżona <__main__.Point object at 0x1006a48d0> >>> phead.min.x 0.5 >>> phead.min.y 0.5 >>> phead.max.x 7.0 >>> phead.max.y 9.2 >>> phead.num_polys 3 >>>
Na tym etapie platforma do obsługi rekordów o stałej długości jest już gotowa, co jednak z komponentami o zmiennym rozmiarze? Np. dalsza część pliku z wielokątami zawiera dane o zmiennej wielkości. Jednym ze sposobów rozwiązania problemu jest napisanie klasy, która reprezentuje porcję danych binarnych, oraz funkcji narzędziowej odpowiednio interpretującej zawartość obiektów tej klasy. Pomysł ten jest ściśle powiązany z kodem z receptury 6.11: class SizedRecord: def __init__(self, bytedata): self._buffer = memoryview(bytedata) @classmethod def from_file(cls, f, size_fmt, includes_size=True): sz_nbytes = struct.calcsize(size_fmt) sz_bytes = f.read(sz_nbytes) sz, = struct.unpack(size_fmt, sz_bytes) buf = f.read(sz - includes_size * sz_nbytes)
6.12. Wczytywanie zagnieżdżonych struktur binarnych o zmiennej długości
197
return cls(buf) def iter_as(self, code): if isinstance(code, str): s = struct.Struct(code) for off in range(0, len(self._buffer), s.size): yield s.unpack_from(self._buffer, off) elif isinstance(code, StructureMeta): size = code.struct_size for off in range(0, len(self._buffer), size): data = self._buffer[off:off+size] yield code(data)
Metoda statyczna SizedRecord.from_file() to narzędzie do wczytywania z pliku porcji danych poprzedzonej wielkością. Takie dane znajdują się w plikach różnych formatów. Danymi wejściowymi metody jest kod formatujący struktury, który określa sposób kodowania wielkości (powinna być ona podana za pomocą bajtów). Opcjonalny argument includes_size określa, czy w liczbie bajtów uwzględniono wielkość nagłówka czy nie. Oto przykład ilustrujący, jak wykorzystać ten kod do wczytania poszczególnych wielokątów z pliku: >>> f = open('polys.bin', 'rb') >>> phead = PolyHeader.from_file(f) >>> phead.num_polys 3 >>> polydata = [ SizedRecord.from_file(f, '>> polydata [<__main__.SizedRecord object at 0x1006a4d50>, <__main__.SizedRecord object at 0x1006a4f50>, <__main__.SizedRecord object at 0x10070da90>] >>>
Jak widać, na razie kod nie interpretuje zawartości obiektów typu SizedRecord. Dlatego trzeba dodać metodę iter_as(), która jako dane wejściowe przyjmuje kod formatujący struktury lub klasę Structure. Daje to dużą swobodę w zakresie interpretowania danych. Oto przykład: >>> for n, poly in enumerate(polydata): ... print('Wielokąt', n) ... for p in poly.iter_as('
>> >>> for n, poly in enumerate(polydata): ... print('Wielokąt', n) ... for p in poly.iter_as(Point): ... print(p.x, p.y) ... Wielokąt 0
Oto nowa wersja funkcji read_polys() powstała przez połączenie wszystkich opisanych elementów: class Point(Structure): _fields_ = [ ('
Omówienie W tej recepturze pokazano, jak w praktyce wykorzystać różne zaawansowane techniki programowania, w tym deskryptory, leniwe wartościowanie, metaklasy, zmienne statyczne i widoki pamięci. Wszystkie te mechanizmy mają ściśle określone przeznaczenie. Ważną cechą przedstawionego kodu jest to, że oparto go na leniwym wypakowywaniu. W momencie tworzenia obiektu typu Structure polecenie __init__() przygotowuje wyłącznie widok pamięci z podanymi danymi bajtowymi. Na tym etapie kod nie wypakowuje danych ani nie przeprowadza innych operacji związanych ze strukturą. Jednym z powodów zastosowania tego podejścia jest to, że programistę może interesować tylko kilka konkretnych elementów z binarnego rekordu. Dlatego zamiast wypakowywać cały plik, można pobrać tylko potrzebne fragmenty. Do obsługi leniwego wypakowywania i pakowania wykorzystano klasę deskryptora — StructField. Każdy atrybut podany przez użytkownika w kolekcji _fields_ jest przekształcany na deskryptor StructField, który zapisuje powiązany kod formatujący struktury i pozycję bajta w buforze. Metaklasa StructureMeta automatycznie tworzy takie deskryptory, 6.12. Wczytywanie zagnieżdżonych struktur binarnych o zmiennej długości
199
gdy definiowane są klasy różnych struktur. Główną przyczyną zastosowania metaklasy jest to, że pozwala użytkownikom na bardzo łatwe określanie formatu struktury. Wystarczy podać jej wysokopoziomowy opis i nie trzeba zajmować się niskopoziomowymi szczegółami. Łatwym do pominięcia aspektem metaklasy StructureMeta jest to, że porządek bitów jest zapamiętywany. Jeśli w atrybucie określono taki porządek (< oznacza rosnący, a > — malejący porządek bitów), jest on stosowany także dla następnych pól. Pomaga to uniknąć wpisywania dodatkowych znaków, przy czym porządek bitów można zmienić wewnątrz definicji. Możliwe, że struktura jest skomplikowana, tak jak poniżej: class ShapeFile(Structure): _fields_ = [ ('>i', 'file_code'), # Malejący porządek bitów ('20s', 'unused'), ('i', 'file_length'), ('
Jak wspomniano, wywołanie memoryview() w rozwiązaniu pełni ważną rolę, ponieważ pozwala uniknąć tworzenia kopii danych w pamięci. Gdy struktury są zagnieżdżone, widok pamięci można wykorzystać do nałożenia różnych części definicji struktury na ten sam obszar pamięci. Ten aspekt rozwiązania jest łatwy do pominięcia. Związany jest z tworzeniem wycinków z wykorzystaniem pamięci lub zwykłych tablic bajtów. Tworzenie wycinka łańcucha lub tablicy bajtów prowadzi zwykle do kopiowania danych. Widok pamięci tego nie wymaga. Wycinki są nałożone na istniejącą pamięć, dlatego to podejście jest wydajniejsze. Wiele powiązanych receptur zawiera rozwinięcie poruszonych tu zagadnień. Ściśle powiązana jest receptura 8.13, w której wykorzystano deskryptory do zbudowania systemu typów. Informacje na temat leniwego sprawdzania właściwości znajdziesz w recepturze 8.10. Receptura ta jest powiązana z kodem deskryptora NestedStruct. W recepturze 9.19 pokazano, jak wykorzystać metaklasę do inicjowania składowych klasy (podobnie działa klasa StructureMeta). Ciekawy może okazać się także kod biblioteki ctypes Pythona, ponieważ zapewnia podobne do opisanych możliwości w zakresie definiowania struktur danych, zagnieżdżania struktur danych i pokrewnych mechanizmów.
6.13. Podsumowywanie danych i obliczanie statystyk Problem Programista chce przetwarzać duże zbiory danych i generować podsumowania oraz statystyki.
Rozwiązanie Do analizowania danych z wykorzystaniem statystyk, szeregów czasowych i powiązanych technik warto zastosować bibliotekę Pandas (http://pandas.pydata.org/).
200
Rozdział 6. Kodowanie i przetwarzanie danych
Aby przedstawić jej możliwości, poniżej pokazano przykład wykorzystania jej do analizy bazy danych szczurów i gryzoni miasta Chicago (https://data.cityofchicago.org/Service-Requests/ 311-Service-Requests-Rodent-Baiting/97t6-zrhs). W czasie gdy powstawała ta książka, plik CSV z tą bazą zawierał około 74 000 pozycji: >>> import pandas >>> # Wczytywanie pliku CSV z pominięciem ostatniego wiersza >>> rats = pandas.read_csv('rats.csv', skip_footer=1) >>> rats Int64Index: 74055 entries, 0 to 74054 Data columns: Creation Date 74055 non-null values Status 74055 non-null values Completion Date 72154 non-null values Service Request Number 74055 non-null values Type of Service Request 74055 non-null values Number of Premises Baited 65804 non-null values Number of Premises with Garbage 65600 non-null values Number of Premises with Rats 65752 non-null values Current Activity 66041 non-null values Most Recent Action 66023 non-null values Street Address 74055 non-null values ZIP Code 73584 non-null values X Coordinate 74043 non-null values Y Coordinate 74043 non-null values Ward 74044 non-null values Police District 74044 non-null values Community Area 74044 non-null values Latitude 74043 non-null values Longitude 74043 non-null values Location 74043 non-null values dtypes: float64(11), object(9) >>> # Sprawdzanie przedziału wartości z określonego pola >>> rats['Current Activity'].unique() array([nan, Dispatch Crew, Request Sanitation Inspector], dtype=object) >>> # Filtrowanie danych >>> crew_dispatched = rats[rats['Current Activity'] == 'Dispatch Crew'] >>> len(crew_dispatched) 65676 >>> >>> # Znajdowanie kodów pocztowych 10 obszarów Chicago, gdzie występuje najwięcej szczurów >>> crew_dispatched['ZIP Code'].value_counts()[:10] 60647 3837 60618 3530 60614 3284 60629 3251 60636 2801 60657 2465 60641 2238 60609 2206 60651 2152 60632 2071 >>> >>> # Grupowanie według daty zakończenia prac >>> dates = crew_dispatched.groupby('Completion Date') >>> len(dates)
6.13. Podsumowywanie danych i obliczanie statystyk
201
472 >>> >>> # Określanie liczby zgłoszeń zakończenia prac dla poszczególnych dni >>> date_counts = dates.size() >>> date_counts[0:10] Completion Date 01/03/2011 4 01/03/2012 125 01/04/2011 54 01/04/2012 38 01/05/2011 78 01/05/2012 100 01/06/2011 100 01/06/2012 58 01/07/2011 1 01/09/2012 12 >>> >>> # Sortowanie według liczby zgłoszeń >>> date_counts.sort() >>> date_counts[-10:] Completion Date 10/12/2012 313 10/21/2011 314 09/20/2011 316 10/26/2011 319 02/22/2011 325 10/26/2012 333 03/17/2011 336 10/13/2011 378 10/14/2011 391 10/07/2011 457 >>>
Tak, 7 października 2011 był bardzo pracowitym dniem dla szczurów.
Omówienie Pandas to duża biblioteka. Nie da się opisać w tym miejscu wszystkich jej funkcji. Jeśli jednak chcesz analizować duże zbiory danych, grupować dane, obliczać statystyki i wykonywać podobne zadania, z pewnością warto zapoznać się z tą biblioteką. Znacznie więcej informacji znajdziesz w książce Wesa McKinneya pt. Python for Data Analysis (wydawnictwo O’Reilly).
202
Rozdział 6. Kodowanie i przetwarzanie danych
ROZDZIAŁ 7.
Funkcje
Definiowanie funkcji za pomocą polecenia def to podstawowa operacja we wszystkich programach. W tym rozdziale poznasz zaawansowane i niestandardowe definicje oraz sposoby używania funkcji. Omówiono tu argumenty domyślne, funkcje przyjmujące dowolną liczbę argumentów, argumenty podawane tylko za pomocą słów kluczowych, uwagi i domknięcia. Ponadto przedstawiono pewne skomplikowane problemy z zakresu przepływu sterowania i przekazywania danych związane z funkcjami wywoływanymi zwrotnie.
7.1. Pisanie funkcji przyjmujących dowolną liczbę argumentów Problem Programista chce napisać funkcję przyjmującą dowolną liczbę argumentów wejściowych.
Rozwiązanie Aby napisać funkcję, która przyjmuje dowolną liczbę argumentów podawanych na określonych pozycjach, zastosuj argument z modyfikatorem *. Oto przykład: def avg(first, *rest): return (first + sum(rest)) / (1 + len(rest)) # Przykład użycia avg(1, 2) avg(1, 2, 3, 4)
# 1.5 # 2.5
W tym kodzie rest to krotka ze wszystkimi dodatkowymi argumentami podanymi na określonych pozycjach. Kod przy wykonywaniu obliczeń traktuje te argumenty jak sekwencję. Aby pobierać dowolną liczbę argumentów podawanych za pomocą słów kluczowych, zastosuj argument rozpoczynający się od modyfikatora **: import html def make_element(name, value, **attrs): keyvals = [' %s="%s"' % item for item in attrs.items()] attr_str = ''.join(keyvals) element = '<{name}{attrs}>{value}{name}>'.format(
203
name=name, attrs=attr_str, value=html.escape(value)) return element # Przykład # Tworzy znacznik 'Albatros' make_element('item', 'Albatros', size='large', quantity=6) # Tworzy znacznik '
' make_element('p', '')
Tu attrs to słownik przechowujący argumenty podane za pomocą słów kluczowych (jeśli takie istnieją). Jeśli chcesz utworzyć funkcję, która przyjmuje dowolną liczbę argumentów podawanych na określonych pozycjach i za pomocą słów kluczowych, zastosuj symbole * i **: def anyargs(*args, **kwargs): print(args) # Krotka print(kwargs) # Słownik
W tej funkcji wszystkie argumenty podane na podstawie pozycji są zapisywane w krotce args , a wszystkie argumenty przekazane za pomocą słów kluczowych są umieszczane w słowniku kwargs .
Omówienie Argument * może występować w definicji funkcji tylko jako ostatni argument podawany na podstawie pozycji. Argument ** musi być ostatnim spośród wszystkich argumentów funkcji. Z definicjami funkcji związany jest pewien szczegół — po argumencie * mogą pojawiać się inne argumenty. def a(x, *args, y): pass def b(x, *args, y, **kwargs): pass
Są to argumenty podawane wyłącznie za pomocą słów kluczowych. Ich opis znajdziesz w recepturze 7.2.
7.2. Tworzenie funkcji przyjmujących argumenty podawane wyłącznie za pomocą słów kluczowych Problem Programista chce napisać funkcję, w której niektóre argumenty można podawać wyłącznie za pomocą słów kluczowych.
Rozwiązanie Łatwo jest uzyskać pożądany efekt, umieszczając argumenty podawane za pomocą słów kluczowych po argumencie z modyfikatorem * lub po pojedynczym znaku *:
Technikę tę można wykorzystać także do określania argumentów podawanych za pomocą słów kluczowych w funkcjach, które pobierają różną liczbę argumentów przekazywanych na podstawie pozycji. Oto przykład: def mininum(*values, clip=None): m = min(values) if clip is not None: m = clip if clip > m else m return m minimum(1, 5, 2, -5, 10) minimum(1, 5, 2, -5, 10, clip=0)
# Zwraca -5 # Zwraca 0
Omówienie Argumenty podawane wyłącznie za pomocą słów kluczowych często są dobrym sposobem na zwiększenie przejrzystości kodu, w którym do funkcji można przekazywać opcjonalne argumenty. Przyjrzyj się poniższemu wywołaniu: msg = recv(1024, False)
Jeśli programista nie zna działania funkcji recv(), może nie wiedzieć, do czego służy argument False. W poniższym wywołaniu znaczenie argumentu jest dużo łatwiejsze do zrozumienia: msg = recv(1024, block=False)
Często lepiej jest zastosować argumenty podawane wyłącznie za pomocą słów kluczowych niż sztuczki z argumentem **kwargs. Wynika to z tego, że te pierwsze są poprawnie wyświetlane, gdy użytkownik żąda pomocy: >>> help(recv) Help on function recv in module __main__: recv(maxsize, *, block) Przyjmuje komunikat
Argumenty podawane wyłącznie za pomocą słów kluczowych są też przydatne w bardziej zaawansowanych rozwiązaniach. Można je wykorzystać np. do przekazywania argumentów do funkcji, w których wszystkie dane wejściowe są pobierane za pomocą argumentów *args i **kwargs. Przykład zastosowania tej techniki znajdziesz w recepturze 9.11.
7.3. Dołączanie metadanych z informacjami do argumentów funkcji Problem Programista napisał funkcję i chce dołączyć do argumentów dodatkowe wiadomości, aby poinformować inne osoby, jak korzystać z tej funkcji.
7.3. Dołączanie metadanych z informacjami do argumentów funkcji
205
Rozwiązanie Dodawanie uwag do argumentów funkcji to przydatny sposób na poinformowanie programistów o tym, jak z niej korzystać. Przyjrzyj się poniższej funkcji z uwagami: def add(x:int, y:int) -> int: return x + y
W interpreterze Pythona dołączone uwagi nie mają znaczenia semantycznego. Nie powodują sprawdzania typów ani odmiennego działania Pythona. Mogą jednak okazać się przydatnymi wskazówkami i pomóc osobom czytającym kod w zrozumieniu zamiarów jego autora. Ponadto niezależne narzędzia i platformy mogą przypisywać uwagom znaczenie semantyczne. Uwagi pojawiają się też w dokumentacji: >>> help(add) Help on function add in module __main__: add(x: int, y: int) -> int >>>
Choć jako uwagę można dołączyć do funkcji dowolny obiekt (np. liczby, łańcuchy znaków, obiekty itd.), najbardziej sensowne jest użycie klas lub łańcuchów znaków.
Omówienie Uwagi na temat funkcji są zapisywane w jej atrybucie __annotations__. Oto przykład: >>> add.__annotations__ {'y': , 'return': , 'x': }
Choć uwagi mają wiele zastosowań, służą przede wszystkim do dokumentowania kodu. Ponieważ w Pythonie nie występują deklaracje typu danych, często trudno jest na podstawie lektury samego kodu źródłowego ustalić, co należy przekazać do funkcji. Uwagi zapewniają programistom dodatkową wskazówkę. W recepturze 9.20 znajdziesz zaawansowany przykład ilustrujący, jak wykorzystać uwagi do zaimplementowania funkcji przeciążonych.
7.4. Zwracanie wielu wartości przez funkcje Problem Programista chce, aby funkcja zwracała wiele wartości.
Rozwiązanie Aby zwrócić z funkcji wiele wartości, wystarczy zwrócić krotkę. Oto przykład: >>> def myfun(): ... return 1, 2, 3 ... >>> a, b, c = myfun() >>> a 1
206
Rozdział 7. Funkcje
>>> b 2 >>> c 3
Omówienie Choć wydaje się, że funkcja myfun() zwraca wiele wartości, w rzeczywistości tworzona jest krotka. Kod wygląda dziwnie, ale do tworzenia krotek wystarczą przecinki (nawiasy nie są konieczne). Oto przykład: >>> >>> (1, >>> >>> (1, >>>
a = (1, 2) a 2) b = 1, 2 b 2)
# Z nawiasami # Bez nawiasów
Przy wywoływaniu funkcji zwracających krotki wynik często przypisuje się do kilku zmiennych, tak jak w przedstawionym wcześniej kodzie. Jest to zwykłe wypakowywanie krotek opisane w recepturze 1.1. Zwracaną wartość można też przypisać do jednej zmiennej: >>> x = myfun() >>> x (1, 2, 3) >>>
7.5. Definiowanie funkcji z argumentami domyślnymi Problem Programista chce zdefiniować funkcję lub metodę, w której przynajmniej jeden argument jest opcjonalny i ma wartość domyślną.
Rozwiązanie Na pozór definiowanie funkcji z argumentami opcjonalnymi jest proste — wystarczy przypisać do nich wartości w definicji i upewnić się, że argumenty domyślne pojawiają się na końcu. Oto przykład: def spam(a, b=42): print(a, b) spam(1) spam(1, 2)
# Poprawnie. a=1, b=42 # Poprawnie. a=1, b=2
Jeśli wartością domyślną ma być zmienny kontener, np. lista, zbiór lub słownik, należy jako wartość domyślną podać None i napisać kod w następujący sposób: # Wartością domyślną jest lista def spam(a, b=None): if b is None: b = [] ...
7.5. Definiowanie funkcji z argumentami domyślnymi
207
Jeżeli zamiast podawać wartość domyślną, chcesz napisać kod, który sprawdza, czy użytkownik przypisał wartość do opcjonalnego argumentu, możesz zastosować następujący idiom: _no_value = object() def spam(a, b=_no_value): if b is _no_value: print('Nie podano wartości argumentu b') ...
Funkcja ta działa w następujący sposób: >>> Nie >>> >>> >>>
spam(1) podano wartości argumentu b spam(1, 2) # b=2 spam(1, None) # b = None
Zauważ, że występuje różnica między nieprzekazaniem żadnej wartości a podaniem wartości None.
Omówienie Definiowanie funkcji z argumentami domyślnymi jest proste, ale z operacją tą związane są pewne aspekty. Po pierwsze, wartości przypisywane jako domyślne są wiązane w definicji funkcji tylko raz. Wypróbuj poniższy przykład, aby się o tym przekonać: >>> x = 42 >>> def spam(a, b=x): ... print(a, b) ... >>> spam(1) 1 42 >>> x = 23 # Nie ma wpływu na wartość argumentu b >>> spam(1) 1 42 >>>
Warto zauważyć, że zmiana wartości zmiennej x (używanej jako wartość domyślna) nie wpływa na działanie funkcji. Wynika to z tego, że wartość domyślna jest ustalana w momencie definiowania funkcji. Po drugie, wartości przypisywane jako domyślne zawsze powinny być obiektami niezmiennymi — None, True, False, liczbami lub łańcuchami znaków. Nigdy nie pisz kodu w następującej postaci: def spam(a, b=[]): ...
# Błąd!
Takie podejście może prowadzić do rozmaitych problemów, gdy wartość domyślna wyjdzie poza funkcję i zostanie zmodyfikowana. Zmiany te spowodują trwałą modyfikację wartości domyślnej w dalszych wywołaniach funkcji. Oto przykład: >>> def spam(a, b=[]): ... print(b) ... return b ... >>> x = spam(1)
208
Rozdział 7. Funkcje
>>> x [] >>> x.append(99) >>> x.append('Oj!') >>> x [99, 'Oj!'] >>> spam(1) # Zwracana jest zmodyfikowana lista! [99, 'Oj!'] >>>
Zwykle jest to niepożądany efekt. Aby go uniknąć, lepiej jest przypisać None jako wartość domyślną, a następnie wykrywać ją w funkcji (tak jak w rozwiązaniu). Ważnym aspektem receptury jest wykorzystanie operatora is przy sprawdzaniu, czy argument ma wartość None. Czasem programiści popełniają następujący błąd: def spam(a, b=None): if not b: # Błąd! Zamiast tego należy zastosować 'b is None' b = [] ...
Problem polega tu na tym, że choć wartość None jest traktowana jak False, dotyczy to także wielu innych obiektów (np. łańcuchów znaków, list, krotek i słowników o zerowej długości). Dlatego przedstawiony fragment błędnie traktuje niektóre dane wejściowe, tak jakby nie istniały. Oto przykład: >>> >>> >>> >>> >>> >>>
spam(1) x = [] spam(1, x) spam(1, 0) spam(1, '')
# Poprawnie # Niezgłaszany błąd. Wartość x jest domyślnie zastępowana # Niezgłaszany błąd. 0 jest ignorowane # Niezgłaszany błąd. '' jest ignorowane
Ostatnia część receptury jest dość wyrafinowana — funkcja sprawdza, czy użytkownik podał jakąkolwiek wartość jako opcjonalny argument. Problem polega na tym, że nie można użyć wartości domyślnej None, 0 ani False do sprawdzania, czy argument został podany. Wynika to z tego, że użytkownik może swobodnie podać każdą z tych wartości. Dlatego przy sprawdzaniu trzeba użyć innej wartości. Aby rozwiązać problem, można utworzyć unikatowy prywatny obiekt typu object, tak jak w rozwiązaniu, gdzie obiekt zapisano w zmiennej _no_value. W funkcji należy następnie porównać argument z tą specjalną wartością, aby ustalić, czy użytkownik podał argument. Jest niezwykle mało prawdopodobne, że użytkownik przekaże wartość wejściową w postaci obiektu _no_value. Dlatego sprawdziwszy, czy argument został podany, można bezpiecznie wykorzystać ten obiekt w porównaniu. Wywołanie object()w tym miejscu może wydawać się dziwne; object to wspólna klasa podstawowa niemal wszystkich obiektów w Pythonie. Można tworzyć obiekty tej klasy, jednak nie są one ciekawe, ponieważ nie mają godnych uwagi metod ani danych (klasa ta nie ma słownika obiektu, dlatego nie można nawet ustawić atrybutów). Niemal jedyną rzeczą, jaką można zrobić z tą klasą, jest wykorzystanie jej do sprawdzania tożsamości. Dzięki temu jest przydatna jako wartość specjalna i tak właśnie zastosowano ją w rozwiązaniu.
7.5. Definiowanie funkcji z argumentami domyślnymi
209
7.6. Definiowanie funkcji anonimowych (wewnątrzwierszowych) Problem Programista chce podać krótką wywoływaną zwrotnie funkcję używaną np. w operacji sort(), jednak nie chce pisać odrębnej jednowierszowej funkcji za pomocą polecenia def. Zamiast tego chce zastosować skrót, który pozwala napisać funkcję wewnątrzwierszowo.
Rozwiązanie Zamiast prostej funkcji, która tylko oblicza wartość wyrażenia, można zastosować wyrażenie lambda. Oto przykład: >>> add = lambda x, y: x + y >>> add(2,3) 5 >>> add('Witaj', 'świecie') 'Witajświecie' >>>
Zastosowanie wyrażenia lambda w tym miejscu odpowiada poniższemu kodowi: >>> def add(x, y): ... return x + y ... >>> add(2,3) 5 >>>
Wyrażenia lambda zwykle stosuje się w kontekście innych operacji, np. przy sortowaniu lub redukowaniu danych: >>> names = ['David Beazley', 'Brian Jones', ... 'Raymond Hettinger', 'Ned Batchelder'] >>> sorted(names, key=lambda name: name.split()[-1].lower()) ['Ned Batchelder', 'David Beazley', 'Raymond Hettinger', 'Brian Jones'] >>>
Omówienie Choć wyrażenia lambda umożliwiają definiowanie prostych funkcji, ich zastosowania są bardzo ograniczone. Przede wszystkim można określić tylko jedno wyrażenie, którego wynikiem jest zwracana wartość. Oznacza to, że nie można stosować innych mechanizmów języka — ani grup instrukcji, warunków, iteracji, ani obsługi wyjątków. Zupełnie możliwe jest napisanie dużej ilości kodu w Pythonie bez stosowania wyrażeń lambda. Czasem jednak można natrafić na nie w programach, których autor pisze dużo krótkich funkcji obliczających wartości różnych wyrażeń, a także w programach wymagających podania wywoływanych zwrotnie funkcji.
210
Rozdział 7. Funkcje
7.7. Pobieranie wartości zmiennych w funkcjach anonimowych Problem Programista zdefiniował funkcję anonimową za pomocą wyrażenia lambda, ale chce też określić wartości różnych zmiennych w miejscu definicji.
Rozwiązanie Zastanów się nad działaniem poniższego kodu: >>> >>> >>> >>> >>>
x a x b
= = = =
10 lambda y: x + y 20 lambda y: x + y
Teraz odpowiedz na pytanie — jakie są wartości wywołań a(10) i b(10)? Jeśli sądzisz, że 20 i 30, mylisz się. >>> a(10) 30 >>> b(10) 30 >>>
Problem polega na tym, że wartość x używana w przedstawionym wyrażeniu lambda to zmienna wolna wiązana w czasie wykonywania programu, a nie w miejscu definicji. Dlatego wartością x w wyrażeniu lambda jest wartość przypisana do zmiennej x w momencie wykonywania wyrażenia. Oto przykład: >>> >>> 25 >>> >>> 13 >>>
x = 15 a(10) x = 3 a(10)
Jeśli chcesz, aby funkcja anonimowa pobierała wartość ustawioną w miejscu definicji i zachowywała ją, zapisz tę wartość jako domyślną: >>> >>> >>> >>> >>> 20 >>> 30 >>>
x = 10 a = lambda y, x=x: x + y x = 20 b = lambda y, x=x: x + y a(10) b(10)
7.7. Pobieranie wartości zmiennych w funkcjach anonimowych
211
Omówienie Problem opisany w tej recepturze pojawia się w kodzie, gdy programista chce być zbyt sprytny przy stosowaniu funkcji lambda. Np. tworzy zestaw poniższych wyrażeń za pomocą wyrażeń listowych lub w pętli i oczekuje, że funkcje lambda zapamiętają wartość zmiennej sterującej z momentu definicji. Oto przykład: >>> funcs = [lambda x: x+n for n in range(5)] >>> for f in funcs: ... print(f(0)) ... 4 4 4 4 4 >>>
Warto zauważyć, że wszystkie funkcje przyjmują za n ostatnią wartość ustawioną w trakcie iterowania. Teraz porównaj to z następującym kodem: >>> funcs = [lambda x, n=n: x+n for n in range(5)] >>> for f in funcs: ... print(f(0)) ... 0 1 2 3 4 >>>
Jak widać, funkcje zapamiętują teraz wartość n z momentu definicji.
7.8. Uruchamianie n-argumentowej jednostki wywoływalnej z mniejszą liczbą argumentów Problem Programista ma jednostkę wywoływalną, którą chce wykorzystać w innym kodzie w Pythonie — np. jako funkcję wywoływaną zwrotnie lub metodę obsługi zdarzeń. Jednostka przyjmuje jednak zbyt wiele argumentów i jej wywołanie powoduje zgłoszenie wyjątku.
Rozwiązanie Jeśli chcesz zmniejszyć liczbę argumentów funkcji, zastosuj wywołanie functools.partial(). Funkcja partial() pozwala przypisać stałe wartości do jednego lub kilku argumentów, co zmniejsza liczbę argumentów, które trzeba podać w późniejszych wywołaniach. Załóżmy, że pierwotna funkcja wygląda tak: def spam(a, b, c, d): print(a, b, c, d)
212
Rozdział 7. Funkcje
Teraz zobacz, jak wykorzystać funkcję partial() do określenia wartości wybranych argumentów: >>> >>> >>> 1 2 >>> 1 4 >>> >>> 1 2 >>> 4 5 >>> >>> 1 2 >>> 1 2 >>> 1 2 >>>
Zauważ, że funkcja partial() określa wartości wybranych argumentów i zwraca nową jednostkę wywoływalną. Nowa jednostka przyjmuje argumenty o nieokreślonej wartości, łączy je z argumentami podanymi w funkcji partial() i przekazuje wszystkie do pierwotnej funkcji.
Omówienie Ta receptura związana jest z problemem współdziałania pozornie niezgodnych ze sobą fragmentów kodu. Do ilustracji tego zagadnienia posłuży kilka przykładów. Załóżmy, że istnieje lista punktów reprezentowanych przez krotki ze współrzędnymi (x, y). Aby obliczyć odległość między dwoma punktami, można zastosować następujący kod: points = [ (1, 2), (3, 4), (5, 6), (7, 8) ] import math def distance(p1, p2): x1, y1 = p1 x2, y2 = p2 return math.hypot(x2 - x1, y2 - y1)
Teraz przyjmijmy, że programista chce posortować wszystkie punkty według odległości od innego punktu. Metoda sort() list przyjmuje argument key, który pozwala zmodyfikować sposób sortowania, jednak technika ta działa tylko dla funkcji jednoargumentowych (funkcja distance() do nich nie należy). Aby rozwiązać problem, można wykorzystać funkcję partial() w następujący sposób: >>> pt = (4, 3) >>> points.sort(key=partial(distance,pt)) >>> points [(3, 4), (1, 2), (5, 6), (7, 8)] >>>
Rozwinięciem tej techniki jest wykorzystanie funkcji partial() do dostosowania sygnatur wywoływanych zwrotnie funkcji używanych w innych bibliotekach. Poniżej znajduje się fragment kodu, w którym wykorzystano moduł multiprocessing do asynchronicznego obliczania wyników przekazywanych do wywoływanej zwrotnie funkcji. Funkcja ta przyjmuje wynik i opcjonalny argument określający sposób rejestrowania danych:
7.8. Uruchamianie n-argumentowej jednostki wywoływalnej z mniejszą liczbą argumentów
213
def output_result(result, log=None): if log is not None: log.debug('Wynik: %r', result) # Przykładowa funkcja def add(x, y): return x + y if __name__ == '__main__': import logging from multiprocessing import Pool from functools import partial logging.basicConfig(level=logging.DEBUG) log = logging.getLogger('test') p = Pool() p.apply_async(add, (3, 4), callback=partial(output_result, log=log)) p.close() p.join()
Gdy podajesz wywoływaną zwrotnie funkcję za pomocą polecenia apply_async(), dodatkowy argument określający sposób rejestrowania jest przekazywany przez funkcję partial(). Dla modułu multiprocessing nie ma to znaczenia — uruchamia on wywoływaną zwrotnie funkcję z jedną wartością. Podobny przykład związany jest z pisaniem serwerów sieciowych. Dzięki modułowi socketserver jest to stosunkowo łatwe. Oto prosty serwer echo: from socketserver import StreamRequestHandler, TCPServer class EchoHandler(StreamRequestHandler): def handle(self): for line in self.rfile: self.wfile.write(b'OTRZYMANO:' + line) serv = TCPServer(('', 15000), EchoHandler) serv.serve_forever()
Załóżmy jednak, że chcesz dodać do klasy EchoHandler metodę __init__(), która przyjmuje dodatkowy argument konfiguracyjny. Oto przykład: class EchoHandler(StreamRequestHandler): # ack to nowy argument podawany wyłącznie za pomocą słów kluczowych. # *args, **kwargs to dowolne standardowe parametry def __init__(self, *args, ack, **kwargs): self.ack = ack super().__init__(*args, **kwargs) def handle(self): for line in self.rfile: self.wfile.write(self.ack + line)
Po wprowadzeniu tej zmiany okazuje się, że nie istnieje prosty sposób na przekazanie nowego obiektu do klasy TCPServer. Wcześniejszy kod będzie teraz zgłaszał wyjątki: Exception happened during processing of request from ('127.0.0.1', 59834) Traceback (most recent call last): ... TypeError: __init__() missing 1 required keyword-only argument: 'ack'
214
Rozdział 7. Funkcje
Na pozór szybkie naprawienie tego kodu jest niemożliwe — wydaje się, że trzeba zmodyfikować kod źródłowy modułu socketserver lub zastosować inne skomplikowane rozwiązanie. Można jednak szybko rozwiązać problem za pomocą funkcji partial(). Wystarczy przy jej użyciu podać wartość argumentu ack: from functools import partial serv = TCPServer(('', 15000), partial(EchoHandler, ack=b'RECEIVED:')) serv.serve_forever()
W tym przykładzie podawanie wartości argumentu ack dla metody __init__() wygląda śmiesznie, jednak argument ten trzeba określić za pomocą słowa kluczowego (zobacz recepturę 7.2). Zamiast funkcji partial() można czasem zastosować wyrażenie lambda. We wcześniejszych przykładach można wykorzystać następujące polecenia: points.sort(key=lambda p: distance(pt, p)) p.apply_async(add, (3, 4), callback=lambda result: output_result(result,log)) serv = TCPServer(('', 15000), lambda *args, **kwargs: EchoHandler(*args, ack=b'RECEIVED:', **kwargs))
Ten kod działa, ale jest dłuższy i mniej zrozumiały dla czytających go osób. Zastosowanie funkcji partial() pozwala bardziej bezpośrednio określić zamiary programisty (który chce podać wartości wybranych argumentów).
7.9. Zastępowanie klas z jedną metodą funkcjami Problem Programista używa klasy, w której oprócz __init__() zdefiniowana jest tylko jedna metoda. Aby uprościć kod, programista chce zastąpić tę klasę zwykłą funkcją.
Rozwiązanie W wielu sytuacjach klasy o jednej metodzie można przekształcić na funkcje za pomocą domknięć. Przyjrzyj się poniższej klasie. Umożliwia ona użytkownikom pobieranie danych z adresów URL z wykorzystaniem systemu szablonów. from urllib.request import urlopen class UrlTemplate: def __init__(self, template): self.template = template def open(self, **kwargs): return urlopen(self.template.format_map(kwargs)) # Przykład zastosowania — pobieranie danych o akcjach z serwisu Yahoo yahoo = UrlTemplate('http://finance.yahoo.com/d/quotes.csv?s={names}&f={fields}') for line in yahoo.open(names='IBM,AAPL,FB', fields='sl1c1v'): print(line.decode('utf-8'))
7.9. Zastępowanie klas z jedną metodą funkcjami
215
Tę klasę można zastąpić znacznie prostszą funkcją: def urltemplate(template): def opener(**kwargs): return urlopen(template.format_map(kwargs)) return opener # Przykład zastosowania yahoo = urltemplate('http://finance.yahoo.com/d/quotes.csv?s={names}&f={fields}') for line in yahoo(names='IBM,AAPL,FB', fields='sl1c1v'): print(line.decode('utf-8'))
Omówienie W wielu sytuacjach klasy o jednej metodzie służą tylko do przechowywania dodatkowego stanu używanego w danej metodzie. Np. klasa UrlTemplate przechowuje wartość template, aby można ją było wykorzystać w metodzie open(). Zastosowanie domknięcia (tak jak w rozwiązaniu) to często bardziej eleganckie rozwiązanie. Domknięcie to funkcja, powiązana jednak ze środowiskiem obejmującym zmienne używane w danej funkcji. Najważniejszą cechą domknięć jest to, że zapamiętują środowisko, w którym zostały zdefiniowane. Dlatego funkcja opener() w rozwiązaniu zapamiętuje wartość argumentu template i wykorzystuje ją w późniejszych wywołaniach. Gdy w trakcie pisania kodu natrafisz na problem dodawania stanu do funkcji, pomyśl o domknięciach. Często są one prostszym i bardziej eleganckim rozwiązaniem niż przekształcanie funkcji na kompletne klasy.
7.10. Dodatkowy stan w funkcjach wywoływanych zwrotnie Problem Programista pisze kod, w którym korzysta z wywoływanych zwrotnie funkcji (np. do obsługi zdarzeń lub wykonywanych po zakończeniu przetwarzania), i chce przy tym, aby funkcje te przechowywały używany w nich dodatkowy stan.
Rozwiązanie Ta receptura dotyczy stosowania wywoływanych zwrotnie funkcji, które występują w wielu bibliotekach i platformach — zwłaszcza związanych z przetwarzaniem asynchronicznym. Na potrzeby przedstawienia tej techniki i późniejszych testów zdefiniujmy następującą funkcję z wywołaniem zwrotnym: def apply_async(func, args, *, callback): # Obliczanie wyniku result = func(*args) # Wywołanie zwrotne z wynikiem jako argumentem callback(result)
216
Rozdział 7. Funkcje
W praktyce taki kod może wykonywać zaawansowane operacje z wykorzystaniem wątków, procesów i zegarów, jednak tu nie to jest najważniejsze. Tu znaczenie ma uruchamianie wywoływanej zwrotnie funkcji. Poniżej pokazano, jak wykorzystać przedstawiony wcześniej kod: >>> def print_result(result): ... print('Otrzymano:', result) ... >>> def add(x, y): ... return x + y ... >>> apply_async(add, (2, 3), callback=print_result) Otrzymano: 5 >>> apply_async(add, ('Witaj', 'świecie'), callback=print_result) Otrzymano: Witajświecie >>>
Jak widać, funkcja print_result() przyjmuje tylko jeden argument — wynik. Nie otrzymuje żadnych innych informacji. Czasem stanowi to problem, gdy w wywoływanej zwrotnie funkcji potrzebne są inne zmienne lub aspekty środowiska. Jednym ze sposobów na przekazywanie dodatkowych informacji do wywoływanej zwrotnie funkcji jest wykorzystanie metod powiązanych zamiast prostych funkcji. Np. w poniższej klasie przechowywana jest wewnętrzna liczba porządkowa, zwiększana każdorazowo po otrzymaniu wyniku: class ResultHandler: def __init__(self): self.sequence = 0 def handler(self, result): self.sequence += 1 print('[{}] Otrzymano: {}'.format(self.sequence, result))
Aby zastosować tę klasę, należy utworzyć obiekt tego typu i użyć powiązanej metody handler w wywołaniu zwrotnym: >>> >>> [1] >>> [2] >>>
Zamiast klasy można też wykorzystać domknięcie do przechowywania stanu. Oto przykład: def make_handler(): sequence = 0 def handler(result): nonlocal sequence sequence += 1 print('[{}] Otrzymano: {}'.format(sequence, result)) return handler
Oto przykład wykorzystania tej wersji kodu: >>> >>> [1] >>> [2] >>>
7.10. Dodatkowy stan w funkcjach wywoływanych zwrotnie
217
Oto jeszcze inna odmiana tego rozwiązania. Czasem do uzyskania tych samych efektów można wykorzystać współprogram: def make_handler(): sequence = 0 while True: result = yield sequence += 1 print('[{}] Otrzymano: {}'.format(sequence, result))
Przy stosowaniu współprogramu należy zastosować jego metodę send() jako wywołanie zwrotne: >>> >>> >>> [1] >>> [2] >>>
Stan można przekazać do wywołania zwrotnego także za pomocą dodatkowego argumentu i funkcji partial(). Oto przykład: >>> ... ... ... >>> ... ... ... >>> >>> >>> [1] >>> [2] >>>
Omówienie Oprogramowanie oparte na wywoływanych zwrotnie funkcjach czasem staje się bardzo skomplikowane. Problem po części wynika z tego, że wywoływana zwrotnie funkcja często jest niepowiązana z kodem, który zgłasza początkowe żądanie prowadzące do wywołania zwrotnego. Dlatego środowisko wykonawcze jest inne w miejscu zgłaszania żądania i jego obsługi. Jeśli chcesz, aby wywoływana zwrotnie funkcja była częścią wieloetapowej procedury, musisz ustalić, jak zapisać i odtworzyć stan. Istnieją dwa podstawowe podejścia pobierania i przekazywania stanu. Można przekazywać go w obiektach (np. połączonych z metodą powiązaną) lub w domknięciach (funkcjach wewnętrznych). Spośród tych dwóch technik domknięcia są nieco prostsze i naturalne, ponieważ są oparte na funkcjach. Ponadto automatycznie pobierają wszystkie używane funkcje. Dzięki temu programista nie musi precyzyjnie określać zapisywanego stanu (jest on ustalany automatycznie na podstawie kodu). Przy stosowaniu domknięć należy zwrócić baczną uwagę na zmienne modyfikowalne. W rozwiązaniu zastosowano deklarację nonlocal, aby określić, że zmienna sequence jest modyfikowana w wywołaniu zwrotnym. Bez tej deklaracji kod zgłosi błąd.
218
Rozdział 7. Funkcje
Zastosowanie współprogramu jako wywołania zwrotnego jest ciekawe, ponieważ technika ta jest ściśle powiązana z metodą opartą na domknięciach. Pod pewnymi względami jest jeszcze bardziej przejrzysta, ponieważ wymaga zastosowania tylko jednej funkcji. Ponadto zmienne można swobodnie modyfikować bez stosowania deklaracji nonlocal. Wadą tej techniki jest to, że współprogramy przez wielu programistów są znane gorzej niż pozostałe części Pythona. Ponadto występują tu pewne kruczki, takie jak konieczność wywołania next() dla współprogramu przed jego użyciem. W praktyce łatwo jest o tym zapomnieć. Mimo to współprogramy mogą być przydatne w opisanej sytuacji, podobnie jak technika definiowania wywołań zwrotnych wewnątrzwierszowo (co opisano w następnej recepturze). Ostatnie podejście, oparte na funkcji partial(), jest przydatne, jeśli trzeba przekazać do wywołania zwrotnego dodatkowe wartości. Zamiast funkcji partial() czasem używa się w tej sytuacji także wyrażeń lambda: >>> apply_async(add, (2, 3), callback=lambda r: handler(r, seq)) [1] Otrzymano: 5 >>>
Więcej przykładów znajdziesz w recepturze 7.8, gdzie pokazano, jak za pomocą funkcji partial() modyfikować sygnatury argumentów.
7.11. Wewnątrzwierszowe zapisywanie wywoływanych zwrotnie funkcji Problem Programista pisze kod, w którym korzysta z wywoływanych zwrotnie funkcji. Martwi się jednak dużą liczbą krótkich funkcji i skomplikowanym przepływem sterowania. Chciałby znaleźć sposób na to, aby kod bardziej przypominał standardową sekwencję kroków.
Rozwiązanie Wywoływane zwrotnie funkcje można zapisać wewnątrzwierszowo za pomocą generatorów i współprogramów. Załóżmy, że funkcja wykonuje pewne operacje i uruchamia wywołanie zwrotne w następujący sposób (zobacz recepturę 7.10): def apply_async(func, args, *, callback): # Obliczanie wyniku result = func(*args) # Wywołanie zwrotne z wynikiem jako argumentem callback(result)
Przyjrzyj się teraz poniższemu kodowi pomocniczemu. Wykorzystano w nim klasę Async i dekorator inlined_async: from queue import Queue from functools import wraps class Async: def __init__(self, func, args): self.func = func self.args = args
7.11. Wewnątrzwierszowe zapisywanie wywoływanych zwrotnie funkcji
219
def inlined_async(func): @wraps(func) def wrapper(*args): f = func(*args) result_queue = Queue() result_queue.put(None) while True: result = result_queue.get() try: a = f.send(result) apply_async(a.func, a.args, callback=result_queue.put) except StopIteration: break return wrapper
Te dwa fragmenty kodu pozwalają zapisać wewnątrzwierszowo kroki wywołania zwrotnego za pomocą poleceń yield. Oto przykład: def add(x, y): return x + y @inlined_async def test(): r = yield Async(add, (2, 3)) print(r) r = yield Async(add, ('Witaj', 'świecie')) print(r) for n in range(10): r = yield Async(add, (n, n)) print(r) print('Żegnaj')
Po wywołaniu funkcji test() uzyskasz następujące dane wyjściowe: 5 Witajświecie 0 2 4 6 8 10 12 14 16 18 Żegnaj
Oprócz specjalnego dekoratora i wywołań yield nic nie wskazuje na to, że używane są tu funkcje wywoływane zwrotnie (program korzysta z nich na zapleczu).
Omówienie Ta receptura wymaga pewnej wiedzy na temat wywoływanych zwrotnie funkcji, generatorów i przepływu sterowania. W kodzie z wywołaniami zwrotnymi ważne jest to, że obecne obliczenia są wstrzymywane, a po pewnym czasie wznawiane (kod działa asynchronicznie). W momencie wznawiania obliczeń uruchamiana jest wywoływana zwrotnie funkcja, która kontynuuje przetwarzanie. Funkcja apply_async() ilustruje te ważne aspekty wywołań zwrotnych, przy czym w praktyce może być znacznie bardziej skomplikowana (może obejmować wątki, procesy, obsługę zdarzeń itd.). 220
Rozdział 7. Funkcje
Pomysł wstrzymywania i wznawiania obliczeń pasuje do modelu działania funkcji generatora. Operacja yield powoduje, że funkcja generatora zwraca wartość i wstrzymuje działanie. Późniejsze wywołania __next__() lub send() generatora powodują wznowienie przez niego pracy. Dlatego najważniejszym aspektem receptury jest kod w funkcji dekoratora inline_async(). Dekorator ten przechodzi przez funkcję generatora krok po kroku, wywołując wszystkie polecenia yield. Dlatego najpierw tworzona jest kolejka wyników, zapełniana początkowo wartościami None. Następnie zaczyna działać pętla, w której wyniki są pobierane z kolejki i przekazywane do generatora. To powoduje przejście do następnego polecenia yield i otrzymywany jest obiekt typu Async. Pętla sprawdza wtedy funkcję i argumenty oraz inicjuje asynchroniczne obliczanie, wywołując funkcję apply_async(). Jednak najbardziej podchwytliwym fragmentem obliczeń jest to, że zamiast normalnej wywoływanej zwrotnie funkcji program używa metody put() kolejki. Na tym etapie dalszy przebieg pracy jest kwestią otwartą. Główna pętla natychmiast wraca do początku i wywołuje operację get() kolejki. Jeśli dane są dostępne, musi być to wynik zapisany w kolejce przez wywołanie zwrotne put(). Jeżeli nie ma danych, operacja jest wstrzymywana w oczekiwaniu na późniejsze otrzymanie wyniku. To, w jaki sposób wynik jest przekazywany, zależy od kodu funkcji apply_async(). Jeśli masz wątpliwości, czy tak szalone rozwiązanie działa prawidłowo, możesz je wypróbować, używając biblioteki multiprocessing i wywołując asynchroniczne operacje w odrębnych procesach: if __name__ == '__main__': import multiprocessing pool = multiprocessing.Pool() apply_async = pool.apply_async # Uruchamianie funkcji test test()
Okazuje się, że kod działa, jednak prześledzenie przepływu sterowania wymaga dużego kubka kawy. Ukrywanie skomplikowanego przepływu sterowania w funkcjach generatora to technika stosowana także w bibliotece standardowej i niezależnych pakietach. Np. dekorator @contextmanager z biblioteki contextlib wykonuje podobną niewiarygodną sztuczkę i łączy dane z wejścia i wyjścia z menedżera kontekstu, używając wywołań yield. Także w popularnym pakiecie Twisted (http://twistedmatrix.com/trac/) występują podobne wewnątrzwierszowe wywołania zwrotne.
7.12. Dostęp do zmiennych zdefiniowanych w domknięciu Problem Programista chce rozwinąć domknięcie za pomocą funkcji, które umożliwiają dostęp do zmiennych wewnętrznych oraz ich modyfikowanie.
Rozwiązanie Zmienne wewnętrzne domknięcia standardowo są niedostępne dla zewnętrznego kodu. Można jednak zapewnić dostęp do nich za pomocą funkcji akcesora dołączanych do domknięcia jako atrybuty funkcyjne. Oto przykład: 7.12. Dostęp do zmiennych zdefiniowanych w domknięciu
221
def sample(): n = 0 # Funkcja domknięcia def func(): print('n=', n) # Akcesory dla zmiennej n def get_n(): return n def set_n(value): nonlocal n n = value # Dołączanie atrybutów funkcyjnych func.get_n = get_n func.set_n = set_n return func
Oto przykład ilustrujący, jak korzystać z tego kodu: >>> f = sample() >>> f() n= 0 >>> f.set_n(10) >>> f() n= 10 >>> f.get_n() 10 >>>
Omówienie Przedstawiona receptura działa dzięki dwóm głównym mechanizmom. Pierwszy, deklaracje nonlocal, pozwala pisać funkcje zmieniające zmienne wewnętrzne. Drugi, atrybuty funkcyjne, umożliwia łatwe dołączenie akcesorów do funkcji domknięcia. Akcesory działają tu jak metody obiektów, przy czym nie jest używana żadna klasa. Recepturę tę można rozwinąć tak, aby domknięcie naśladowało działanie obiektów danej klasy. W tym celu wystarczy skopiować funkcje wewnętrzne do słownika obiektu i zwrócić go. Oto przykład: import sys class ClosureInstance: def __init__(self, locals=None): if locals is None: locals = sys._getframe(1).f_locals # Aktualizowanie słownika obiektu jednostkami wywoływalnymi self.__dict__.update((key,value) for key, value in locals.items() if callable(value) ) # Przekierowanie metod specjalnych def __len__(self): return self.__dict__['__len__']() # Przykład zastosowania def Stack(): items = [] def push(item): items.append(item)
Oto interaktywna sesja, w której pokazano, jak działa ten kod: >>> s = Stack() >>> s <__main__.ClosureInstance object at 0x10069ed10> >>> s.push(10) >>> s.push(20) >>> s.push('Witaj') >>> len(s) 3 >>> s.pop() 'Witaj' >>> s.pop() 20 >>> s.pop() 10 >>>
Co ciekawe, kod ten działa szybciej niż rozwiązanie oparte na normalnej definicji klasy. Możesz porównać wydajność tej wersji z następującą klasą: class Stack2: def __init__(self): self.items = [] def push(self, item): self.items.append(item) def pop(self): return self.items.pop() def __len__(self): return len(self.items)
Powinieneś uzyskać wyniki podobne do poniższych: >>> from timeit import timeit >>> # Test domknięć >>> s = Stack() >>> timeit('s.push(1);s.pop()', 'from __main__ import s') 0.9874754269840196 >>> # Test klasy >>> s = Stack2() >>> timeit('s.push(1);s.pop()', 'from __main__ import s') 1.0707052160287276 >>>
Jak widać, wersja z domknięciami działa około 8% szybciej. Większość różnicy wynika z łatwiejszego dostępu do zmiennych. Domknięcia są szybsze, ponieważ nie trzeba w nich używać dodatkowej zmiennej self. Raymond Hettinger wymyślił jeszcze bardziej skomplikowaną wersję tego rozwiązania (http:// code.activestate.com/recipes/578091-simple-tool-for-simulating-classes-using-closures-/). Jeśli jednak zechcesz zastosować podobne rozwiązanie w swoim kodzie, pamiętaj, że jest to dziwaczny
7.12. Dostęp do zmiennych zdefiniowanych w domknięciu
223
zastępnik zwykłej klasy. Nie udostępnia on podstawowych mechanizmów klas — dziedziczenia, właściwości, deskryptorów ani metod statycznych. Ponadto aby działały metody specjalne, trzeba stosować różne sztuczki (zwróć uwagę np. na kod metody __len__() w klasie ClosureInstance). Pokazana technika może okazać się niezrozumiała dla osób, które będą czytać kod i zastanawiać się, dlaczego nie przypomina on zwykłej definicji klasy (oczywiście mogą też dociekać, dlaczego program działa szybciej). Jest to jednak ciekawy przykład ilustrujący, co można osiągnąć, zapewniając dostęp do wewnętrznych elementów domknięcia. Dodawanie metod do domknięć jest zwykle bardziej przydatne, gdy programista chce zerować stan wewnętrzny, opróżniać bufory, czyścić zawartość pamięci podręcznej lub utworzyć mechanizm przekazywania informacji zwrotnych.
224
Rozdział 7. Funkcje
ROZDZIAŁ 8.
Klasy i obiekty
W tym rozdziale chcemy przede wszystkim zaprezentować receptury dotyczące popularnych wzorców programowania związanych z definicjami klas. Opisano tu takie zagadnienia jak dodawanie do obiektów obsługi standardowych mechanizmów Pythona, korzystanie z metod specjalnych, techniki hermetyzacji, dziedziczenie, zarządzanie pamięcią oraz przydatne wzorce projektowe.
8.1. Modyfikowanie tekstowej reprezentacji obiektów Problem Programista chce przekształcić dane wyjściowe generowane w momencie wyświetlania lub oglądania obiektów na zrozumiałą postać.
Rozwiązanie Aby zmienić tekstową reprezentację obiektu, należy zdefiniować metody __str__() i __repr__(): class Pair: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return 'Pair({0.x!r}, {0.y!r})'.format(self) def __str__(self): return '({0.x!s}, {0.y!s})'.format(self)
Metoda __repr__() zwraca reprezentację obiektu w postaci kodu. Zwykle jest to tekst, który trzeba wpisać, aby ponownie utworzyć obiekt. Tekst ten jest zwracany przez wbudowaną funkcję repr(), a także przez interaktywny interpreter w trakcie sprawdzania wartości. Metoda __str__() przekształca obiekt na łańcuch znaków, który jest zwracany przez funkcje str() i print(). Oto przykład: >>> p = Pair(3, 4) >>> p Pair(3, 4) # Dane wyjściowe metody __repr__() >>> print(p) (3, 4) # Dane wyjściowe metody __str__() >>>
225
W kodzie tej receptury pokazano też, jak wykorzystać różne reprezentacje tekstowe w trakcie formatowania. Specjalny kod formatowania !r oznacza, że należy zastosować dane wyjściowe metody __repr__() zamiast zwracanych domyślnie danych wyjściowych metody __str__(). Możesz wypróbować ten kod dla przedstawionej wcześniej klasy. Uzyskasz wtedy następujący efekt: >>> p = Pair(3, 4) >>> print('p to {0!r}'.format(p)) p to Pair(3, 4) >>> print('p to {0}'.format(p)) p to (3, 4) >>>
Omówienie Definiowanie metod __repr__() i __str__() to często dobre rozwiązanie, pomagające uprościć debugowanie i wyświetlanie obiektów. Dzięki samemu wyświetleniu obiektu lub zarejestrowaniu go w dzienniku programista może uzyskać dodatkowe przydatne informacje na temat zawartości danego obiektu. Metoda __repr__() standardowo generuje tekst w formie eval(repr(x)) == x. Jeśli jest to niemożliwe lub niepożądane, można utworzyć przydatną reprezentację tekstową umieszczoną między znakami < i >. Oto przykład: >>> f = open('file.dat') >>> f <_io.TextIOWrapper name='file.dat' mode='r' encoding='UTF-8'> >>>
Jeśli programista nie zdefiniuje metody __str__(), jako rozwiązanie rezerwowe używana będzie metoda __repr__(). Wywołanie format() w rozwiązaniu wygląda nietypowo, jednak kod formatowania {0.x} oznacza atrybut x argumentu zerowego. W poniższej funkcji 0 to obiekt self: def __repr__(self): return 'Pair({0.x!r}, {0.y!r})'.format(self)
Zamiast tej techniki można wykorzystać operator % i następujący kod: def __repr__(self): return 'Pair(%r, %r)' % (self.x, self.y)
8.2. Modyfikowanie formatowania łańcuchów znaków Problem Programista chce, aby obiekt umożliwiał niestandardowe formatowanie w funkcji format() i metodach łańcuchów znaków.
Rozwiązanie Aby zmodyfikować formatowanie łańcuchów znaków, należy zdefiniować w klasie metodę __format__(). Oto przykład:
class Date: def __init__(self, year, month, day): self.year = year self.month = month self.day = day def __format__(self, code): if code == '': code = 'ymd' fmt = _formats[code] return fmt.format(d=self)
Dzięki temu obiekty klasy Date zapewniają obsługę operacji formatowania, co pokazano poniżej: >>> d = Date(2012, 12, 21) >>> format(d) '2012-12-21' >>> format(d, 'mdy') '12/21/2012' >>> 'Data to {:ymd}'.format(d) 'Data to 2012-12-21' >>> 'Data to {:mdy}'.format(d) 'Data to 12/21/2012' >>>
Omówienie Metoda __format__() zapewnia w Pythonie dostęp do mechanizmu formatowania łańcuchów znaków. Należy podkreślić, że sposób interpretowania kodów formatowania zależy od klasy. Dlatego jako kody można stosować dowolny tekst. Przyjrzyj się poniższemu fragmentowi modułu datetime: >>> from datetime import date >>> d = date(2012, 12, 21) >>> format(d) '2012-12-21' >>> format(d,'%A, %B %d, %Y') 'Friday, December 21, 2012' >>> 'Koniec nastąpi {:%d%b %Y}. Żegnajcie'.format(d) 'Koniec nastąpi 21 Dec 2012. Żegnajcie' >>>
Istnieją pewne konwencje związane z formatowaniem typów wbudowanych. Ich formalną specyfikację znajdziesz w dokumentacji modułu string (http://docs.python.org/3/library/ string.html).
8.2. Modyfikowanie formatowania łańcuchów znaków
227
8.3. Dodawanie do obiektów obsługi protokołu zarządzania kontekstem Problem Programista chce, aby obiekty obsługiwały protokół zarządzania kontekstem (polecenie with).
Rozwiązanie Aby obiekt był zgodny z poleceniem with, trzeba zaimplementować metody __enter__() i __exit__(). Przyjrzyj się poniższej klasie, która udostępnia połączenia sieciowe: from socket import socket, AF_INET, SOCK_STREAM class LazyConnection: def __init__(self, address, family=AF_INET, type=SOCK_STREAM): self.address = address self.family = AF_INET self.type = SOCK_STREAM self.sock = None def __enter__(self): if self.sock is not None: raise RuntimeError('Połączenie zostało już nawiązane') self.sock = socket(self.family, self.type) self.sock.connect(self.address) return self.sock def __exit__(self, exc_ty, exc_val, tb): self.sock.close() self.sock = None
Klasa ta reprezentuje połączenie sieciowe, jednak początkowo nie wykonuje żadnych operacji (m.in. nie próbuje nawiązać połączenia). Połączenie jest nawiązywane i zrywane za pomocą polecenia with (na żądanie). Oto przykład: from functools import partial conn = LazyConnection(('www.python.org', 80)) # Połączenie jest zamknięte with conn as s: # Wywołanie conn.__enter__() — połączenie jest otwarte s.send(b'GET /index.html HTTP/1.0\r\n') s.send(b'Host: www.python.org\r\n') s.send(b'\r\n') resp = b''.join(iter(partial(s.recv, 8192), b'')) # Wywołanie conn.__exit__() — połączenie jest zamknięte
Omówienie Główną regułą przy pisaniu menedżera kontekstu jest to, że jego kod ma otaczać blok instrukcji zdefiniowany za pomocą polecenia with. Przy pierwszym napotkaniu tego polecenia uruchamiana jest metoda __enter__(). Jeśli metoda ta zwraca wartość, jest ona umieszczana w zmiennej wskazanej za pomocą kwalifikatora as. Następnie wykonywane są polecenia z instrukcji with. W ostatnim kroku uruchamiana jest metoda __exit__(), która wykonuje operacje porządkujące. 228
Rozdział 8. Klasy i obiekty
Ten przepływ sterowania ma miejsce niezależnie od tego, co dzieje się w poleceniu with (także gdy zgłoszone zostaną wyjątki). Trzy argumenty metody __exit__() to typ wyjątku, wartość i ślad prowadzący do nieobsłużonych wyjątków (jeśli istnieją). W metodzie __exit__() można wykorzystać informacje o wyjątkach lub zignorować je, nie wykonując żadnych operacji i zwracając None. Jeśli metoda __exit__() zwróci True, wyjątek jest usuwany, jakby nic się nie stało, a program kontynuuje wykonywanie operacji po bloku with. Ciekawym pytaniem związanym z tą recepturą jest to, czy klasa LazyConnection umożliwia korzystanie z połączenia w zagnieżdżonych poleceniach with. Tu w danym momencie może działać tylko jedno połączenie z gniazdem. Jeśli gniazdo jest już używane, próba ponownego wywołania with prowadzi do zgłoszenia wyjątku. Aby zlikwidować to ograniczenie, należy nieco zmienić kod: from socket import socket, AF_INET, SOCK_STREAM class LazyConnection: def __init__(self, address, family=AF_INET, type=SOCK_STREAM): self.address = address self.family = AF_INET self.type = SOCK_STREAM self.connections = [] def __enter__(self): sock = socket(self.family, self.type) sock.connect(self.address) self.connections.append(sock) return sock def __exit__(self, exc_ty, exc_val, tb): self.connections.pop().close() # Przykład zastosowania from functools import partial conn = LazyConnection(('www.python.org', 80)) with conn as s1: ... with conn as s2: ... # s1 i s2 to niezależne gniazda
W drugiej wersji klasa LazyConnections pełni funkcję fabryki połączeń. Wewnętrznie używana jest lista do przechowywania stosu. Przy każdym wywołaniu metoda __enter__() tworzy nowe połączenie i dodaje je na stos. Metoda __exit__() zdejmuje ze stosu ostatnie połączenie i zamyka je. Jest to niuans, który jednak umożliwia jednoczesne tworzenie wielu połączeń w zagnieżdżonych poleceniach with. Menedżery kontekstu najczęściej są używane w programach, które muszą zarządzać zasobami — plikami, połączeniami sieciowymi i blokadami. Ważną cechą takich zasobów jest to, że trzeba je jawnie zamykać i zwalniać, jeśli mają poprawnie działać. Jeśli np. zajmiesz blokadę, musisz ją później zwolnić. W przeciwnym razie w programie może wystąpić zakleszczenie. Napisanie metod __enter__() i __exit__() oraz korzystanie z nich za pomocą polecenia with pomaga uniknąć takich problemów, ponieważ kod porządkujący z metody __exit__() jest uruchamiany niezależnie od okoliczności. Inny sposób korzystania z menedżerów kontekstu znajdziesz w module contextmanager (zobacz recepturę 9.22). Bezpieczną ze względu na wątki wersję tej receptury opisano w recepturze 12.6.
8.3. Dodawanie do obiektów obsługi protokołu zarządzania kontekstem
229
8.4. Zmniejszanie zużycia pamięci przy tworzeniu dużej liczby obiektów Problem Program tworzy dużą liczbę obiektów (liczoną w milionach) i zużywa dużo pamięci.
Rozwiązanie W klasach, które przede wszystkim pełnią funkcję prostych struktur danych, często można znacznie zmniejszyć ilość pamięci zajmowaną przez obiekty, dodając do definicji klasy atrybut __slots__. Oto przykład: class Date: __slots__ = ['year', 'month', 'day'] def __init__(self, year, month, day): self.year = year self.month = month self.day = day
Gdy zdefiniujesz atrybut __slots__, Python będzie stosował znacznie zwięźlejszą reprezentację obiektów. Zamiast dodawać słownik do każdego obiektu, Python tworzy wtedy obiekty oparte na małej tablicy o stałym rozmiarze (przypominającej krotkę lub listę). Nazwy atrybutów wymienione w specyfikatorze __slots__ są wewnętrznie odwzorowywane na konkretne indeksy tablicy. Efektem ubocznym stosowania tej techniki jest to, że do obiektów nie można dodawać nowych atrybutów. Dozwolone są tylko atrybuty wymienione w specyfikatorze __slots__.
Omówienie Ilość pamięci, jaką można zaoszczędzić dzięki tej technice, zależy od liczby i typu atrybutów. Zwykle ilość zajmowanej pamięci jest tu podobna jak przy przechowywaniu danych w krotce. W 64-bitowej wersji Pythona zapisanie jednego obiektu typu Date w standardowy sposób wymaga 428 bajtów pamięci. Opisana tu technika pozwala zmniejszyć tę ilość do 156 bajtów. W programach, które manipulują jednocześnie dużą liczbą dat, zapewnia to znaczne ograniczenie ilości zajmowanej pamięci. Choć może się wydawać, że przedstawione tu rozwiązanie jest przydatne w wielu sytuacjach, staraj się go nie nadużywać. W wielu miejscach Pythona stosuje się standardowy kod oparty na słownikach. Ponadto klasy utworzone za pomocą opisanej techniki nie obsługują niektórych mechanizmów, np. wielodziedziczenia. Dlatego technikę tę należy stosować tylko w tych klasach, które są często używane w programie (np. gdy program tworzy miliony obiektów danej klasy). Częstym błędem związanym ze specyfikatorem __slots__ jest traktowanie go jako narzędzia zapewniającego hermetyzację, które uniemożliwia użytkownikom dodawanie nowych atrybutów do obiektów. Choć stosowanie tej techniki rzeczywiście ma taki efekt, nie została ona opracowana w tym celu. Specyfikator __slots__ od zawsze jest narzędziem przeznaczonym do optymalizowania kodu.
230
Rozdział 8. Klasy i obiekty
8.5. Hermetyzowanie nazw w klasie Problem Programista chce hermetyzować prywatne dane w obiektach danej klasy, jednak martwi go to, że Python nie zapewnia kontroli dostępu.
Rozwiązanie Programiści Pythona zamiast hermetyzować dane za pomocą mechanizmów języka, powinni zwrócić uwagę na pewne konwencje nazewnicze związane z przeznaczeniem danych i metod. Pierwsza konwencja związana jest z tym, że każda nazwa rozpoczynająca się od jednego podkreślenia (_) oznacza jednostkę wewnętrzną. Oto przykład: class A: def __init__(self): self._internal = 0 self.public = 1
# Atrybut wewnętrzny # Atrybut publiczny
def public_method(self): ''' Metoda publiczna ''' ... def _internal_method(self): ...
Python nie blokuje dostępu do jednostek wewnętrznych. Jednak korzystanie z nich jest uznawane za nieeleganckie i może prowadzić do powstania kodu podatnego na błędy. Warto też zauważyć, że początkowe podkreślenie stosuje się też w nazwach modułów i funkcji modułów. Jeśli natrafisz kiedyś na moduł o nazwie rozpoczynającej się od podkreślenia (np. _socket), pamiętaj, że jest to implementacja wewnętrzna. Także z funkcji modułów o takich nazwach (np. sys._getframe()) należy korzystać z wielką ostrożnością. Możesz też napotkać dwa początkowe podkreślenia (__) w nazwach w definicjach klas. Oto przykład: class B: def __init__(self): self.__private = 0 def __private_method(self): ... def public_method(self): ... self.__private_method() ...
Dwa początkowe podkreślenia powodują, że nazwa jest modyfikowana. Nazwy prywatnych atrybutów z poprzedniej klasy są zmieniane na _B__private i _B__private_method. Możesz się zastanawiać, po co stosuje się tego rodzaju modyfikacje. Jest to związane z dziedziczeniem. Atrybutów tego rodzaju nie można przesłonić w wyniku dziedziczenia. Oto przykład: class C(B): def __init__(self): super().__init__() self.__private = 1
# Nie przesłania atrybutu B.__private
8.5. Hermetyzowanie nazw w klasie
231
# Nie przesłania metody override B.__private_method() def __private_method(self): ...
Tu nazwy prywatnych jednostek __private i __private_method są zmieniane na _C__private i _C__private_method. Różnią się one od zmodyfikowanych nazw z klasy bazowej (nadrzędnej) B.
Omówienie Ponieważ istnieją dwie różne konwencje tworzenia atrybutów prywatnych (z pojedynczymi i podwójnymi podkreśleniami), można się zastanawiać, którą z nich stosować. W większości kodu nazwy atrybutów prywatnych powinny rozpoczynać się od pojedynczego podkreślenia. Jeśli jednak wiesz, że na podstawie kodu tworzone będą klasy pochodne (podrzędne), a niektóre wewnętrzne atrybuty należy ukryć, zastosuj podwójne podkreślenie. Ponadto programiści chcą czasem zdefiniować zmienną o nazwie takiej samej jak zarezerwowane słowo. W takiej sytuacji należy zastosować pojedynczy końcowy znak podkreślenia: lambda_ = 2.0
# Końcowy znak _ pozwala uniknąć konfliktu ze słowem kluczowym lambda
Nie należy tu stosować początkowego znaku podkreślenia, ponieważ może on błędnie sugerować inne znaczenie nazwy (tzn. informuje, że wartość jest prywatna, a nie o tym, że programista chce uniknąć konfliktu nazw). Podanie pojedynczego końcowego podkreślenia pozwala rozwiązać ten problem.
8.6. Tworzenie atrybutów zarządzanych Problem Programista chce dodać nowe kroki przetwarzania (sprawdzanie typu lub poprawności) w procesie pobierania lub ustawiania atrybutów obiektu.
Rozwiązanie Prostym sposobem na zmodyfikowanie dostępu do atrybutu jest zdefiniowanie go jako właściwości (za pomocą słowa property). W poniższym kodzie zdefiniowano właściwość, która zapewnia proste sprawdzanie typu atrybutu: class Person: def __init__(self, first_name): self.first_name = first_name # Funkcja do pobierania wartości @property def first_name(self): return self._first_name # Funkcja do ustawiania wartości @first_name.setter def first_name(self, value): if not isinstance(value, str): raise TypeError('Oczekiwano łańcucha znaków') self._first_name = value
232
Rozdział 8. Klasy i obiekty
# Funkcja do usuwania wartości (opcjonalna) @first_name.deleter def first_name(self): raise AttributeError("Nie można usunąć atrybutu")
W tym kodzie znajdują się trzy powiązane metody. Wszystkie muszą mieć tę samą nazwę. Pierwsza metoda służy do pobierania wartości. To tu atrybut first_name jest określony jako właściwość. Dwie pozostałe metody to opcjonalne funkcje do ustawiania i usuwania właściwości first_name. Warto zauważyć, że dekoratorów @first_name.setter i @first_name.deleter nie można zdefiniować, jeśli wcześniej za pomocą słowa @property nie określono atrybutu first_name jako właściwości. Ważną cechą właściwości jest to, że wygląda ona jak normalny atrybut, jednak przy dostępie do niego automatycznie uruchamiane są metody do pobierania, ustawiania i usuwania wartości. Oto przykład: >>> a = Person('Gucio') >>> a.first_name # Wywołuje funkcję do pobierania wartości 'Gucio' >>> a.first_name = 42 # Wywołuje funkcję do ustawiania wartości Traceback (most recent call last): File "", line 1, in File "prop.py", line 14, in first_name raise TypeError('Oczekiwano łańcucha znaków') TypeError: Oczekiwano łańcucha znaków >>> del a.first_name Traceback (most recent call last): File "", line 1, in AttributeError: can't delete attribute >>>
Przy stosowaniu właściwości powiązane z nimi dane (jeśli są potrzebne) należy gdzieś przechowywać. Dlatego w metodach do pobierania i ustawiania wartości kod manipuluje bezpośrednio atrybutem _first_name, który zawiera dane. Możesz się zastanawiać, dlaczego metoda __init__() ustawia atrybut self.first_name zamiast self._first_name. W tym przykładzie jedynym zadaniem właściwości jest sprawdzanie typu przy ustawianiu wartości atrybutu. Dlatego operacja ta powinna być wykonywana także w trakcie inicjowania obiektu. Polecenie self.first_image sprawia, że w operacji ustawiania wartości używana jest przeznaczona do tego funkcja (kod nie pomija jej przez bezpośredni dostęp do atrybutu self._ first_name). Właściwości można też definiować dla istniejących metod do pobierania i ustawiania wartości. Oto przykład: class Person: def __init__(self, first_name): self.set_first_name(first_name) # Funkcja do pobierania wartości def get_first_name(self): return self._first_name # Funkcja do ustawiania wartości def set_first_name(self, value): if not isinstance(value, str): raise TypeError('Oczekiwano łańcucha znaków') self._first_name = value
8.6. Tworzenie atrybutów zarządzanych
233
# Funkcja do usuwania wartości (opcjonalna) def del_first_name(self): raise AttributeError("Nie można usunąć atrybutu") # Tworzenie właściwości na podstawie istniejących metod do pobierania i ustawiania wartości name = property(get_first_name, set_first_name, del_first_name)
Omówienie Właściwość jest kolekcją powiązanych ze sobą metod. Jeśli przyjrzysz się klasie zawierającej właściwość, znajdziesz metody powiązane z atrybutami fget, fset i fdel właściwości. Oto przykład: >>> Person.first_name.fget >>> Person.first_name.fset >>> Person.first_name.fdel >>>
Standardowo metod fget i fset nie wywołuje się bezpośrednio — są one uruchamiane automatycznie przy dostępie do właściwości. Z właściwości należy korzystać tylko wtedy, gdy potrzebne są dodatkowe operacje przy dostępie do atrybutu. Czasem programiści Javy i podobnych języków są nauczeni, że dostęp do atrybutów zawsze powinien odbywać się za pomocą funkcji do pobierania i ustawiania wartości, dlatego piszą następujący kod: class Person: def __init__(self, first_name): self.first_name = name @property def first_name(self): return self._first_name @first_name.setter def first_name(self, value): self._first_name = value
Nie należy pisać właściwości, które — jak w tym kodzie — nie wykonują żadnych dodatkowych operacji. Po pierwsze, kod jest przez to dłuższy i trudniejszy do zrozumienia. Po drugie, programy działają wtedy wolniej. Po trzecie, takie podejście nie przynosi żadnych konkretnych korzyści. Jeśli później zdecydujesz, że do zwykłego atrybutu trzeba dodać nowe operacje, możesz przekształcić go na właściwość bez konieczności modyfikowania innego kodu. Wynika to z tego, że składnia kodu korzystającego z atrybutu się nie zmienia. Właściwości można też wykorzystać do definiowania atrybutów obliczanych. Chodzi o atrybuty, które nie są przechowywane w gotowej postaci, tylko wyliczane na żądanie. Oto przykład: import math class Circle: def __init__(self, radius): self.radius = radius @property def area(self): return math.pi * self.radius ** 2 @property def perimeter(self): return 2 * math.pi * self.radius
234
Rozdział 8. Klasy i obiekty
Tu wykorzystanie właściwości prowadzi do bardzo jednorodnego interfejsu obiektu, ponieważ radius, area i perimeter są używane jak proste atrybuty, a nie jak połączenie atrybutów i wywołań metod. Oto przykład: >>> c = Circle(4.0) >>> c.radius 4.0 >>> c.area # Zwróć uwagę na brak znaków () 50.26548245743669 >>> c.perimeter # Zwróć uwagę na brak znaków () 25.132741228718345 >>>
Choć właściwości zapewniają elegancki interfejs programowania, czasem możesz zechcieć bezpośrednio wywołać funkcje do pobierania i ustawiania wartości: >>> p = Person('Gucio') >>> p.get_first_name() 'Gucio' >>> p.set_first_name('Maja') >>>
Często dzieje się tak, gdy kod Pythona jest łączony z większą infrastrukturą systemów lub programów. Możliwe, że programista chce włączyć klasę Pythona do dużego rozproszonego systemu opartego na zdalnych wywołaniach procedur lub obiektach rozproszonych. W takim środowisku łatwiej jest bezpośrednio używać metod do pobierania i ustawiania wartości (stosując standardowe wywołania), niż korzystać z właściwości, która pośrednio je wywołuje. Ponadto w Pythonie nie należy pisać kodu, w którym pojawia się dużo powtarzalnych definicji właściwości: class Person: def __init__(self, first_name, last_name): self.first_name = first_name self.last_name = last_name @property def first_name(self): return self._first_name @first_name.setter def first_name(self, value): if not isinstance(value, str): raise TypeError('Oczekiwano łańcucha znaków') self._first_name = value # Powtórzony kod właściwości, ale dla innej nazwy (nieeleganckie!) @property def last_name(self): return self._last_name @last_name.setter def last_name(self, value): if not isinstance(value, str): raise TypeError('Oczekiwano łańcucha znaków') self._last_name = value
Powtarzający się kod prowadzi do niepotrzebnie długich, podatnych na błędy i nieeleganckich programów. Istnieją znacznie lepsze sposoby na uzyskanie tego samego efektu — wystarczy wykorzystać deskryptory i domknięcia (zobacz receptury 8.9 i 9.21).
8.6. Tworzenie atrybutów zarządzanych
235
8.7. Wywoływanie metod klasy bazowej Problem Programista chce wywołać metodę klasy bazowej zamiast przesłaniającej ją wersji metody z klasy pochodnej.
Rozwiązanie Aby wywołać metodę z klasy bazowej, zastosuj funkcję super(): class A: def spam(self): print('A.spam') class B(A): def spam(self): print('B.spam') super().spam() # Wywołanie metody spam() z klasy bazowej
Funkcję super() bardzo często stosuje się w metodzie __init__(), aby mieć pewność, że elementy z klasy bazowej zostały poprawnie zainicjowane: class A: def __init__(self): self.x = 0 class B(A): def __init__(self): super().__init__() self.y = 1
Funkcja super() często używana jest także w kodzie, który przesłania metody specjalne Pythona. Oto przykład: class Proxy: def __init__(self, obj): self._obj = obj # Delegowanie pobierania atrybutu do obiektu wewnętrznego def __getattr__(self, name): return getattr(self._obj, name) # Delegowanie przypisywania wartości do atrybutu def __setattr__(self, name, value): if name.startswith('_'): super().__setattr__(name, value) else: setattr(self._obj, name, value)
# Wywołanie pierwotnej wersji __setattr__
W tym kodzie __setattr__() sprawdza nazwę. Jeśli zaczyna się ona od podkreślenia (_), wywoływana jest pierwotna wersja __setattr__() (przy użyciu funkcji super()). W przeciwnym razie zadanie delegowane jest do wewnętrznie przechowywanego obiektu self._obj. Wygląda to dość zaskakująco, ale funkcja super() działa nawet wtedy, gdy nie określono bezpośrednio klasy bazowej.
236
Rozdział 8. Klasy i obiekty
Omówienie Poprawne stosowanie funkcji super() jest jednym z najsłabiej zrozumiałych aspektów Pythona. Czasem możesz natrafić na kod, który bezpośrednio wywołuje metody klasy bazowej: class Base: def __init__(self): print('Base.__init__') class A(Base): def __init__(self): Base.__init__(self) print('A.__init__')
Choć rozwiązanie to zwykle działa, może prowadzić do dziwnych problemów w zaawansowanym kodzie z wykorzystaniem wielodziedziczenia. Przyjrzyj się np. poniższemu kodowi: class Base: def __init__(self): print('Base.__init__') class A(Base): def __init__(self): Base.__init__(self) print('A.__init__') class B(Base): def __init__(self): Base.__init__(self) print('B.__init__') class C(A,B): def __init__(self): A.__init__(self) B.__init__(self) print('C.__init__')
Gdy uruchomisz ten kod, zobaczysz, że metoda Base.__init__() jest wywoływana dwukrotnie: >>> c = C() Base.__init__ A.__init__ Base.__init__ B.__init__ C.__init__ >>>
Możliwe, że dwukrotne wywołanie metody Base.__init__() jest nieszkodliwe, ale może też prowadzić do problemów. Jeśli wprowadzisz zmiany i zastosujesz funkcję super(), kod zacznie działać prawidłowo: class Base: def __init__(self): print('Base.__init__') class A(Base): def __init__(self): super().__init__() print('A.__init__') class B(Base): def __init__(self):
8.7. Wywoływanie metod klasy bazowej
237
super().__init__() print('B.__init__') class C(A,B): def __init__(self): super().__init__() # Tylko jedno wywołanie funkcji super() print('C.__init__')
Gdy uruchomisz nową wersję kodu, przekonasz się, że każda metoda __init__() jest uruchamiana tylko raz: >>> c = C() Base.__init__ B.__init__ A.__init__ C.__init__ >>>
Aby wyjaśnić, dlaczego to rozwiązanie działa, trzeba wytłumaczyć działanie dziedziczenia w Pythonie. Dla każdej definiowanej klasy Python wyznacza listę kolejności określania metod (ang. method resolution order — MRO). Lista MRO zawiera wszystkie klasy bazowe uporządkowane jedna po drugiej. Oto przykład: >>> C.__mro__ (, , , , ) >>>
Przy obsłudze dziedziczenia Python rozpoczyna od klasy pierwszej od lewej i przechodzi przez listę MRO w prawo do momentu znalezienia pierwszego pasującego atrybutu. Do wyznaczania listy MRO służy technika C3 Linearization. Pomińmy tu matematyczne aspekty tego algorytmu — wykorzystuje on sortowanie przez scalanie list MRO klas bazowych z uwzględnieniem trzech ograniczeń: klasy podrzędne są sprawdzane przed nadrzędnymi; grupa klas nadrzędnych jest sprawdzana w kolejności występowania na liście; jeśli można wybrać dwie klasy, używana jest klasa podrzędna pierwszej klasy nadrzędnej.
Wystarczy zapamiętać, że kolejność klas na liście MRO ma sens niemal w każdej definiowanej hierarchii klas. Gdy używasz funkcji super(), Python kontynuuje przeszukiwanie od następnej klasy z listy MRO. Jeśli każda ponownie zdefiniowana metoda jednokrotnie wywołuje funkcję super(), sterowanie przechodzi przez całą listę MRO i każda z metod jest uruchamiana tylko raz. To dlatego w drugim przykładzie nie ma dwukrotnych wywołań metody Base.__init__(). Nieco zaskakującą cechą funkcji super() jest to, że nie zawsze przechodzi do bezpośredniej klasy bazowej następnej klasy z listy MRO. Ponadto z funkcji tej można czasem korzystać nawet w klasach, które w ogóle nie mają bezpośrednio określonej klasy bazowej. Przyjrzyj się następującej klasie: class A: def spam(self): print('A.spam') super().spam()
238
Rozdział 8. Klasy i obiekty
Jeśli spróbujesz wykorzystać tę klasę w kodzie, zauważysz, że nie działa: >>> a = A() >>> a.spam() A.spam Traceback (most recent call last): File "", line 1, in File "", line 4, in spam AttributeError: 'super' object has no attribute 'spam' >>>
Zobacz jednak, co się stanie, gdy klasa ta pojawi się w kodzie, w którym wykorzystano wielodziedziczenie: >>> class B: ... def spam(self): ... print('B.spam') ... >>> class C(A,B): ... pass ... >>> c = C() >>> c.spam() A.spam B.spam >>>
Widać tu, że polecenie super().spam() z klasy A powoduje wywołanie metody spam() z klasy B, która jest zupełnie niepowiązana z A! Aby to zrozumieć, należy przyjrzeć się liście MRO klasy C: >>> C.__mro__ (, , , ) >>>
Funkcję super() stosuje się w ten sposób zwykle przy definiowaniu klas łączonych (zobacz receptury 8.13 i 8.18). Ponieważ jednak funkcja super() może wywoływać nieoczekiwane metody, warto starać się przestrzegać kilku prostych reguł. Po pierwsze, należy się upewnić, że w hierarchii dziedziczenia wszystkie metody o tej samej nazwie mają zgodne sygnatury (mają tę samą liczbę argumentów o identycznych nazwach). Dzięki temu funkcja super() będzie mogła wywoływać metody klas innych niż bezpośrednie klasy bazowe danej klasy. Po drugie, warto się upewnić, że wywoływana metoda znajduje się w nadrzędnej klasie hierarchii. Dzięki temu wiadomo, że wyszukiwanie metody w klasach z listy MRO zakończy się powodzeniem. W społeczności programistów Pythona stosowanie funkcji super() jest kwestią dyskusyjną. Jednak w nowym kodzie prawdopodobnie warto z niej korzystać. Raymond Hettinger zamieścił na blogu doskonały artykuł Python’s super() Considered Super! (http://rhettinger.wordpress.com/ 2011/05/26/super-considered-super/). Znajdziesz tam więcej przykładów i przyczyn dowodzących, że funkcja super() naprawdę jest super.
8.7. Wywoływanie metod klasy bazowej
239
8.8. Rozszerzanie właściwości w klasie pochodnej Problem Programista chce rozszerzyć w klasie pochodnej funkcje właściwości zdefiniowanej w klasie bazowej.
Rozwiązanie Przyjrzyj się poniższemu kodowi ze zdefiniowaną właściwością: class Person: def __init__(self, name): self.name = name # Funkcja do pobierania wartości @property def name(self): return self._name # Funkcja do ustawiania wartości @name.setter def name(self, value): if not isinstance(value, str): raise TypeError('Oczekiwano łańcucha znaków') self._name = value # Funkcja do usuwania wartości @name.deleter def name(self): raise AttributeError("Nie można usunąć atrybutu")
Oto przykładowa klasa pochodna od klasy Person. Wzbogacono tu możliwości właściwości name: class SubPerson(Person): @property def name(self): print('Pobieranie imienia') return super().name @name.setter def name(self, value): print('Ustawianie imienia na', value) super(SubPerson, SubPerson).name.__set__(self, value) @name.deleter def name(self): print('Usuwanie imienia') super(SubPerson, SubPerson).name.__delete__(self)
Oto przykład zastosowania nowej klasy: >>> s = SubPerson('Gucio') Ustawianie imienia na Gucio >>> s.name Pobieranie imienia 'Gucio' >>> s.name = 'Maja' Ustawianie imienia na Maja >>> s.name = 42
240
Rozdział 8. Klasy i obiekty
Traceback (most recent call last): File "", line 1, in File "example.py", line 16, in name raise TypeError('Oczekiwano łańcucha znaków') TypeError: Oczekiwano łańcucha znaków >>>
Jeśli chcesz jedynie rozszerzyć jedną metodę właściwości, zastosuj kod w następującej postaci: class SubPerson(Person): @Person.name.getter def name(self): print('Pobieranie imienia') return super().name
Gdy modyfikowana jest tylko funkcja ustawiająca wartość, można zastosować następujący kod: class SubPerson(Person): @Person.name.setter def name(self, value): print('Ustawianie imienia na', value) super(SubPerson, SubPerson).name.__set__(self, value)
Omówienie Rozszerzanie właściwości w klasie pochodnej powoduje kilka wyrafinowanych problemów. Związane są one z tym, że właściwości definiuje się jako kolekcje metod do pobierania, ustawiania i usuwania wartości, a nie jako pojedyncze metody. Dlatego przy rozszerzaniu właściwości trzeba ustalić, czy modyfikowane są wszystkie metody, czy tylko jedna z nich. W pierwszym przykładzie modyfikowana jest jednocześnie definicja wszystkich metod właściwości. W każdej z nich użyto funkcji super() do wywołania pierwotnej wersji kodu. Wywołanie super(SubPerson, SubPerson).name.__set__(self, value) w funkcji ustawiającej wartość to nie pomyłka. Aby przekazać zadanie do pierwotnej wersji funkcji ustawiającej wartość, trzeba przekazać sterowanie do metody __set__() wcześniej zdefiniowanej właściwości name. Jednak jedyny sposób na uzyskanie dostępu do tej metody polega na użyciu jej jak zmiennej klasy (a nie jak zmiennej egzemplarza). Tak właśnie działa wywołanie super(SubPerson, SubPerson). Jeśli chcesz zmienić definicję tylko jednej z metod, nie wystarczy użyć słowa @property. Poniższy kod nie zadziała: class SubPerson(Person): @property # Nie działa def name(self): print('Pobieranie imienia') return super().name
Gdy wypróbujesz uzyskany kod, przekonasz się, że funkcja do ustawiania wartości jest niedostępna: >>> s = SubPerson('Gucio') Traceback (most recent call last): File "", line 1, in File "example.py", line 5, in __init__ self.name = name AttributeError: can't set attribute >>>
8.8. Rozszerzanie właściwości w klasie pochodnej
241
Zamiast tego należy zmienić kod na wersję przedstawioną w rozwiązaniu: class SubPerson(Person): @Person.getter def name(self): print('Pobieranie imienia') return super().name
Powoduje on skopiowanie wszystkich wcześniej zdefiniowanych metod właściwości i zastąpienie funkcji do pobierania wartości. Teraz kod działa w oczekiwany sposób: >>> s = SubPerson('Gucio') >>> s.name Pobieranie imienia 'Gucio' >>> s.name = 'Maja' >>> s.name Pobieranie imienia 'Maja' >>> s.name = 42 Traceback (most recent call last): File "", line 1, in File "example.py", line 16, in name raise TypeError('Oczekiwano łańcucha znaków') TypeError: Oczekiwano łańcucha znaków >>>
W tym rozwiązaniu nie można zastąpić zapisanej na stałe nazwy klasy Person bardziej uniwersalnym odpowiednikiem. Jeśli nie wiesz, w której klasie bazowej zdefiniowano właściwość, powinieneś zastosować rozwiązanie oparte na ponownym definiowaniu wszystkich metod właściwości i używaniu funkcji super() do przekazywania sterowania do pierwotnej implementacji. Warto zauważyć, że pierwszą z technik przedstawionych w tej recepturze można wykorzystać także do rozszerzania deskryptorów (zobacz recepturę 8.9). Oto przykład: # Deskryptor class String: def __init__(self, name): self.name = name def __get__(self, instance, cls): if instance is None: return self return instance.__dict__[self.name] def __set__(self, instance, value): if not isinstance(value, str): raise TypeError('Oczekiwano łańcucha znaków') instance.__dict__[self.name] = value # Klasa z deskryptorem class Person: name = String('name') def __init__(self, name): self.name = name # Rozszerzenie deskryptora przy użyciu właściwości class SubPerson(Person): @property def name(self): print('Pobieranie imienia') return super().name
Na zakończenie warto wspomnieć, że do czasu wydania tej książki modyfikowanie w klasach pochodnych metod ustawiających i pobierających wartości może zostać uproszczone. Przedstawione tu rozwiązanie wciąż będzie działać, jednak błąd zgłoszony na stronie z problemami z Pythonem (http://bugs.python.org/issue14965) może doprowadzić do udostępnienia bardziej przejrzystej techniki w nowych wersjach tego języka.
8.9. Tworzenie nowego rodzaju atrybutów klasy lub egzemplarza Problem Programista chce utworzyć nowy rodzaj atrybutu egzemplarza, udostępniający dodatkowe możliwości (np. sprawdzanie typu).
Rozwiązanie Jeśli chcesz utworzyć atrybut egzemplarza nowego rodzaju, zdefiniuj go w formie klasy deskryptora. Oto przykład: # Deskryptor atrybutu całkowitoliczbowego ze sprawdzaniem typu class Integer: def __init__(self, name): self.name = name def __get__(self, instance, cls): if instance is None: return self else: return instance.__dict__[self.name] def __set__(self, instance, value): if not isinstance(value, int): raise TypeError('Oczekiwano liczby całkowitej') instance.__dict__[self.name] = value def __delete__(self, instance): del instance.__dict__[self.name]
Deskryptor to klasa z trzema podstawowymi operacjami dostępu do atrybutu (pobieraniem, ustawianiem i usuwaniem wartości) w postaci metod specjalnych __get__(), __set__() i __delete__(). Metody te przyjmują dane wejściowe w formie obiektu, a następnie w odpowiedni sposób manipulują jego słownikiem.
8.9. Tworzenie nowego rodzaju atrybutów klasy lub egzemplarza
243
Aby móc używać deskryptora, jego egzemplarz należy umieścić w definicji klasy jako zmienną klasy. Oto przykład: class Point: x = Integer('x') y = Integer('y') def __init__(self, x, y): self.x = x self.y = y
Dzięki temu dostęp do atrybutów deskryptora (x lub y) odbywa się poprzez metody __get__(), __set__() i __delete__(): >>> p = Point(2, 3) >>> p.x # Wywołuje Point.x.__get__(p,Point) 2 >>> p.y = 5 # Wywołuje Point.y.__set__(p, 5) >>> p.x = 2.3 # Wywołuje Point.x.__set__(p, 2.3) Traceback (most recent call last): File "", line 1, in File "descrip.py", line 12, in __set__ raise TypeError('Oczekiwano liczby całkowitej') TypeError: Oczekiwano liczby całkowitej >>>
Jako dane wejściowe każda metoda deskryptora przyjmuje używany obiekt. W celu wykonania żądanej operacji metody w odpowiedni sposób manipulują słownikiem obiektu (atrybutem __dict__). Atrybut self.name deskryptora przechowuje klucz używany do zapisywania danych w słowniku obiektu.
Omówienie Deskryptory obsługują operacje związane z większością mechanizmów klas Pythona — konstrukcjami @classmethod, @staticmethod i @property, a nawet specyfikacją atrybutu __slots__. Zdefiniowanie deskryptora pozwala przechwycić podstawowe operacje obiektu (pobieranie, ustawianie i usuwanie wartości) na bardzo niskim poziomie i dowolnie je zmodyfikować. Zapewnia to bardzo duże możliwości. Technika ta to jedno z najważniejszych narzędzi stosowanych przez autorów zaawansowanych bibliotek i platform. Jednym z problemów związanych z deskryptorami jest to, że można je zdefiniować tylko na poziomie klasy, a nie dla poszczególnych obiektów. Dlatego poniższy kod jest niepoprawny: # NIE zadziała class Point: def __init__(self, x, y): self.x = Integer('x') # Błąd! Trzeba użyć zmiennej klasy self.y = Integer('y') self.x = x self.y = y
Ponadto implementacja metody __get__() jest bardziej skomplikowana, niż może się to wydawać: # Deskryptor dla całkowitoliczbowego atrybutu ze sprawdzaniem typu class Integer: ... def __get__(self, instance, cls): if instance is None: return self else: return instance.__dict__[self.name] ...
244
Rozdział 8. Klasy i obiekty
Metoda __get__() jest nieco skomplikowana, ponieważ trzeba uwzględnić różnicę między zmiennymi egzemplarza a zmiennymi klasy. Gdy dostęp do deskryptora odbywa się przy użyciu zmiennej klasy, argument instance ma wartość None. Wtedy standardowo zwracany jest obiekt deskryptora (choć można też wykonać niestandardowe operacje). Oto przykład: >>> p = Point(2,3) >>> p.x # Wywołuje Point.x.__get__(p, Point) 2 >>> Point.x # Wywołuje Point.x.__get__(None, Point) <__main__.Integer object at 0x100671890> >>>
Deskryptory często są tylko jednym składnikiem większej platformy programowania obejmującej dekoratory lub metaklasy. Dlatego mogą być niewidoczne w kodzie. Oto przykładowy zaawansowany kod oparty na deskryptorze. Wykorzystano tu także dekorator: # Deskryptor dla atrybutu ze sprawdzaniem typu class Typed: def __init__(self, name, expected_type): self.name = name self.expected_type = expected_type def __get__(self, instance, cls): if instance is None: return self else: return instance.__dict__[self.name] def __set__(self, instance, value): if not isinstance(value, self.expected_type): raise TypeError('Oczekiwano ' + str(self.expected_type)) instance.__dict__[self.name] = value def __delete__(self, instance): del instance.__dict__[self.name] # Dekorator klasy, który dołącza deskryptor do wybranych atrybutów def typeassert(**kwargs): def decorate(cls): for name, expected_type in kwargs.items(): # Dołączanie deskryptora Typed do klasy setattr(cls, name, Typed(name, expected_type)) return cls return decorate # Przykład zastosowania @typeassert(name=str, shares=int, price=float) class Stock: def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price
Na zakończenie warto podkreślić, że zwykle nie należy tworzyć deskryptora, jeśli ma służyć wyłącznie do zmodyfikowania dostępu do jednego atrybutu konkretnej klasy. Wtedy łatwiej jest zastosować właściwość, co opisano w recepturze 8.6. Deskryptory są przydatniejsze w sytuacjach, gdy dany kod jest wielokrotnie używany (np. gdy programista chce wykorzystać deskryptor w setkach miejsc w kodzie lub udostępnić go jako funkcję biblioteki).
8.9. Tworzenie nowego rodzaju atrybutów klasy lub egzemplarza
245
8.10. Stosowanie właściwości obliczanych w leniwy sposób Problem Programista chce zdefiniować atrybut tylko do odczytu jako właściwość, która jest obliczana dopiero przy dostępie do niej. Jednak po operacji dostępu wartość ma być zachowywana, aby nie trzeba było ponownie jej obliczać.
Rozwiązanie Wydajnym sposobem na zdefiniowanie atrybutu obliczanego w leniwy sposób jest zastosowanie klasy deskryptora: class lazyproperty: def __init__(self, func): self.func = func def __get__(self, instance, cls): if instance is None: return self else: value = self.func(instance) setattr(instance, self.func.__name__, value) return value
Aby wykorzystać ten kod, należy umieścić go w klasie: import math class Circle: def __init__(self, radius): self.radius = radius @lazyproperty def area(self): print('Obliczanie powierzchni') return math.pi * self.radius ** 2 @lazyproperty def perimeter(self): print('Obliczanie obwodu') return 2 * math.pi * self.radius
Oto interaktywna sesja ilustrująca, jak działa ten kod: >>> c = Circle(4.0) >>> c.radius 4.0 >>> c.area Obliczanie powierzchni 50.26548245743669 >>> c.area 50.26548245743669 >>> c.perimeter Obliczanie obwodu 25.132741228718345 >>> c.perimeter 25.132741228718345 >>>
Zauważ, że komunikaty Obliczanie powierzchni i Obliczanie obwodu pojawiają się tylko raz. 246
Rozdział 8. Klasy i obiekty
Omówienie W wielu sytuacjach obliczanie atrybutów w leniwy sposób służy tylko poprawie wydajności. Technika ta pozwala uniknąć obliczania wartości dopóty, dopóki nie będzie potrzebna. Przedstawione tu rozwiązanie działa dokładnie w ten sposób, przy czym wykorzystano tu pewną cechę deskryptorów, która sprawia, że kod jest bardzo wydajny. Już w innych recepturach (np. w recepturze 8.9) pokazano, że gdy deskryptor jest umieszczany w definicji klasy, jego metody __get__(), __set__() i __delete__() są uruchamiane przy dostępie do atrybutu. Jeśli jednak w deskryptorze zdefiniowana jest tylko metoda __get__(), powiązanie między deskryptorem a klasą jest znacznie słabsze. Metoda __get__() jest uruchamiana tylko wtedy, gdy używany atrybut nie znajduje się w słowniku danego obiektu. Tę cechę wykorzystano w klasie lazyproperty. Metoda __get__() zapisuje obliczoną wartość w obiekcie, używając nazwy właściwości. Dzięki temu wartość jest zapisywana w słowniku obiektu i blokuje późniejsze obliczanie właściwości. Można się o tym przekonać w trakcie dokładniejszej analizy przykładu: >>> c = Circle(4.0) >>> # Pobieranie zmiennych egzemplarza >>> vars(c) {'radius': 4.0} >>> # Obliczanie powierzchni i sprawdzanie wartości zmiennych >>> c.area Computing area 50.26548245743669 >>> vars(c) {'area': 50.26548245743669, 'radius': 4.0} >>> # Zauważ, że tym razem dostęp nie powoduje obliczania właściwości >>> c.area 50.26548245743669 >>> # Po usunięciu zmiennej właściwość znów jest obliczana >>> del c.area >>> vars(c) {'radius': 4.0} >>> c.area Obliczanie powierzchni 50.26548245743669 >>>
Wadą tej receptury jest to, że obliczoną wartość można zmodyfikować. Oto przykład: >>> c.area Obliczanie powierzchni 50.26548245743669 >>> c.area = 25 >>> c.area 25 >>>
Jeśli stanowi to problem, można zastosować nieco wolniejszą wersję kodu: def lazyproperty(func): name = '_lazy_' + func.__name__ @property def lazy(self): if hasattr(self, name): return getattr(self, name)
8.10. Stosowanie właściwości obliczanych w leniwy sposób
247
else: value = func(self) setattr(self, name, value) return value return lazy
Jeśli uruchomisz tę wersję, odkryjesz, że ustawianie wartości jest niedozwolone: >>> c = Circle(4.0) >>> c.area Obliczanie powierzchni 50.26548245743669 >>> c.area 50.26548245743669 >>> c.area = 25 Traceback (most recent call last): File "", line 1, in AttributeError: can't set attribute >>>
Wadą tego rozwiązania jest to, że wszystkie operacje trzeba przekazywać do funkcji pobierającej wartość we właściwości. Jest to mniej wydajne niż samo sprawdzanie wartości w słowniku obiektu, co robiono w pierwotnym rozwiązaniu. Więcej informacji na temat właściwości i atrybutów zarządzanych znajdziesz w recepturze 8.6. Deskryptory opisano w recepturze 8.9.
8.11. Upraszczanie procesu inicjowania struktur danych Problem Programista tworzy wiele klas pełniących funkcję struktur danych, jednak męczy go pisanie powtarzalnych i szablonowych funkcji __init__().
Rozwiązanie Często można uogólnić inicjowanie struktur danych za pomocą jednej funkcji __init__() zdefiniowanej we wspólnej klasie bazowej. Oto przykład: class Structure: # Zmienna klasy określająca oczekiwane pola _fields= [] def __init__(self, *args): if len(args) != len(self._fields): raise TypeError('Oczekiwana liczba argumentów: {}'.format(len(self._fields))) # Ustawianie argumentów for name, value in zip(self._fields, args): setattr(self, name, value) # Przykładowe definicje klas if __name__ == '__main__': class Stock(Structure): _fields = ['name', 'shares', 'price'] class Point(Structure): _fields = ['x','y']
Gdy będziesz korzystał z gotowych klas, zauważysz, że ich tworzenie jest proste. Oto przykład: >>> s = Stock('ACME', 50, 91.1) >>> p = Point(2, 3) >>> c = Circle(4.5) >>> s2 = Stock('ACME', 50) Traceback (most recent call last): File "", line 1, in File "structure.py", line 6, in __init__ raise TypeError('Oczekiwana liczba argumentów: {}'.format(len(self._fields))) TypeError: Oczekiwana liczba argumentów: 3
Jeśli chcesz umożliwić podawanie argumentów za pomocą słów kluczowych, możesz zastosować kilka podejść. Jedna z możliwości to porównywanie nazw takich argumentów z nazwami ze zmiennej _fields: class Structure: _fields= [] def __init__(self, *args, **kwargs): if len(args) > len(self._fields): raise TypeError('Oczekiwana liczba argumentów {}'.format(len(self._fields))) # Ustawianie wszystkich argumentów podawanych na podstawie pozycji for name, value in zip(self._fields, args): setattr(self, name, value) # Ustawianie pozostałych argumentów, podawanych za pomocą słów kluczowych for name in self._fields[len(args):]: setattr(self, name, kwargs.pop(name)) # Sprawdzanie, czy nie podano dodatkowych, nieznanych argumentów if kwargs: raise TypeError('Nieprawidłowe argumenty: {}'.format(','.join(kwargs))) # Przykład zastosowania if __name__ == '__main__': class Stock(Structure): _fields = ['name', 'shares', 'price'] s1 = Stock('ACME', 50, 91.1) s2 = Stock('ACME', 50, price=91.1) s3 = Stock('ACME', shares=50, price=91.1)
Argumenty podawane za pomocą słów kluczowych można też wykorzystać do dodania do struktury atrybutów, których nie określono w zmiennej _fields. Oto przykład: class Structure: # Zmienna klasy określająca oczekiwane pola _fields= [] def __init__(self, *args, **kwargs): if len(args) != len(self._fields): raise TypeError('Oczekiwana liczba argumentów: {}'.format(len(self._fields))) # Ustawianie argumentów for name, value in zip(self._fields, args): setattr(self, name, value)
8.11. Upraszczanie procesu inicjowania struktur danych
249
# Ustawianie dodatkowych argumentów (jeśli występują) extra_args = kwargs.keys() - self._fields for name in extra_args: setattr(self, name, kwargs.pop(name)) if kwargs: raise TypeError('Powtarzające się argumenty: {}'.format(','.join(kwargs))) # Przykład zastosowania if __name__ == '__main__': class Stock(Structure): _fields = ['name', 'shares', 'price'] s1 = Stock('ACME', 50, 91.1) s2 = Stock('ACME', 50, 91.1, date='8/2/2012')
Omówienie Technika polegająca na definiowaniu ogólnej metody __init__() może okazać się bardzo przydatna przy pisaniu programów wykorzystujących dużą liczbę niewielkich struktur danych. Pozwala to znacznie zmniejszyć ilość kodu w porównaniu z wersją, w której metody __init__() są pisane ręcznie, tak jak poniżej: class Stock: def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price class Point: def __init__(self, x, y): self.x = x self.y = y class Circle: def __init__(self, radius): self.radius = radius def area(self): return math.pi * self.radius ** 2
Z tym kodem związany jest pewien szczegół dotyczący ustawiania wartości za pomocą funkcji setattr(). Zamiast stosować to podejście, można spróbować bezpośredniego dostępu do słownika obiektu. Oto przykład: class Structure: # Zmienna klasy określająca ustawiane pola _fields= [] def __init__(self, *args): if len(args) != len(self._fields): raise TypeError('Liczba oczekiwanych argumentów {}'.format(len(self._fields))) # Ustawianie argumentów (inne podejście) self.__dict__.update(zip(self._fields,args))
Choć to rozwiązanie działa, często nie można z całą pewnością stwierdzić, jak wygląda kod klasy pochodnej. Jeśli w takiej klasie zastosowano atrybut __slots__ lub umieszczono dany atrybut we właściwości (lub w deskryptorze), bezpośredni dostęp do słownika obiektu może okazać się niemożliwy. Zaprezentowane wcześniej rozwiązanie jest możliwie uniwersalne i nie wymaga przyjmowania założeń dotyczących klas pochodnych.
250
Rozdział 8. Klasy i obiekty
Wadą przedstawionej techniki jest to, że wpływa na system dokumentacji i pomocy środowiska IDE. Gdy użytkownik poprosi o pomoc związaną z konkretną klasą, wymagane argumenty nie zostaną przedstawione w standardowy sposób. Oto przykład: >>> help(Stock) Help on class Stock in module __main__: class Stock(Structure) ... | Methods inherited from Structure: | | __init__(self, *args, **kwargs) | ... >>>
Wiele tego typu problemów można naprawić przez dołączenie sygnatury typu w funkcji __init__() (zobacz recepturę 9.16). Warto zauważyć, że można automatycznie inicjować zmienne egzemplarza, używając funkcji narzędziowej i „sztuczki z ramką”. Oto przykład: def init_fromlocals(self): import sys locs = sys._getframe(1).f_locals for k, v in locs.items(): if k != 'self': setattr(self, k, v) class Stock: def __init__(self, name, shares, price): init_fromlocals(self)
W tej wersji funkcja init_fromlocals() wywołuje polecenie sys._getframe(), aby podejrzeć zmienne lokalne z wywołującej metody. Jeśli wywołanie to znajdzie się na początku metody __init__(), zmienne lokalne będą identyczne z przekazanymi argumentami, co pozwala łatwo ustawić atrybuty o identycznych nazwach. Choć to podejście pozwala zlikwidować problem braku poprawnych sygnatur w środowisku IDE, jest ponad 50% wolniejsze niż rozwiązanie przedstawione w recepturze, wymaga więcej kodu i jest oparte na bardziej skomplikowanych operacjach wykonywanych na zapleczu. Jeśli w kodzie dodatkowe możliwości nie są niezbędne, często wystarczające okaże się prostsze rozwiązanie.
8.12. Definiowanie interfejsu lub abstrakcyjnej klasy bazowej Problem Programista chce zdefiniować klasę, która posłuży za interfejs lub abstrakcyjną klasę bazową (pozwala to na sprawdzanie typów i gwarantowanie, że w klasie pochodnej znajdują się określone metody).
Rozwiązanie Aby zdefiniować abstrakcyjną klasę bazową, zastosuj moduł abc: 8.12. Definiowanie interfejsu lub abstrakcyjnej klasy bazowej
251
from abc import ABCMeta, abstractmethod class IStream(metaclass=ABCMeta): @abstractmethod def read(self, maxbytes=-1): pass @abstractmethod def write(self, data): pass
Ważną cechą abstrakcyjnej klasy bazowej jest to, że nie można tworzyć obiektów bezpośrednio na jej podstawie. Jeśli spróbujesz uruchomić poniższe wywołanie, spowodujesz błąd: a = IStream()
# TypeError: Can't instantiate abstract class # IStream with abstract methods read, write
Abstrakcyjne klasy bazowe mają pełnić funkcję klas bazowych dla innych klas, w których należy umieścić wymagane metody. Oto przykład: class SocketStream(IStream): def read(self, maxbytes=-1): ... def write(self, data): ...
Abstrakcyjne klasy bazowe znajdują zastosowanie przede wszystkim w kodzie, w którym trzeba wymusić dostępność oczekiwanego interfejsu programowania. Klasę bazową IStream można traktować jak ogólną specyfikację interfejsu umożliwiającego odczyt i zapis danych. Kod bezpośrednio sprawdzający dostępność tego interfejsu może wyglądać tak: def serialize(obj, stream): if not isinstance(stream, IStream): raise TypeError('Oczekiwano interfejsu IStream') ...
Możesz sądzić, że tego rodzaju sprawdzanie typu działa tylko dla klas pochodnych od abstrakcyjnej klasy bazowej. Jednak za pomocą abstrakcyjnych klas bazowych można określić także w innych klasach, że są zgodne z danym interfejsem. Poprawne jest np. poniższe rozwiązanie: import io # Rejestrowanie wbudowanych klas wejścia-wyjścia jako klas udostępniających nowy interfejs IStream.register(io.IOBase) # Otwieranie normalnego pliku i sprawdzanie typu f = open('foo.txt') isinstance(f, IStream) # Zwraca True
Warto zauważyć, że słowo @abstractmethod można zastosować także dla metod statycznych, metod klasy i właściwości. Trzeba tylko zachować właściwą kolejność — słowo @abstractmethod musi znajdować się bezpośrednio przed definicją funkcji, tak jak poniżej: from abc import ABCMeta, abstractmethod class A(metaclass=ABCMeta): @property @abstractmethod def name(self): pass @name.setter @abstractmethod def name(self, value):
Omówienie Gotowe abstrakcyjne klasy bazowe znajdują się w różnych miejscach biblioteki standardowej. W module collections występuje wiele tego rodzaju klas związanych z kontenerami i iteratorami (sekwencjami, odwzorowaniami, zbiorami itd.), w bibliotece numbers znajdują się abstrakcyjne klasy bazowe dla obiektów liczbowych (liczb całkowitych, liczb zmiennoprzecinkowych, liczb wymiernych itd.), a biblioteka io zawiera takie klasy związane z obsługą wejścia-wyjścia. Za pomocą gotowych abstrakcyjnych klas bazowych można wykonywać ogólne sprawdzanie typów. Oto kilka przykładów: import collections # Sprawdzanie, czy x to sekwencja if isinstance(x, collections.Sequence): ... # Sprawdzanie, czy x to obiekt iterowalny if isinstance(x, collections.Iterable): ... # Sprawdzanie, czy x ma rozmiar if isinstance(x, collections.Sized): ... # Sprawdzanie, czy x to odwzorowanie if isinstance(x, collections.Mapping): ...
Warto zauważyć, że w czasie gdy powstawała ta książka, w niektórych modułach bibliotecznych gotowe abstrakcyjne klasy bazowe nie działały w oczekiwany sposób. Oto przykład: from decimal import Decimal import numbers x = Decimal('3.4') isinstance(x, numbers.Real)
# Zwraca False
Choć wartość 3.4 jest liczbą rzeczywistą, sprawdzanie typu tego nie potwierdza. Pomaga to uniknąć przypadkowego mylenia ze sobą liczb zmiennoprzecinkowych i dziesiętnych. Dlatego jeśli korzystasz z możliwości abstrakcyjnych klas bazowych, powinieneś starannie napisać testy i upewnić się, że kod działa w oczekiwany sposób. Choć abstrakcyjne klasy bazowe ułatwiają sprawdzanie typów, nie należy nadużywać ich w programach. Python jest językiem dynamicznym, który zapewnia dużą swobodę. Próba wymuszania typu we wszystkich możliwych miejscach sprawia, że kod jest niepotrzebnie skomplikowany. Powinieneś korzystać ze swobody, jaką daje Python.
8.12. Definiowanie interfejsu lub abstrakcyjnej klasy bazowej
253
8.13. Tworzenie modelu danych lub systemu typów Problem Programista chce zdefiniować różnego rodzaju struktury danych, a przy tym wymusić ograniczenia wartości przypisywanych do niektórych atrybutów.
Rozwiązanie W tym problemie trzeba umieścić testy lub asercje w kodzie ustawiającym wartości wybranych atrybutów obiektu. Wymaga to dostosowania kodu ustawiającego wartości poszczególnych atrybutów. Należy wykorzystać do tego deskryptory. W poniższym kodzie pokazano, jak wykorzystać deskryptory do utworzenia systemu typów i mechanizmu sprawdzania wartości: # Klasa bazowa. Do ustawiania wartości służy deskryptor class Descriptor: def __init__(self, name=None, **opts): self.name = name for key, value in opts.items(): setattr(self, key, value) def __set__(self, instance, value): instance.__dict__[self.name] = value # Deskryptor do wymuszania typów class Typed(Descriptor): expected_type = type(None) def __set__(self, instance, value): if not isinstance(value, self.expected_type): raise TypeError('Oczekiwany typ: ' + str(self.expected_type)) super().__set__(instance, value) # Deskryptor do wymuszania wartości class Unsigned(Descriptor): def __set__(self, instance, value): if value < 0: raise ValueError('Oczekiwano wartości >= 0') super().__set__(instance, value) class MaxSized(Descriptor): def __init__(self, name=None, **opts): if 'size' not in opts: raise TypeError('Brak opcji size') super().__init__(name, **opts) def __set__(self, instance, value): if len(value) >= self.size: raise ValueError('Opcja size musi być < ' + str(self.size)) super().__set__(instance, value)
Klasy te należy traktować jak podstawowe cegiełki modelu danych lub systemu typów. A oto dalszy kod, w którym utworzono dane różnego rodzaju:
254
Rozdział 8. Klasy i obiekty
class Integer(Typed): expected_type = int class UnsignedInteger(Integer, Unsigned): pass class Float(Typed): expected_type = float class UnsignedFloat(Float, Unsigned): pass class String(Typed): expected_type = str class SizedString(String, MaxSized): pass
Za pomocą tych typów można zdefiniować klasę w następujący sposób: class Stock: # Określanie ograniczeń name = SizedString('name',size=8) shares = UnsignedInteger('shares') price = UnsignedFloat('price') def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price
Po utworzeniu ograniczeń można się przekonać, że kod sprawdza poprawność przy przypisywaniu wartości do atrybutów: >>> s = Stock('ACME', 50, 91.1) >>> s.name 'ACME' >>> s.shares = 75 >>> s.shares = -10 Traceback (most recent call last): File "", line 1, in File "example.py", line 17, in __set__ super().__set__(instance, value) File "example.py", line 23, in __set__ raise ValueError('Oczekiwano wartości >= 0') ValueError: Oczekiwano wartości >= 0 >>> s.price = 'dużo' Traceback (most recent call last): File "", line 1, in File "example.py", line 16, in __set__ raise TypeError('Oczekiwany typ: ' + str(self.expected_type)) TypeError: Oczekiwany typ: >>> s.name = 'ABRACADABRA' Traceback (most recent call last): File "", line 1, in File "example.py", line 17, in __set__ super().__set__(instance, value) File "example.py", line 35, in __set__ raise ValueError('Opcja size musi być < ' + str(self.size)) ValueError: Opcja size musi być < 8 >>>
8.13. Tworzenie modelu danych lub systemu typów
255
Istnieją techniki, które pozwalają uprościć podawanie ograniczeń w klasach. Jednym ze sposobów jest wykorzystanie dekoratora klas: # Dekorator klas pozwalający dodawać ograniczenia def check_attributes(**kwargs): def decorate(cls): for key, value in kwargs.items(): if isinstance(value, Descriptor): value.name = key setattr(cls, key, value) else: setattr(cls, key, value(key)) return cls return decorate # Przykład @check_attributes(name=SizedString(size=8), shares=UnsignedInteger, price=UnsignedFloat) class Stock: def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price
Inne podejście pozwalające uprościć podawanie ograniczeń polega na zastosowaniu metaklasy. Oto przykład: # Metaklasa do sprawdzania ograniczeń class checkedmeta(type): def __new__(cls, clsname, bases, methods): # Dołączanie nazw atrybutów do deskryptorów for key, value in methods.items(): if isinstance(value, Descriptor): value.name = key return type.__new__(cls, clsname, bases, methods) # Przykład class Stock(metaclass=checkedmeta): name = SizedString(size=8) shares = UnsignedInteger() price = UnsignedFloat() def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price
Omówienie W tej recepturze wykorzystano wiele zaawansowanych technik — w tym deskryptory, klasy mieszane, funkcję super(), dekoratory klas i metaklasy. Nie da się omówić wszystkich tych zagadnień w tym miejscu (związane z nimi przykłady znajdziesz w innych recepturach — zobacz receptury 8.9, 8.18, 9.12 i 9.19). Warto jednak zwrócić uwagę na kilka szczegółów. Przede wszystkim w klasie bazowej Descriptor występuje metoda __set__(), natomiast nie ma odpowiadającej jej metody __get__(). Jeśli deskryptor służy tylko do pobierania wartości o danej nazwie ze słownika obiektu, nie trzeba definiować metody __get__(). Zdefiniowanie jej sprawia, że kod działa wolniej. Dlatego w recepturze zaimplementowano tylko metodę __set__().
256
Rozdział 8. Klasy i obiekty
Ogólny projekt różnych deskryptorów jest oparty na klasach mieszanych. Np. klasy Unsigned i MaxSized są przeznaczone do łączenia z innymi deskryptorami, pochodnymi od klasy Typed. Do utworzenia tego specjalnego typu danych posłużyło wielodziedziczenie, które pozwala połączyć potrzebne mechanizmy. Warto też zauważyć, że metody __init__() wszystkich deskryptorów napisano tak, aby miały identyczną sygnaturę, obejmującą podawane za pomocą słów kluczowych argumenty **opts. Klasa MaxSized wyszukuje wymagane atrybuty w opts i przekazuje je do klasy bazowej Descriptor, która ustawia wartość atrybutu. Podchwytliwym aspektem łączenia klas w ten sposób (dotyczy to zwłaszcza klas mieszanych) jest to, że nie zawsze wiadomo, jak będzie wyglądał łańcuch klas lub jak zadziała funkcja super(). Dlatego kod powinien działać dla dowolnej możliwej kombinacji klas. W definicjach różnych klas reprezentujących typy (np. Integer, Float i String) wykorzystano przydatną technikę. Polega ona na zastosowaniu zmiennych klas do dostosowania kodu. Deskryptor Typed musi tylko sprawdzić wartość atrybutu expected_type określonego w każdej z klas pochodnych. Zastosowanie dekoratorów klas lub metaklas często pozwala uprościć proces pisania kodu przez użytkownika. Zauważ, że w przykładach nazwę atrybutu wystarczy wprowadzić raz. Oto przykład: # Zwykła klasa class Point: x = Integer('x') y = Integer('y') # Metaklasa class Point(metaclass=checkedmeta): x = Integer() y = Integer()
Kod dekoratora klasy i metaklasy sprawdza słownik klasy pod kątem deskryptorów. Gdy je znajdzie, uzupełnia nazwę deskryptora na podstawie wartości klucza. Spośród przedstawionych technik rozwiązanie oparte na dekoratorach klas pozwala zapewnić największą swobodę i poprawność. Po pierwsze, nie wymaga stosowania zaawansowanych mechanizmów, np. metaklas. Po drugie, w zależności od potrzeb dekoracje można łatwo dodawać i usuwać w definicji klasy. Dekorator może np. umożliwiać całkowite pominięcie sprawdzania typów. W ten sposób mechanizm sprawdzania można na żądanie włączać i wyłączać (np. w wersji diagnostycznej i produkcyjnej). Podejście oparte na dekoratorach klas można też zastosować zamiast klas mieszanych, wielodziedziczenia i skomplikowanych wywołań funkcji super(). Oto inna wersja receptury. Wykorzystano tu dekoratory klas: # Klasa bazowa. Wykorzystanie deskryptora do ustawiania wartości class Descriptor: def __init__(self, name=None, **opts): self.name = name for key, value in opts.items(): setattr(self, key, value) def __set__(self, instance, value): instance.__dict__[self.name] = value
8.13. Tworzenie modelu danych lub systemu typów
257
# Dekorator obsługujący sprawdzanie typów def Typed(expected_type, cls=None): if cls is None: return lambda cls: Typed(expected_type, cls) super_set = cls.__set__ def __set__(self, instance, value): if not isinstance(value, expected_type): raise TypeError('Oczekiwany typ: ' + str(expected_type)) super_set(self, instance, value) cls.__set__ = __set__ return cls # Dekorator dla wartości bez znaku def Unsigned(cls): super_set = cls.__set__ def __set__(self, instance, value): if value < 0: raise ValueError('Oczekiwano wartości >= 0') super_set(self, instance, value) cls.__set__ = __set__ return cls # Dekorator dla wartości o określonym rozmiarze def MaxSized(cls): super_init = cls.__init__ def __init__(self, name=None, **opts): if 'size' not in opts: raise TypeError('Brak opcji size') super_init(self, name, **opts) cls.__init__ = __init__ super_set = cls.__set__ def __set__(self, instance, value): if len(value) >= self.size: raise ValueError('Opcja size musi być < ' + str(self.size)) super_set(self, instance, value) cls.__set__ = __set__ return cls # Specjalne deskryptory @Typed(int) class Integer(Descriptor): pass @Unsigned class UnsignedInteger(Integer): pass @Typed(float) class Float(Descriptor): pass @Unsigned class UnsignedFloat(Float): pass @Typed(str) class String(Descriptor): pass @MaxSized class SizedString(String): pass
258
Rozdział 8. Klasy i obiekty
Klasy zdefiniowane w tej wersji działają tak samo jak we wcześniejszym kodzie (w poprzednich przykładach nie trzeba wprowadzać żadnych zmian). Różnica polega na tym, że są znacznie wydajniejsze. Prosty test ustawiania wartości atrybutów o określonym typie pokazuje, że technika oparta na dekoratorach działa niemal 100% szybciej niż podejście wykorzystujące klasy mieszane. Nie cieszysz się teraz, że przeczytałeś recepturę do końca?
8.14. Tworzenie niestandardowych kontenerów Problem Programista chce utworzyć niestandardową klasę, która działa jak wbudowane typy kontenerów (takie jak lista lub słownik). Nie wie jednak, które metody musi w tym celu napisać.
Rozwiązanie W bibliotece collections zdefiniowane są różne abstrakcyjne klasy bazowe niezwykle przydatne przy tworzeniu niestandardowych klas kontenerów. Załóżmy, że chcesz, aby klasa obsługiwała iterowanie. Zacznij od utworzenia klasy pochodnej od klasy collections.Iterable: import collections class A(collections.Iterable): pass
Wyjątkową cechą tworzenia klas pochodnych od klasy collections.Iterable jest to, że trzeba udostępnić w nich wszystkie wymagane metody specjalne. Jeśli tego nie zrobisz, w momencie tworzenia obiektu wystąpi błąd: >>> a = A() Traceback (most recent call last): File "", line 1, in TypeError: Can't instantiate abstract class A with abstract methods __iter__ >>>
Aby rozwiązać problem, wystarczy dodać do klasy wymaganą metodę __iter__() i umieścić w niej potrzebny kod (zobacz receptury 4.2 i 4.7). Inne godne uwagi klasy zdefiniowane w module collections to Sequence, MutableSequence, Mapping, MutableMapping, Set i MutableSet. Wiele z tych klas tworzy hierarchie, w których kolejne klasy udostępniają coraz więcej możliwości (jedna z takich hierarchii obejmuje klasy Container, Iterable, Sized, Sequence i MutableSequence). Wystarczy utworzyć obiekt dowolnej z tych klas, aby zobaczyć, które metody trzeba napisać w celu utworzenia niestandardowego kontenera o określonych możliwościach: >>> import collections >>> collections.Sequence() Traceback (most recent call last): File "", line 1, in TypeError: Can't instantiate abstract class Sequence with abstract methods \ __getitem__, __len__ >>>
8.14. Tworzenie niestandardowych kontenerów
259
Oto prosta przykładowa klasa, w której umieszczono wymienione metody, aby utworzyć sekwencję z elementami przechowywanymi zawsze w posortowanej kolejności (kod ten jest nieco niewydajny, jednak pozwala przedstawić ogólną technikę): import collections import bisect class SortedItems(collections.Sequence): def __init__(self, initial=None): self._items = sorted(initial) if initial is None else [] # Metody niezbędne w sekwencji def __getitem__(self, index): return self._items[index] def __len__(self): return len(self._items) # Metoda umieszczająca element w odpowiednim miejscu def add(self, item): bisect.insort(self._items, item)
Oto przykład zastosowania tej klasy: >>> items = SortedItems([5, 1, 3]) >>> list(items) [1, 3, 5] >>> items[0] 1 >>> items[-1] 5 >>> items.add(2) >>> list(items) [1, 2, 3, 5] >>> items.add(-10) >>> list(items) [-10, 1, 2, 3, 5] >>> items[1:4] [1, 2, 3] >>> 3 in items True >>> len(items) 5 >>> for n in items: ... print(n) ... -10 1 2 3 5 >>>
Jak widać, obiekty typu SortedItems działają jak standardowa sekwencja i obsługują wszystkie typowe operacje — w tym używanie indeksów, iterowanie, metodę len(), operator in (sprawdza, czy element znajduje się w sekwencji), a nawet tworzenie wycinków. W ramach dygresji warto wspomnieć, że wykorzystany w tej recepturze moduł bisect to wygodne narzędzie pozwalające zachować elementy na liście w posortowanej kolejności. Wywołanie bisect.insort() wstawia element na listę w taki sposób, aby pozostała ona posortowana.
260
Rozdział 8. Klasy i obiekty
Omówienie Tworzenie klasy pochodnej od jednej z abstrakcyjnych klas bazowych modułu collections pozwala zagwarantować, że w niestandardowym kontenerze znajdą się wszystkie wymagane metody danego kontenera. Dziedziczenie sprawia też, że latwiej jest sprawdzać typy. Przedstawiony tu niestandardowy kontener przechodzi różne testy typów: >>> items = SortedItems() >>> import collections >>> isinstance(items, collections.Iterable) True >>> isinstance(items, collections.Sequence) True >>> isinstance(items, collections.Container) True >>> isinstance(items, collections.Sized) True >>> isinstance(items, collections.Mapping) False >>>
Wiele abstrakcyjnych klas bazowych z modułu collections udostępnia domyślną implementację standardowych metod kontenerów. Załóżmy, że utworzyłeś klasę pochodną od klasy collections.MutableSequence: class Items(collections.MutableSequence): def __init__(self, initial=None): self._items = list(initial) if initial is None else [] # Wymagane metody sekwencji def __getitem__(self, index): print('Pobieranie:', index) return self._items[index] def __setitem__(self, index, value): print('Ustawianie:', index, value) self._items[index] = value def __delitem__(self, index): print('Usuwanie:', index) del self._items[index] def insert(self, index, value): print('Wstawianie:', index, value) self._items.insert(index, value) def __len__(self): print('Len') return len(self._items)
Gdy utworzysz obiekt typu Items, przekonasz się, że udostępnia on niemal wszystkie podstawowe metody list (append(), remove(), count() itd.). Metody te napisano w taki sposób, aby wykorzystywały tylko wymagane metody. Oto interaktywna sesja, która to ilustruje: >>> a = Items([1, 2, 3]) >>> len(a) Len 3 >>> a.append(4) Len Wstawianie: 3 4
W tej recepturze tylko pokrótce opisano mechanizm klas abstrakcyjnych Pythona. Moduł numbers udostępnia podobną kolekcję klas abstrakcyjnych związanych z liczbowymi typami danych. W recepturze 8.12 znajdziesz więcej informacji na temat tworzenia własnych abstrakcyjnych klas bazowych.
8.15. Delegowanie obsługi dostępu do atrybutów Problem Programista chce, aby obiekt delegował obsługę dostępu do atrybutów do wewnętrznie przechowywanego obiektu. Technikę tę można stosować zamiast dziedziczenia lub w celu utworzenia pośrednika.
Rozwiązanie Delegowanie to wzorzec programowania polegający na przekazywaniu odpowiedzialności za wykonanie konkretnej operacji innemu obiektowi. W najprostszej postaci wygląda to często tak: class A: def spam(self, x): pass def foo(self): pass class B: def __init__(self): self._a = A() def spam(self, x): # Delegowanie zadania do wewnętrznego obiektu self._a return self._a.spam(x) def foo(self): # Delegowanie zadania do wewnętrznego obiektu self._a return self._a.foo() def bar(self): pass
262
Rozdział 8. Klasy i obiekty
Jeśli delegowanie dotyczy tylko kilku metod, napisanie takiego kodu jest łatwe. Jeżeli jednak liczba delegowanych metod jest duża, można zdefiniować metodę __getattr__(): class A: def spam(self, x): pass def foo(self): pass class B: def __init__(self): self._a = A() def bar(self): pass # Udostępnianie wszystkich metod zdefiniowanych w klasie A def __getattr__(self, name): return getattr(self._a, name)
Metoda __getattr__() przechwytuje wszystkie operacje wyszukiwania atrybutu. Jest wywoływana, gdy kod próbuje uzyskać dostęp do nieistniejącego atrybutu. W przedstawionym kodzie metoda ta przechwytuje operacje dostępu do niezdefiniowanych metod klasy B i deleguje zadanie do klasy A. Oto przykład: b = B() b.bar() b.spam(42)
# Wywołanie B.bar() (metoda ta istnieje w klasie B) # Wywołanie B.__getattr__('spam') i oddelegowanie zadania do metody A.spam
Delegowanie wykorzystuje się też w klasach pośredniczących. Oto przykład: # Klasa pośrednicząca, która jest nakładką na inny obiekt, ale # udostępnia jego publiczne atrybuty class Proxy: def __init__(self, obj): self._obj = obj # Delegowanie wyszukiwania atrybutów do wewnętrznego obiektu def __getattr__(self, name): print('getattr:', name) return getattr(self._obj, name) # Delegowanie przypisywania wartości do atrybutów def __setattr__(self, name, value): if name.startswith('_'): super().__setattr__(name, value) else: print('setattr:', name, value) setattr(self._obj, name, value) # Delegowanie usuwania atrybutów def __delattr__(self, name): if name.startswith('_'): super().__delattr__(name) else: print('delattr:', name) delattr(self._obj, name)
8.15. Delegowanie obsługi dostępu do atrybutów
263
Aby zastosować tę klasę pośredniczącą, wystarczy umieścić w niej inny obiekt. Oto przykład: class Spam: def __init__(self, x): self.x = x def bar(self, y): print('Spam.bar:', self.x, y) # Tworzenie obiektu s = Spam(2) # Tworzenie pośrednika dla obiektu p = Proxy(s) # Dostęp do pośrednika print(p.x) # Zwraca 2 p.bar(3) # Zwraca "Spam.bar: 2 3" p.x = 37 # Zmienia s.x na 37
Zmiana kodu metod dostępu do atrybutów pozwala sprawić, że pośrednik będzie działał w inny sposób (może np. rejestrować operacje dostępu, pozwalać na dostęp tylko do odczytu itd.).
Omówienie Delegowanie czasem stosuje się zamiast dziedziczenia. Zamiast pisania następującego kodu: class A: def spam(self, x): print('A.spam', x) def foo(self): print('A.foo') class B(A): def spam(self, x): print('B.spam') super().spam(x) def bar(self): print('B.bar')
Delegowanie zadań w ten sposób jest często przydatne w sytuacjach, gdy bezpośrednie dziedziczenie nie ma sensu lub gdy programista chce uzyskać większą kontrolę nad relacjami między obiektami (zamierza udostępniać tylko wybrane metody, implementować interfejsy itd.). Przy stosowaniu delegowania do tworzenia pośredników warto zwrócić uwagę na kilka dodatkowych szczegółów. Po pierwsze, metoda __getattr__() jest metodą rezerwową, wywoływaną tylko wtedy, gdy nie można znaleźć danego atrybutu. Dlatego przy dostępie do obiektu pośrednika (np. do atrybutu _obj) metoda ta nie jest uruchamiana. Po drugie, metody __setattr__() i __delattr__() wymagają dodania logiki do obsługi atrybutów obiektu pośrednika i atrybutów obiektu wewnętrznego _obj. W pośrednikach często deleguje się zadania dotyczące tylko tych atrybutów, których nazwy nie rozpoczynają się od podkreślenia (pośrednik udostępnia wtedy tylko atrybuty publiczne przechowywanego obiektu). Należy też podkreślić, że metoda __getattr__() zwykle nie współdziała z większością metod specjalnych (o nazwach rozpoczynających się i kończących dwoma podkreśleniami). Przyjrzyj się następującej klasie: class ListLike: def __init__(self): self._items = [] def __getattr__(self, name): return getattr(self._items, name)
Jeśli spróbujesz utworzyć obiekt typu ListLike, przekonasz się, że obsługuje on standardowe metody list (np. append() i insert()). Nie udostępnia on jednak operatorów (metody len(), wyszukiwania elementów itd.). Oto przykład: >>> a = ListLike() >>> a.append(2) >>> a.insert(0, 1) >>> a.sort() >>> len(a) Traceback (most recent call last): File "", line 1, in TypeError: object of type 'ListLike' has no len() >>> a[0] Traceback (most recent call last): File "", line 1, in TypeError: 'ListLike' object does not support indexing >>>
Aby dodać obsługę operatorów, trzeba ręcznie delegować zadania do odpowiednich metod specjalnych: class ListLike: def __init__(self): self._items = [] def __getattr__(self, name): return getattr(self._items, name) # Dodatkowe metody specjalne obsługujące wybrane operacje na listach def __len__(self): return len(self._items) def __getitem__(self, index): return self._items[index] def __setitem__(self, index, value): self._items[index] = value def __delitem__(self, index): del self._items[index]
W recepturze 11.8 opisano następny przykład wykorzystania delegowania w kontekście tworzenia klas pośredników obsługujących zdalne wywołania procedur. 8.15. Delegowanie obsługi dostępu do atrybutów
265
8.16. Definiowanie więcej niż jednego konstruktora w klasie Problem Programista pisze klasę i chce, aby użytkownicy mogli tworzyć obiekty tej klasy nie tylko za pomocą metody __init__(), ale też w inny sposób.
Rozwiązanie Aby zdefiniować klasę o więcej niż jednym konstruktorze, należy wykorzystać metody klasy. Oto prosty przykład: import time class Date: # Główny konstruktor def __init__(self, year, month, day): self.year = year self.month = month self.day = day # Dodatkowy konstruktor @classmethod def today(cls): t = time.localtime() return cls(t.tm_year, t.tm_mon, t.tm_mday)
Aby uruchomić dodatkowy konstruktor, wystarczy wywołać go jak funkcję, np. Date.today(): a = Date(2012, 12, 21) b = Date.today()
# Główny # Dodatkowy
Omówienie Jednym z głównych zastosowań metod klasy jest definiowanie dodatkowych konstruktorów (tak jak w tej recepturze). Główną cechą metod klasy jest to, że przyjmują klasę jako pierwszy argument. Zauważ, że klasa w metodzie używana jest do utworzenia i zwrócenia gotowego obiektu tej klasy. Jest to bardzo wyrafinowane rozwiązanie, jednak ten aspekt metod klasy sprawia, że współdziałają one prawidłowo z dziedziczeniem i podobnymi mechanizmami. Oto przykład: class NewDate(Date): pass c = Date.today() d = NewDate.today()
# Tworzy obiekt typu Date (cls=Date) # Tworzy obiekt typu NewDate (cls=NewDate)
Przy definiowaniu klas o wielu konstruktorach należy zadbać o to, aby funkcja __init__() była jak najprostsza. Najlepiej, gdy tylko przypisuje podane wartości do atrybutów. Zaawansowane operacje można w razie potrzeby wykonać w dodatkowych konstruktorach.
266
Rozdział 8. Klasy i obiekty
Zamiast definiować odrębną metodę klasy, można zaimplementować metodę __init__() w taki sposób, który umożliwi wywoływanie jej na kilka sposobów. Oto przykład: class Date: def __init__(self, *args): if len(args) == 0: t = time.localtime() args = (t.tm_year, t.tm_mon, t.tm_mday) self.year, self.month, self.day = args
Choć w niektórych sytuacjach technika ta się sprawdza, często prowadzi ona do powstawania kodu trudnego do zrozumienia i konserwowania. Przedstawiona wersja nie wyświetla np. przydatnych informacji z systemu pomocy (z nazwami argumentów). Ponadto kod do tworzenia obiektów typu Date jest w niej mniej przejrzysty. Porównaj ze sobą poniższe wywołania: a = Date(2012, 12, 21) b = Date()
# Zrozumiałe — konkretna data # Jak działa ten kod?
# Wersja z metodą klasy c = Date.today()
# Zrozumiałe — dzisiejsza data
Polecenie Date.today() wywołuje normalną metodę Date.__init__(), tworząc obiekt typu Date z odpowiednimi argumentami reprezentującymi rok, miesiąc i dzień. W razie konieczności obiekty można tworzyć bez wywoływania metody __init__(). Opis tej techniki znajdziesz w następnej recepturze.
8.17. Tworzenie obiektów bez wywoływania metody __init__() Problem Programista chce utworzyć obiekt, ale z pewnych przyczyn zamierza pominąć wywoływanie metody __init__().
Rozwiązanie Niezainicjowany obiekt można utworzyć, wywołując bezpośrednio metodę __new__() klasy. Przyjrzyj się następującej klasie: class Date: def __init__(self, year, month, day): self.year = year self.month = month self.day = day
Poniżej pokazano, jak utworzyć obiekt typu Date bez wywoływania metody __init__(): >>> d = Date.__new__(Date) >>> d <__main__.Date object at 0x1006716d0> >>> d.year Traceback (most recent call last): File "", line 1, in AttributeError: 'Date' object has no attribute 'year' >>>
8.17. Tworzenie obiektów bez wywoływania metody __init__()
267
Widać tu, że utworzony obiekt nie jest zainicjowany. Dlatego programista musi następnie ustawić odpowiednie zmienne egzemplarza. Oto przykład: >>> data = {'year':2012, 'month':8, 'day':29} >>> for key, value in data.items(): ... setattr(d, key, value) ... >>> d.year 2012 >>> d.month 8 >>>
Omówienie Problem pomijania metody __init__() pojawia się czasem przy tworzeniu obiektów w niestandardowy sposób, np. przy deserializacji danych lub w kodzie metod klasy zdefiniowanych jako dodatkowy konstruktor. W przedstawionej tu klasie Date można zdefiniować dodatkowy konstruktor today() w następujący sposób: from time import localtime class Date: def __init__(self, year, month, day): self.year = year self.month = month self.day = day @classmethod def today(cls): d = cls.__new__(cls) t = localtime() d.year = t.tm_year d.month = t.tm_mon d.day = t.tm_mday return d
Teraz załóżmy, że chcesz przeprowadzić deserializację danych w formacie JSON i w efekcie uzyskać słownik podobny do poniższego: data = { 'year': 2012, 'month': 8, 'day': 29 }
Jeśli chcesz przekształcić ten słownik w obiekt typu Date, zastosuj technikę zaprezentowaną w rozwiązaniu. Przy tworzeniu obiektów w niestandardowy sposób zwykle najlepiej jest nie przyjmować zbyt wielu założeń dotyczących implementacji. Dlatego przeważnie nie należy pisać kodu, który bezpośrednio manipuluje słownikiem obiektu (__dict__), chyba że ma się pewność, iż słownik ten jest już zdefiniowany. Gdy w klasie używane są atrybuty __slots__, właściwości, deskryptory lub inne zaawansowane techniki, manipulowanie słownikiem może spowodować, że kod przestanie działać. Wykorzystanie metody setattr() do ustawiania wartości sprawia, że kod jest tak uniwersalny, jak to tylko możliwe.
268
Rozdział 8. Klasy i obiekty
8.18. Rozszerzanie klas za pomocą klas mieszanych Problem Programista używa kolekcji przydatnych ogólnych metod i chce, aby były one dostępne do rozszerzania możliwości innych klas. Jednak klasy, do których zamierza dodawać te metody, nie zawsze są powiązane ze sobą relacją dziedziczenia. Dlatego nie wystarczy umieścić potrzebnych metod we wspólnej klasie bazowej.
Rozwiązanie Problem opisywany w tej recepturze często pojawia się w kodzie, w którym programista chce modyfikować klasy. Możliwe, że biblioteka udostępnia podstawowy zbiór klas wraz z opcjonalnymi modyfikacjami, które użytkownik może w razie potrzeby zastosować. W ramach ilustracji tego problemu załóżmy, że chcesz dodać nowe mechanizmy (rejestrowanie operacji, jednokrotne ustawianie wartości, sprawdzanie typów itd.) do obiektów odwzorowań. Oto zbiór klas mieszanych, które to umożliwiają: class LoggedMappingMixin: ''' Dodawanie w celach diagnostycznych rejestrowania operacji pobierania, ustawiania i usuwania ''' __slots__ = () def __getitem__(self, key): print('Pobieranie ' + str(key)) return super().__getitem__(key) def __setitem__(self, key, value): print('Ustawianie {} = {!r}'.format(key, value)) return super().__setitem__(key, value) def __delitem__(self, key): print('Usuwanie ' + str(key)) return super().__delitem__(key) class SetOnceMappingMixin: ''' Klucz można ustawić tylko raz ''' __slots__ = () def __setitem__(self, key, value): if key in self: raise KeyError(str(key) + ' został już ustawiony') return super().__setitem__(key, value) class StringKeysMappingMixin: ''' Kluczami mogą być tylko łańcuchy znaków ''' __slots__ = () def __setitem__(self, key, value): if not isinstance(key, str): raise TypeError('Klucz musi być łańcuchem znaków') return super().__setitem__(key, value)
8.18. Rozszerzanie klas za pomocą klas mieszanych
269
Te klasy same w sobie są bezużyteczne. Jeśli utworzysz obiekt jednej z tych klas, nie będzie on wykonywał żadnych przydatnych operacji (będzie jedynie generował wyjątki). Klasy te należy łączyć z klasami odwzorowań za pomocą wielodziedziczenia. Oto przykład: >>> class LoggedDict(LoggedMappingMixin, dict): ... pass ... >>> d = LoggedDict() >>> d['x'] = 23 Ustawianie x = 23 >>> d['x'] Pobieranie x 23 >>> del d['x'] Usuwanie x >>> from collections import defaultdict >>> class SetOnceDefaultDict(SetOnceMappingMixin, defaultdict): ... pass ... >>> d = SetOnceDefaultDict(list) >>> d['x'].append(2) >>> d['y'].append(3) >>> d['x'].append(10) >>> d['x'] = 23 Traceback (most recent call last): File "", line 1, in File "mixin.py", line 24, in __setitem__ raise KeyError(str(key) + ' został już ustawiony') KeyError: 'x został już ustawiony' >>> from collections import OrderedDict >>> class StringOrderedDict(StringKeysMappingMixin, ... SetOnceMappingMixin, OrderedDict): ... pass ... >>> d = StringOrderedDict() >>> d['x'] = 23 >>> d[42] = 10 Traceback (most recent call last): File "", line 1, in File "mixin.py", line 45, in __setitem__ ''' TypeError: Klucz musi być łańcuchem znaków >>> d['x'] = 42 Traceback (most recent call last): File "", line 1, in File "mixin.py", line 46, in __setitem__ __slots__ = () File "mixin.py", line 24, in __setitem__ if key in self: KeyError: 'x jest już ustawiony' >>>
Zwróć uwagę, że klasy mieszane są łączone z istniejącymi klasami (np. dict, defaultdict, OrderedDict), a nawet innymi klasami mieszanymi. Połączone klasy współdziałają ze sobą i zapewniają pożądane funkcje.
270
Rozdział 8. Klasy i obiekty
Omówienie Klasy mieszane występują w różnych miejscach biblioteki standardowej. Służą głównie do rozszerzania możliwości innych klas w przedstawiony tu sposób. Ponadto są jednym z głównych elementów używanych w wielodziedziczeniu (np. w trakcie pisania kodu korzystającego z sieci często używa się klasy ThreadingMixIn z modułu socketserver, aby dodać obsługę wątków do klas związanych z siecią). Oto wielowątkowy serwer XML-RPC: from xmlrpc.server import SimpleXMLRPCServer from socketserver import ThreadingMixIn class ThreadedXMLRPCServer(ThreadingMixIn, SimpleXMLRPCServer): pass
Klasy mieszane często są zdefiniowane w dużych bibliotekach i systemach. Zwykle służą do wzbogacania możliwości istniejących klas o opcjonalne funkcje. Teoria klas mieszanych ma długą historię. Zamiast jednak opisywać wszystkie szczegóły tej dziedziny, przedstawiamy kilka ważnych kwestii z zakresu implementacji, które warto zapamiętać. Po pierwsze, klas mieszanych nigdy nie należy bezpośrednio używać do tworzenia obiektów. Żadna z klas z tej receptury nie działa samodzielnie. Trzeba je połączyć z inną klasą, w której zaimplementowano potrzebne mechanizmy odwzorowań. Także klasy ThreadingMixIn z biblioteki socketserver nie można używać niezależnie — trzeba ją połączyć z odpowiednią klasą serwera. Po drugie, klasy mieszane zwykle nie przechowują stanu. Oznacza to, że nie mają metody __init__() ani zmiennych egzemplarza. W tej recepturze wywołanie __slots__ = () ma wyraźnie sugerować, że tworzone klasy mieszane nie udostępniają danych egzemplarza. Jeśli zamierzasz zdefiniować klasę mieszaną z metodą __init__() i zmiennymi egzemplarza, pamiętaj, że związane jest z tym poważne zagrożenie — w klasie mieszanej nie wiadomo, z jakimi innymi klasami będzie ona łączona. Dlatego nazwy zmiennych egzemplarza trzeba tworzyć w taki sposób, aby uniknąć kolizji nazw. Ponadto metodę __init__() trzeba napisać tak, aby poprawnie wywoływała metodę __init__() łączonych klas. Zwykle trudno jest poprawnie utworzyć taki kod, ponieważ sygnatury innych klas nie są znane. W ostateczności można napisać bardzo ogólne rozwiązanie, używając argumentów *arg i **kwargs. Jeśli metoda __init__() klasy mieszanej przyjmuje argumenty, należy je podawać wyłącznie za pomocą słów kluczowych i nazwać w taki sposób, aby uniknąć kolizji z nazwami innych argumentów. Oto możliwy kod klasy mieszanej z metodą __init__() przyjmującej argumenty podawane za pomocą słów kluczowych: class RestrictKeysMixin: def __init__(self, *args, _restrict_key_type, **kwargs): self.__restrict_key_type = _restrict_key_type super().__init__(*args, **kwargs) def __setitem__(self, key, value): if not isinstance(key, self.__restrict_key_type): raise TypeError('Klucze muszą być typu ' + str(self.__restrict_key_type)) super().__setitem__(key, value)
8.18. Rozszerzanie klas za pomocą klas mieszanych
271
Oto przykład ilustrujący, jak wykorzystać taką klasę: >>> class RDict(RestrictKeysMixin, dict): ... pass ... >>> d = RDict(_restrict_key_type=str) >>> e = RDict([('name','Adam'), ('n',37)], _restrict_key_type=str) >>> f = RDict(name='Adam', n=37, _restrict_key_type=str) >>> f {'n': 37, 'name': 'Adam'} >>> f[42] = 10 Traceback (most recent call last): File "", line 1, in File "mixin.py", line 83, in __setitem__ raise TypeError('Klucze muszą być typu ' + str(self.__restrict_key_type)) TypeError: Klucze muszą być typu >>>
W tym przykładzie zwróć uwagę na to, że przy inicjowaniu obiektu metoda RDict() przyjmuje argumenty zrozumiałe w wywołaniu dict(). Występuje jednak podawany za pomocą słowa kluczowego dodatkowy argument (restrict_key_type) przeznaczony dla klasy mieszanej. Funkcja super() to niezwykle istotny aspekt klas mieszanych. W rozwiązaniu w klasach zmieniono definicje kilku ważnych metod, np. __getitem__() i __setitem__(). Należy jednak wywoływać w nich pierwotne wersje poszczególnych metod. Wywołanie funkcji super() pozwala oddelegować zadanie do następnej klasy z listy MRO. Ten aspekt receptury może być nieoczywisty dla początkujących programistów, ponieważ funkcja super() jest tu używana w klasach, które nie mają klasy bazowej (na pozór wygląda to na błąd). Jednak w następującym kontekście: class LoggedDict(LoggedMappingMixin, dict): pass
wywołanie funkcji super() w klasie LoggedMappingMixin powoduje oddelegowanie zadania do następnej klasy z listy MRO. Dlatego wywołanie super().__getitem__() w klasie LoggedMappingMixin prowadzi do wywołania metody dict.__getitem__(). Bez tego mechanizmu klasy mieszane w ogóle nie mogłyby działać. Innym sposobem na utworzenie klas mieszanych jest wykorzystanie dekoratorów klas. Przyjrzyj się następującemu kodowi: def LoggedMapping(cls): cls_getitem = cls.__getitem__ cls_setitem = cls.__setitem__ cls_delitem = cls.__delitem__ def __getitem__(self, key): print('Pobieranie ' + str(key)) return cls_getitem(self, key) def __setitem__(self, key, value): print('Ustawianie {} = {!r}'.format(key, value)) return cls_setitem(self, key, value) def __delitem__(self, key): print('Usuwanie ' + str(key)) return cls_delitem(self, key) cls.__getitem__ = __getitem__ cls.__setitem__ = __setitem__ cls.__delitem__ = __delitem__ return cls
272
Rozdział 8. Klasy i obiekty
Funkcję tę można podać jako dekorator w definicji klasy: @LoggedMapping class LoggedDict(dict): pass
Jeśli wypróbujesz ten kod, uzyskasz ten sam efekt co wcześniej, przy czym tutaj nie wymaga to stosowania wielodziedziczenia. Dekorator modyfikuje definicję klasy i zastępuje niektóre metody. Więcej informacji o dekoratorach klas znajdziesz w recepturze 9.12. Zaawansowane rozwiązanie z wykorzystaniem klas mieszanych i dekoratorów klas przedstawiono w recepturze 8.13.
8.19. Implementowanie obiektów ze stanem lub maszyn stanowych Problem Programista zamierza utworzyć maszynę stanową lub obiekt działający w różnych stanach, jednak nie chce komplikować kodu wieloma instrukcjami warunkowymi.
Rozwiązanie W niektórych sytuacjach obiekty działają w odmienny sposób w zależności od wewnętrznego stanu. Przyjrzyj się prostej klasie reprezentującej połączenie: class Connection: def __init__(self): self.state = 'CLOSED' def read(self): if self.state != 'OPEN': raise RuntimeError('Połączenie nie jest otwarte') print('Odczyt') def write(self, data): if self.state != 'OPEN': raise RuntimeError('Połączenie nie jest otwarte') print('Zapis') def open(self): if self.state == 'OPEN': raise RuntimeError('Połączenie już jest otwarte') self.state = 'OPEN' def close(self): if self.state == 'CLOSED': raise RuntimeError('Połączenie już jest zamknięte') self.state = 'CLOSED'
Z tą wersją związane są pewne problemy. Po pierwsze, kod jest skomplikowany z uwagi na dużą liczbę instrukcji warunkowych sprawdzających stan. Po drugie, wydajność rozwiązania jest niska, ponieważ standardowe operacje (takie jak read() i write()) zawsze są poprzedzone sprawdzaniem stanu.
8.19. Implementowanie obiektów ze stanem lub maszyn stanowych
273
Bardziej eleganckie podejście polega na zakodowaniu każdego stanu w odrębnej klasie i delegowaniu zadań przez klasę Connection do odpowiednich klas stanu. Oto przykład: class Connection: def __init__(self): self.new_state(ClosedConnectionState) def new_state(self, newstate): self._state = newstate # Delegowanie zadań do klasy stanu def read(self): return self._state.read(self) def write(self, data): return self._state.write(self, data) def open(self): return self._state.open(self) def close(self): return self._state.close(self) # Klasa bazowa dla klas stanu połączenia class ConnectionState: @staticmethod def read(conn): raise NotImplementedError() @staticmethod def write(conn, data): raise NotImplementedError() @staticmethod def open(conn): raise NotImplementedError() @staticmethod def close(conn): raise NotImplementedError() # Implementacja różnych stanów class ClosedConnectionState(ConnectionState): @staticmethod def read(conn): raise RuntimeError('Połączenie nie jest otwarte') @staticmethod def write(conn, data): raise RuntimeError('Połączenie nie jest otwarte') @staticmethod def open(conn): conn.new_state(OpenConnectionState) @staticmethod def close(conn): raise RuntimeError('Połączenie już jest zamknięte') class OpenConnectionState(ConnectionState): @staticmethod
274
Rozdział 8. Klasy i obiekty
def read(conn): print('Odczyt') @staticmethod def write(conn, data): print('Zapis') @staticmethod def open(conn): raise RuntimeError('Połączenie już jest otwarte') @staticmethod def close(conn): conn.new_state(ClosedConnectionState)
Oto interaktywna sesja, w której pokazano, jak zastosować te klasy: >>> c = Connection() >>> c._state >>> c.read() Traceback (most recent call last): File "", line 1, in File "example.py", line 10, in read return self._state.read(self) File "example.py", line 43, in read raise RuntimeError('Połączenie nie jest otwarte') RuntimeError: Połączenie nie jest otwarte >>> c.open() >>> c._state >>> c.read() Odczyt >>> c.write('Witaj') Zapis >>> c.close() >>> c._state >>>
Omówienie Kod z dużą liczbą skomplikowanych instrukcji warunkowych i różnymi stanami jest trudny w konserwacji oraz do wytłumaczenia. Zaprezentowane tu rozwiązanie pozwala uniknąć problemów, ponieważ poszczególne stany są rozdzielone na odrębne klasy. Przedstawiony kod może wyglądać dziwnie, jednak do zaimplementowania każdego stanu wykorzystano klasę z metodami statycznymi przyjmującymi jako pierwszy argument obiekt typu Connection. Ten projekt wynika z rezygnacji z przechowywania danych obiektu w poszczególnych klasach reprezentujących różne stany. Wszystkie dane są zapisane w obiekcie typu Connection. Powiązanie stanów ze wspólną klasą bazową przede wszystkim pomaga uporządkować kod i zagwarantować, że dostępne będą odpowiednie metody. Wyjątek NotImplementedError zgłaszany w metodach klasy bazowej pozwala się upewnić, że wymagane metody są dostępne w klasach pochodnych. Inna możliwość to zastosowanie abstrakcyjnej klasy bazowej (zobacz recepturę 8.12).
8.19. Implementowanie obiektów ze stanem lub maszyn stanowych
275
Inna technika polega na bezpośrednim manipulowaniu atrybutem __class__ obiektów. Przyjrzyj się następującemu kodowi: class Connection: def __init__(self): self.new_state(ClosedConnection) def new_state(self, newstate): self.__class__ = newstate def read(self): raise NotImplementedError() def write(self, data): raise NotImplementedError() def open(self): raise NotImplementedError() def close(self): raise NotImplementedError() class ClosedConnection(Connection): def read(self): raise RuntimeError('Połączenie nie jest otwarte') def write(self, data): raise RuntimeError('Połączenie nie jest otwarte') def open(self): self.new_state(OpenConnection) def close(self): raise RuntimeError('Połączenie już jest zamknięte') class OpenConnection(Connection): def read(self): print('Odczyt') def write(self, data): print('Zapis') def open(self): raise RuntimeError('Połączenie już jest otwarte') def close(self): self.new_state(ClosedConnection)
Główną cechą tego kodu jest to, że wyeliminowano w nim jeden poziom pośredni. Zamiast tworzyć odrębne klasy Connection i ConnectionState, tu dwie klasy scalono w jedną. Zmiana stanu powoduje zmianę typu, co pokazano w poniższym kodzie: >>> c = Connection() >>> c <__main__.ClosedConnection object at 0x1006718d0> >>> c.read() Traceback (most recent call last): File "", line 1, in File "state.py", line 15, in read raise RuntimeError('Połączenie nie jest otwarte') RuntimeError: Połączenie nie jest otwarte >>> c.open() >>> c
276
Rozdział 8. Klasy i obiekty
<__main__.OpenConnection object at 0x1006718d0> >>> c.read() Odczyt >>> c.close() >>> c <__main__.ClosedConnection object at 0x1006718d0> >>>
Obiektowym purystom może nie podobać się modyfikowanie atrybutu __class__ obiektu, jednak technicznie jest to dopuszczalne. Przedstawiony tu kod jest też szybszy niż poprzednia wersja, ponieważ metody połączenia nie wymagają dodatkowego kroku w postaci delegowania zadań. Obie zaprezentowane techniki są przydatne przy implementowaniu bardziej skomplikowanych maszyn stanowych (zwłaszcza w kodzie, w którym bez stosowania tych metod konieczne byłoby używanie dużych bloków if-elif-else). Oto przykład: # Pierwotna implementacja class State: def __init__(self): self.state = 'A' def action(self, x): if state == 'A': # Operacje dla stanu A ... state = 'B' elif state == 'B': # Operacje dla stanu B ... state = 'C' elif state == 'C': # Operacje dla stanu C ... state = 'A' # Inna wersja class State: def __init__(self): self.new_state(State_A) def new_state(self, state): self.__class__ = state def action(self, x): raise NotImplementedError() class State_A(State): def action(self, x): # Operacje dla stanu A ... self.new_state(State_B) class State_B(State): def action(self, x): # Operacje dla stanu B ... self.new_state(State_C) class State_C(State): def action(self, x): # Operacje dla stanu C ... self.new_state(State_A)
8.19. Implementowanie obiektów ze stanem lub maszyn stanowych
277
Receptura ta jest luźno oparta na wzorcu projektowym „stan” z książki Wzorce projektowe. Elementy oprogramowania obiektowego Ericha Gammy, Richarda Helma, Ralpha Johnsona i Johna Vlissidesa (Addison-Wesley, 1995; wydanie polskie Helion, 2010).
8.20. Wywoływanie metod obiektu na podstawie nazwy w łańcuchu znaków Problem Programista chce wywołać dla obiektu metodę, której nazwa zapisana jest w łańcuchu znaków.
Rozwiązanie W prostych sytuacjach można wykorzystać metodę getattr(): import math class Point: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return 'Point({!r:},{!r:})'.format(self.x, self.y) def distance(self, x, y): return math.hypot(self.x - x, self.y - y) p = Point(2, 3) d = getattr(p, 'distance')(0, 0) # Wywołuje p.distance(0, 0)
Inna technika polega na zastosowaniu wywołania operator.methodcaller(): import operator operator.methodcaller('distance', 0, 0)(p)
Wywołanie operator.methodcaller() może być przydatne, gdy chcesz wskazać metodę na podstawie nazwy i wielokrotnie przekazywać te same argumenty. Możliwe, że zamierzasz posortować listę punktów: points = [ Point(1, 2), Point(3, 0), Point(10, -3), Point(-5, -7), Point(-1, 8), Point(3, 2) ] # Sortowanie według odległości od początku układu współrzędnych (0, 0) points.sort(key=operator.methodcaller('distance', 0, 0))
Omówienie Wywołanie metody to proces składający się z dwóch odrębnych kroków — wyszukiwania atrybutu i wywoływania funkcji. Dlatego aby wywołać metodę, należy znaleźć atrybut za pomocą wywołania getattr(). W ten sam sposób można uzyskać dostęp do dowolnego 278
Rozdział 8. Klasy i obiekty
atrybutu. Aby wywołać zwróconą wartość tak jak metodę, wystarczy potraktować efekt wyszukiwania jak funkcję. Polecenie operator.methodcaller() tworzy obiekt wywoływalny, przy czym określa argumenty przekazywane do danej metody. Programista musi tylko podać odpowiedni argument self. Oto przykład: >>> p = Point(3, 4) >>> d = operator.methodcaller('distance', 0, 0) >>> d(p) 5.0 >>>
Wywoływanie metod na podstawie nazw zapisanych w łańcuchach znaków jest stosunkowo często potrzebne w kodzie, który ma działać jak instrukcje case lub jedna z wersji wzorca odwiedzający. Bardziej zaawansowany przykład znajdziesz w następnej recepturze.
8.21. Implementowanie wzorca odwiedzający Problem Programista chce napisać kod, który przetwarza (lub przechodzi) skomplikowane struktury danych składające się z obiektów wielu różnych rodzajów, wymagających odmiennej obsługi. Możliwe, że kod ma przechodzić po strukturze drzewiastej i wykonywać różne operacje w zależności od rodzaju napotkanych węzłów drzewa.
Rozwiązanie Problem opisany w tej recepturze często powstaje w programach, które tworzą struktury danych obejmujące dużą liczbę obiektów różnego rodzaju. Załóżmy, że chcesz napisać program, który reprezentuje wyrażenia matematyczne. W programie tym można zastosować wiele klas: class Node: pass class UnaryOperator(Node): def __init__(self, operand): self.operand = operand class BinaryOperator(Node): def __init__(self, left, right): self.left = left self.right = right class Add(BinaryOperator): pass class Sub(BinaryOperator): pass class Mul(BinaryOperator): pass class Div(BinaryOperator):
8.21. Implementowanie wzorca odwiedzający
279
pass class Negate(UnaryOperator): pass class Number(Node): def __init__(self, value): self.value = value
Klasy te można wykorzystać do tworzenia zagnieżdżonych struktur danych: # Zapis wyrażenia 1 + 2 * (3 - 4) / 5 t1 = Sub(Number(3), Number(4)) t2 = Mul(Number(2), t1) t3 = Div(t2, Number(5)) t4 = Add(Number(1), t3)
Problem związany jest nie z tworzeniem takich struktur, ale z pisaniem kodu, który je przetwarza. Programista może chcieć wykonywać na takich wyrażeniach różne operacje — tworzyć dane wyjściowe, generować instrukcje, przeprowadzać przekształcenia itd. Aby umożliwić wykonywanie różnych zadań, często stosuje się wzorzec odwiedzający. Jest on oparty na klasach podobnych do poniższej: class NodeVisitor: def visit(self, node): methname = 'visit_' + type(node).__name__ meth = getattr(self, methname, None) if meth is None: meth = self.generic_visit return meth(node) def generic_visit(self, node): raise RuntimeError('Brak metody {}'.format('visit_' + type(node).__name__))
Aby zastosować tę klasę, programista tworzy na jej podstawie klasę pochodną i implementuje różne metody w postaci visit_Name(), gdzie za człon Name należy podstawić typ węzła. Jeśli chcesz obliczyć wartość wyrażenia, możesz napisać następujący kod: class Evaluator(NodeVisitor): def visit_Number(self, node): return node.value def visit_Add(self, node): return self.visit(node.left) + self.visit(node.right) def visit_Sub(self, node): return self.visit(node.left) - self.visit(node.right) def visit_Mul(self, node): return self.visit(node.left) * self.visit(node.right) def visit_Div(self, node): return self.visit(node.left) / self.visit(node.right) def visit_Negate(self, node): return -node.operand
Oto przykład pokazujący, jak zastosować nową klasę do wygenerowanego wcześniej wyrażenia: >>> e = Evaluator() >>> e.visit(t4) 0.6 >>>
280
Rozdział 8. Klasy i obiekty
Oto zupełnie inny przykład — klasa przekształcająca wyrażenie na operacje prostej maszyny stosowej: class StackCode(NodeVisitor): def generate_code(self, node): self.instructions = [] self.visit(node) return self.instructions def visit_Number(self, node): self.instructions.append(('PUSH', node.value)) def binop(self, node, instruction): self.visit(node.left) self.visit(node.right) self.instructions.append((instruction,)) def visit_Add(self, node): self.binop(node, 'ADD') def visit_Sub(self, node): self.binop(node, 'SUB') def visit_Mul(self, node): self.binop(node, 'MUL') def visit_Div(self, node): self.binop(node, 'DIV') def unaryop(self, node, instruction): self.visit(node.operand) self.instructions.append((instruction,)) def visit_Negate(self, node): self.unaryop(node, 'NEG')
Oto przykład zastosowania tej klasy: >>> s = StackCode() >>> s.generate_code(t4) [('PUSH', 1), ('PUSH', 2), ('PUSH', 3), ('PUSH', 4), ('SUB',), ('MUL',), ('PUSH', 5), ('DIV',), ('ADD',)] >>>
Omówienie W tej recepturze wykorzystano dwa ważne pomysły. Pierwszy związany jest ze strategią projektową, polegającą na tym, że kod manipulujący skomplikowaną strukturą danych jest oddzielony od samej struktury. Oznacza to, że w tej recepturze żadna z różnych klas Node nie zawiera kodu do obsługi danych. Wszystkie manipulacje są przeprowadzane przez konkretny kod z odrębnej klasy NodeVisitor. Ten podział sprawia, że kod jest bardzo ogólny. Drugim ważnym pomysłem jest implementacja klasy ze wzorca odwiedzający. W klasie tej należy wybierać różne metody obsługi zadań na podstawie pewnej wartości, np. typu węzła. W naiwnej wersji można napisać bardzo rozbudowaną instrukcję if: class NodeVisitor: def visit(self, node): nodetype = type(node).__name__ if nodetype == 'Number': return self.visit_Number(node)
Szybko jednak staje się oczywiste, że jest to złe podejście. Kod jest nie tylko bardzo rozwlekły, ale też działa wolno i jest trudny w konserwacji, gdy trzeba dodać lub zmienić rodzaje obsługiwanych węzłów. Znacznie lepiej jest zastosować pewną sztuczkę — odpowiednio nazwać metody i pobierać je za pomocą funkcji getattr() w zaprezentowany wcześniej sposób. Metoda generic_visit() w rozwiązaniu to metoda rezerwowa. Program wywołuje ją, gdy nie może znaleźć odpowiedniej metody do obsługi danego zadania. W tej recepturze metoda generic_visit() zgłasza wyjątek, aby poinformować programistę o napotkaniu nieoczekiwanego typu węzła. W każdej klasie „odwiedzającej” obliczenia są wykonywane przez rekurencyjne wywołania metody visit(). Oto przykład: class Evaluator(NodeVisitor): ... def visit_Add(self, node): return self.visit(node.left) + self.visit(node.right)
Rekurencja powoduje, że klasa „odwiedzająca” przechodzi po całej strukturze danych. Metoda visit() jest wywoływana do momentu natrafienia na węzeł końcowy, którym tu jest węzeł Number. Kolejność wywołań w rekurencji oraz innych operacji zależy od programu. Warto zauważyć, że ta konkretna technika określania wywoływanych metod jest też standardowym sposobem na tworzenie odpowiedników instrukcji switch lub case z innych języków. W trakcie pisania mechanizmu obsługi wywołań HTTP można utworzyć klasy, które w podobny sposób określają wywoływane metody: class HTTPHandler: def handle(self, request): methname = 'do_' + request.request_method getattr(self, methname)(request) def do_GET(self, request): ... def do_POST(self, request): ... def do_HEAD(self, request): ...
Wadą wzorca odwiedzający jest zależność od rekurencji. Jeśli zastosujesz tę technikę do głęboko zagnieżdżonej struktury, program może dojść do obowiązującego w Pythonie limitu poziomu rekurencji (możesz go ustalić za pomocą funkcji sys.getrecursionlimit()). Aby uniknąć tego problemu, można wprowadzić pewne zmiany w strukturach danych — np. zastosować normalne listy Pythona zamiast list wiązanych lub umieścić więcej danych w każdym węźle, aby zmniejszyć liczbę poziomów. Jeszcze inna możliwość to wykorzystanie nierekurencyjnych algorytmów przechodzenia po strukturze, opartych na generatorach lub iteratorach (zobacz recepturę 8.22). Wzorzec odwiedzający bardzo często znajduje zastosowanie w programach związanych z parsowaniem i kompilowaniem. Godną uwagi implementację tego wzorca znajdziesz w module ast Pythona. Nie tylko umożliwia on poruszanie się po strukturach drzewiastych, ale pozwala
282
Rozdział 8. Klasy i obiekty
również na zmianę struktury danych i przekształcanie jej (np. dodawanie i usuwanie węzłów) w trakcie przechodzenia po niej. Więcej szczegółów znajdziesz w kodzie źródłowym modułu ast. W recepturze 9.24 pokazano przykład wykorzystania modułu ast do przetwarzania kodu źródłowego w języku Python.
8.22. Implementowanie wzorca odwiedzający bez stosowania rekurencji Problem Programista pisze oparty na wzorcu odwiedzający kod, który przechodzi po głęboko zagnieżdżonej strukturze drzewiastej. Jednak z uwagi na przekroczenie limitu rekurencji program nie działa. Programista chce wyeliminować rekurencję, a jednocześnie zachować podobny styl programowania jak we wzorcu odwiedzający.
Rozwiązanie Czasem można w pomysłowy sposób wykorzystać generatory, aby wyeliminować rekurencję z algorytmów przechodzenia lub przeszukiwania drzew. W recepturze 8.21 przedstawiono klasę „odwiedzającą”. Tu znajdziesz inną implementację tej klasy. Obliczenia są tu przeprowadzane w zupełnie inny sposób — z wykorzystaniem stosu i generatorów: import types class Node: pass import types class NodeVisitor: def visit(self, node): stack = [ node ] last_result = None while stack: try: last = stack[-1] if isinstance(last, types.GeneratorType): stack.append(last.send(last_result)) last_result = None elif isinstance(last, Node): stack.append(self._visit(stack.pop())) else: last_result = stack.pop() except StopIteration: stack.pop() return last_result def _visit(self, node): methname = 'visit_' + type(node).__name__ meth = getattr(self, methname, None) if meth is None: meth = self.generic_visit return meth(node) def generic_visit(self, node): raise RuntimeError('Brak metody {}'.format('visit_' + type(node).__name__))
8.22. Implementowanie wzorca odwiedzający bez stosowania rekurencji
283
Jeśli zastosujesz tę klasę, przekonasz się, że współdziała ona poprawnie z istniejącym kodem, który korzystał z wersji opartej na rekurencji. Klasa ta może nawet zastąpić implementację wzorca odwiedzający z poprzedniej receptury. Przyjrzyj się następującemu kodowi (występują w nim drzewa wyrażeń): class UnaryOperator(Node): def __init__(self, operand): self.operand = operand class BinaryOperator(Node): def __init__(self, left, right): self.left = left self.right = right class Add(BinaryOperator): pass class Sub(BinaryOperator): pass class Mul(BinaryOperator): pass class Div(BinaryOperator): pass class Negate(UnaryOperator): pass class Number(Node): def __init__(self, value): self.value = value # Przykładowa klasa „odwiedzająca” do obliczania wartości wyrażeń class Evaluator(NodeVisitor): def visit_Number(self, node): return node.value def visit_Add(self, node): return self.visit(node.left) + self.visit(node.right) def visit_Sub(self, node): return self.visit(node.left) - self.visit(node.right) def visit_Mul(self, node): return self.visit(node.left) * self.visit(node.right) def visit_Div(self, node): return self.visit(node.left) / self.visit(node.right) def visit_Negate(self, node): return -self.visit(node.operand) if __name__ == '__main__': # 1 + 2*(3-4) / 5 t1 = Sub(Number(3), Number(4)) t2 = Mul(Number(2), t1) t3 = Div(t2, Number(5)) t4 = Add(Number(1), t3) # Obliczanie wartości e = Evaluator() print(e.visit(t4))
284
Rozdział 8. Klasy i obiekty
# Zwraca 0.6
Wcześniejszy kod działa dla prostych wyrażeń. Jednak w klasie Evaluator wykorzystano rekurencję, dlatego przy zbyt dużej liczbie poziomów zagnieżdżenia program przestanie działać. Oto przykład: >>> a = Number(0) >>> for n in range(1, 100000): ... a = Add(a, Number(n)) ... >>> e = Evaluator() >>> e.visit(a) Traceback (most recent call last): ... File "visitor.py", line 29, in _visit return meth(node) File "visitor.py", line 67, in visit_Add return self.visit(node.left) + self.visit(node.right) RuntimeError: maximum recursion depth exceeded >>>
Jeśli spróbujesz uruchomić ten sam kod, który wcześniej uruchamiałeś dla wersji rekurencyjnej, okaże się, że zadziała. To prawdziwa magia! >>> a = Number(0) >>> for n in range(1,100000): ... a = Add(a, Number(n)) ... >>> e = Evaluator() >>> e.visit(a) 4999950000 >>>
Jeśli dodasz niestandardowe przetwarzanie w wybranych metodach, kod także będzie działał. Oto przykład: class Evaluator(NodeVisitor): ... def visit_Add(self, node): print('Dodawanie:', node) lhs = yield node.left print('left=', lhs) rhs = yield node.right print('right=', rhs) yield lhs + rhs ...
8.22. Implementowanie wzorca odwiedzający bez stosowania rekurencji
285
Poniżej pokazano przykładowe dane wyjściowe: >>> e = Evaluator() >>> e.visit(t4) Dodawanie: <__main__.Add object at 0x1006a8d90> left= 1 right= -0.4 0.6 >>>
Omówienie Ta receptura jest dobrą ilustracją tego, jak za pomocą generatorów i współprogramów można wykonywać skomplikowane sztuczki z przepływem sterowania (często ze świetnymi wynikami). Aby zrozumieć tę recepturę, trzeba zwrócić uwagę na kilka kwestii. Po pierwsze, w kontekście problemów związanych z przechodzeniem po drzewie często stosuje się strategię unikania rekurencji dzięki pisaniu algorytmów opartych na stosie lub kolejce. Np. przechodzenie w głąb można zaimplementować, umieszczając węzły na stosie przy ich napotkaniu i zdejmując je po zakończeniu przetwarzania. Najważniejszy fragment metody visit() w rozwiązaniu zbudowano na podstawie tego pomysłu. Algorytm najpierw umieszcza początkowy węzeł na liście stack, a następnie działa do czasu zdjęcia wszystkich elementów z tej listy. W trakcie działania algorytmu lista jest wydłużana odpowiednio do głębokości przetwarzanej struktury drzewiastej. Druga kwestia dotyczy działania polecenia yield w generatorach. Po napotkaniu tego polecenia generator zwraca wartość i wstrzymuje pracę. W tej recepturze wykorzystano to zamiast rekurencji. Zamiast wyrażenia rekurencyjnego: value = self.visit(node.left)
w kodzie pojawia się następujący fragment: value = yield node.left
Na zapleczu prowadzi to do przesłania danego węzła (node.left) z powrotem do metody visit(). Metoda visit() wywołuje wtedy dla węzła odpowiednią metodę visit_Name(). Pod pewnymi względami jest to niemal przeciwieństwo rekurencji. Zamiast rekurencyjnie wywoływać metodę visit() w celu przejścia do dalszych kroków algorytmu, polecenie yield tymczasowo wstrzymuje trwające obliczenia. Jest też sygnałem informującym algorytm, że zwrócony węzeł należy przetworzyć przed przejściem do dalszych operacji. Ostatni aspekt receptury dotyczy przekazywania wyników. Gdy używane są funkcje generatora, nie można używać instrukcji return do zwracania wartości (prowadzi to do zgłoszenia wyjątku SyntaxError). Dlatego polecenie yield musi wykonać dwa zadania. W tej recepturze program przyjmuje, że jeśli wartość zwrócona przez polecenie yield nie jest węzłem, to znaczy, że jest to wartość, którą należy przekazać do następnego etapu obliczeń. Do tego służy zmienna last_return w zaprezentowanym kodzie. Zwykle znajduje się w niej ostatnia wartość zwrócona przez metodę visit(). Wartość ta jest przesyłana do wcześniej wykonywanej metody, gdzie pojawia się jako wartość zwrócona przez polecenie yield. Np. w poniższym kodzie: value = yield node.left
zmienna value przyjmuje wartość zmiennej last_return, która jest wynikiem zwróconym przez metodę klasy „odwiedzającej” wywołaną dla węzła node.left. 286
Rozdział 8. Klasy i obiekty
Wszystkie opisane aspekty receptury występują w poniższym fragmencie kodu: try: last = stack[-1] if isinstance(last, types.GeneratorType): stack.append(last.send(last_result)) last_result = None elif isinstance(last, Node): stack.append(self._visit(stack.pop())) else: last_result = stack.pop() except StopIteration: stack.pop()
Ten kod sprawdza element ze szczytu stosu i na tej podstawie określa, co ma robić dalej. Jeśli tym elementem jest generator, program wywołuje jego metodę send() z ostatnim wynikiem (jeśli jest dostępny), a następnie umieszcza zwróconą wartość na stosie w celu dalszego przetwarzania. Wartość zwrócona przez metodę send() to wartość przekazana do polecenia yield. Dlatego w poleceniu yield node.left obiekt node.left typu Node jest zwracany przez metodę send() i umieszczany na szczycie stosu. Jeśli na szczycie stosu znajduje się obiekt typu Node, zostaje on zastąpiony wynikiem wywołania dla niego odpowiedniej metody klasy „odwiedzającej”. To w tym miejscu eliminowana jest rekurencja. Różne metody klasy „odwiedzającej” wywołują na tym etapie metodę visit(), zamiast robić to rekurencyjnie. O ile w metodach używane jest polecenie yield, całe rozwiązanie działa prawidłowo. Jeśli na szczycie stosu znajduje się inny element, program przyjmuje, że jest to zwrócona wartość. Kod zdejmuje ją ze stosu i umieszcza w zmiennej last_result. Jeśli następnym elementem na stosie jest generator, jest on przekazywany jako wartość zwrócona do polecenia yield. Warto zauważyć, że wartość ostatecznie zwracana przez metodę visit() to wartość zmiennej last_result. To sprawia, że receptura współdziała z kodem napisanym pod kątem tradycyjnej, rekurencyjnej implementacji. Gdy nie są używane generatory, zmienna przechowuje wartość przekazaną do dowolnego polecenia return wywołanego w kodzie. Jeden z problemów związanych z tą recepturą dotyczy rozróżnienia na zwracanie wartości typu Node i innych typów. W przedstawionej wersji kod automatycznie przechodzi po wszystkich obiektach typu Node. Oznacza to, że nie można wykorzystać obiektu tego typu jako zwróconej wartości, którą należy przekazać dalej. W praktyce może to być nieistotne. Jeśli jednak stanowi to problem, konieczne może być wprowadzenie zmian w algorytmie. Można w tym celu dodać nową klasę: class Visit: def __init__(self, node): self.node = node class NodeVisitor: def visit(self, node): stack = [ Visit(node) ] last_result = None while stack: try: last = stack[-1] if isinstance(last, types.GeneratorType): stack.append(last.send(last_result)) last_result = None
8.22. Implementowanie wzorca odwiedzający bez stosowania rekurencji
W tej wersji poszczególne metody klasy „odwiedzającej” wyglądają tak: class Evaluator(NodeVisitor): ... def visit_Add(self, node): yield (yield Visit(node.left)) + (yield Visit(node.right)) def visit_Sub(self, node): yield (yield Visit(node.left)) - (yield Visit(node.right)) ...
Po zapoznaniu się z tą recepturą możesz zechcieć zbadać rozwiązanie bez poleceń yield. Jednak także w takim kodzie występuje wiele opisanych tu problemów. Aby wyeliminować rekurencję, trzeba wykorzystać stos. Ponadto należy wymyślić system zarządzania przechodzeniem przez strukturę i wywoływaniem logiki klasy „odwiedzającej”. Bez generatorów kod stanie się bardzo skomplikowanym zbitkiem fragmentów manipulujących stosem, zwrotnie wywoływanych funkcji i innych mechanizmów. Główną zaletą stosowania polecenia yield jest to, że można pisać nierekurencyjny kod w eleganckim stylu, który wygląda niemal dokładnie tak jak implementacja rekurencyjna.
8.23. Zarządzanie pamięcią w cyklicznych strukturach danych Problem Program tworzy struktury danych z cyklami (np. drzewa, grafy, strukturę opartą na wzorcu obserwator), przy czym programista ma problem z zarządzaniem pamięcią.
Rozwiązanie Prostą przykładową cykliczną strukturą danych jest struktura drzewiasta, w której element nadrzędny prowadzi do elementu podrzędnego, a ten — z powrotem do elementu nadrzędnego. W takim kodzie jedno z powiązań należy utworzyć jako słabą referencję, używając biblioteki weakref. Oto przykład:
288
Rozdział 8. Klasy i obiekty
import weakref class Node: def __init__(self, value): self.value = value self._parent = None self.children = [] def __repr__(self): return 'Node({!r:})'.format(self.value) # Właściwość do zarządzania elementem nadrzędnym za pomocą słabej referencji @property def parent(self): return self._parent if self._parent is None else self._parent() @parent.setter def parent(self, node): self._parent = weakref.ref(node) def add_child(self, child): self.children.append(child) child.parent = self
Ten kod umożliwia bezproblemowe usunięcie elementu nadrzędnego. Oto przykład: >>> root = Node('parent') >>> c1 = Node('child') >>> root.add_child(c1) >>> print(c1.parent) Node('parent') >>> del root >>> print(c1.parent) None >>>
Omówienie Cykliczne struktury danych są dość skomplikowanym aspektem Pythona, który wymaga starannej analizy, ponieważ w obszarze tym często nie obowiązują standardowe reguły przywracania pamięci. Przyjrzyj się następującemu kodowi: # Klasa ilustrująca moment usuwania danych class Data: def __del__(self): print('Data.__del__') # Klasa węzła z cyklem class Node: def __init__(self): self.data = Data() self.parent = None self.children = [] def add_child(self, child): self.children.append(child) child.parent = self
Na podstawie tego kodu można przeprowadzić pewne eksperymenty, aby dostrzec trudne do zauważenia problemy z przywracaniem pamięci: >>> a = Data() >>> del a Data.__del__
# Usuwane natychmiast
8.23. Zarządzanie pamięcią w cyklicznych strukturach danych
289
>>> a = Node() >>> del a # Usuwane natychmiast Data.__del__ >>> a = Node() >>> a.add_child(Node()) >>> del a # Nie jest usuwane (brak komunikatu) >>>
Obiekty są tu usuwane natychmiast. Wyjątkiem jest ostatnia sytuacja, w której występuje cykl. Wynika to z tego, że mechanizm przywracania pamięci w Pythonie jest oparty na prostym zliczaniu referencji. Gdy liczba referencji do obiektu spada do 0, program natychmiast go usuwa. Jednak w cyklicznych strukturach danych liczba referencji zawsze jest większa. W ostatniej części przykładowego kodu węzły nadrzędny i podrzędny prowadzą do siebie nawzajem, dlatego liczba referencji jest niezerowa. Do obsługi cykli służy uruchamiany okresowo odrębny mechanizm przywracania pamięci. Zwykle nie wiadomo, kiedy zadziała. Dlatego nie wiadomo też, kiedy program usunie cykliczne struktury danych. W razie konieczności można wymusić przywrócenie pamięci, jest to jednak stosunkowo nieeleganckie: >>> import gc >>> gc.collect() Data.__del__ Data.__del__ >>>
# Wymuszanie przywracania pamięci
Jeszcze poważniejszy problem ma miejsce, gdy w obiektach występujących w cyklu zdefiniowana jest metoda __del__(). Załóżmy, że kod wygląda tak: # Klasa ilustrująca moment usuwania class Data: def __del__(self): print('Data.__del__') # Klasa węzła z cyklem class Node: def __init__(self): self.data = Data() self.parent = None self.children = [] # NIGDY NIE PISZ TAKIEGO KODU # Ten fragment ma tylko ilustrować błędne działanie kodu def __del__(self): del self.data del.parent del.children def add_child(self, child): self.children.append(child) child.parent = self
Mechanizm przywracania pamięci nigdy nie usunie struktur danych z tego kodu, dlatego z programu będzie wyciekała pamięć. Jeśli uruchomisz kod, przekonasz się, że komunikat Data.__del__ nigdy się nie pojawia — nawet po wymuszonym przywracaniu pamięci: >>> >>> >>> >>> >>> >>>
290
a = Node() a.add_child(Node() del a # Brak komunikatu (pamięć nie jest przywracana) import gc gc.collect() # Brak komunikatu (pamięć nie jest przywracana)
Rozdział 8. Klasy i obiekty
Słabe referencje rozwiązują ten problem, ponieważ eliminują cykle referencji. Słaba referencja to wskaźnik do obiektu niezwiększający liczby referencji. Do tworzenia słabych referencji służy biblioteka weakref. Oto przykład: >>> import weakref >>> a = Node() >>> a_ref = weakref.ref(a) >>> a_ref >>>
Aby pobrać obiekt wskazywany przez słabą referencję, należy wywołać go jak funkcję. Jeśli wskazywany obiekt istnieje, zostanie zwrócony. W przeciwnym razie program zwróci wartość None. Ponieważ liczba referencji pierwotnego obiektu nie jest zwiększana, można go w normalny sposób usunąć. Oto przykład: >>> print(a_ref()) <__main__.Node object at 0x1005c5410> >>> del a Data.__del__ >>> print(a_ref()) None >>>
Jeśli używasz słabych referencji (takich jak w rozwiązaniu), nie występują cykle referencji i przywracanie pamięci zachodzi natychmiast, gdy węzeł nie jest już używany. Inny przykład wykorzystania słabych referencji znajdziesz w recepturze 8.25.
8.24. Tworzenie klas z obsługą porównań Problem Programista chce móc porównywać obiekty danej klasy za pomocą standardowych operatorów porównywania (>=, !=, <= itd.), jednak bez konieczności pisania dużej liczby metod specjalnych.
Rozwiązanie W klasach Pythona można dodać obsługę porównań, pisząc specjalną metodę dla każdego operatora porównywania. Np. aby dodać obsługę operatora >=, należy zdefiniować w klasie metodę __ge__(). Choć zdefiniowanie jednej metody zwykle nie stanowi problemu, pisanie kodu każdego możliwego operatora porównywania jest żmudne. Aby uprościć proces, można wykorzystać dekorator functools.total_ordering. W tym celu należy dodać go do klasy i zdefiniować operację __eq__() oraz jedną z metod do obsługi porównań (__lt__, __le__, __gt__ lub __ge__). Dekorator uzupełni wtedy pozostałe metody tego rodzaju. W ramach przykładu zbudujmy klasy reprezentujące domy i pokoje, a następnie przeprowadźmy porównania wielkości domów:
8.24. Tworzenie klas z obsługą porównań
291
from functools import total_ordering class Room: def __init__(self, name, length, width): self.name = name self.length = length self.width = width self.square_feet = self.length * self.width @total_ordering class House: def __init__(self, name, style): self.name = name self.style = style self.rooms = list() @property def living_space_footage(self): return sum(r.square_feet for r in self.rooms) def add_room(self, room): self.rooms.append(room) def __str__(self): return '{}: {} stóp kwadratowych {}'.format(self.name, self.living_space_footage, self.style) def __eq__(self, other): return self.living_space_footage == other.living_space_footage def __lt__(self, other): return self.living_space_footage < other.living_space_footage
Tu do klasy House dodano dekorator @total_ordering. Udostępniono też metody __eq__() i __lt__(), aby umożliwić porównywanie domów na podstawie łącznej powierzchni pokojów. Ta najprostsza możliwa definicja wystarczy, aby działały także wszystkie pozostałe operacje porównywania: # Tworzenie domów i dodawanie do nich pokojów h1 = House('h1', 'Na cyplu') h1.add_room(Room('Główna sypialnia', 14, 21)) h1.add_room(Room('Salon', 18, 20)) h1.add_room(Room('Kuchnia', 12, 16)) h1.add_room(Room('Gabinet', 12, 12)) h2 = House('h2', 'Ranczo') h2.add_room(Room('Główna sypialnia', 14, 21)) h2.add_room(Room('Salon', 18, 20)) h2.add_room(Room('Kuchnia', 12, 16)) h3 = House('h3', 'Bliźniak') h3.add_room(Room('Główna sypialnia', 14, 21)) h3.add_room(Room('Salon', 18, 20)) h3.add_room(Room('Gabinet', 12, 16)) h3.add_room(Room('Kuchnia', 15, 17)) houses = [h1, h2, h3] print('Czy dom h1 jest większy od h2?', h1 > h2) # Wyświetla True print('Czy dom h2 jest mniejszy od h3?', h2 < h3) # Wyświetla True print('Czy dom h2 jest większy lub równy h1?', h2 >= h1) # Wyświetla False print('Który dom jest największy?', max(houses)) # Wyświetla 'h3: 1101-square-foot Bliźniak' print('Który dom jest najmniejszy?', min(houses)) # Wyświetla 'h2: 846-square-foot Ranczo'
292
Rozdział 8. Klasy i obiekty
Omówienie Jeśli napisałeś w klasie kod obsługujący wszystkie podstawowe operatory porównywania, działanie dekoratora total_ordering prawdopodobnie nie wyda Ci się niczym wyjątkowym. Dekorator ten wykorzystuje dostępne metody obsługujące porównania do utworzenia wszystkich pozostałych, które są potrzebne. Tak więc jeśli w klasie zdefiniowałeś metodę __lt__() (tak jak w rozwiązaniu), posłuży ona do utworzenia wszystkich pozostałych operatorów porównywania. Dekorator zapełnia klasę metodami w następującej postaci: class House: def __eq__(self, other): ... def __lt__(self, other): ... # Metody tworzone przez dekorator @total_ordering __le__ = lambda self, other: self < other or self == other __gt__ = lambda self, other: not (self < other or self == other) __ge__ = lambda self, other: not (self < other) __ne__ = lambda self, other: not self == other
Oczywiście łatwo można napisać te metody samodzielnie, jednak dekorator @total_ordering pozwala uniknąć potrzebnych przy tym eksperymentów.
8.25. Tworzenie obiektów zapisywanych w pamięci podręcznej Problem Programista chce, aby przy tworzeniu obiektów danej klasy zwracane były zapisane w pamięci podręcznej referencje do wcześniejszych obiektów (jeśli te istnieją) utworzonych za pomocą tych samych argumentów.
Rozwiązanie Problem występuje, gdy programista chce mieć pewność, że dla określonego zestawu argumentów wejściowych istnieje tylko jeden obiekt danej klasy. W praktyce działają tak niektóre biblioteki, np. moduł logging, w której z daną nazwą wiązany jest tylko jeden obiekt rejestrujący operacje. Oto przykład: >>> import logging >>> a = logging.getLogger('foo') >>> b = logging.getLogger('bar') >>> a is b False >>> c = logging.getLogger('foo') >>> a is c True >>>
Aby uzyskać taki efekt, należy wykorzystać funkcję fabryczną niezależną od samej klasy: # Dana klasa class Spam: def __init__(self, name):
8.25. Tworzenie obiektów zapisywanych w pamięci podręcznej
293
self.name = name # Obsługa pamięci podręcznej import weakref _spam_cache = weakref.WeakValueDictionary() def get_spam(name): if name not in _spam_cache: s = Spam(name) _spam_cache[name] = s else: s = _spam_cache[name] return s
Jeśli zastosujesz ten kod, odkryjesz, że działa w przedstawiony wcześniej sposób: >>> a >>> b >>> a False >>> c >>> a True >>>
= get_spam('foo') = get_spam('bar') is b = get_spam('foo') is c
Omówienie Napisanie prostej funkcji fabrycznej to często stosowany sposób na łatwą zmianę standardowych reguł tworzenia obiektów. Czy jednak nie istnieje bardziej elegancka technika? Możesz np. pomyśleć o rozwiązaniu, w którym metoda __new__() klasy jest zdefiniowana w następujący sposób: # Uwaga — ten kod nie jest do końca prawidłowy import weakref class Spam: _spam_cache = weakref.WeakValueDictionary() def __new__(cls, name): if name in cls._spam_cache: return cls._spam_cache[name] else: self = super().__new__(cls) cls._spam_cache[name] = self return self def __init__(self, name): print('Inicjowanie obiektu typu Spam') self.name = name
Na pozór wydaje się, że kod ten jest poprawny. Związany jest z nim jednak poważny problem — metoda __init__() jest wywoływana zawsze, niezależnie od tego, czy obiekt znajduje się w pamięci podręcznej czy nie. Oto przykład: >>> s = Spam('Dawid') Inicjowanie obiektu typu Spam >>> t = Spam('Dawid') Inicjowanie obiektu typu Spam >>> s is t True >>>
294
Rozdział 8. Klasy i obiekty
Program prawdopodobnie nie powinien tak działać. Dlatego aby rozwiązać problem zapisywania w pamięci podręcznej bez ponownego inicjowania obiektu, należy zastosować nieco odmienne podejście. Wykorzystanie słabych referencji w tej recepturze związane jest z ważnym zagadnieniem z obszaru przywracania pamięci (opisano to w recepturze 8.23). Obiekty powinny być przechowywane w pamięci podręcznej tylko dopóty, dopóki są używane w programie. Obiekt typu WeakValueDictionary przechowuje elementy tylko wtedy, gdy istnieją w innym miejscu. Jeśli obiekt nie jest już używany, powiązany z nim klucz zostaje usunięty ze słownika. Przyjrzyj się poniższemu kodowi: >>> a = get_spam('foo') >>> b = get_spam('bar') >>> c = get_spam('foo') >>> list(_spam_cache) ['foo', 'bar'] >>> del a >>> del c >>> list(_spam_cache) ['bar'] >>> del b >>> list(_spam_cache) [] >>>
W wielu programach prosty kod zaprezentowany w tej recepturze jest wystarczający. Istnieją też jednak bardziej zaawansowane techniki, nad którymi można się zastanowić. Przedstawiona receptura budzi wątpliwości związane z zależnością od zmiennych globalnych i funkcji fabrycznej, które są oddzielone od definicji pierwotnej klasy. Jedna z możliwości uporządkowania rozwiązania polega na umieszczeniu kodu do obsługi pamięci podręcznej w odrębnej klasie menedżera i powiązaniu elementów w następujący sposób: import weakref class CachedSpamManager: def __init__(self): self._cache = weakref.WeakValueDictionary() def get_spam(self, name): if name not in self._cache: s = Spam(name) self._cache[name] = s else: s = self._cache[name] return s def clear(self): self._cache.clear() class Spam: manager = CachedSpamManager() def __init__(self, name): self.name = name def get_spam(name): return Spam.manager.get_spam(name)
8.25. Tworzenie obiektów zapisywanych w pamięci podręcznej
295
Jedną z cech tego podejścia jest to, że zapewnia ono większą swobodę. Można np. opracować różne systemy zarządzania (i umieścić je w odrębnych klasach), a następnie dołączać je do klasy Spam zamiast domyślnego kodu do obsługi pamięci podręcznej. Aby to rozwiązanie działało, nie trzeba wprowadzać żadnych innych zmian w kodzie (np. w metodzie get_spam). Inna kwestia projektowa dotyczy tego, czy udostępniać definicję klasy użytkownikom. Jeśli nie zastosujesz żadnych specjalnych technik, użytkownik będzie mógł łatwo tworzyć obiekty z pominięciem pamięci podręcznej: >>> a = Spam('foo') >>> b = Spam('foo') >>> a is b False >>>
Jeśli chcesz temu zapobiec, możesz podjąć pewne kroki, np. rozpocząć nazwę klasy od podkreślenia (_Spam), co jest dla użytkowników wskazówką, że nie powinni bezpośrednio uzyskiwać do niej dostępu. Jeżeli chcesz bardziej jednoznacznie poinformować użytkowników, że nie powinni bezpośrednio tworzyć obiektów typu Spam, możesz zgłaszać wyjątek w metodzie __init__() i udostępnić zastępczy konstruktor za pomocą metody klasy: class Spam: def __init__(self, *args, **kwargs): raise RuntimeError("Nie można bezpośrednio tworzyć obiektów tego typu") # Konstruktor zastępczy @classmethod def _new(cls, name): self = cls.__new__(cls) self.name = name
Aby zastosować to rozwiązanie, należy zmodyfikować kod do obsługi pamięci podręcznej i tworzyć w nim obiekty za pomocą metody Spam._new(), a nie standardowego wywołania metody Spam(): import weakref class CachedSpamManager: def __init__(self): self._cache = weakref.WeakValueDictionary() def get_spam(self, name): if name not in self._cache: s = Spam._new(name) # Zmodyfikowany sposób tworzenia obiektów self._cache[name] = s else: s = self._cache[name] return s
Choć można zastosować bardziej skrajne rozwiązania, aby ukryć klasę Spam, najlepiej jest nie komplikować nadmiernie kodu. Zastosowanie podkreślenia w nazwie lub zdefiniowanie konstruktora w postaci metody klasy zwykle wystarczy, aby programiści zrozumieli wskazówkę. Do obsługi pamięci podręcznej i innych wzorców tworzenia obiektów można wykorzystać także bardziej elegancki (choć również bardziej zaawansowany) sposób, oparty na metaklasach (zobacz recepturę 9.13).
296
Rozdział 8. Klasy i obiekty
ROZDZIAŁ 9.
Metaprogramowanie
Jednym z najważniejszych haseł w dziedzinie rozwijania oprogramowania jest „nie powtarzaj się”. Oznacza to, że gdy musisz napisać wysoce powtarzalny kod (lub wycinać i wklejać kod źródłowy), często warto poszukać bardziej eleganckiego rozwiązania. W Pythonie tego rodzaju problemy często rozwiązuje się za pomocą metaprogramowania. Podejście to polega na tworzeniu funkcji i klas, których głównym zadaniem jest manipulowanie kodem (modyfikowanie i generowanie istniejącego kodu oraz tworzenie nakładek na niego). Głównymi mechanizmami są tu dekoratory, dekoratory klas i metaklasy, jednak metaprogramowanie związane jest też z wieloma innymi przydatnymi zagadnieniami — obiektami sygnatur, wykonywaniem kodu za pomocą wywołania exec() i sprawdzaniem wewnętrznych elementów funkcji lub klas. W rozdziale tym chcemy przede wszystkim przedstawić różne techniki metaprogramowania i pokazać, jak przy ich użyciu dostosować działanie Pythona do własnych potrzeb.
9.1. Tworzenie nakładek na funkcje Problem Programista chce powiązać z funkcją warstwę nakładki, która wykonuje dodatkowe zadania (np. rejestruje operacje lub mierzy czas).
Rozwiązanie Jeśli chcesz dołączyć do funkcji dodatkowy kod, zdefiniuj dekorator. Oto przykład: import time from functools import wraps def timethis(func): ''' Dekorator informujący o czasie wykonania ''' @wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(func.__name__, end-start) return result return wrapper
297
Oto przykład ilustrujący, jak używać tego dekoratora: >>> @timethis ... def countdown(n): ... ''' ... Odliczanie do zera ... ''' ... while n > 0: ... n -= 1 ... >>> countdown(100000) countdown 0.008917808532714844 >>> countdown(10000000) countdown 0.87188299392912 >>>
Omówienie Dekorator to funkcja, która jako dane wejściowe przyjmuje funkcję i zwraca nową funkcję. Gdy piszesz kod w następującej postaci: @timethis def countdown(n): ...
jest to odpowiednik wykonania następujących odrębnych kroków: def countdown(n): ... countdown = timethis(countdown)
Wbudowane dekoratory (np. @staticmethod, @classmethod i @property) działają w ten sam sposób. Dwa poniższe fragmenty kodu są swoimi odpowiednikami: class A: @classmethod def method(cls): pass class B: # Równoznaczna definicja metody klasy def method(cls): pass method = classmethod(method)
Kod w dekoratorze zwykle tworzy nową funkcję, która przyjmuje argumenty przekazane w *args i **kwargs. Tak działa funkcja wrapper() w tej recepturze. W dekoratorze funkcji należy umieścić wywołanie pierwotnej funkcji i zwrócić otrzymany z niej wynik. Jednak oprócz tego można zastosować w dekoratorze dowolny dodatkowy kod (np. do pomiaru czasu działania). Efektem tego jest nowo utworzona funkcja wrapper, która zajmuje miejsce pierwotnej funkcji. Bardzo ważne jest to, że dekoratory zwykle nie zmieniają sygnatury ani wartości zwracanej przez funkcję, dla której tworzona jest nakładka. Zastosowanie argumentów *args i **kwargs pozwala na upewnienie się, że funkcja przyjmie dowolne argumenty wejściowe. Wartością zwracaną przez dekorator jest prawie zawsze wynik wywołania func(*args, **kwargs), gdzie func to pierwotna funkcja (bez nakładki). Przy poznawaniu dekoratorów zrozumienie prostych przykładów, takich jak z tej receptury, jest zwykle bardzo łatwe. Jeśli jednak zamierzasz pisać dekoratory w prawdziwych programach, powinieneś uwzględnić pewne szczegóły. W rozwiązaniu np. łatwo jest zapomnieć 298
Rozdział 9. Metaprogramowanie
o zastosowaniu dekoratora @wraps(func), a to jest jednak ważna kwestia techniczna pozwalająca zachować metadane funkcji (zagadnienie to opisano w następnej recepturze). W kilku kolejnych recepturach zapoznasz się z pewnymi szczegółami, które są ważne przy samodzielnym pisaniu dekoratorów.
9.2. Zachowywanie metadanych funkcji przy pisaniu dekoratorów Problem Programista napisał dekorator, jednak gdy zastosował go dla funkcji, utracił ważne metadane — nazwę, łańcuch znaków z dokumentacją, uwagi i sygnaturę.
Rozwiązanie Przy definiowaniu dekoratorów należy zawsze pamiętać, aby w funkcji używanej jako nakładka zastosować dekorator @wraps z biblioteki functools. Oto przykład: import time from functools import wraps def timethis(func): ''' Dekorator zwracający czas wykonania kodu ''' @wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(func.__name__, end-start) return result return wrapper
Poniżej pokazano, jak zastosować dekorator i sprawdzić metadane uzyskanej funkcji: >>> @timethis ... def countdown(n:int): ... ''' ... Odliczanie do zera ... ''' ... while n > 0: ... n -= 1 ... >>> countdown(100000) countdown 0.008917808532714844 >>> countdown.__name__ 'countdown' >>> countdown.__doc__ '\n\tOdliczanie do zera\n\t' >>> countdown.__annotations__ {'n': } >>>
9.2. Zachowywanie metadanych funkcji przy pisaniu dekoratorów
299
Omówienie Kopiowanie metadanych to ważny aspekt pisania dekoratorów. Jeśli zapomnisz o dekoratorze @wraps, funkcja z dekoracją zostanie pozbawiona wielu przydatnych informacji. W takiej sytuacji metadane z ostatniego przykładu będą wyglądały tak: >>> countdown.__name__ 'wrapper' >>> countdown.__doc__ >>> countdown.__annotations__ {} >>>
Ważną cechą dekoratora @wraps jest to, że udostępnia funkcję z nakładki w atrybucie __wrapped__. Jeśli chcesz uzyskać dostęp bezpośrednio do funkcji z nakładki, możesz zastosować następujący kod: >>> countdown.__wrapped__(100000) >>>
Atrybut __wrapped__ sprawia też, że funkcja z dekoracją poprawnie udostępnia sygnaturę funkcji z nakładki. Oto przykład: >>> from inspect import signature >>> print(signature(countdown)) (n:int) >>>
Czasem można się zastanawiać, jak utworzyć dekorator, który bezpośrednio kopiuje sygnaturę pierwotnej funkcji używanej w nakładce (zamiast stosować argumenty *args i **kwargs). Trudno jest uzyskać taki efekt bez zastosowania sztuczki opartej na generatorze łańcuchów znaków z kodem i wywołaniach exec(). Zwykle najlepiej jest zastosować dekorator @wraps i wykorzystać to, że sygnatura funkcji jest dostępna w atrybucie __wrapped__. Więcej informacji na temat sygnatur znajdziesz w recepturze 9.16.
9.3. Pobieranie pierwotnej funkcji z nakładki Problem Do funkcji zastosowano dekorator, jednak programista chce anulować tę operację i uzyskać dostęp do pierwotnej funkcji.
Rozwiązanie Jeśli odpowiednio przygotowano dekorator z wykorzystaniem dekoratora @wraps (zobacz recepturę 9.2), zwykle można uzyskać dostęp do pierwotnej funkcji za pomocą atrybutu __wrapped__. Oto przykład: >>> >>> ... ... >>> >>> 7 >>>
300
@somedecorator def add(x, y): return x + y orig_add = add.__wrapped__ orig_add(3, 4)
Rozdział 9. Metaprogramowanie
Omówienie Bezpośredni dostęp do pierwotnej funkcji ukrytej w dekoratorze może być przydatny przy debugowaniu, introspekcji kodu i innych operacjach związanych z funkcjami. Jednak receptura ta działa tylko wtedy, gdy dekorator prawidłowo kopiuje metadane za pomocą dekoratora @wraps z modułu functools lub bezpośrednio ustawia atrybut __wrapped__. Jeśli do funkcji zastosowano kilka dekoratorów, operacja dostępu do atrybutu __wrapped__ jest niezdefiniowana i należy jej unikać. W Pythonie 3.3 operacja ta jest przekazywana przez wszystkie warstwy. Załóżmy, że używany jest następujący kod: from functools import wraps def decorator1(func): @wraps(func) def wrapper(*args, **kwargs): print('Dekorator 1') return func(*args, **kwargs) return wrapper def decorator2(func): @wraps(func) def wrapper(*args, **kwargs): print('Dekorator 2') return func(*args, **kwargs) return wrapper @decorator1 @decorator2 def add(x, y): return x + y
Poniżej pokazano, co się stanie po wywołaniu funkcji z dekoracją i pierwotnej funkcji (poprzez atrybut __wrapped__): >>> add(2, 3) Dekorator 1 Dekorator 2 5 >>> add.__wrapped__(2, 3) 5 >>>
Jednak takie działanie zostało zgłoszone jako błąd (zobacz stronę http://bugs.python.org/issue17482) i w przyszłych wersjach języka może zostać zmienione, tak aby program rozwijał łańcuch dekoratorów. Ponadto warto pamiętać, że nie we wszystkich dekoratorach używany jest dekorator @wraps. Dlatego niektóre dekoratory mogą działać w sposób inny od opisanego. Np. wbudowane dekoratory @staticmethod i @classmethod tworzą deskryptory niezgodne z opisaną konwencją (te dekoratory zapisują pierwotną funkcję w atrybucie __func__). Możesz więc natrafić na różne rozwiązania.
9.3. Pobieranie pierwotnej funkcji z nakładki
301
9.4. Tworzenie dekoratorów przyjmujących argumenty Problem Programista chce napisać dekorator przyjmujący argumenty.
Rozwiązanie Przedstawmy proces przyjmowania argumentów na przykładzie. Załóżmy, że programista chce napisać dekorator, który dodaje do funkcji obsługę rejestrowania operacji i umożliwia użytkownikowi określenie za pomocą argumentów poziomu rejestrowania oraz innych aspektów działania funkcji. Taki dekorator można zdefiniować w następujący sposób: from functools import wraps import logging def logged(level, name=None, message=None): ''' Dodaje rejestrowanie operacji do funkcji. level to poziom rejestrowania, name to nazwa mechanizmu rejestrowania, a message to rejestrowany komunikat. Jeśli użytkownik pominie argumenty name i message, domyślnie odpowiadają one modułowi i nazwie funkcji. ''' def decorate(func): logname = name if name else func.__module__ log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): log.log(level, logmsg) return func(*args, **kwargs) return wrapper return decorate # Przykład zastosowania @logged(logging.DEBUG) def add(x, y): return x + y @logged(logging.CRITICAL, 'example') def spam(): print('Spam!')
Kod ten wydaje się skomplikowany, ale zastosowany tu pomysł jest stosunkowo prosty. Zewnętrzna funkcja logged() przyjmuje pożądane argumenty i udostępnia je w wewnętrznych funkcjach dekoratora. Funkcja wewnętrzna decorate() przyjmuje funkcję i w standardowy sposób umieszcza ją w nakładce. Najważniejsze jest to, że w nakładce można korzystać z argumentów przekazanych do funkcji logged().
Omówienie Z uwagi na sekwencję wywołań pisanie dekoratorów przyjmujących argumenty jest skomplikowane. Jeśli kod wygląda w następujący sposób:
302
Rozdział 9. Metaprogramowanie
@decorator(x, y, z) def func(a, b): pass
po dodaniu dekoracji jest uruchamiany tak: def func(a, b): pass func = decorator(x, y, z)(func)
Zauważ, że wynikiem polecenia decorator(x, y, z) musi być jednostka wywoływana, która jako dane wejściowe przyjmuje funkcję i umieszcza ją w nakładce. Inny przykład dekoratora przyjmującego argumenty znajdziesz w recepturze 9.7.
9.5. Definiowanie dekoratora z atrybutami dostosowywanymi przez użytkownika Problem Programista chce napisać dekorator, który jest nakładką na funkcję, a jednocześnie ma atrybuty, które użytkownik może zmodyfikować, aby kontrolować działanie dekoratora w trakcie wykonywania programu.
Rozwiązanie Oto rozwiązanie stanowiące rozwinięcie poprzedniej receptury. Wprowadzono tu akcesory, w których za pomocą deklaracji nonlocal modyfikowane są zmienne wewnętrzne. Akcesory są następnie dołączone do nakładki jako atrybuty funkcji. from functools import wraps, partial import logging # Dekorator narzędziowy dołączający funkcję jako atrybut obiektu def attach_wrapper(obj, func=None): if func is None: return partial(attach_wrapper, obj) setattr(obj, func.__name__, func) return func def logged(level, name=None, message=None): ''' Dodaje do funkcji rejestrowanie operacji. level to poziom rejestrowania, name to nazwa mechanizmu rejestrującego, a message to zapisywany komunikat. Jeśli użytkownik nie poda nazwy ani komunikatu, domyślnie używane są moduł i nazwa funkcji. ''' def decorate(func): logname = name if name else func.__module__ log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): log.log(level, logmsg) return func(*args, **kwargs)
9.5. Definiowanie dekoratora z atrybutami dostosowywanymi przez użytkownika
303
# Dołączanie funkcji ustawiających wartość @attach_wrapper(wrapper) def set_level(newlevel): nonlocal level level = newlevel @attach_wrapper(wrapper) def set_message(newmsg): nonlocal logmsg logmsg = newmsg return wrapper return decorate # Przykład zastosowania @logged(logging.DEBUG) def add(x, y): return x + y @logged(logging.CRITICAL, 'example') def spam(): print('Spam!')
Oto interaktywna sesja, w której pokazano, jak zmodyfikować różne atrybuty funkcji po jej zdefiniowaniu: >>> import logging >>> logging.basicConfig(level=logging.DEBUG) >>> add(2, 3) DEBUG:__main__:add 5 >>> # Zmiana rejestrowanego komunikatu >>> add.set_message('Wywołano funkcję add') >>> add(2, 3) DEBUG:__main__:Wywołano funkcję add 5 >>> # Zmiana poziomu rejestrowania >>> add.set_level(logging.WARNING) >>> add(2, 3) WARNING:__main__:Wywołano funkcję add 5 >>>
Omówienie Najważniejszy aspekt tej receptury związany jest z akcesorami (czyli funkcjami set_message() i set_level()) dołączanymi do nakładki jako atrybuty. Każdy z tych akcesorów umożliwia modyfikację wewnętrznych parametrów za pomocą deklaracji nonlocal. Niezwykłą cechą tej receptury jest to, że akcesory są przekazywane przez wiele poziomów dekoracji (jeśli we wszystkich dekoratorach używany jest dekorator @functools.wraps). Załóżmy, że napisałeś dodatkowy dekorator (np. @timethis z receptury 9.2) i wykorzystałeś go w kodzie w następujący sposób: @timethis @logged(logging.DEBUG) def countdown(n): while n > 0: n -= 1
304
Rozdział 9. Metaprogramowanie
Okazuje się, że akcesory nadal działają: >>> countdown(10000000) DEBUG:__main__:countdown countdown 0.8198461532592773 >>> countdown.set_level(logging.WARNING) >>> countdown.set_message("Odliczanie do zera") >>> countdown(10000000) WARNING:__main__:Odliczanie do zera countdown 0.8225970268249512 >>>
Ponadto technika działa także wtedy, gdy dekoratory są połączone w odwrotnej kolejności: @logged(logging.DEBUG) @timethis def countdown(n): while n > 0: n -= 1
Choć nie pokazano tego w przykładach, można napisać także akcesory zwracające wartość różnych ustawień. Wystarczy dodać nowy kod: ... @attach_wrapper(wrapper) def get_level(): return level # Inna możliwość wrapper.get_level = lambda: level ...
Bardzo wyrafinowanym aspektem tej receptury jest przede wszystkim wykorzystanie akcesorów. Zamiast tego można zastosować inne podejście, oparte na bezpośrednim dostępie do atrybutów funkcji: ... @wraps(func) def wrapper(*args, **kwargs): wrapper.log.log(wrapper.level, wrapper.logmsg) return func(*args, **kwargs) # Dołączanie możliwych do zmodyfikowania atrybutów wrapper.level = level wrapper.logmsg = logmsg wrapper.log = log ...
To podejście działa tylko wtedy, gdy zastosuje się je w pierwszym z podawanych dekoratorów. Jeśli najpierw podasz inny dekorator (taki jak @timethis w przykładzie), zablokuje on dostęp do atrybutów i uniemożliwi wprowadzanie zmian. Zastosowanie akcesorów pozwala uniknąć tego problemu. Rozwiązanie przedstawione w tej recepturze można czasem wykorzystać zamiast dekoratorów definiowanych w postaci klas (zobacz recepturę 9.9).
9.5. Definiowanie dekoratora z atrybutami dostosowywanymi przez użytkownika
305
9.6. Definiowanie dekoratorów przyjmujących opcjonalny argument Problem Programista chce napisać dekorator, który można stosować bez argumentów (@decorator) lub z argumentami opcjonalnymi (@decorator(x, y, z)). Jednak z uwagi na różnice w sposobie wywoływania prostych dekoratorów i dekoratorów z argumentami wydaje się, że nie istnieje prosty sposób na uzyskanie tego efektu.
Rozwiązanie Oto jedna z wersji przedstawionego w recepturze 9.5 kodu do rejestrowania operacji. Zdefiniowano tu opisany dekorator: from functools import wraps, partial import logging def logged(func=None, *, level=logging.DEBUG, name=None, message=None): if func is None: return partial(logged, level=level, name=name, message=message) logname = name if name else func.__module__ log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): log.log(level, logmsg) return func(*args, **kwargs) return wrapper # Przykład zastosowania @logged def add(x, y): return x + y @logged(level=logging.CRITICAL, name='example') def spam(): print('Spam!')
W przykładzie tym widać, że dekorator można stosować zarówno w prostej formie (@logged), jak i z argumentami opcjonalnymi (@logged(level=logging.CRITICAL, name='example')).
Omówienie Problem omawiany w tej recepturze dotyczy spójności kodu. Przy stosowaniu dekoratorów większość programistów jest przyzwyczajona albo do podawania ich bez żadnych argumentów, albo z argumentami. W ujęciu technicznym dekorator, w którym wszystkie argumenty są opcjonalne, można podać w następujący sposób: @logged() def add(x, y): return x+y
306
Rozdział 9. Metaprogramowanie
Jednak postać ta nie jest używana zbyt często i może prowadzić do błędów, gdy programista zapomni dodać nawiasy. W tej recepturze dekorator działa zarówno z nawiasami, jak i bez nich. Aby zrozumieć działanie przedstawionego kodu, trzeba wiedzieć, jak dekoratory są stosowane do funkcji i jak są wywoływane. W przypadku prostych dekoratorów, np.: # Przykład zastosowania @logged def add(x, y): return x + y
sekwencja wywołań wygląda tak: def add(x, y): return x + y add = logged(add)
W tej sytuacji funkcja umieszczana w nakładce jest przekazywana do dekoratora logged jako pierwszy argument. Dlatego w rozwiązaniu pierwszym argumentem wywołania logged() jest funkcja zapisywana w nakładce. Wszystkie pozostałe argumenty muszą mieć wartości domyślne. Gdy dekorator przyjmuje argumenty, tak jak poniżej: @logged(level=logging.CRITICAL, name='example') def spam(): print('Spam!')
W pierwszym wywołaniu funkcji logged() nie podano funkcji przekazywanej do nakładki. Dlatego w dekoratorze przekazywana funkcja musi być opcjonalna. To sprawia, że pozostałe argumenty trzeba podawać za pomocą słów kluczowych. Ponadto gdy argumenty te są przekazywane, dekorator powinien zwracać funkcję, która przyjmuje inną funkcję i umieszcza ją w nakładce (zobacz recepturę 9.5). Dlatego w rozwiązaniu wykorzystano ciekawą sztuczkę z wykorzystaniem funkcji functools.partial. Zwraca ona wersję funkcji, w której wszystkie argumenty oprócz umieszczanej w nakładce funkcji mają określone wartości. Więcej informacji na temat korzystania z funkcji partial() znajdziesz w recepturze 7.8.
9.7. Wymuszanie sprawdzania typów w funkcji za pomocą dekoratora Problem Programista chce opcjonalnie wymuszać sprawdzanie typów argumentów funkcji na podstawie asercji lub kontraktu.
Rozwiązanie Przed przedstawieniem kodu rozwiązania warto wspomnieć, że receptura ta ma umożliwiać wymuszanie typu określonego dla argumentów wejściowych funkcji. Oto krótki przykład ilustrujący to podejście:
9.7. Wymuszanie sprawdzania typów w funkcji za pomocą dekoratora
307
>>> @typeassert(int, int) ... def add(x, y): ... return x + y ... >>> >>> add(2, 3) 5 >>> add(2, 'Witaj') Traceback (most recent call last): File "", line 1, in File "contract.py", line 33, in wrapper TypeError: Argument y musi być typu >>>
Poniżej przedstawiono kod dekoratora @typeassert: from inspect import signature from functools import wraps def typeassert(*ty_args, **ty_kwargs): def decorate(func): # W trybie optymalizacji należy wyłączyć sprawdzanie typu if not __debug__: return func # Odwzorowanie nazw argumentów funkcji na podane typy sig = signature(func) bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments @wraps(func) def wrapper(*args, **kwargs): bound_values = sig.bind(*args, **kwargs) # Enforce type assertions across supplied arguments for name, value in bound_values.arguments.items(): if name in bound_types: if not isinstance(value, bound_types[name]): raise TypeError( 'Argument {} must być typu {}'.format(name, bound_types[name]) ) return func(*args, **kwargs) return wrapper return decorate
Okazuje się, że przedstawiony dekorator daje dużą swobodę i pozwala określać typy wszystkich lub wybranych argumentów funkcji. Ponadto typy można określać na podstawie pozycji lub za pomocą słów kluczowych. Oto przykład: >>> @typeassert(int, z=int) ... def spam(x, y, z=42): ... print(x, y, z) ... >>> spam(1, 2, 3) 1 2 3 >>> spam(1, 'Witaj', 3) 1 Witaj 3 >>> spam(1, 'Witaj', 'świecie') Traceback (most recent call last): File "", line 1, in File "contract.py", line 33, in wrapper TypeError: Argument z musi być typu >>>
308
Rozdział 9. Metaprogramowanie
Omówienie Ta receptura to zaawansowany przykład wykorzystania dekoratorów. Przedstawiono tu liczne ważne i przydatne zagadnienia. Po pierwsze, jedną z cech dekoratorów jest to, że są stosowane raz — w miejscu definicji funkcji. W niektórych sytuacjach przydatne może być wyłączenie możliwości dodanych przez dekorator. Wtedy dekorator powinien zwracać funkcję bez nakładki. W rozwiązaniu poniższy fragment kodu zwraca niezmodyfikowaną funkcję, jeśli zmienna globalna __debug__ ma wartość False (Python działa wtedy w trybie zoptymalizowanym, z opcjami -0 lub -00 interpretera): ... def decorate(func): # W trybie optymalizacji należy wyłączyć sprawdzanie typu if not __debug__: return func ...
Skomplikowanym aspektem tego dekoratora jest to, że sprawdza on i wykorzystuje sygnaturę funkcji umieszczanej w nakładce. Do sprawdzania sygnatury należy wykorzystać funkcję inspect.signature(). Umożliwia ona pobranie z jednostki wywoływalnej informacji o sygnaturze. Oto przykład: >>> from inspect import signature >>> def spam(x, y, z=42): ... pass ... >>> sig = signature(spam) >>> print(sig) (x, y, z=42) >>> sig.parameters mappingproxy(OrderedDict([('x', ), ('y', ), ('z', )])) >>> sig.parameters['z'].name 'z' >>> sig.parameters['z'].default 42 >>> sig.parameters['z'].kind <_ParameterKind: 'POSITIONAL_OR_KEYWORD'> >>>
W pierwszej części dekoratora metoda bind_partial() obiektu sygnatury używana jest do częściowego powiązania podanych typów z nazwami argumentów. Oto przykład ilustrujący, co się wtedy dzieje: >>> bound_types = sig.bind_partial(int,z=int) >>> bound_types >>> bound_types.arguments OrderedDict([('x', ), ('z', )]) >>>
Zwróć uwagę, że przy tego rodzaju częściowym wiązaniu brakujące argumenty są ignorowane (argument y nie jest tu wiązany z żadnym typem). Najważniejszym aspektem tej operacji jest jednak utworzenie uporządkowanego słownika bound_types.arguments. Słownik ten odwzorowuje nazwy argumentów na podane wartości w porządku odpowiadającym kolejności argumentów w sygnaturze funkcji. W omawianym dekoratorze odwzorowanie to zawiera wymuszane asercje typów. 9.7. Wymuszanie sprawdzania typów w funkcji za pomocą dekoratora
309
W nakładce tworzonej przez dekorator używana jest metoda sig.bind(). Metoda ta działa podobnie jak bind_partial(), ale nie pozwala na pomijanie argumentów. Oto efekt uruchomienia kodu: >>> bound_values = sig.bind(1, 2, 3) >>> bound_values.arguments OrderedDict([('x', 1), ('y', 2), ('z', 3)]) >>>
Za pomocą tego odwzorowania można stosunkowo łatwo wymusić potrzebne asercje: >>> for name, value in bound_values.arguments.items(): ... if name in bound_types.arguments: ... if not isinstance(value, bound_types.arguments[name]): ... raise TypeError() ... >>>
Ciekawą cechą tego rozwiązania jest to, że asercje nie są stosowane do niepodanych argumentów (przyjmują one wartości domyślne). Poniższy kod działa, choć wartość domyślna argumentu items ma nieodpowiedni typ: >>> @typeassert(int, list) ... def bar(x, items=None): ... if items is None: ... items = [] ... items.append(x) ... return items >>> bar(2) [2] >>> bar(2,3) Traceback (most recent call last): File "", line 1, in File "contract.py", line 33, in wrapper TypeError: Argument items musi być typu >>> bar(4, [1, 2, 3]) [1, 2, 3, 4] >>>
Omawianie przedstawionego projektu można zakończyć porównaniem argumentów dekoratorów i uwag dodawanych do funkcji. Dlaczego nie napisać dekoratora, który w następujący sposób sprawdza uwagi? @typeassert def spam(x:int, y, z:int = 42): print(x,y,z)
Jedną z przyczyn, dla których stosowanie uwag nie jest wskazane, jest to, że do każdego argumentu funkcji można przypisać tylko jedną uwagę. Dlatego jeśli uwaga posłuży do określenia typu, nie będzie można jej wykorzystać w innych celach. Ponadto dekorator @typeassert nie będzie działał dla funkcji, w których uwagi zastosowano do czegoś innego niż określenie typu. Dzięki zastosowaniu argumentów dekoratora (tak jak w rozwiązaniu) jest on dużo bardziej uniwersalny i można go stosować do dowolnych funkcji — także tych, w których występują uwagi. Więcej informacji na temat obiektów sygnatur funkcji znajdziesz w dokumencie PEP 362 (http://www.python.org/dev/peps/pep-0362/), a także w dokumentacji modułu inspect (http://docs. python.org/3/library/inspect.html). Inny przykład przedstawiono w recepturze 9.16.
310
Rozdział 9. Metaprogramowanie
9.8. Definiowanie dekoratorów jako elementów klasy Problem Programista chce zdefiniować dekorator w definicji klasy i zastosować go do innych funkcji lub metod.
Rozwiązanie Definiowanie dekoratorów w klasach jest łatwe, najpierw jednak trzeba ustawić, w jaki sposób użytkownicy mają stosować dany dekorator — czy jako metodę egzemplarza, czy jako metodę klasy. Oto przykład ilustrujący różnicę między tymi podejściami: from functools import wraps class A: # Dekorator jako metoda egzemplarza def decorator1(self, func): @wraps(func) def wrapper(*args, **kwargs): print('Dekorator 1') return func(*args, **kwargs) return wrapper # Dekorator jako metoda klasy @classmethod def decorator2(cls, func): @wraps(func) def wrapper(*args, **kwargs): print('Dekorator 2') return func(*args, **kwargs) return wrapper
Oto przykład ilustrujący, jak można zastosować dwa przedstawione dekoratory: # Jako metoda egzemplarza a = A() @a.decorator1 def spam(): pass # Jako metoda klasy @A.decorator2 def grok(): pass
Jeśli przyjrzysz się temu kodowi, zauważysz, że jeden dekorator jest stosowany z poziomu obiektu a, a drugi — z poziomu klasy A.
Omówienie Definiowanie dekoratorów w klasie może wydawać się dziwne, jednak technikę tę stosuje się czasem w bibliotece standardowej. Np. wbudowany dekorator @property jest klasą z metodami getter(), setter() i deleter(), które działają jak dekoratory. Oto przykład:
9.8. Definiowanie dekoratorów jako elementów klasy
311
class Person: # Tworzenie obiektu typu property first_name = property() # Stosowanie metod dekoratora @first_name.getter def first_name(self): return self._first_name @first_name.setter def first_name(self, value): if not isinstance(value, str): raise TypeError('Oczekiwano łańcucha znaków') self._first_name = value
Głównym powodem, dla którego rozwiązanie zdefiniowano w ten sposób, jest to, że poszczególne metody dekoratora manipulują stanem powiązanego obiektu typu property. Dlatego jeśli zdarzy się kiedyś, że będziesz potrzebował dekoratorów do rejestrowania lub łączenia informacji na zapleczu, możesz zastosować przedstawione podejście. Przy pisaniu dekoratorów w klasach często nie wiadomo, jak odpowiednio zastosować dodatkowe argumenty self i cls w kodzie dekoratora. Choć zewnętrzny dekorator (np. decorator1() lub decorator2()) musi udostępniać argumenty self lub cls (ponieważ są one częścią klasy), tworzona wewnątrz nakładka zwykle nie musi zawierać dodatkowych argumentów. To dlatego funkcja wrapper() tworzona w obu dekoratorach nie zawiera argumentu self. Jest on potrzebny tylko wtedy, gdy w nakładce niezbędny jest dostęp do składowych obiektu. W innych sytuacjach argument ten nie ma znaczenia. Ostatnim ciekawym aspektem definiowania dekoratorów w klasie jest stosowanie ich przy dziedziczeniu. Załóżmy, że chcesz dodać do metod z klasy pochodnej B jeden z dekoratorów zdefiniowanych w klasie A. Wymaga to napisania kodu w następującej postaci: class B(A): @A.decorator2 def bar(self): pass
Dekorator trzeba zdefiniować jako metodę klasy, a przy jego stosowaniu należy podać nazwę klasy bazowej (tu jest to A). Nie można podać nazwy @B.decorator2, ponieważ w momencie definiowania metody klasa B jeszcze nie istniała.
9.9. Definiowanie dekoratorów jako klas Problem Programista chce umieścić funkcję w nakładce za pomocą dekoratora, przy czym wynikiem tej operacji ma być obiekt wywoływalny. Dekorator powinien działać zarówno w definicji klasy, jak i poza nią.
Rozwiązanie Aby zdefiniować dekorator jako obiekt, trzeba udostępnić w nim metody __call__() i __get__(). Poniższy kod to definicja klasy, która dodaje do funkcji prostą warstwę profilującą:
312
Rozdział 9. Metaprogramowanie
import types from functools import wraps class Profiled: def __init__(self, func): wraps(func)(self) self.ncalls = 0 def __call__(self, *args, **kwargs): self.ncalls += 1 return self.__wrapped__(*args, **kwargs) def __get__(self, instance, cls): if instance is None: return self else: return types.MethodType(self, instance)
Aby wykorzystać tę klasę, należy jej użyć jak normalnego dekoratora (w klasie lub poza nią): @Profiled def add(x, y): return x + y class Spam: @Profiled def bar(self, x): print(self, x)
Oto interaktywna sesja ilustrująca działanie tych funkcji: >>> add(2, 3) 5 >>> add(4, 5) 9 >>> add.ncalls 2 >>> s = Spam() >>> s.bar(1) <__main__.Spam object at 0x10069e9d0> 1 >>> s.bar(2) <__main__.Spam object at 0x10069e9d0> 2 >>> s.bar(3) <__main__.Spam object at 0x10069e9d0> 3 >>> Spam.bar.ncalls 3
Omówienie Definiowanie dekoratorów jako klasy jest zwykle proste. Jednak pewne szczegóły zasługują na dodatkowe wyjaśnienia — zwłaszcza gdy dekorator ma być stosowany do metod egzemplarza. Po pierwsze, funkcja functools.wraps() pełni tu tę samą rolę co w normalnych dekoratorach (służy do kopiowania ważnych metadanych z funkcji z nakładki do obiektu wywoływalnego). Po drugie, programiści często zapominają o przedstawionej w rozwiązaniu metodzie __get__(). Jeśli ją pominiesz i pozostawisz resztę kodu w pierwotnej postaci, próba wywołania metod egzemplarza, do których zastosowano dekorator, może powodować dziwne efekty. Oto przykład:
Problem wynika z tego, że przy wyszukiwaniu w klasie kodu metod wywoływana jest metoda __get__() (w ramach protokołu działania deskryptorów), co opisano w recepturze 8.9. Tu metoda __get__() tworzy obiekt powiązanej metody (który ostatecznie przekazuje do metody argument self). Oto przykład ilustrujący działanie tego mechanizmu: >>> s = Spam() >>> def grok(self, x): ... pass ... >>> grok.__get__(s, Spam) > >>>
W tej recepturze metoda __get__() gwarantuje, że obiekty metody powiązanej są poprawnie tworzone. Wywołanie type.MethodType() tworzy używaną tu metodę powiązaną. Takie metody są tworzone tylko wtedy, gdy używany jest obiekt. Jeśli metoda jest wywoływana w klasie, argument instance metody __get__() ma wartość None i zwracany jest obiekt typu Profiled. Dzięki temu użytkownik może w przedstawiony tu sposób pobrać wartość ncalls tego obiektu. Jeśli chcesz uniknąć opisanych problemów, możesz rozważyć zastosowanie innej implementacji dekoratora, opartej na domknięciach i zmiennych nonlocal, co opisano w recepturze 9.5. Oto przykład: import types from functools import wraps def profiled(func): ncalls = 0 @wraps(func) def wrapper(*args, **kwargs): nonlocal ncalls ncalls += 1 return func(*args, **kwargs) wrapper.ncalls = lambda: ncalls return wrapper # Przykład @profiled def add(x, y): return x + y
Ten przykład działa niemal dokładnie tak samo, przy czym dostęp do atrybutu ncalls jest możliwy dzięki funkcji dołączonej jako jej atrybut. Oto przykład: >>> add(2, 3) 5 >>> add(4, 5) 9 >>> add.ncalls() 2 >>>
314
Rozdział 9. Metaprogramowanie
9.10. Stosowanie dekoratorów do metod klasy i metod statycznych Problem Programista chce zastosować dekorator do metody klasy lub metody statycznej.
Rozwiązanie Stosowanie dekoratorów do metod klasy i metod statycznych jest proste, trzeba jednak pamiętać, aby dodać te dekoratory po dekoratorach @classmethod lub @staticmethod. Oto przykład: import time from functools import wraps # Prosty dekorator def timethis(func): @wraps(func) def wrapper(*args, **kwargs): start = time.time() r = func(*args, **kwargs) end = time.time() print(end-start) return r return wrapper # Klasa ilustrująca stosowanie dekoratora do różnych rodzajów metod class Spam: @timethis def instance_method(self, n): print(self, n) while n > 0: n -= 1 @classmethod @timethis def class_method(cls, n): print(cls, n) while n > 0: n -= 1 @staticmethod @timethis def static_method(n): print(n) while n > 0: n -= 1
Uzyskane metody klasy i metody statyczne powinny działać w standardowy sposób i dodatkowo udostępniać pomiar czasu: >>> s = Spam() >>> s.instance_method(1000000) <__main__.Spam object at 0x1006a6050> 1000000 0.11817407608032227 >>> Spam.class_method(1000000) 1000000 0.11334395408630371 >>> Spam.static_method(1000000) 1000000 0.11740279197692871 >>>
9.10. Stosowanie dekoratorów do metod klasy i metod statycznych
315
Omówienie Jeśli podasz dekoratory w nieodpowiedniej kolejności, wystąpi błąd. Np. w poniższym kodzie: class Spam: ... @timethis @staticmethod def static_method(n): print(n) while n > 0: n -= 1
metoda statyczna przestanie działać: >>> Spam.static_method(1000000) Traceback (most recent call last): File "", line 1, in File "timethis.py", line 6, in wrapper start = time.time() TypeError: 'staticmethod' object is not callable >>>
Problem wynika z tego, że dekoratory @classmethod i @staticmethod nie tworzą obiektów, które można bezpośrednio wywoływać. Zamiast tego tworzą specjalne obiekty deskryptora (zobacz recepturę 8.9). Dlatego jeśli spróbujesz zastosować taki obiekt jako funkcję w innym dekoratorze, ten przestanie działać. Aby rozwiązać ten problem, trzeba zawsze umieszczać wspomniane dekoratory na początku listy. Receptura ta jest bardzo ważna m.in. przy definiowaniu metody klasy i metod statycznych w abstrakcyjnych klasach bazowych, co opisano w recepturze 8.12. Jeśli chcesz zdefiniować abstrakcyjną metodę klasy, możesz zastosować następujący kod: from abc import ABCMeta, abstractmethod class A(metaclass=ABCMeta): @classmethod @abstractmethod def method(cls): pass
W tym kodzie ważna jest kolejność dekoratorów @classmethod i @abstractmethod. Jeśli ją zmienisz, kod przestanie działać.
9.11. Pisanie dekoratorów, które dodają argumenty do funkcji w nakładkach Problem Programista chce napisać dekorator, który dodaje argument do sygnatury funkcji z nakładki. Jednak dodanie argumentu nie może wymagać zmiany istniejących wywołań funkcji.
Rozwiązanie Dodatkowe argumenty można dodać do sygnatury jako argumenty podawane wyłącznie za pomocą słów kluczowych. Przyjrzyj się poniższemu dekoratorowi: 316
Rozdział 9. Metaprogramowanie
from functools import wraps def optional_debug(func): @wraps(func) def wrapper(*args, debug=False, **kwargs): if debug: print('Wywołanie', func.__name__) return func(*args, **kwargs) return wrapper
Oto przykład ilustrujący działanie tego dekoratora: >>> @optional_debug ... def spam(a,b,c): ... print(a,b,c) ... >>> spam(1,2,3) 1 2 3 >>> spam(1,2,3, debug=True) Wywołanie spam 1 2 3 >>>
Omówienie Dekoratory nieczęsto wykorzystuje się do dodawania argumentów do sygnatury funkcji z nakładki. Technika ta może się jednak w pewnych sytuacjach przydać do uniknięcia powielania kodu. Np. poniższy kod: def a(x, debug=False): if debug: print('Wywołanie a') ... def b(x, y, z, debug=False): if debug: print('Wywołanie b') ... def c(x, y, debug=False): if debug: print('Calling c') ...
można przekształcić na następującą postać: @optional_debug def a(x): ... @optional_debug def b(x, y, z): ... @optional_debug def c(x, y): ...
Kod tej receptury oparty jest na tym, że do funkcji przyjmujących parametry *args i **kwargs można łatwo dodać argumenty podawane wyłącznie za pomocą słów kluczowych. Takie argumenty są traktowane w specjalny sposób i pomijane w wywołaniach, w których występują pozostałe argumenty podawane na podstawie pozycji i przy użyciu słów kluczowych.
9.11. Pisanie dekoratorów, które dodają argumenty do funkcji w nakładkach
317
Pewną komplikacją jest tu to, że może wystąpić kolizja nazw między dodanymi argumentami a argumentami funkcji z nakładki. Gdyby dekorator @optional_debug zastosowano do funkcji, w której już występuje argument debug, kod przestałby działać. Jeśli stanowi to problem, można dodać nowy test: from functools import wraps import inspect def optional_debug(func): if 'debug' in inspect.getargspec(func).args: raise TypeError('Argument debug jest już zdefiniowany') @wraps(func) def wrapper(*args, debug=False, **kwargs): if debug: print('Wywołanie', func.__name__) return func(*args, **kwargs) return wrapper
Ostatnie usprawnienie receptury dotyczy odpowiedniego zarządzania sygnaturami funkcji. Wnikliwy programista zauważy, że sygnatura funkcji z nakładki jest nieprawidłowa. Oto przykład: >>> ... ... ... >>> >>> (x, >>>
Problem można rozwiązać, wprowadzając następującą zmianę: from functools import wraps import inspect def optional_debug(func): if 'debug' in inspect.getargspec(func).args: raise TypeError('Argument debug jest już zdefiniowany') @wraps(func) def wrapper(*args, debug=False, **kwargs): if debug: print('Wywołanie', func.__name__) return func(*args, **kwargs) sig = inspect.signature(func) parms = list(sig.parameters.values()) parms.append(inspect.Parameter('debug', inspect.Parameter.KEYWORD_ONLY, default=False)) wrapper.__signature__ = sig.replace(parameters=parms) return wrapper
W sygnaturze nakładki po wprowadzeniu tej zmiany pojawia się argument debug, co odzwierciedla rzeczywistą postać sygnatury: >>> @optional_debug ... def add(x,y): ... return x+y ...
318
Rozdział 9. Metaprogramowanie
>>> print(inspect.signature(add)) (x, y, *, debug=False) >>> add(2,3) 5 >>>
Więcej informacji na temat sygnatur funkcji znajdziesz w recepturze 9.16.
9.12. Stosowanie dekoratorów do poprawiania definicji klas Problem Programista zamierza sprawdzić lub zmodyfikować fragmenty definicji klasy, aby zmienić jej działanie, przy czym nie chce stosować dziedziczenia ani metaklas.
Rozwiązanie Do rozwiązania problemu doskonale nadają się dekoratory klas. Poniżej przedstawiono dekorator, który modyfikuje metodę specjalną __getattribute__, tak aby obsługiwała rejestrowanie operacji: def log_getattribute(cls): # Pobieranie pierwotnej wersji orig_getattribute = cls.__getattribute__ # Tworzenie nowej definicji def new_getattribute(self, name): print('Pobieranie:', name) return orig_getattribute(self, name) # Dołączanie nowej wersji do klasy i zwracanie klasy cls.__getattribute__ = new_getattribute return cls # Przykład zastosowania @log_getattribute class A: def __init__(self,x): self.x = x def spam(self): pass
Poniżej pokazano, co się stanie, gdy zastosuje się nową klasę z rozwiązania: >>> a = A(42) >>> a.x Pobieranie: x 42 >>> a.spam() Pobieranie: spam >>>
9.12. Stosowanie dekoratorów do poprawiania definicji klas
319
Omówienie Dekoratory klasy często można wykorzystać jako prosty zastępnik bardziej zaawansowanych technik obejmujących klasy mieszane i metaklasy. Zamiast przedstawionego rozwiązania można zastosować dziedziczenie: class LoggedGetattribute: def __getattribute__(self, name): print('Pobieranie:', name) return super().__getattribute__(name) # Przykład class A(LoggedGetattribute): def __init__(self,x): self.x = x def spam(self): pass
To podejście działa, ale aby je zrozumieć, trzeba znać kolejność określania metod, funkcję super() i inne aspekty dziedziczenia (opisano je w recepturze 8.7). W pewnym sensie rozwiązanie oparte na dekoratorze klasy jest dużo bardziej bezpośrednie i nie wprowadza nowych zależności w hierarchii dziedziczenia. Okazuje się też, że działa nieco szybciej (ponieważ nie trzeba używać funkcji super()). Jeśli stosujesz do klasy kilka dekoratorów, znaczenie może mieć kolejność ich podawania. Dekorator, który zastępuje metodę zupełnie nową wersją, należy zastosować przed dekoratorem dołączającym do istniejącej metody dodatkową logikę. W recepturze 8.13 znajdziesz inny przykład zastosowania dekoratorów klasy.
9.13. Używanie metaklasy do kontrolowania tworzenia obiektów Problem Programista chce zmienić sposób tworzenia obiektów, aby móc zaimplementować wzorzec singleton, obsługę pamięci podręcznej i inne podobne mechanizmy.
Rozwiązanie Programiści Pythona wiedzą, że po zdefiniowaniu klasy można tworzyć na jej podstawie obiekty, wywołując ją jak funkcję: class Spam: def __init__(self, name): self.name = name a = Spam('Gucio') b = Spam('Diana')
320
Rozdział 9. Metaprogramowanie
Jeśli chcesz zmodyfikować ten etap, możesz zdefiniować metaklasę i zmienić kod metody __call__(). Załóżmy, że nie chcesz, aby ktokolwiek tworzył obiekty danej klasy: class NoInstances(type): def __call__(self, *args, **kwargs): raise TypeError("Nie można bezpośrednio tworzyć obiektów tej klasy") # Przykład class Spam(metaclass=NoInstances): @staticmethod def grok(x): print('Spam.grok')
Użytkownicy mogą wywoływać zdefiniowane metody statyczne tej klasy, ale nie mogą w standardowy sposób tworzyć na jej podstawie obiektów. Oto przykład: >>> Spam.grok(42) Spam.grok >>> s = Spam() Traceback (most recent call last): File "", line 1, in File "example1.py", line 7, in __call__ raise TypeError("Nie można bezpośrednio tworzyć obiektów tej klasy") TypeError: Nie można bezpośrednio tworzyć obiektów tej klasy >>>
Teraz przyjmijmy, że chcesz zaimplementować wzorzec singleton (czyli klasę, na podstawie której w danym momencie można utworzyć tylko jeden obiekt). Także tu rozwiązanie jest stosunkowo proste: class Singleton(type): def __init__(self, *args, **kwargs): self.__instance = None super().__init__(*args, **kwargs) def __call__(self, *args, **kwargs): if self.__instance is None: self.__instance = super().__call__(*args, **kwargs) return self.__instance else: return self.__instance # Przykład class Spam(metaclass=Singleton): def __init__(self): print('Tworzenie obiektu klasy Spam')
W tej sytuacji tworzony jest tylko jeden obiekt danej klasy: >>> a = Spam() Tworzenie obiektu klasy Spam >>> b = Spam() >>> a is b True >>> c = Spam() >>> a is c True >>>
Teraz przyjmijmy, że chcesz tworzyć obiekty zapisywane w pamięci podręcznej (zagadnienie to opisano w recepturze 8.25). Oto metaklasa, która to umożliwia:
9.13. Używanie metaklasy do kontrolowania tworzenia obiektów
321
import weakref class Cached(type): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__cache = weakref.WeakValueDictionary() def __call__(self, *args): if args in self.__cache: return self.__cache[args] else: obj = super().__call__(*args) self.__cache[args] = obj return obj # Przykład class Spam(metaclass=Cached): def __init__(self, name): print('Tworzenie obiektu klasy Spam({!r})'.format(name)) self.name = name
Poniżej pokazano, jak działa ta klasa: >>> a = Spam('Gucio') Tworzenie obiektu klasy Spam('Gucio') >>> b = Spam('Diana') Tworzenie obiektu klasy Spam('Diana') >>> c = Spam('Gucio') # Zapisane w pamięci podręcznej >>> a is b False >>> a is c # Zwracana jest wartość z pamięci podręcznej True >>>
Omówienie Zastosowanie metaklas do implementowania różnych wzorców tworzenia obiektów często jest dużo bardziej eleganckim rozwiązaniem niż inne techniki. Np. jeśli nie zastosujesz metaklasy, konieczne może być ukrycie klas za dodatkową funkcją fabryczną. Aby uzyskać singleton, możesz zastosować poniższą sztuczkę: class _Spam: def __init__(self): print('Tworzenie obiektu klasy Spam') _spam_instance = None def Spam(): global _spam_instance if _spam_instance is not None: return _spam_instance else: _spam_instance = _Spam() return _spam_instance
Choć rozwiązanie z użyciem metaklas wymaga zastosowania dużo bardziej zaawansowanej techniki, uzyskany kod jest bardziej przejrzysty i prostszy. Więcej informacji na temat tworzenia obiektów w pamięci podręcznej, słabych referencji i innych zagadnień z tego obszaru znajdziesz w recepturze 8.25.
322
Rozdział 9. Metaprogramowanie
9.14. Sprawdzanie kolejności definiowania atrybutów klasy Problem Programista chce automatycznie rejestrować kolejność, w jakiej atrybuty i metody są definiowane w ciele klasy. Ma to umożliwić wykorzystanie ich w różnych operacjach (przy serializowaniu, odwzorowywaniu w bazie danych itd.).
Rozwiązanie Informacje na temat ciała definicji klasy można łatwo uzyskać za pomocą metaklasy. Oto przykładowa metaklasa, w której słownik typu OrderedDict wykorzystano do zapisania kolejności definicji deskryptorów: from collections import OrderedDict # Zestaw deskryptorów różnych typów class Typed: _expected_type = type(None) def __init__(self, name=None): self._name = name def __set__(self, instance, value): if not isinstance(value, self._expected_type): raise TypeError('Oczekiwano typu ' + str(self._expected_type)) instance.__dict__[self._name] = value class Integer(Typed): _expected_type = int class Float(Typed): _expected_type = float class String(Typed): _expected_type = str # Metaklasa używająca słownika typu OrderedDict z informacjami o ciele klasy class OrderedMeta(type): def __new__(cls, clsname, bases, clsdict): d = dict(clsdict) order = [] for name, value in clsdict.items(): if isinstance(value, Typed): value._name = name order.append(name) d['_order'] = order return type.__new__(cls, clsname, bases, d) @classmethod def __prepare__(cls, clsname, bases): return OrderedDict()
W tej metaklasie kolejność definicji deskryptorów jest zapisywana w słowniku typu OrderedDict w trakcie wykonywania kodu z ciała klasy. Następnie kolejność nazw jest pobierana ze słownika i zapisywana w atrybucie _order klasy. Metody klasy mogą korzystać z tego atrybutu na różne sposoby. Poniżej pokazano prostą klasę, w której kolejność definicji pozwoliła napisać metodę serializującą dane obiektu w wierszu danych w formacie CSV: 9.14. Sprawdzanie kolejności definiowania atrybutów klasy
323
class Structure(metaclass=OrderedMeta): def as_csv(self): return ','.join(str(getattr(self,name)) for name in self._order) # Przykład zastosowania class Stock(Structure): name = String() shares = Integer() price = Float() def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price
Oto interaktywna sesja ilustrująca wykorzystanie przykładowej klasy Stock: >>> s = Stock('GOOG',100,490.1) >>> s.name 'GOOG' >>> s.as_csv() 'GOOG,100,490.1' >>> t = Stock('AAPL','dużo', 610.23) Traceback (most recent call last): File "", line 1, in File "dupmethod.py", line 34, in __init__ TypeError: Oczekiwano typu >>>
Omówienie Najważniejszym elementem tej receptury jest metoda __prepare__() zdefiniowana w metaklasie OrderedMeta. Metoda ta jest wywoływana bezpośrednio na początku definicji klasy i przyjmuje jako argumenty nazwę klasy oraz klasy bazowe. Następnie musi zwrócić obiekt odwzorowania używany w trakcie przetwarzania ciała klasy. Ponieważ metoda zwraca obiekt typu OrderedDict, a nie zwyczajny słownik, można łatwo zapisać kolejność definicji. Mechanizm ten możesz dodatkowo rozwinąć, jeśli chcesz przygotować własny obiekt podobny do słownika. Przyjrzyj się nowej wersji rozwiązania, która odrzuca powtarzające się definicje: from collections import OrderedDict class NoDupOrderedDict(OrderedDict): def __init__(self, clsname): self.clsname = clsname super().__init__() def __setitem__(self, name, value): if name in self: raise TypeError('{} jest już zdefiniowany w {}'.format(name, self.clsname)) super().__setitem__(name, value) class OrderedMeta(type): def __new__(cls, clsname, bases, clsdict): d = dict(clsdict) d['_order'] = [name for name in clsdict if name[0] != '_'] return type.__new__(cls, clsname, bases, d) @classmethod def __prepare__(cls, clsname, bases): return NoDupOrderedDict(clsname)
324
Rozdział 9. Metaprogramowanie
Poniżej pokazano, co się dzieje, gdy zastosuje się tę metaklasę do klasy z powtarzającymi się elementami: >>> class A(metaclass=OrderedMeta): ... def spam(self): ... pass ... def spam(self): ... pass ... Traceback (most recent call last): File "", line 1, in File "", line 4, in A File "dupmethod2.py", line 25, in __setitem__ (name, self.clsname)) TypeError: spam jest już zdefiniowany w A >>>
Ostatni ważny aspekt tej receptury dotyczy traktowania zmodyfikowanego słownika w metodzie __new__() metaklasy. Choć klasę zdefiniowano z wykorzystaniem specjalnego słownika, trzeba go przekształcić na obiekt typu dict w momencie tworzenia ostatecznego obiektu klasy. Służy do tego polecenie d = dict(clsdict). Możliwość zapisania kolejności definicji jest drobną, ale w niektórych sytuacjach ważną kwestią. Np. w mapperze obiektowo-relacyjnym klasy mogą mieć postać podobną do poniższej: class Stock(Model): name = String() shares = Integer() price = Float()
Na zapleczu programista może chcieć zapisać kolejność definicji, aby odwzorować obiekty na krotki lub wiersze w tabeli bazy danych (podobnie działa metoda as_csv() w przedstawionym przykładzie). Opisane tu rozwiązanie jest bardzo proste i często łatwiejsze od innych podejść, które zwykle wymagają przechowywania ukrytych liczników w klasach deskryptorów.
9.15. Definiowanie metaklas przyjmujących argumenty opcjonalne Problem Programista chce zdefiniować metaklasę, która umożliwi podawanie argumentów opcjonalnych w definicjach klas. Może to posłużyć do kontrolowania lub konfigurowania pewnych aspektów przetwarzania w trakcie tworzenia typu.
Rozwiązanie Python pozwala przy definiowaniu klasy określić metaklasę. Można ją wskazać w poleceniu class w argumencie podawanym przy użyciu słowa kluczowego metaclass. Oto przykład z abstrakcyjnymi klasami bazowymi: from abc import ABCMeta, abstractmethod class IStream(metaclass=ABCMeta): @abstractmethod
W niestandardowych metaklasach można za pomocą słów kluczowych podawać dodatkowe argumenty: class Spam(metaclass=MyMeta, debug=True, synchronize=True): ...
Aby udostępnić w metaklasie argumenty podawane za pomocą słów kluczowych, należy zdefiniować je w metodach __prepare__(), __new__() i __init__(), używając argumentów podawanych wyłącznie przy użyciu słów kluczowych: class MyMeta(type): # Opcjonalnie @classmethod def __prepare__(cls, name, bases, *, debug=False, synchronize=False): # Niestandardowe przetwarzanie ... return super().__prepare__(name, bases) # Wymagane def __new__(cls, name, bases, ns, *, debug=False, synchronize=False): # Niestandardowe przetwarzanie ... return super().__new__(cls, name, bases, ns) # Wymagane def __init__(self, name, bases, ns, *, debug=False, synchronize=False): # Niestandardowe przetwarzanie ... super().__init__(name, bases, ns)
Omówienie Aby dodać do metaklasy opcjonalne argumenty podawane za pomocą słów kluczowych, trzeba rozumieć wszystkie etapy tworzenia klas. Wynika to z tego, że dodatkowe argumenty są przekazywane do każdej metody. Najpierw wywoływana jest metoda __prepare__(), która służy do tworzenia przestrzeni nazw klasy przed rozpoczęciem przetwarzania ciała definicji klasy. Metoda ta zwykle tylko zwraca słownik lub inny obiekt odwzorowania. Metoda __new__() służy do tworzenia obiektu wynikowego typu. Jest wywoływana po przetworzeniu całego ciała klasy. Metoda __init__() jest wywoływana jako ostatnia i służy do wykonania dodatkowych etapów inicjujących obiekt. W trakcie pisania metaklas często definiuje się tylko metodę __new__() lub __init__(), a nie obie. Jeśli jednak metaklasa ma przyjmować dodatkowe argumenty podawane za pomocą słów kluczowych, trzeba dodać obie metody i muszą one mieć zgodne ze sobą sygnatury. Domyślna metoda __prepare__() przyjmuje dowolny zbiór argumentów podawanych za pomocą słów kluczowych, ale je ignoruje. Trzeba ją zdefiniować samodzielnie tylko wtedy, jeśli dodatkowe argumenty wpływają na zarządzanie procesem tworzenia przestrzeni nazw klasy. Wykorzystanie w tej recepturze argumentów podawanych wyłącznie za pomocą słów kluczowych związane jest z tym, że argumenty te w trakcie tworzenia klasy będą przekazywane tylko przy użyciu słów kluczowych. 326
Rozdział 9. Metaprogramowanie
Konfigurowanie metaklasy przy użyciu argumentów podawanych za pomocą słów kluczowych można traktować jak alternatywę do używania zmiennych klasy w tym samym celu. Oto przykład: class Spam(metaclass=MyMeta): debug = True synchronize = True ...
Zaletą podawania parametrów za pomocą argumentów jest to, że nie zaśmieca się przestrzeni nazw klasy jednostkami, które są potrzebne tylko na etapie tworzenia klasy, a nie w czasie późniejszego wykonywania poleceń. Ponadto argumenty są dostępne w metodzie __prepare__() uruchamianej przed rozpoczęciem przetwarzania wywołań z ciała klasy. Natomiast zmienne klasy są dostępne dopiero w metodach __new__() i __init__() metaklasy.
9.16. Sprawdzanie sygnatury na podstawie argumentów *args i **kwargs Problem Programista napisał funkcję (lub metodę) z argumentami *args i **kwargs, dlatego może to być funkcja do użytku ogólnego. Oprócz tego chce jednak sprawdzać przekazane argumenty, aby stwierdzić, czy pasują do sygnatury określonej funkcji.
Rozwiązanie W każdej sytuacji, w której chcesz manipulować sygnaturami funkcji, powinieneś korzystać ze związanych z sygnaturami mechanizmów modułu inspect. Przydatne są tu zwłaszcza dwie klasy — Signature i Parameter. Poniżej przedstawiono interaktywny przykład tworzenia sygnatury funkcji: >>> >>> >>> ... ... >>> >>> (x, >>>
Po utworzeniu obiektu sygnatury można go łatwo powiązać z argumentami *args i **kwargs, używając metody bind() sygnatury, co pokazano w poniższym prostym przykładzie: >>> def func(*args, **kwargs): ... bound_values = sig.bind(*args, **kwargs) ... for name, value in bound_values.arguments.items(): ... print(name,value) ... >>> # Wypróbuj różne możliwości >>> func(1, 2, z=3) x 1 y 2 z 3
9.16. Sprawdzanie sygnatury na podstawie argumentów *args i **kwargs
327
>>> func(1) x 1 >>> func(1, z=3) x 1 z 3 >>> func(y=2, x=1) x 1 y 2 >>> func(1, 2, 3, 4) Traceback (most recent call last): ... File "/usr/local/lib/python3.3/inspect.py", line 1972, in _bind raise TypeError('too many positional arguments') TypeError: too many positional arguments >>> func(y=2) Traceback (most recent call last): ... File "/usr/local/lib/python3.3/inspect.py", line 1961, in _bind raise TypeError(msg) from None TypeError: 'x' parameter lacking default value >>> func(1, y=2, x=3) Traceback (most recent call last): ... File "/usr/local/lib/python3.3/inspect.py", line 1985, in _bind '{arg!r}'.format(arg=param.name)) TypeError: multiple values for argument 'x' >>>
Jak widać, powiązanie sygnatury z przekazywanymi argumentami pozwala wymusić wszystkie standardowe reguły wywoływania funkcji, związane z wymaganymi argumentami, domyślnymi wartościami, powtórzeniami itd. Oto bardziej konkretny przykład wymuszania sygnatur funkcji. W tym kodzie w klasie bazowej zdefiniowana jest bardzo ogólna wersja metody __init__(), jednak w podklasach należy podawać oczekiwane sygnatury. from inspect import Signature, Parameter def make_sig(*names): parms = [Parameter(name, Parameter.POSITIONAL_OR_KEYWORD) for name in names] return Signature(parms) class Structure: __signature__ = make_sig() def __init__(self, *args, **kwargs): bound_values = self.__signature__.bind(*args, **kwargs) for name, value in bound_values.arguments.items(): setattr(self, name, value) # Przykład zastosowania class Stock(Structure): __signature__ = make_sig('name', 'shares', 'price') class Point(Structure): __signature__ = make_sig('x', 'y')
Oto przykład ilustrujący działanie klasy Stock: >>> import inspect >>> print(inspect.signature(Stock)) (name, shares, price) >>> s1 = Stock('ACME', 100, 490.1)
Omówienie Funkcje z argumentami *args i **kwargs bardzo często stosuje się w trakcie rozwijania bibliotek ogólnego użytku, pisania dekoratorów lub implementowania pośredników. Jednak wadą takich funkcji jest to, że jeśli zechcesz zaimplementować sprawdzanie argumentów, kod stanie się bardzo skomplikowany. Przyjrzyj się np. recepturze 8.11. Zastosowanie obiektu sygnatury pozwala uprościć rozwiązanie. W ostatnim przykładzie sensowne może być tworzenie obiektów sygnatur z wykorzystaniem niestandardowej metaklasy. Oto inna wersja, w której pokazano, jak to zrobić: from inspect import Signature, Parameter def make_sig(*names): parms = [Parameter(name, Parameter.POSITIONAL_OR_KEYWORD) for name in names] return Signature(parms) class StructureMeta(type): def __new__(cls, clsname, bases, clsdict): clsdict['__signature__'] = make_sig(*clsdict.get('_fields',[])) return super().__new__(cls, clsname, bases, clsdict) class Structure(metaclass=StructureMeta): _fields = [] def __init__(self, *args, **kwargs): bound_values = self.__signature__.bind(*args, **kwargs) for name, value in bound_values.arguments.items(): setattr(self, name, value) # Przykład class Stock(Structure): _fields = ['name', 'shares', 'price'] class Point(Structure): _fields = ['x', 'y']
Przy definiowaniu niestandardowych sygnatur często warto zapisać sygnaturę w specjalnym atrybucie __signature__, tak jak w przedstawionym kodzie. Wtedy kod przeprowadzający introspekcję z wykorzystaniem modułu inspect wykryje sygnaturę i poda ją jako sposób wywoływania. Oto przykład: >>> import inspect >>> print(inspect.signature(Stock)) (name, shares, price) >>> print(inspect.signature(Point)) (x, y) >>>
9.16. Sprawdzanie sygnatury na podstawie argumentów *args i **kwargs
329
9.17. Wymuszanie przestrzegania konwencji pisania kodu w klasie Problem Program składa się z dużej hierarchii klas i programista chce wymuszać pewne konwencje pisania kodu (lub zdiagnozować kod), aby pomóc innym w pracy.
Rozwiązanie Jeśli chcesz sprawdzać definicje klas, często możesz posłużyć się metaklasą. Aby zdefiniować prostą metaklasę, zwykle tworzy się klasę pochodną od klasy type i modyfikuje metody __new__() oraz __init__(). Oto przykład: class MyMeta(type): def __new__(self, clsname, bases, clsdict): # clsname to nazwa definiowanej klasy; # bases to krotka z klasami bazowymi; # clsdict to słownik klasy return super().__new__(cls, clsname, bases, clsdict)
Jeśli definiowana jest metoda __init__(), kod wygląda tak: class MyMeta(type): def __init__(self, clsname, bases, clsdict): super().__init__(clsname, bases, clsdict) # clsname to nazwa definiowanej klasy; # bases to krotka z klasami bazowymi; # clsdict to słownik klasy
Aby zastosować metaklasę, zwykle należy umieścić ją w klasie bazowej najwyższego poziomu, po której dziedziczą pozostałe obiekty: class Root(metaclass=MyMeta): pass class A(Root): pass class B(Root): pass
Ważną cechą metaklas jest to, że umożliwiają sprawdzenie zawartości klasy w trakcie wczytywania jej definicji. W nowej wersji metody __init__() można sprawdzić słownik klasy, klasy bazowe itd. Ponadto metaklasa określona w danej klasie jest dziedziczona przez wszystkie klasy pochodne. Dlatego sprytny twórca systemu może dodać metaklasę do jednej z klas najwyższego poziomu w dużej hierarchii i sprawdzać definicje wszystkich klas pochodnych. Poniżej przedstawiono konkretny, choć nieco dziwaczny przykład. Metaklasa odrzuca tu definicje klas zawierających metody, których nazwy składają się z dużych i małych liter (w ten sposób można zirytować programistów Javy): class NoMixedCaseMeta(type): def __new__(cls, clsname, bases, clsdict): for name in clsdict: if name.lower() != name:
330
Rozdział 9. Metaprogramowanie
raise TypeError('Bad attribute name: ' + name) return super().__new__(cls, clsname, bases, clsdict) class Root(metaclass=NoMixedCaseMeta): pass class A(Root): def foo_bar(self): pass class B(Root): def fooBar(self): pass
# Poprawne
# Błąd TypeError
Poniżej przedstawiono bardziej zaawansowany i przydatny przykład. Ta metaklasa sprawdza definicje ponownie definiowanych metod, aby zagwarantować, że mają one takie same sygnatury jak pierwotne metody w klasie bazowej: from inspect import signature import logging class MatchSignaturesMeta(type): def __init__(self, clsname, bases, clsdict): super().__init__(clsname, bases, clsdict) sup = super(self, self) for name, value in clsdict.items(): if name.startswith('_') or not callable(value): continue # Pobieranie wcześniejszej definicji (jeśli istnieje) i porównywanie sygnatur prev_dfn = getattr(sup,name,None) if prev_dfn: prev_sig = signature(prev_dfn) val_sig = signature(value) if prev_sig != val_sig: logging.warning('Niepasująca sygnatura w %s. %s!= %s', value.__qualname__, prev_sig, val_sig) # Przykład class Root(metaclass=MatchSignaturesMeta): pass class A(Root): def foo(self, x, y): pass def spam(self, x, *, z): pass # Klasa z ponownie zdefiniowanymi metodami o zmodyfikowanych sygnaturach class B(A): def foo(self, a, b): pass def spam(self,x,z): pass
Jeśli uruchomisz ten kod, otrzymasz następujące dane wyjściowe: WARNING:root:Niepasująca sygnatura w B.spam. (self, x, *, z) != (self, x, z) WARNING:root:Niepasująca sygnatura w B.foo. (self, x, y) != (self, a, b)
Takie ostrzeżenia mogą być przydatne przy przechwytywaniu drobnych błędów programu. Jeśli w klasie pochodnej zmienione zostaną nazwy argumentów, wówczas kod, który wymaga przekazywania do metody argumentów za pomocą słów kluczowych, przestanie działać.
9.17. Wymuszanie przestrzegania konwencji pisania kodu w klasie
331
Omówienie W dużych programach obiektowych czasem przydatne jest kontrolowanie definicji klas za pomocą metaklasy. W metaklasie można sprawdzać definicje klas i ostrzegać programistów o problemach (np. o drobnych niezgodnościach w sygnaturach metod), które mogą nie zostać dostrzeżone. Można stwierdzić, że tego rodzaju błędy lepiej jest przechwytywać za pomocą narzędzi do analizy programów lub środowisk IDE. Takie narzędzia z pewnością są przydatne. Jeśli jednak tworzysz platformę lub bibliotekę, które będą użytkowane również przez inne osoby, często nie masz żadnej kontroli nad stosowanym przez nie procesem programowania. Dlatego w niektórych sytuacjach warto dodać do metaklasy kod sprawdzający poprawność, jeśli pozwoli to zwiększyć komfort pracy użytkowników. Wybór metody, którą będziesz modyfikować w metaklasie (__new__() lub __init__()), zależy od tego, jak chcesz używać klasy wynikowej. Metoda __new__() jest wywoływana przed utworzeniem klasy i zwykle używa się jej wtedy, gdy metaklasa ma w pewien sposób zmienić definicję klasy (w wyniku zmiany zawartości jej słownika). Metoda __init__() jest wywoływana po utworzeniu klasy i przydaje się do pisania kodu, który manipuluje gotowymi obiektami danej klasy. W ostatnim przykładzie jest to niezbędne, ponieważ kod używa funkcji super() do wyszukiwania wcześniejszych definicji metod. Jest to możliwe tylko po utworzeniu obiektu danej klasy i ustawieniu kolejności określania metod. W ostatnim przykładzie pokazano też, jak korzystać z obiektów sygnatur funkcji Pythona. Metaklasa pobiera definicje wszystkich jednostek wywoływalnych klasy, znajduje ich wcześniejsze definicje (jeśli istnieją), a następnie porównuje sygnatury za pomocą polecenia inspect.signature(). W wierszu kodu z wywołaniem super(self, self) nie ma literówki. W trakcie korzystania z metaklasy należy pamiętać, że self to obiekt klasy. Dlatego polecenie to służy do znajdowania definicji znajdujących się na wyższych poziomach hierarchii klas, w klasach nadrzędnych wobec self.
9.18. Programowe definiowanie klas Problem Programista pisze kod, który ma tworzyć nowy obiekt klasy. Wymyślił, że może zapisywać kod źródłowy klasy w łańcuchu znaków i przetwarzać go za pomocą funkcji exec(), jednak wolałby zastosować bardziej eleganckie podejście.
Rozwiązanie Do tworzenia nowych obiektów klas można wykorzystać funkcję types.new_class(). Wystarczy podać w niej nazwę klasy, krotkę z klasami bazowymi, argumenty przekazywane za pomocą słów kluczowych i wywołanie zwrotne, które słownik klasy zapełni składowymi. Oto przykład:
332
Rozdział 9. Metaprogramowanie
# stock.py # Przykład ilustrujący ręczne tworzenie klas na podstawie ich fragmentów # Metody def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price def cost(self): return self.shares * self.price cls_dict = { '__init__' : __init__, 'cost' : cost, } # Tworzenie klasy import types Stock = types.new_class('Stock', (), {}, lambda ns: ns.update(cls_dict)) Stock.__module__ = __name__
Ten kod tworzy normalny obiekt klasy, który działa w oczekiwany sposób: >>> s = Stock('ACME', 50, 91.1) >>> s >>> s.cost() 4555.0 >>>
Ciekawym aspektem tego rozwiązania jest przypisanie wartości do atrybutu Stock.__module__ po wywołaniu funkcji types.new_class(). Gdy definiowana jest klasa, atrybut __module__ zawiera nazwę modułu, w którym znajduje się definicja. Nazwa ta jest wykorzystywana przy generowaniu danych wyjściowych przez metody takie jak __repr__(). Jest też używana w różnych bibliotekach, np. w pickle. Dlatego aby klasa była poprawna, trzeba odpowiednio ustawić wartość tego atrybutu. Jeśli w tworzonej klasie znajduje się inna metaklasa, trzeba ją podać jako trzeci argument funkcji types.new_class(): >>> import abc >>> Stock = types.new_class('Stock', (), {'metaclass': abc.ABCMeta}, ... lambda ns: ns.update(cls_dict)) ... >>> Stock.__module__ = __name__ >>> Stock >>> type(Stock) >>>
W trzecim argumencie można też podać inne argumenty przekazywane za pomocą słów kluczowych. Poniższej definicji klasy: class Spam(Base, debug=True, typecheck=False): ...
odpowiada następujące wywołanie funkcji new_class(): Spam = types.new_class('Spam', (Base,), {'debug': True, 'typecheck': False}, lambda ns: ns.update(cls_dict))
9.18. Programowe definiowanie klas
333
Czwarty argument funkcji new_class() jest najbardziej tajemniczy. Jest nim funkcja, która przyjmuje obiekt odwzorowania używany dla przestrzeni nazw klasy. Zwykle tym obiektem jest słownik, jednak może to być dowolny obiekt zwrócony przez metodę __prepare__() (zobacz recepturę 9.14). Funkcja przekazywana tu jako argument powinna dodawać do przestrzeni nazw nowe pozycje za pomocą metody update() (tak jak w kodzie) lub innych operacji na odwzorowaniach.
Omówienie Możliwość tworzenia nowych obiektów klas jest przydatna w niektórych sytuacjach. Jednym ze znanych miejsc, w których jest to potrzebne, jest funkcja collections.namedtuple(). Oto przykład: >>> Stock = collections.namedtuple('Stock', ['name', 'shares', 'price']) >>> Stock >>>
W funkcji namedtuple()zamiast przedstawionej tu techniki używane jest wywołanie exec(). Oto prosta zmodyfikowana wersja tej funkcji, w której klasa jest tworzona bezpośrednio: import operator import types import sys def named_tuple(classname, fieldnames): # Zapełnianie słownika akcesorami właściwości powiązanych z polami cls_dict = { name: property(operator.itemgetter(n)) for n, name in enumerate(fieldnames) } # Tworzenie funkcji __new__ i dodawanie elementów do słownika klasy def __new__(cls, *args): if len(args) != len(fieldnames): raise TypeError('Oczekiwana liczba argumentów: {}'.format(len(fieldnames))) return tuple.__new__(cls, args) cls_dict['__new__'] = __new__ # Tworzenie klasy cls = types.new_class(classname, (tuple,), {}, lambda ns: ns.update(cls_dict)) # Ustawianie modułu na moduł jednostki, która wywołała kod cls.__module__ = sys._getframe(1).f_globals['__name__'] return cls
W ostatnim fragmencie kodu zastosowano „sztuczkę z ramką” — za pomocą wywołania sys._getframe() pobrano nazwę modułu jednostki, która wywołała kod. Inny przykład wykorzystania tej sztuczki znajdziesz w recepturze 2.15. W poniższym przykładzie pokazano, jak działa ten kod: >>> Point = named_tuple('Point', ['x', 'y']) >>> Point >>> p = Point(4, 5) >>> len(p) 2 >>> p.x
334
Rozdział 9. Metaprogramowanie
4 >>> p.y 5 >>> p.x = 2 Traceback (most recent call last): File "", line 1, in AttributeError: can't set attribute >>> print('%s %s' % p) 4 5 >>>
Ważnym aspektem techniki zastosowanej w tej recepturze jest prawidłowe współdziałanie z metaklasami. Możliwe, że wpadniesz na pomysł zbudowania klasy przez bezpośrednie tworzenie obiektu metaklasy: Stock = type('Stock', (), cls_dict)
Problem polega tu na pominięciu kilku ważnych etapów, takich jak wywołanie metody __prepare__() metaklasy. Jeśli zamiast tego wywołasz funkcję types.new_class(), zagwarantujesz wykonanie wszystkich niezbędnych kroków w procesie inicjowania. Np. wywoływana zwrotnie funkcja podana jako czwarty argument funkcji types.new_class() otrzyma obiekt odwzorowania zwrócony przez metodę __prepare__(). Jeśli chcesz wykonać tylko etap przygotowań, wywołaj metodę types.prepare_class(): import types metaclass, kwargs, ns = types.prepare_class('Stock', (), {'metaclass': type})
Metoda ta znajdzie odpowiednią metaklasę i wywoła jej metodę __prepare__(). Zwracane są tu metaklasa, pozostałe argumenty podawane za pomocą słów kluczowych i przygotowana przestrzeń nazw. Więcej informacji znajdziesz w dokumencie PEP 3115 (http://www.python.org/dev/peps/pep-3115/) i dokumentacji Pythona (http://docs.python.org/3/reference/datamodel.html#metaclasses).
9.19. Inicjowanie składowych klasy w miejscu definicji klasy Problem Programista chce inicjować fragmenty klasy w miejscu jej definicji, a nie w trakcie tworzenia obiektów danej klasy.
Rozwiązanie Wykonywanie operacji związanych z inicjowaniem lub konfigurowaniem to klasyczne zadanie wykonywane za pomocą metaklas. Metaklasa jest uruchamiana w miejscu definicji, co pozwala na wykonanie dodatkowych operacji. Oto przykład, w którym wykorzystano tę możliwość do utworzenia klas podobnych do krotek nazwanych z modułu collections:
9.19. Inicjowanie składowych klasy w miejscu definicji klasy
335
import operator class StructTupleMeta(type): def __init__(cls, *args, **kwargs): super().__init__(*args, **kwargs) for n, name in enumerate(cls._fields): setattr(cls, name, property(operator.itemgetter(n))) class StructTuple(tuple, metaclass=StructTupleMeta): _fields = [] def __new__(cls, *args): if len(args) != len(cls._fields): raise ValueError('Wymaganych argumentów: {}'.format(len(cls._fields))) return super().__new__(cls,args)
Kod ten umożliwia definiowanie prostych struktur danych opartych na krotkach: class Stock(StructTuple): _fields = ['name', 'shares', 'price'] class Point(StructTuple): _fields = ['x', 'y']
Struktury te działają w następujący sposób: >>> s = Stock('ACME', 50, 91.1) >>> s ('ACME', 50, 91.1) >>> s[0] 'ACME' >>> s.name 'ACME' >>> s.shares * s.price 4555.0 >>> s.shares = 23 Traceback (most recent call last): File "", line 1, in AttributeError: can't set attribute >>>
Omówienie W tej recepturze klasa StructTupleMeta przyjmuje nazwy atrybutów z atrybutu _fields klasy i przekształca je w metody właściwości, które zapewniają dostęp do konkretnych pozycji krotki. Funkcja operator.itemgetter() tworzy akcesor, a funkcja property() przekształca go we właściwość. Najbardziej skomplikowanym aspektem tej receptury jest ustalenie, kiedy zachodzą poszczególne etapy inicjowania. Metoda __init__() klasy StructTupleMeta jest wywoływana dla każdej definiowanej klasy tylko raz. Argument cls to definiowana klasa. Kod wykorzystuje zmienną _fields klasy do pobrania nowo zdefiniowanej klasy i dodania do niej elementów. Klasa StructTuple to wspólna klasa bazowa do tworzenia klas pochodnych. Metoda __new__() w tej klasie odpowiada za tworzenie nowych obiektów. Sposób wykorzystania metody __new__() jest tu dość nietypowy. Po części związane jest to z tym, że rozwiązanie ma modyfikować sygnatury wywołań krotek, tak aby można było tworzyć obiekty za pomocą kodu wywoływanego w standardowy sposób: s = Stock('ACME', 50, 91.1) s = Stock(('ACME', 50, 91.1))
336
Rozdział 9. Metaprogramowanie
# Poprawnie # Błąd
Metoda __new__() (w odróżnieniu od __init__()) jest wywoływana przed utworzeniem obiektu. Ponieważ krotki są niezmienne, po ich utworzeniu nie można wprowadzić w nich zmian. Funkcja __init__() jest wywoływana w procesie tworzenia obiektu zbyt późno, aby można w niej wykonać potrzebne operacje. Dlatego zdefiniowano metodę __new__(). Choć przedstawiona receptura jest krótka, jej staranna analiza pomoże dobrze zrozumieć proces definiowania klas Pythona, tworzenie obiektów, a także miejsca, w których wywoływane są różne metody metaklas i klas. W dokumencie PEP 422 (http://www.python.org/dev/peps/pep-0422/) przedstawiono inne sposoby wykonywania zadania opisanego w tej recepturze. Jednak w czasie gdy powstawała ta książka, techniki te nie były ani stosowane, ani zaakceptowane. Mimo to mogą się one okazać ciekawe dla programistów używających wersji Pythona nowszych niż 3.3.
9.20. Przeciążanie metod z wykorzystaniem uwag do funkcji Problem Programista poznał technikę dodawania uwag do funkcji i wpadł na pomysł, że może wykorzystać ją do obsługi przeciążania metod na podstawie typów. Nie wie jednak, jak to zrobić (ani nawet czy jest to dobry pomysł).
Rozwiązanie Ta receptura oparta jest na prostym spostrzeżeniu — Python umożliwia dodawanie uwag do argumentów, dlatego można napisać następujący kod: class Spam: def bar(self, print('Bar def bar(self, print('Bar s = Spam() s.bar(2, 3) s.bar('Witaj')
Oto początek rozwiązania, które działa w ten sposób (wykorzystano tu metaklasy i deskryptory): # multiple.py import inspect import types class MultiMethod: ''' Odpowiada jednej wielometodzie ''' def __init__(self, name): self._methods = {} self.__name__ = name def register(self, meth):
9.20. Przeciążanie metod z wykorzystaniem uwag do funkcji
337
''' Rejestrowanie nowej metody jako wielometody ''' sig = inspect.signature(meth) # Tworzenie sygnatury typu na podstawie uwag z metody types = [] for name, parm in sig.parameters.items(): if name == 'self': continue if parm.annotation is inspect.Parameter.empty: raise TypeError( 'Argument {} musi mieć typ określony w uwagach'.format(name) ) if not isinstance(parm.annotation, type): raise TypeError( 'Uwaga przy argumencie {} musi określać typ'.format(name) ) if parm.default is not inspect.Parameter.empty: self._methods[tuple(types)] = meth types.append(parm.annotation) self._methods[tuple(types)] = meth def __call__(self, *args): ''' Wywołanie metody na podstawie sygnatury obejmującej typy argumentów ''' types = tuple(type(arg) for arg in args[1:]) meth = self._methods.get(types, None) if meth: return meth(*args) else: raise TypeError('Brak metody dla typów {}'.format(types)) def __get__(self, instance, cls): ''' Metoda deskryptora potrzebna, aby wywołania działały w klasie ''' if instance is not None: return types.MethodType(self, instance) else: return self class MultiDict(dict): ''' Specjalny słownik do tworzenia wielometod w metaklasie ''' def __setitem__(self, key, value): if key in self: # Jeśli klucz już istnieje, element jest wielometodą lub jednostką wywoływalną current_value = self[key] if isinstance(current_value, MultiMethod): current_value.register(value) else: mvalue = MultiMethod(key) mvalue.register(current_value) mvalue.register(value) super().__setitem__(key, mvalue) else: super().__setitem__(key, value) class MultipleMeta(type): '''
Poniżej pokazano, jak zastosować tę klasę: class Spam(metaclass=MultipleMeta): def bar(self, x:int, y:int): print('Bar 1:', x, y) def bar(self, s:str, n:int = 0): print('Bar 2:', s, n) # Przykład: przeciążona metoda __init__ import time class Date(metaclass=MultipleMeta): def __init__(self, year: int, month:int, day:int): self.year = year self.month = month self.day = day def __init__(self): t = time.localtime() self.__init__(t.tm_year, t.tm_mon, t.tm_mday)
Oto interaktywna sesja, w której sprawdzono działanie rozwiązania: >>> s = Spam() >>> s.bar(2, 3) Bar 1: 2 3 >>> s.bar('witaj') Bar 2: witaj 0 >>> s.bar('witaj', 5) Bar 2: witaj 5 >>> s.bar(2, 'witaj') Traceback (most recent call last): File "", line 1, in File "multiple.py", line 42, in __call__ raise TypeError('Brak metody dla typów {}'.format(types)) TypeError: Brak metody dla typów (, ) >>> # Przeciążona metoda __init__ >>> d = Date(2012, 12, 21) >>> # Pobieranie aktualnej daty >>> e = Date() >>> e.year 2012 >>> e.month 12 >>> e.day 3 >>>
Omówienie Przyznajemy, że w kodzie zastosowano chyba zbyt wiele sztuczek, aby tę recepturę można było wykorzystać w praktyce. Wykorzystano tu mechanizmy działania metaklas i deskryptorów oraz przypomniano niektóre związane z nimi zagadnienia. Dlatego choć może nie będziesz
9.20. Przeciążanie metod z wykorzystaniem uwag do funkcji
339
bezpośrednio stosował tej receptury, niektóre z opisanych pomysłów możesz wykorzystać w innych technikach programowania opartych na metaklasach, deskryptorach i uwagach do funkcji. Główny pomysł, na którym oparto kod, jest stosunkowo prosty. W metodzie __prepare__() metaklasy MultipleMeta podawany jest niestandardowy słownik klasy (obiekt typu MultiDict). Słowniki typu MultiDict w odróżnieniu od zwykłych słowników sprawdzają w momencie ustawiania elementów, czy nie istnieje już dotyczący ich wpis. Jeśli istnieje, powtarzające się wpisy są scalane w obiekcie typu MultiMethod. Obiekty typu MultiMethod zawierają grupy metod i tworzą odwzorowanie z sygnatur (z typami) na funkcje. Uwagi w funkcjach służą przy tym do określania sygnatur i budowania odwzorowań. Operacje te mają miejsce w metodzie MultiMethod.register(). Ważną cechą opisywanych odwzorowań jest to, że w przypadku wielometod określone muszą być typy wszystkich argumentów. W przeciwnym razie kod zgłosi błąd. Aby obiekty typu MultiMethod działały jak jednostka wywoływalna, zaimplementowano metodę __call__(). Metoda ta tworzy krotkę z typami na podstawie wszystkich argumentów (z wyjątkiem argumentu self), wyszukuje odpowiednią metodę w wewnętrznym odwzorowaniu i wywołuje ją. Metoda __get__() jest potrzebna, aby obiekty typu MultiMethod działały prawidłowo w definicjach klasy. W przedstawionym kodzie metoda ta jest używana do tworzenia odpowiednio powiązanych metod. Oto przykład: >>> b = s.bar >>> b > >>> b.__self__ <__main__.Spam object at 0x1006a46d0> >>> b.__func__ <__main__.MultiMethod object at 0x1006a4d50> >>> b(2, 3) Bar 1: 2 3 >>> b('witaj') Bar 2: witaj 0 >>>
To prawda, receptura ta jest skomplikowana. Jest to pewien problem, jeśli uwzględnić, jak wiele ograniczeń występuje w tym rozwiązaniu. Nie działa ono np. dla argumentów podawanych za pomocą słów kluczowych: >>> s.bar(x=2, y=3) Traceback (most recent call last): File "", line 1, in TypeError: __call__() got an unexpected keyword argument 'y' >>> s.bar(s='hello') Traceback (most recent call last): File "", line 1, in TypeError: __call__() got an unexpected keyword argument 's' >>>
Można dodać obsługę słów kluczowych, jednak wymaga to zastosowania całkowicie innej techniki odwzorowywania metod. Problem związany jest z tym, że argumenty podawane za pomocą słów kluczowych nie mają konkretnej kolejności. Gdy połączyć je z argumentami podawanymi na podstawie pozycji, powstanie zestaw wymieszanych argumentów, które trzeba jakoś uporządkować w metodzie __call__().
340
Rozdział 9. Metaprogramowanie
Przedstawiona receptura nie zapewnia też pełnej obsługi dziedziczenia. Nie zadziała np. poniższy kod: class A: pass class B(A): pass class C: pass class Spam(metaclass=MultipleMeta): def foo(self, x:A): print('Foo 1:', x) def foo(self, x:C): print('Foo 2:', x)
Problem wynika z tego, że uwaga x:A nie pasuje do obiektów klas pochodnych (takich jak obiektów klasy B). Oto przykład: >>> s = Spam() >>> a = A() >>> s.foo(a) Foo 1: <__main__.A object at 0x1006a5310> >>> c = C() >>> s.foo(c) Foo 2: <__main__.C object at 0x1007a1910> >>> b = B() >>> s.foo(b) Traceback (most recent call last): File "", line 1, in File "multiple.py", line 44, in __call__ raise TypeError('Brak metody dla typów {}'.format(types)) TypeError: Brak metody dla typów (,) >>>
Zamiast stosować metaklasy i uwagi, można napisać podobne rozwiązanie oparte na dekoratorach: import types class multimethod: def __init__(self, self._methods = self.__name__ = self._default =
func): {} func.__name__ func
def match(self, *types): def register(func): ndefaults = len(func.__defaults__) if func.__defaults__ else 0 for n in range(ndefaults+1): self._methods[types[:len(types) - n]] = func return self return register def __call__(self, *args): types = tuple(type(arg) for arg in args[1:]) meth = self._methods.get(types, None) if meth: return meth(*args) else:
9.20. Przeciążanie metod z wykorzystaniem uwag do funkcji
341
return self._default(*args) def __get__(self, instance, cls): if instance is not None: return types.MethodType(self, instance) else: return self
Aby zastosować wersję z dekoratorem, należy napisać kod następującej postaci: class Spam: @multimethod def bar(self, *args): # Domyślna metoda wywoływana, gdy nie znaleziono pasującej wersji raise TypeError('Brak metody dla typu bar') @bar.match(int, int) def bar(self, x, y): print('Bar 1:', x, y) @bar.match(str, int) def bar(self, s, n = 0): print('Bar 2:', s, n)
Rozwiązanie oparte na dekoratorze ma te same ograniczenia co poprzednia wersja (nie obsługuje argumentów podawanych za pomocą słów kluczowych i dziedziczenia). W kodzie do ogólnego użytku prawdopodobnie najlepiej jest zrezygnować z przeciążania metod. Stosowanie tej techniki może mieć sens w specjalnych sytuacjach, np. w programach, gdzie wywoływane metody są określane na podstawie dopasowywania do wzorca. Np. w opisanym w recepturze 8.21 wzorcu odwiedzający można wykorzystać klasę z przeciążonymi metodami. Jednak w innych sytuacjach zwykle warto wybrać proste podejście i zastosować metody o odmiennych nazwach. Pomysły na różne sposoby implementowania przeciążania metod pojawiają się w środowisku Pythona od lat. Dobrym punktem wyjścia do zapoznania się z nimi jest artykuł Guida van Rossuma z bloga — Five-Minute Multimethods in Python (http://www.artima.com/weblogs/ viewpost.jsp?thread=101605).
9.21. Unikanie powtarzających się metod właściwości Problem Programista pisze klasy, w których wielokrotnie musi definiować metody właściwości do wykonywania standardowych zadań (np. sprawdzania typu). Chciałby uprościć kod, aby uniknąć licznych powtórzeń.
Rozwiązanie Przyjrzyj się prostej klasie, w której atrybuty są obsługiwane za pomocą metod właściwości: class Person: def __init__(self, name ,age): self.name = name self.age = age
342
Rozdział 9. Metaprogramowanie
@property def name(self): return self._name @name.setter def name(self, value): if not isinstance(value, str): raise TypeError('Atrybut name musi być typu str') self._name = value @property def age(self): return self._age @age.setter def age(self, value): if not isinstance(value, int): raise TypeError('Atrybut age musi być typu int') self._age = value
Jak widać, potrzebna jest duża ilość kodu, aby wymusić pewne asercje związane z typem wartości atrybutów. Zawsze gdy natrafisz na taki kod, powinieneś zastanowić się nad możliwością jego uproszczenia. Jednym z możliwych rozwiązań jest napisanie funkcji, która definiuje właściwość i zwraca ją. Oto przykład: def typed_property(name, expected_type): storage_name = '_' + name @property def prop(self): return getattr(self, storage_name) @prop.setter def prop(self, value): if not isinstance(value, expected_type): raise TypeError('Atrybut {} musi być typu {}'.format(name, expected_type)) setattr(self, storage_name, value) return prop # Przykład zastosowania class Person: name = typed_property('name', str) age = typed_property('age', int) def __init__(self, name, age): self.name = name self.age = age
Omówienie W tej recepturze wykorzystano ważną cechę funkcji wewnętrznych (domknięć) — można je zastosować do napisania kodu, który działa jak makro. Funkcja typed_property() w tym przykładzie może wyglądać dziwnie, jednak generuje kod właściwości i zwraca wynikowy obiekt właściwości. W klasie działa tak, jakby w jej definicji umieszczono kod z tej funkcji. Choć metody właściwości przeznaczone do pobierania i ustawiania atrybutów korzystają ze zmiennych lokalnych name, expected_type i storage_name, nie stanowi to problemu. Na zapleczu wartości te są przechowywane w domknięciu.
9.21. Unikanie powtarzających się metod właściwości
343
Przedstawioną recepturę można w ciekawy sposób zmodyfikować, używając funkcji functools. partial(). Oto przykład: from functools import partial String = partial(typed_property, expected_type=str) Integer = partial(typed_property, expected_type=int) # Przykład class Person: name = String('name') age = Integer('age') def __init__(self, name, age): self.name = name self.age = age
Ten kod przypomina kod deskryptorów systemu plików przedstawiony w recepturze 8.13.
9.22. Definiowanie w łatwy sposób menedżerów kontekstu Problem Programista chce zaimplementować nowego rodzaju menedżery kontekstu używane w poleceniu with.
Rozwiązanie Jednym z najprostszych sposobów na napisanie nowego menedżera kontekstu jest wykorzystanie dekoratora @contextmanager z modułu contextlib. Oto przykładowy menedżer kontekstu, który mierzy czas wykonywania bloku kodu: import time from contextlib import contextmanager @contextmanager def timethis(label): start = time.time() try: yield finally: end = time.time() print('{}: {}'.format(label, end - start)) # Przykład zastosowania with timethis('counting'): n = 10000000 while n > 0: n -= 1
W funkcji timethis() cały kod do polecenia yield jest wykonywany jako metoda __enter__() menedżera kontekstu. Cały kod po wywołaniu yield jest wykonywany jako metoda __exit__(). Jeśli wystąpi wyjątek, zostanie zgłoszony w poleceniu yield.
344
Rozdział 9. Metaprogramowanie
Oto nieco bardziej zaawansowany menedżer kontekstu, w którym zaimplementowano transakcję związaną z obiektem listy: @contextmanager def list_transaction(orig_list): working = list(orig_list) yield working orig_list[:] = working
Pomysł polega na tym, że zmiany wprowadzone na liście są zatwierdzane dopiero wtedy, gdy cały blok kodu zostanie wykonany bez zgłaszania wyjątków. Oto przykład, który to ilustruje: >>> items = [1, 2, 3] >>> with list_transaction(items) as working: ... working.append(4) ... working.append(5) ... >>> items [1, 2, 3, 4, 5] >>> with list_transaction(items) as working: ... working.append(6) ... working.append(7) ... raise RuntimeError('ups') ... Traceback (most recent call last): File "", line 4, in RuntimeError: ups >>> items [1, 2, 3, 4, 5] >>
Omówienie Zwykle menedżery kontekstu tworzy się, definiując klasę z metodami __enter__() i __exit__() : import time class timethis: def __init__(self, label): self.label = label def __enter__(self): self.start = time.time() def __exit__(self, exc_ty, exc_val, exc_tb): end = time.time() print('{}: {}'.format(self.label, end - self.start))
Choć kod nie jest skomplikowany, napisanie go jest dużo bardziej żmudne niż utworzenie prostej funkcji za pomocą dekoratora @contextmanager. Dekorator @contextmanager służy tylko do pisania niezależnych funkcji do zarządzania kontekstem. Jeśli istnieje obiekt (np. plik, połączenie sieciowe lub blokada), który ma współdziałać z poleceniem with, należy napisać metody __enter__() i __exit__().
9.22. Definiowanie w łatwy sposób menedżerów kontekstu
345
9.23. Wykonywanie kodu powodującego lokalne efekty uboczne Problem Programista używa funkcji exec() do wykonywania fragmentu kodu w zasięgu jednostki wywołującej, jednak efekty uruchomienia kodu po zakończeniu jego pracy są niewidoczne.
Rozwiązanie Aby lepiej zrozumieć problem, przeprowadź pewien eksperyment. Najpierw wykonaj fragment kodu w globalnej przestrzeni nazw: >>> a = 13 >>> exec('b = a + 1') >>> print(b) 14 >>>
Teraz spróbuj zrobić to samo w funkcji: >>> def test(): ... a = 13 ... exec('b = a + 1') ... print(b) ... >>> test() Traceback (most recent call last): File "", line 1, in File "", line 4, in test NameError: global name 'b' is not defined >>>
Jak widać, zgłaszany jest błąd NameError (tak jakby nie uruchomiono polecenia exec()). Stanowi to problem, jeśli chcesz wykorzystać wyniki z funkcji exec() w dalszych obliczeniach. Aby rozwiązać ten problem, trzeba zastosować funkcję locals() i pobrać zmienne lokalne przed wywołaniem polecenia exec(). Bezpośrednio po jego wywołaniu można pobrać zmodyfikowane wartości ze słownika zmiennych lokalnych. Oto przykład: >>> def test(): ... a = 13 ... loc = locals() ... exec('b = a + 1') ... b = loc['b'] ... print(b) ... >>> test() 14 >>>
Omówienie Poprawne stosowanie funkcji exec() jest w praktyce stosunkowo trudne. W większości sytuacji, w których możesz rozważać jej użycie, istnieje bardziej eleganckie rozwiązanie (oparte np. na dekoratorach, domknięciach lub metaklasach). 346
Rozdział 9. Metaprogramowanie
Jeśli jednak musisz użyć funkcji exec(), zwróć uwagę na opisane w tej recepturze aspekty jej poprawnego stosowania. Domyślnie funkcja ta wykonuje kod w zasięgu lokalnym i globalnym jednostki wywołującej. Jednak zasięg lokalny przekazywany do funkcji exec() to słownik z kopią zmiennych lokalnych. Dlatego jeśli kod w funkcji exec() wprowadzi zmiany, nie będą one widoczne w zmiennych lokalnych. Oto inny przykład ilustrujący ten efekt: >>> def test1(): ... x = 0 ... exec('x += 1') ... print(x) ... >>> test1() 0 >>>
Gdy pobierzesz zmienne lokalne za pomocą funkcji locals(), jak pokazano w rozwiązaniu, uzyskasz kopię zmiennych lokalnych przekazanych do funkcji exec(). Sprawdzając wartości ze słownika po wykonaniu tej funkcji, można pobrać zmodyfikowane wartości. Dowodem na to jest poniższy eksperyment: >>> def test2(): ... x = 0 ... loc = locals() ... print('przed:', loc) ... exec('x += 1') ... print('po:', loc) ... print('x =', x) ... >>> test2() przed: {'x': 0} po: {'loc': {...}, 'x': 1} x = 0 >>>
Starannie przyjrzyj się danym wyjściowym w ostatnim kroku. Jeśli nie skopiujesz zmodyfikowanych wartości ze zmiennej loc do x, wartość zmiennej pozostanie niezmieniona. Gdy stosujesz funkcję locals(), zwróć uwagę na kolejność operacji. Przy każdym wywołaniu funkcja ta pobiera bieżącą wartość zmiennych lokalnych i zastępuje istniejące wpisy w słowniku. Przyjrzyj się skutkom następującego eksperymentu: >>> def test3(): ... x = 0 ... loc = locals() ... print(loc) ... exec('x += 1') ... print(loc) ... locals() ... print(loc) ... >>> test3() {'x': 0} {'loc': {...}, 'x': 1} {'loc': {...}, 'x': 0} >>>
Zauważ, że ostatnie wywołanie funkcji locals() spowodowało zastąpienie wartości zmiennej x. Zamiast wywoływać funkcję locals(), można utworzyć własny słownik i przekazać go do funkcji exec(). Oto przykład:
9.23. Wykonywanie kodu powodującego lokalne efekty uboczne
347
>>> def test4(): ... a = 13 ... loc = { 'a' : a } ... glb = { } ... exec('b = a + 1', glb, loc) ... b = loc['b'] ... print(b) ... >>> test4() 14 >>>
W większości sytuacji, gdy stosuje się funkcję exec(), jest to dobre rozwiązanie. Trzeba jedynie pamiętać o odpowiednim zainicjowaniu słowników globalnego i lokalnego nazwami, których używa wykonywany kod. Ponadto przed zastosowaniem funkcji exec() warto się zastanowić, czy nie jest możliwe inne rozwiązanie. W wielu sytuacjach, w których używa się funkcji exec(), można zastąpić ją domknięciami, dekoratorami, metaklasami i innymi mechanizmami metaprogramowania.
9.24. Parsowanie i analizowanie kodu źródłowego Pythona Problem Programista chce pisać programy, które parsują i analizują kod źródłowy Pythona.
Rozwiązanie Większość programistów wie, że Python potrafi obliczać lub wykonywać kod źródłowy podany w postaci łańcucha znaków. Oto przykład: >>> x = 42 >>> eval('2 + 3*4 + x') 56 >>> exec('for i in range(10): print(i)') 0 1 2 3 4 5 6 7 8 9 >>>
Można też wykorzystać moduł ast do skompilowania kodu źródłowego Pythona do drzewa składni abstrakcyjnej (ang. abstract syntax tree — AST), które można poddać analizie. Oto przykład: >>> import ast >>> ex = ast.parse('2 + 3*4 + x', mode='eval') >>> ex <_ast.Expression object at 0x1007473d0> >>> ast.dump(ex)
348
Rozdział 9. Metaprogramowanie
"Expression(body=BinOp(left=BinOp(left=Num(n=2), op=Add(), right=BinOp(left=Num(n=3), op=Mult(), right=Num(n=4))), op=Add(), right=Name(id='x', ctx=Load())))" >>> top = ast.parse('for i in range(10): print(i)', mode='exec') >>> top <_ast.Module object at 0x100747390> >>> ast.dump(top) "Module(body=[For(target=Name(id='i', ctx=Store()), iter=Call(func=Name(id='range', ctx=Load()), args=[Num(n=10)], keywords=[], starargs=None, kwargs=None), body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Name(id='i', ctx=Load())], keywords=[], starargs=None, kwargs=None))], orelse=[])])" >>>
Analizowanie drzewa kodu źródłowego wymaga wiedzy. Takie drzewo składa się z węzłów AST. Najłatwiejszy sposób na zarządzanie takimi węzłami to zdefiniowanie klasy odwiedzającej z implementacją różnych metod visit_NazwaWęzła(), gdzie NazwaWęzła() odpowiada poszczególnym rodzajom węzłów. Oto przykładowa klasa tego rodzaju. Rejestruje ona informacje na temat wczytywanych, zapisywanych i usuwanych nazw: import ast class CodeAnalyzer(ast.NodeVisitor): def __init__(self): self.loaded = set() self.stored = set() self.deleted = set() def visit_Name(self, node): if isinstance(node.ctx, ast.Load): self.loaded.add(node.id) elif isinstance(node.ctx, ast.Store): self.stored.add(node.id) elif isinstance(node.ctx, ast.Del): self.deleted.add(node.id) # Przykład zastosowania if __name__ == '__main__': # Kod w Pythonie code = ''' for i in range(10): print(i) del i ''' # Parsowanie, aby uzyskać drzewo AST top = ast.parse(code, mode='exec') # Przekazywanie drzewa AST do kodu analizującego używane nazwy c = CodeAnalyzer() c.visit(top) print('Wczytano:', c.loaded) print('Zapisano:', c.stored) print('Usunięto:', c.deleted)
Jeśli uruchomisz ten program, uzyskasz dane wyjściowe w następującej postaci: Wczytano: {'i', 'range', 'print'} Zapisano: {'i'} Usunięto: {'i'}
9.24. Parsowanie i analizowanie kodu źródłowego Pythona
349
Drzewa AST można też kompilować i wykonywać, używając funkcji compile(). Oto przykład: >>> exec(compile(top,'', 'exec')) 0 1 2 3 4 5 6 7 8 9 >>>
Omówienie Możliwość analizowania kodu źródłowego i pobierania z niego informacji może być punktem wyjścia do rozwijania rozmaitych narzędzi do analizowania, optymalizowania i sprawdzania poprawności kodu. Np. zamiast przekazywać fragment kodu bez sprawdzania do funkcji exec(), można najpierw przekształcić go na drzewo AST i sprawdzić, jakie zadania wykonuje. Można też tworzyć narzędzia, które sprawdzają cały kod źródłowy modułu i przeprowadzają jego statyczne analizy. Warto zauważyć, że jeśli masz odpowiednią wiedzę, możesz zmodyfikować drzewo AST, aby odpowiadało nowej wersji kodu. Oto przykładowy dekorator, który umieszcza globalnie używane nazwy w ciele funkcji. Dekorator ten parsuje kod źródłowy ciała funkcji, zmienia drzewo AST i odtwarza obiekt kodu funkcji: # namelower.py import ast import inspect # Klasa „odwiedzająca” dla węzłów. Umieszcza globalne nazwy w # ciele funkcji jako zmienne lokalne class NameLower(ast.NodeVisitor): def __init__(self, lowered_names): self.lowered_names = lowered_names def visit_FunctionDef(self, node): # Kompilowanie przypisań w celu przeniesienia stałych do funkcji code = '__globals = globals()\n' code += '\n'.join("{0} = __globals['{0}']".format(name) for name in self.lowered_names) code_ast = ast.parse(code, mode='exec') # Umieszczanie nowych poleceń w ciele funkcji node.body[:0] = code_ast.body # Zapisywanie obiektu funkcji self.func = node # Dekorator przekształcający nazwy globalne w lokalne def lower_names(*namelist): def lower(func): srclines = inspect.getsource(func).splitlines() # Pomijanie wierszy kodu źródłowego do miejsca napotkania dekoratora @lower_names for n, line in enumerate(srclines):
350
Rozdział 9. Metaprogramowanie
if '@lower_names' in line: break src = '\n'.join(srclines[n+1:]) # Sztuczka pozwalająca na obsługę kodu z wcięciami if src.startswith((' ','\t')): src = 'if 1:\n' + src top = ast.parse(src, mode='exec') # Przekształcanie drzewa AST cl = NameLower(namelist) cl.visit(top) # Wykonywanie kodu ze zmodyfikowanego drzewa AST temp = {} exec(compile(top,'','exec'), temp, temp) # Zwracanie zmodyfikowanego obiektu kodu func.__code__ = temp[func.__name__].__code__ return func return lower
Aby wykorzystać to rozwiązanie, należy napisać kod w następującej formie: INCR = 1 @lower_names('INCR') def countdown(n): while n > 0: n -= INCR
Dekorator zmieni wtedy kod źródłowy funkcji countdown() na poniższy: def countdown(n): __globals = globals() INCR = __globals['INCR'] while n > 0: n -= INCR
Testy wydajności wykazały, że nowa wersja funkcji działa około 20% szybciej od pierwotnej. Czy należy stosować ten dekorator do wszystkich funkcji? Raczej nie. Ilustruje on jednak, w jaki sposób można opracować bardzo zaawansowane rozwiązania dzięki manipulowaniu drzewami AST, manipulowaniu kodem źródłowym i innym technikom. Inspiracją dla tej receptury było podobne rozwiązanie z serwisu ActiveState (http://code. activestate.com/recipes/277940-decorator-for-bindingconstants-at-compile-time/), oparte na manipulowaniu kodem bajtowym Pythona. Praca na drzewach AST to podejście wyższego poziomu i może okazać się prostsze. Więcej informacji o kodzie bajtowym znajdziesz w następnej recepturze.
9.25. Dezasemblacja kodu bajtowego Pythona Problem Programista chce dokładnie wiedzieć, jak działa kod. W tym celu chce przeprowadzić jego dezasemblację do niskopoziomowego kodu bajtowego używanego przez interpreter.
9.25. Dezasemblacja kodu bajtowego Pythona
351
Rozwiązanie Aby otrzymać dowolną funkcję Pythona po dezasemblacji, można wykorzystać moduł dis. Oto przykład: >>> def countdown(n): ... while n > 0: ... print('T-minus', n) ... n -= 1 ... print('Blastoff!') ... >>> import dis >>> dis.dis(countdown) 2 0 SETUP_LOOP >> 3 LOAD_FAST 6 LOAD_CONST 9 COMPARE_OP 12 POP_JUMP_IF_FALSE
Omówienie Moduł dis może okazać się przydatny, gdy będziesz chciał sprawdzić, jak program działa na bardzo niskim poziomie (np. gdy próbujesz poznać pewne aspekty związane z wydajnością kodu). Nieprzetworzony kod bajtowy interpretowany przez funkcję dis() jest dostępny w następującej postaci: >>> countdown.__code__.co_code b"x'\x00|\x00\x00d\x01\x00k\x04\x00r)\x00t\x00\x00d\x02\x00|\x00\x00\x83 \x02\x00\x01|\x00\x00d\x03\x008}\x00\x00q\x03\x00Wt\x00\x00d\x04\x00\x83 \x01\x00\x01d\x00\x00S" >>>
Jeśli chcesz samodzielnie zinterpretować ten kod, musisz wykorzystać stałe zdefiniowane w module opcode: >>> c = countdown.__code__.co_code >>> import opcode >>> opcode.opname[c[0]]
Co dziwne, moduł dis nie udostępnia funkcji, która umożliwia łatwe przetwarzanie w programach kodu bajtowego. Jednak poniższa funkcja generatora przyjmuje nieprzetworzony kod bajtowy i przekształca go na kody operacji oraz argumenty: import opcode def generate_opcodes(codebytes): extended_arg = 0 i = 0 n = len(codebytes) while i < n: op = codebytes[i] i += 1 if op >= opcode.HAVE_ARGUMENT: oparg = codebytes[i] + codebytes[i+1]*256 + extended_arg extended_arg = 0 i += 2 if op == opcode.EXTENDED_ARG: extended_arg = oparg * 65536 continue else: oparg = None yield (op, oparg)
Nie wszyscy wiedzą, że można zastąpić nieprzetworzony kod bajtowy dowolnej funkcji. Wymaga to trochę pracy. Poniżej pokazano, jak uzyskać pożądany efekt:
9.25. Dezasemblacja kodu bajtowego Pythona
353
>>> def add(x, y): ... return x + y ... >>> c = add.__code__ >>> c ", line 1> >>> c.co_code b'|\x00\x00|\x01\x00\x17S' >>> >>> # Tworzenie całkowicie nowego obiektu kodu na podstawie spreparowanego kodu bajtowego >>> import types >>> newbytecode = b'xxxxxxx' >>> nc = types.CodeType(c.co_argcount, c.co_kwonlyargcount, ... c.co_nlocals, c.co_stacksize, c.co_flags, newbytecode, c.co_consts, ... c.co_names, c.co_varnames, c.co_filename, c.co_name, ... c.co_firstlineno, c.co_lnotab) >>> nc ", line 1> >>> add.__code__ = nc >>> add(2,3) Segmentation fault
Stosowanie tak szalonych sztuczek może sprawić, że interpreter przestanie działać. Jednak programiści pracujący nad zaawansowanymi narzędziami z obszaru optymalizacji i metaprogramowania mogą chcieć modyfikować kod bajtowy w sensowny sposób. W ostatniej części przykładu pokazano, jak to zrobić. Inny przykład działania podobnego kodu znajdziesz w serwisie ActiveState (http://code.activestate.com/recipes/277940-decorator-for-bindingconstants-at-compile-time/).
354
Rozdział 9. Metaprogramowanie
ROZDZIAŁ 10.
Moduły i pakiety
Moduły i pakiety są podstawą każdego dużego projektu — w tym samego Pythona. W tym rozdziale skoncentrowano się na popularnych technikach programowania związanych z modułami i pakietami. Dowiesz się tu, jak porządkować pakiety, dzielić duże moduły na kilka plików i tworzyć pakiety z przestrzenią nazw. Ponadto znajdziesz tu receptury, które pozwalają zmodyfikować działanie polecenia import.
10.1. Tworzenie hierarchicznych pakietów z modułami Problem Programista chce uporządkować kod, umieszczając go w pakiecie w postaci hierarchicznego zestawu modułów.
Rozwiązanie Tworzenie pakietów jest proste. Wystarczy uporządkować kod w pożądany sposób w systemie plików i upewnić się, że w każdym katalogu znajduje się plik __init__.py. Oto przykład: graphics/ __init__.py primitive/ __init__.py line.py fill.py text.py formats/ __init__.py png.py jpg.py
Następnie można stosować różne polecenia import: import graphics.primitive.line from graphics.primitive import line import graphics.formats.jpg as jpg
355
Omówienie Definiowanie hierarchii modułów jest bardzo proste. Wystarczy utworzyć strukturę katalogów w systemie plików. W plikach __init__.py można umieścić opcjonalny kod inicjujący, uruchamiany po napotkaniu różnych poziomów pakietu. Np. po natrafieniu na polecenie import graphics importowany jest plik graphics/__init__.py, który tworzy zawartość przestrzeni nazw graphics. Polecenia w postaci import graphics.formats.jpg powodują, że najpierw importowane są pliki graphics/__init__.py i graphics/formats/__init__.py, a dopiero potem graphics/formats/jpg.py. Zazwyczaj pliki __init__.py mogą pozostawać puste. Jednak w pewnych sytuacjach mogą zawierać kod. Za pomocą pliku __init__.py można np. automatycznie wczytywać moduły podrzędne: # graphics/formats/__init__.py from . import jpg from . import png
Wtedy wystarczy, że użytkownik zastosuje jedno polecenie import graphics.formats zamiast odrębnych poleceń dla modułów graphics.formats.jpg i graphics.formats.png. Pliki __init__.py często stosuje się też do łączenia definicji z wielu plików w jednej logicznej przestrzeni nazw (czasem robi się tak przy podziale modułów). Zagadnienie to opisano w recepturze 10.4. Czujni programiści zauważą, że Python 3.3 importuje pakiety także wtedy, gdy brak jest plików __init__.py. Jeśli nie zdefiniujesz takich plików, powstanie pakiet oparty na przestrzeni nazw, co opisano w recepturze 10.5. Jeżeli jednak dopiero zaczynasz tworzenie nowego pakietu, warto dodać pliki __init__.py.
10.2. Kontrolowanie importowania wszystkich symboli Problem Programista chce uzyskać pełną kontrolę nad tym, które symbole są eksportowane z modułu lub pakietu, gdy użytkownik używa polecenia from module import *.
Rozwiązanie Należy zdefiniować w module zmienną __all__ zawierającą listę eksportowanych nazw. Oto przykład: # somemodule.py def spam(): pass def grok(): pass blah = 42 # Eksportowanie tylko nazw 'spam' i 'grok' __all__ = ['spam', 'grok']
356
Rozdział 10. Moduły i pakiety
Omówienie Choć stosowanie polecenia from module import * jest niezalecane, programiści często używają je w modułach, w których zdefiniowana jest duża liczba nazw. Jeśli nic nie zrobisz, polecenie importu w tej postaci spowoduje wyeksportowanie wszystkich nazw, które nie zaczynają się od podkreślenia. Jeżeli zdefiniowana jest zmienna __all__, wyeksportowane zostaną tylko podane w niej nazwy. Jeśli zdefiniujesz zmienną __all__ jako pustą listę, żadne symbole nie zostaną wyeksportowane. Próba importu, gdy zmienna __all__ zawiera niezdefiniowane nazwy, powoduje zgłoszenie błędu AttributeError.
10.3. Importowanie modułów podrzędnych z pakietu za pomocą nazw względnych Problem Programista umieścił kod w pakiecie i chce zaimportować inny moduł podrzędny z tego pakietu bez podawania nazwy pakietu w poleceniu importu.
Rozwiązanie Aby zaimportować inne moduły z tego samego pakietu, należy zastosować względne polecenie importu. Załóżmy, że używasz pakietu mypackage uporządkowanego w systemie plików w następujący sposób: mypackage/ __init__.py A/ __init__.py spam.py grok.py B/ __init__.py bar.py
Jeśli moduł mypackage.A.spam ma importować moduł grok z tego samego katalogu, polecenie importu powinno wyglądać tak: # mypackage/A/spam.py from . import grok
Jeżeli ten sam moduł ma importować moduł B.bar zlokalizowany w innym katalogu, można zastosować polecenie importu w następującej postaci: # mypackage/A/spam.py from ..B import bar
Oba te polecenia działają względem lokalizacji pliku spam.py i nie wymagają podawania nazwy pakietu z najwyższego poziomu.
10.3. Importowanie modułów podrzędnych z pakietu za pomocą nazw względnych
357
Omówienie W pakietach w poleceniach importu dotyczących modułów z tego samego pakietu można stosować albo kompletne nazwy bezwzględne, albo nazwy względne podawane z użyciem przedstawionej składni. Oto przykład: # mypackage/A/spam.py from mypackage.A import grok from . import grok import grok
# OK # OK # Błąd (nie znaleziono)
Wadą stosowania nazw bezwzględnych, np. mypackage.A, jest zapisywanie na stałe w kodzie źródłowym nazwy pakietu najwyższego poziomu. To sprawia, że kod jest bardziej podatny na błędy i trudniej jest zmienić jego strukturę. Jeśli zechcesz zmienić nazwę pakietu, będziesz musiał otworzyć każdy plik i poprawić w nim kod źródłowy. Zapisane na stałe nazwy utrudniają też przenoszenie kodu. Możliwe, że użytkownik chce zainstalować dwie różne wersje pakietu, różniące się tylko nazwą. Jeśli importowanie odbywa się na podstawie nazw względnych, jest to możliwe. Podejście to jednak nie zadziała, jeśli stosowane są nazwy bezwzględne. Składnia . i .. w poleceniach import może wydawać się dziwna, pomyśl jednak o niej jak o sposobie określania nazwy katalogu. Znak . oznacza bieżący katalog, a sekwencja ..B powoduje przejście do katalogu ../B. Składnia ta działa tylko przy importowaniu za pomocą słowa from. Oto przykład: from . import grok import .grok
# Poprawnie # Błąd
Choć może się wydawać, że importowanie przy użyciu względnych nazw pozwala na poruszanie się po systemie plików, technika ta nie pozwala wyjść poza katalog, w którym zdefiniowano pakiet. Oznacza to, że kombinacje wzorców z kropkami prowadzące do innych katalogów powodują błąd. Warto też zauważyć, że importowanie na podstawie względnych nazw działa tylko w modułach umieszczonych w poprawnych pakietach. Metoda ta nie działa dla prostych modułów zlokalizowanych w skryptach na najwyższym poziomie hierarchii, a także we fragmentach pakietu uruchamianych bezpośrednio jako skrypty. Oto przykład: % python3 mypackage/A/spam.py
# Importowanie na podstawie względnej ścieżki się nie powiedzie
Jeśli jednak uruchomisz ten sam skrypt poprzedzony opcją –m Pythona, importowanie na podstawie względnych nazw będzie działać poprawnie: % python3 -m mypackage.A.spam
# Relative imports work
Więcej informacji na temat importowania pakietów na podstawie względnych nazw znajdziesz w dokumencie PEP 328 (http://www.python.org/dev/peps/pep-0328/).
10.4. Podział modułu na kilka plików Problem Programista ma moduł, który zamierza rozbić na kilka plików. Nie chce jednak, aby istniejący kod przestał działać. Dlatego zależy mu na tym, aby odrębne pliki tworzyły jeden moduł logiczny. 358
Rozdział 10. Moduły i pakiety
Rozwiązanie Moduł programu można podzielić na odrębne pliki, przekształcając go w pakiet. Przyjrzyj się poniższemu prostemu modułowi: # mymodule.py class A: def spam(self): print('A.spam') class B(A): def bar(self): print('B.bar')
Załóżmy, że chcesz podzielić moduł mymodule.py na dwa pliki — po jednym na każdą definicję klasy. W tym celu należy zastąpić plik mymodule.py katalogiem mymodule i utworzyć w tym katalogu następujące pliki: mymodule/ __init__.py a.py b.p
W pliku a.py powinien znaleźć się następujący kod: # a.py class A: def spam(self): print('A.spam')
W pliku b.py umieść poniższy kod: # b.py from .a import A class B(A): def bar(self): print('B.bar')
Plik __init__.py służy do połączenia obu pozostałych plików: # __init__.py from .a import A from .b import B
Jeśli wykonasz te operacje, uzyskany pakiet mymodule będzie wyglądał jak jeden logiczny moduł: >>> import mymodule >>> a = mymodule.A() >>> a.spam() A.spam >>> b = mymodule.B() >>> b.bar() B.bar >>>
10.4. Podział modułu na kilka plików
359
Omówienie W tej recepturze najważniejsza jest pewna kwestia projektowa — czy chcesz, aby użytkownicy korzystali z wielu małych modułów, czy z pojedynczego modułu. Gdy baza kodu jest duża, można podzielić ją na odrębne pliki. Użytkownicy muszą wtedy stosować wiele poleceń import: from mymodule.a import A from mymodule.b import B ...
To podejście działa, ale wymaga od użytkowników wiedzy o lokalizacji poszczególnych elementów. Często łatwiej jest ujednolicić kod i umożliwić stosowanie jednego polecenia importu: from mymodule import A, B
W tej sytuacji moduł mymodule jest traktowany jak jeden duży plik z kodem źródłowym. Jednak w tej recepturze pokazano, jak scalić różne pliki w jedną logiczną przestrzeń nazw. W tym celu trzeba utworzyć katalog pakietu i połączyć poszczególne części w pliku __init__.py. Przy podziale modułu trzeba zwrócić baczną uwagę na referencje między plikami. W tej recepturze klasa B potrzebuje dostępu do klasy A (jest to klasa bazowa dla B). Umożliwia to polecenie importu .a import A określone względem pakietu. Polecenia importu podawane względem pakietu pozwalają w tej recepturze uniknąć zapisywania na stałe w kodzie źródłowym nazwy modułu najwyższego poziomu. Ułatwia to późniejszą zmianę nazwy modułu lub przeniesienie kodu w inne miejsce (zobacz recepturę 10.3). Rozwinięciem tej receptury jest zastosowanie „leniwego” importowania. W pierwotnej wersji plik __init__.py pobiera wszystkie potrzebne podkomponenty w jednym kroku. Jeśli jednak moduł jest bardzo duży, można wczytywać komponenty wtedy, gdy są potrzebne. Oto nowa wersja pliku __init__.py, działająca właśnie w ten sposób: # __init__.py def A(): from .a import A return A() def B(): from .b import B return B()
W tej wersji klasy A i B zastąpiono funkcjami, które wczytują potrzebne klasy przy pierwszym dostępie do nich. Z perspektywy użytkownika kod wygląda tak samo: >>> import mymodule >>> a = mymodule.A() >>> a.spam() A.spam >>>
Główną wadą „leniwego” wczytywania jest to, że mechanizmy dziedziczenia i sprawdzania typów mogą działać nieprawidłowo. Możliwe, że konieczne okaże się wprowadzenie drobnych zmian w kodzie: if isinstance(x, mymodule.A): ...
# Błąd
if isinstance(x, mymodule.a.A): ...
# Poprawnie
Aby zapoznać się z praktycznym przykładem „leniwego” wczytywania, przyjrzyj się kodowi źródłowemu z pliku multiprocessing/__init__.py z biblioteki standardowej. 360
Rozdział 10. Moduły i pakiety
10.5. Tworzenie odrębnych katalogów z importowanym kodem z jednej przestrzeni nazw Problem Programista ma dużą bazę kodu. Konserwacją i udostępnianiem poszczególnych fragmentów zajmują się różne osoby. Każda część przypomina pakiet — ma postać katalogu z plikami. Jednak zamiast instalować każdy element jako pakiet o odmiennej nazwie, programista chce, aby wszystkie części miały wspólną nazwę pakietu pełniącą funkcję przedrostka.
Rozwiązanie Problem polega tu na tym, że programista chce zdefiniować pythonowy pakiet najwyższego poziomu pełniący funkcję przestrzeni nazw dla dużej grupy odrębnie zarządzanych podpakietów. Problem ten często występuje przy pracy nad dużymi platformami do rozwijania aplikacji, gdy twórcy platformy chcą zachęcić użytkowników do udostępniania wtyczek lub dodatków. Aby przypisać wspólną przestrzeń nazw do odrębnych katalogów, wystarczy uporządkować kod tak jak w normalnym pakiecie Pythona, ale pominąć pliki __init__.py w katalogach ze złączanymi komponentami. Załóżmy, że istnieją dwa odrębne katalogi z kodem Pythona: foo-package/ spam/ blah.py bar-package/ spam/ grok.py
W tych katalogach wspólną przestrzenią nazw jest spam. Zauważ, że w żadnym z katalogów nie ma pliku __init__.py. Zobacz teraz, co się stanie po dodaniu pakietów foo-package i bar-package do ścieżki modułów Pythona oraz wywołaniu poleceń importu: >>> >>> >>> >>> >>>
Widać, że w „magiczny” sposób katalogi obu różnych pakietów są łączone ze sobą, dzięki czemu można zaimportować pakiety spam.blah i spam.grok. To działa!
Omówienie Wykorzystany tu mechanizm to pakiet oparty na przestrzeni nazw. Jest to specjalny rodzaj pakietu przeznaczony do scalania różnych katalogów z kodem przy użyciu wspólnej przestrzeni nazw (tak jak w rozwiązaniu). Jest to przydatne w dużych platformach, ponieważ pozwala podzielić je na odrębnie instalowane elementy. Dzięki temu użytkownicy mogą też łatwo tworzyć niezależne dodatki i inne rozszerzenia platform.
10.5. Tworzenie odrębnych katalogów z importowanym kodem z jednej przestrzeni nazw
361
Najważniejsze przy tworzeniu pakietów opartych na przestrzeni nazw jest to, aby nie umieszczać plików __init__.py w katalogu najwyższego poziomu, który ma pełnić funkcję wspólnej przestrzeni nazw. Pominięcie takiego pliku ma ciekawe skutki w kontekście importowania pakietu. Interpreter nie zgłasza wtedy błędu, a zaczyna tworzyć listę wszystkich katalogów o pasującej nazwie pakietu. Następnie tworzy specjalny moduł pakietu opartego na przestrzeni nazw i zapisuje w zmiennej __path__ kopię ze wspomnianą listą katalogów (zmienna ta jest przeznaczona tylko do odczytu). Oto przykład: >>> import spam >>> spam.__path__ _NamespacePath(['foo-package/spam', 'bar-package/spam']) >>>
Katalogi ze zmiennej __path__ są używane przy lokalizowaniu innych podkomponentów pakietu (np. przy importowaniu podkomponentów spam.grok lub spam.blah). Ważną cechą pakietów opartych na przestrzeni nazw jest to, że każdy może wzbogacić przestrzeń nazw o własny kod. Załóżmy, że utworzyłeś własny katalog z kodem: my-package/ spam/ custom.py
Jeśli dodałeś ten katalog wraz z innymi pakietami do zmiennej sys.path, zostanie on scalony z innymi katalogami z pakietem spam: >>> import spam.custom >>> import spam.grok >>> import spam.blah >>>
W ramach debugowania podstawowym sposobem na ustalenie, czy pakiet jest oparty na przestrzeni nazw, jest sprawdzenie atrybutu __file__. Jeśli ten atrybut nie istnieje, pakiet jest oparty na przestrzeni nazw. Wskazuje na to także słowo namespace w łańcuchowej reprezentacji pakietu: >>> spam.__file__ Traceback (most recent call last): File "", line 1, in AttributeError: 'module' object has no attribute '__file__' >>> spam >>>
Więcej informacji na temat pakietów opartych na przestrzeni nazw znajdziesz w dokumencie PEP 420 (http://www.python.org/dev/peps/pep-0420/).
10.6. Ponowne wczytywanie modułów Problem Programista chce ponownie wczytać załadowany już moduł, ponieważ zmodyfikował jego kod źródłowy.
Rozwiązanie Aby ponownie wczytać załadowany już moduł, należy zastosować funkcję imp.reload(). Oto przykład: 362
Omówienie Ponowne wczytywanie modułu to operacja, która często przydaje się w trakcie debugowania i rozwijania kodu. Jednak w kodzie produkcyjnym jest ona niebezpieczna, ponieważ nie zawsze działa w oczekiwany sposób. Na zapleczu funkcja reload() usuwa zawartość słownika modułu i odświeża go w wyniku ponownego wykonania kodu źródłowego modułu. Tożsamość obiektu modułu się nie zmienia. Operacja ta powoduje aktualizację modułu we wszystkich miejscach, w których zaimportowano go do programu. Funkcja reload() nie aktualizuje jednak definicji zaimportowanych za pomocą poleceń w postaci from moduł import nazwa. Przyjrzyj się następującemu kodowi: # spam.py def bar(): print('bar') def grok(): print('grok')
Teraz rozpocznij interaktywną sesję: >>> import spam >>> from spam import grok >>> spam.bar() bar >>> grok() grok >>>
Bez wychodzenia z Pythona zmodyfikuj kod źródłowy w pliku spam.py, tak aby funkcja grok() wyglądała w następujący sposób: def grok(): print('Nowa funkcja grok')
Wróć do interaktywnej sesji, ponownie wczytaj moduł i przeprowadź eksperyment: >>> import imp >>> imp.reload(spam) >>> spam.bar() bar >>> grok() # Zwróć uwagę na dane wyjściowe z dawnej wersji grok >>> spam.grok() # Zwróć uwagę na dane wyjściowe z nowej wersji Nowa funkcja grok >>>