Każdy kto programuje w Pythonie z pewnością szybko przekonał się do zalet tzw. list comprehensions. Ten sposób tworzenia list jest nie tylko zgodny z filozofią języka, ale przede wszystkim zgrabny, krótki i czytelny. Czasami jednak zdarza się spotkać konstrukcje, które na pierwszy rzut oka wcale takie czytelne nie są. Przykładem niech będzie fragment, na który natknąłem się podczas jednego z code review:
1 |
data = [j for i in accounts for v in i.values() for j in v] |
Czy jego działanie będzie jest oczywiste, a czytelnik w mgnieniu oka potrafi wyobrazić sobie strukturę, która jest tu przetwarzana? Biorąc pod uwagę kiepskie nazewnictwo zmiennych (jeśli mamy do czynienia z kodem, który operuje na konkretnych danych biznesowych, a nie na uogólnionych strukturach, to używanie nazw typu j
czy v
zupełnie mnie nie przekonuje), pewnie nie za bardzo. Pierwotna lista wyglądała mniej więcej tak:
1 2 3 4 |
accounts = [ {"firstAccount": ["DATA#1", "DATA#2"]}, {"secondAccount": ["ACCOUNT_DATA#1", "ACCOUNT_DATA#2"]} ] |
Założę się, że część osób, które nie miały jeszcze do czynienia z zagnieżdżonymi konstrukcjami tego typu, chcąc napisać list comprehension, spłaszczające ją do postaci listy, zawierającej tylko łańcuchy znakowe DATA#1
, DATA#2
itd., zrobi to w następujący sposób:
1 |
data = [entry for entry in account_data for account_data in account_entry.values() for account_entry in accounts] |
Jak widać, kod został tu skonstruowany zaczynając od najbardziej zagnieżdżonego fragmentu i podążając ku strukturom zewnętrznym. W myśl tej logiki account_data
będzie równe np. ["DATA#1", "DATA#2"]
, zaś account_entry
będzie całym słownikiem. Tymczasem podejście takie jest błędne, a prawidłowym list comprehension jest w tym wypadku:
1 |
data = [entry for account_entry in accounts for account_data in account_entry.values() for entry in account_data] |
Zrozumienie logiki takiej konstrukcji jest banalnie proste, jeśli uświadomimy sobie jak zagnieżdżone list comprehensions mają się do zwykłej pętli for
. Dla naszego przypadku musielibyśmy stworzyć taką pętlę:
1 2 3 4 |
for account_entry in accounts: for account_data in account_entry.values(): for entry in account_data: yield entry |
Teraz wystarczy tylko przerzucić ostatnie entry
na początek, pominąć yield
, które nie jest nam już potrzebne, a pozostałą część kodu złączyć w jedną linijkę, usuwając przy tym dwukropki. I voila – mamy poprawne potrójne zagnieżdżone list comprehension!
Gdybyśmy chcieli dorzucić gdzieś instrukcję warunkową, to zasady się nie zmieniają i zamiana zagnieżdżonych pętli w list comprehension wciąż pozostaje łatwa i przyjemna, np.:
1 2 3 4 5 6 7 |
ACCOUNT_NAME = "firstAccount" for account_entry in accounts: if ACCOUNT_NAME in account_entry: for account_data in account_entry.values(): for entry in account_data: yield entry |
zamienimy bez problemu na:
1 |
data = [entry for account_entry in accounts if ACCOUNT_NAME in account_entry for account_data in account_entry.values() for entry in account_data] |
Biorąc zaś pod uwagę długość linii, to pewnie bardziej adekwatne będzie przeformatowanie na:
1 2 3 4 5 6 7 |
data = [ entry for account_entry in accounts if ACCOUNT_NAME in account_entry for account_data in account_entry.values() for entry in account_data ] |
Na sam koniec pozostaje tylko pytanie czy tworzenie tak zanieżdżonych list comprehensions ma w ogóle sens, skoro czytelność tego rozwiązania nie jest wcale oczywista. Czy w Waszych projektach przeszedłby taki kod, czy może poległ już na code review? Jakimi zasadami kierujecie się przy rozwiązywaniu podobnych problemów?