Krok Do Pythonicznego Kodu – Comprehension
- Jarosław Piszczała
- 14 May, 2020
- 14 May, 2020
W społeczności Pythonowej istnieje takie określenie kodu, że jest on Pythoniczny
. Oznacza to, że wykorzystuje on możliwości i cechy charakterystyczne tego języka. Jednym z takich elementów są tak zwane złożenia (ang. comprehension), nazywane też wyrażeniami (np. wyrażenia listowe ang. list comprehension). Ich rodzaje oraz sposoby użycia powinien znać każdy fan Pythona!
Comprehension
Aby zrozumieć, jak wygląda implementacja złożeń, oraz jak zastąpić zwykłą pętlę tą konstrukcją, przejdźmy do przykładu. Stworzymy trzy funkcje, które w tym pomogą. Pierwsza to map_func
, której zadaniem mogłoby być przekształcenie naszych danych w pewien sposób. Kolejna to filter_func
, której zadaniem byłoby określenie, czy dana zmienna powinna się znaleźć w nowej kolekcji, czy nie. Funkcja ta zwraca bool
, ponieważ oczekujemy konkretnej odpowiedzi: True
albo False
. Ostatnia funkcja to iterable
, która zwraca nam obiekt iterowalny - po jego elementach będzie tworzyć pętlę.
Gdy mamy już zdefiniowane takie elementy, to możemy podstawić je pod standardową konstrukcję pythonową. Nie korzystając ze złożeń, musielibyśmy najpierw utworzyć pustą kolekcję, a następnie poprzez pętle sprawdzać kolejne elementy i dodawać tylko te, które są poprawne (z naszego punktu widzenia).
Złożenia Pythonowe bardzo uproszczą nam ten kod. Cała logika mieści się nam w jednej linii kodu. Zamykamy ją w odpowiednich nawiasach (w zależności od typu złożenia). Wynik operacji będzie automatycznie dodawany do listy. Poniższe to przykład list comprehension
.
Aby dokładniej przyjrzeć się, jak zmieniła się nasza logika, przeformatujmy nasze złożenie. Po pierwsze nie definiujemy już pustej kolekcji. Po drugie zmieniła się trochę kolejność czytania kodu. Poprzednie rozwiązanie mogliśmy czytać jako:
Dla każdego obiektu w obiekcie iterowalnym, jeżeli spełnia on pewne założenie, to przekonwertuj go i dodaj do kolekcji.
Nowe rozwiązanie będziemy czytać jako:
Przekonwertuj i dodaj do nowej kolekcji każdy obiekt w obiekcie iterowalnym, jeżeli spełnia on pewne założenia.
Dlaczego korzystamy w takim razie z tej konstrukcji w naszym kodzie? Jest to czytelna i szybka metoda przekształcania, filtrowania i tworzenia nowych kolekcji, na podstawie już istniejących.
Aby nie było wątpliwości co do powyższych przykładów. Gdyby nie chęć pokazania przykładu z opisami poszczególnych elementów, nasze złożenie wyglądałoby w tym przypadku następująco:
Przypadek użycia
Aby uatrakcyjnić nasze przykłady, skupmy się na jednym przypadku użycia. Użytkownik na wejściu do naszej aplikacji przekazuje string
liczb oddzielonych białym znakiem. Naszym celem jest odpowiednio przekształcić to wejście w pewną kolekcję. Aby kod był czytelniejszy, utworzymy zmienną input_iter
, która będzie kolekcją kolejnych liczb przekazanych przez użytkownika, wciąż w formacie str
.
List comprehension
Najprostszym i najczęściej spotykanym przykładem jest list comprehension
. Poniższy przykład pokazuje, w jaki sposób przekształcić naszą kolekcję liczb w formacie str
do int
. Jest to dość prosty zabieg, więc widzimy zdecydowanie, dlaczego developerzy tak bardzo lubią tę konsturkcję :)
Dict comprehension
Gdy potrzebujemy przekształcić kolekcję na słownik, z pomocą przychodzi dict comprehension
. Nasz nowy przykład pokazuje mapowanie liczb jako kluczy do wartości, którą jest bool
definiujący czy przed liczbą stoi znak. Jak widać, różne typy złożeń definiujemy poprzez zmianę nawiasów.
Set comprehension
W sytuacji, gdy zachodzi potrzeba utworzenia kolekcji unikalnych elementów, przydać się może set comprehension
. Dzięki niemu dość szybko stworzymy kolekcje unikalnych liczb podanych przez użytkownika.
If
Złożenia oferują nam także możliwość filtrowania danych w kolekcjach. Aby to zrobić, w list comprehension
należy na końcu po pętli dodać if
z konkretnym predykatem. Na nasze potrzeby chcemy utworzyć listę wyłącznie dodatnich liczb.
If else
Nie zawsze poprzez if
pragniemy odfiltrować dane. Zdarza się, że w różnych przypadkach chcemy otrzymać inną wartość. Wtedy możemy skorzystać z if else
w formie tzw. conditional expression. W tym przypadku każdą ujemną wartość będziemy przekształcać na dodatnią.
For x,y
Tak jak w prostych pętlach, tak i tutaj możemy iterować po kilku elementach poprzez rozpakowanie tupli. W ten sposób możemy, chociażby iterować po kluczach i wartościach słowników, lub odpowiednio do naszych potrzeb rozpakowywać dane.
Nested
Złożenia nie są ograniczone. Dzięki temu możemy pozwolić także na zagnieżdżone pętle w złożeniach! Poniższy przykład utworzy kolekcje unikalnych cyfr użytych przez użytkownika!
Generator expression
Jak wiemy, nie zawsze tworzenie kolekcji ma sens. Gdy chcemy przetworzyć dane, aby je zaraz wykorzystać, dużo lepszym rozwiązaniem jest utworzenie generatora. Jednak tworzenie zwykłego generatora oznacza tworzenie funkcji, która poprzez yield
zwracać będzie kolejne wartości.
W tym wypadku możemy taką konstrukcję zastąpić poprzez tzw. generator expression które niczym nie różni się od możliwości, które zaprezentowane zostały powyżej przy innych złożeniach. Taka konstrukcja wymaga zamknięcia naszego list comprehension
nawiasami okrągłymi, jak przy definicji tuple
.
Jeżeli powyższe rozwiązanie nie do końca Ci się nie podoba, to bardzo dobrze! Ponieważ gen expr
można wstawiać bezpośrednio jako argument, pod tym względem jego zachowanie można porównać z funkcją anonimową lambda
.
Jest pewien haczyk. Generator pozwoli nam przejść po danych wyłącznie jeden raz. Miejmy to na uwadze, ponieważ jest to ważny wyznacznik tego, czy warto z niego skorzystać w danej sytuacji.
Nested comprehension
Jeżeli powyższe przykłady nie rozwiązują do końca problemu, to może pewna wariacja to zrobi. Pomyślmy, że potrzebujemy zrobić pewną operację na danych, a następnie te dane przetworzyć. Możemy do tego skorzystać z zagnieżdżenia się jednego złożenia w drugim. W poniższym przykładzie chcemy przechowywać tuple
sąsiadów liczb przekazanych przez użytkownika.
To przekształcanie potrójne tej samej wartości odbiera trochę sens, i nasz kod wydaje się nie być wcale lepszy. Jednak jest na to rada, i jest nią zagnieżdżenie. Zagnieżdżmy gen expr
, które przekształcać będzie nasze liczby w formacie str
na int
przed pobraniem kolejnej wartości.
I teraz spójrzmy, jak byśmy zrobili tę samą konstrukcję, nie korzystając ze złożeń:
Albo bez użycia generatora tak:
Jak pokazują przykłady, poznana tutaj konstrukcja daje dużą elastyczność przy pisaniu kodu. W dodatku zdecydowanie upraszcza kod, który musielibyśmy napisać w to miejsce.
Use cases
Skoro już wiemy, jak to wszystko działa, spróbujmy z tego skorzystać w innym przypadku. Dużo ciekawiej jest, gdy operujemy na słownikach i potrzebujemy wyciągnąć na ich podstawie pewne informacje. Do przykładów skorzystamy z prostego słownika, który zawiera kilku użytkowników:
W pierwszej kolejności chcemy sprawdzić, czy na naszej liście użytkowników jest jakiś aktywny administrator. Robimy to poprzez set comprehension
i sprawdzamy, czy w kolekcji, którą otrzymaliśmy, jest wartość True
.
Skoro już wiemy, że wśród użytkowników istnieje jakiś administrator, stwórzmy ich listę. Nowa lista będzie zawierać tylko tych użytkowników, których wartość spod klucza is_admin
jest równa True
.
Co jeżeli potrzebujemy obojętnie jakiego administratora? Zamiast tworzyć ich listę, tworzymy prosty generator, który zwróci nam pierwszego administratora. Dzięki temu otrzymamy do niego dostęp szybciej, niż w przypadku gdybyśmy tworzyli najpierw całą listę, aby następnie chcieli pobrać tylko jej pierwszy element.
Może potrzebujemy adresy email wszystkich użytkowników, aby przesłać im wiadomości? Wystarczy dla każdego użytkownika wyciągnąć wartość spod odpowiedniego klucza
Timing
Skoro już wiemy, z czym mamy do czynienia, i jak z tego korzystać - dowiedzmy się czy komputer też wolałby comprehension
. Użyjemy do tego biblioteki timeit
. Użyjemy do tego celu prostej pętli z predykatem.
Wynik to ostateczny powód, dlaczego warto się tej konstrukcji nauczyć. W tym wypadku jest ona szybsza od standardowej funkcji aż o 33%!
Kiedy Nie Stosować?
Złożeń nie należy stosować w kilku sytuacjach:
- Gdy robimy pętle po obiektach, które nas nie interesują. Może to być pętla, która wysyłać będzie wiadomości z listy, a metoda do wysyłania będzie zwracała None. Stosujemy to tylko wtedy gdy chcemy utworzyć nową kolekcję. Więc prostą zasadą, jeżeli ktoś zrobi taką konstrukcję, ale nie przypisze jej do żadnej wartości, to należy ją przepisać na prostą pętlę.
- Gdy nasza aktualna implementacja jest skomplikowana, a zapisanie jej poprzez złożenie tylko utrudni zrozumienie kodu. Jest to fajne wyzwanie na napisanie jak najmniejszej ilości linii kodu, ale zdecydowanie antywzorzec w kodzie produkcyjnym. Pamiętajmy, że w Pythonie stawiamy na czytelność. Przykład takiego zakazanego kodu poniżej.
Podsumowanie
I to (chyba?) tyle. Przyjrzeliśmy się dokładnie wszystkim formą złożeń, jakie spotkać możemy w Pythonie, przyjrzeliśmy się pewnym przypadkom użycia, a na koniec udowodniliśmy, że jest to rozwiązanie szybsze!
Najlepszą praktyką, jest stosowanie złożeń wszędzie tam, gdzie potrzebujemy utworzyć kolekcje, lub gdy na podstawie jednej kolekcji chcemy utworzyć nową. Gdy chcemy przetworzyć jakieś argumenty lub je odfiltrować. Nie powinniśmy natomiast złożeń stosować także wtedy, gdy nasze rozwiązanie straci przez to na czytelności. Pamiętajmy też o generatorach, gdy kolekcja, którą tworzymy, zostanie wykorzystana tylko raz.
W ramach nauki polecam także wykorzystać plugin do flake8
, który zwróci uwagę dodatkowo na złożenia w kodzie, jest to flake8-comprehensions.