Co musisz wiedzieć, zanim użyjesz funkcji zip w Pythonie
Python posiada szereg wbudowanych funkcji, które czynią nasz kod bardziej eleganckim. Jedną z nich jest funkcja zip
. Trzeba jednak uważać - jej użycie może być nieintuicyjne dla początkujących, co zwiększa prawdopodobieństwo błędów.
Weźmy na przykład macierz 2*3 przedstawiony jako zagnieżdżona lista:
matrix = [[1, 2, 3], [1, 2, 3]]
A oto pytanie, dotyczące Pythona, które często pada na rozmowach technicznych:
Jak transponować taką macierz?
Junior napisze pewnie pętle for
, a senior będzie potrzebował do tego jednej linijki kodu:
matrix_T = [list(i) for i in zip(*matrix)]
Wygląda elegancko, prawda?
Nie martw się, jeśli powyższy kod jest dla Ciebie niezrozumiały. Postaram się pewne rzeczy wyjaśnić, przedstawiając przypadki użycia i sztuczki, które można wykonać przy pomocy funkcji zip
. Nasze rozważania podzielimy na 7 etapów. Myślę, że nawet jeśli wiesz, o co w powyższym kodzie chodzi, to i tak znajdziesz tutaj coś dla siebie.
Etap 0: podstawowe użycie funkcji zip
Funkcja zip łączy ze sobą elementy z różnych obiektów iterowalnych, takich jak listy, krotki, zbiory, i zwraca nam iterator. Możemy jej użyć to połączenia ze sobą dwóch list:
id = [1, 2, 3, 4]
leaders = ['Elon Mask', 'Tim Cook', 'Bill Gates', 'Yang Zhou']
record = zip(id, leaders)
print(record)
# <zip object at 0x7f266a707d80>
print(list(record))
# [(1, 'Elon Mask'), (2, 'Tim Cook'), (3, 'Bill Gates'), (4, 'Yang Zhou')]
Jak widać powyżej, funkcja zip
zwraca iterator z krotkami, gdzie n-ta krotka zawiera n-ty element z każdej z list. Działa jak prawdziwy suwak/zamek błyskawiczny, prawda?
Etap 1: łączenie ze sobą większej lub mniejszej ilości obiektów iterowalnych na raz
Tak naprawdę to funkcja zip
ma w Pythonie o wiele większe możliwości od normalnego suwaka - może ona działać z dowolną liczbą obiektów iterowalnych, a nie tylko z dwoma. Jeśli przekażemy do funkcji zip
listę:
id = [1, 2, 3, 4]
record = zip(id)
print(list(record))
# [(1,), (2,), (3,), (4,)]
Albo trzy listy:
id = [1, 2, 3, 4]
leaders = ['Elon Mask', 'Tim Cook', 'Bill Gates', 'Yang Zhou']
sex = ['male', 'male', 'male', 'male']
record = zip(id, leaders, sex)
print(list(record))
# [(1, 'Elon Mask', 'male'), (2, 'Tim Cook', 'male'), (3, 'Bill Gates', 'male'), (4, 'Yang Zhou', 'male')]
To, tak czy siak, wszystko będzie dobrze działało. Tak na marginesie, jeśli nie mamy żadnego argumentu, to funkcja zip
zwróci pusty iterator.
Etap 2: radzenie sobie z nierówną długością argumentów
Prawdziwe dane nie zawsze są czyste i pełne - czasami musimy uporać się z nierówną długością obiektów iterowalnych. Wynik funkcji zip
jest domyślnie oparty na najkrótszym z obiektów iterowalnych.
id = [1, 2]
leaders = ['Elon Mask', 'Tim Cook', 'Bill Gates', 'Yang Zhou']
record = zip(id, leaders)
print(list(record))
# [(1, 'Elon Mask'), (2, 'Tim Cook')]
Jak widać powyżej, najkrótsza lista to id
, a więc record
zawiera jedynie dwie krotki, a dwa ostatnie elementy w liście leaders
zostały pominięte.
Co możemy z tym zrobić?
Możemy tutaj skorzystać z funkcji znajdującej się w module itertools o nazwie zip_longest
. Jak sama nazwa wskazuje, funkcja ta jest związana z zip
, a jej wynik zależy od najdłuższego argumentu.
Wykorzystajmy zip_longest
do wygenerowania listy record:
from itertools import zip_longest
id = [1, 2]
leaders = ['Elon Mask', 'Tim Cook', 'Bill Gates', 'Yang Zhou']
long_record = zip_longest(id, leaders)
print(list(long_record))
# [(1, 'Elon Mask'), (2, 'Tim Cook'), (None, 'Bill Gates'), (None, 'Yang Zhou')]
long_record_2 = zip_longest(id, leaders, fillvalue='Top')
print(list(long_record_2))
# [(1, 'Elon Mask'), (2, 'Tim Cook'), ('Top', 'Bill Gates'), ('Top', 'Yang Zhou')]
Jak widać powyżej, wynik zip_longest
rzeczywiście opiera się na najdłuższym argumencie. Argument fillvalue
, którego domyślną wartością jest None
, pomoże nam wypełnić brakujące wartości.
Etap 3: poznanie operacji rozpakowującej
A co by było, gdybyśmy otrzymali listę record z poprzedniego przykładu i chcielibyśmy rozpakować ją do osobnych list? Niestety Python nie ma przeznaczonej do tego funkcji. Chociaż, jeśli znamy specjalne użycia *, to rozpakowywanie stanie się niezwykle proste.
record = [(1, 'Elon Mask'), (2, 'Tim Cook'), (3, 'Bill Gates'), (4, 'Yang Zhou')]
id, leaders = zip(*record)
print(id)
# (1, 2, 3, 4)
print(leaders)
# ('Elon Mask', 'Tim Cook', 'Bill Gates', 'Yang Zhou')
Powyżej widzimy, że asterysk wykonał operację rozpakowującą, czyli wyodrębnił wszystkie 4 krotki z listy record
. Jeśli nie użyjemy tej techniki, to następująca metoda będzie identyczna:
record = [(1, 'Elon Mask'), (2, 'Tim Cook'), (3, 'Bill Gates'), (4, 'Yang Zhou')]
print(*record) # unpack the list by one asterisk
# (1, 'Elon Mask') (2, 'Tim Cook') (3, 'Bill Gates') (4, 'Yang Zhou')
id, leaders = zip((1, 'Elon Mask'), (2, 'Tim Cook'), (3, 'Bill Gates'), (4, 'Yang Zhou'))
print(id)
# (1, 2, 3, 4)
print(leaders)
# ('Elon Mask', 'Tim Cook', 'Bill Gates', 'Yang Zhou')
Etap 4: tworzenie i aktualizacja słowników przy pomocy funkcji zip
Dzięki funkcji zip, tworzenie i aktualizowanie dict
opartego na oddzielnych listach jest dosyć proste. Mamy tutaj dwa jednolinijkowe rozwiązania:
- Używanie wyrażeń słownikowych razem z
zip
- Używanie funkcji
dict
razem zzip
id = [1, 2, 3, 4]
leaders = ['Elon Mask', 'Tim Cook', 'Bill Gates', 'Yang Zhou']
# create dict by dict comprehension
leader_dict = {i: name for i, name in zip(id, leaders)}
print(leader_dict)
# {1: 'Elon Mask', 2: 'Tim Cook', 3: 'Bill Gates', 4: 'Yang Zhou'}
# create dict by dict function
leader_dict_2 = dict(zip(id, leaders))
print(leader_dict_2)
# {1: 'Elon Mask', 2: 'Tim Cook', 3: 'Bill Gates', 4: 'Yang Zhou'}
# update
other_id = [5, 6]
other_leaders = ['Larry Page', 'Sergey Brin']
leader_dict.update(zip(other_id, other_leaders))
print(leader_dict)
# {1: 'Elon Mask', 2: 'Tim Cook', 3: 'Bill Gates', 4: 'Yang Zhou', 5: 'Larry Page', 6: 'Sergey Brin'}
W powyższym przykładzie w ogóle nie ma pętli for
. To bardzo eleganckie i pythonowe
Etap 5: korzystanie z funkcji zip w pętlach for
Często zdarza się, że używamy wielu obiektów iterowalnych na raz. Funkcja zip
ma tutaj spore pole do popisu, jeśli będziemy jej używać z pętlami for
.
Sprawdźmy, jak to wygląda:
products = ["cherry", "strawberry", "banana"]
price = [2.5, 3, 5]
cost = [1, 1.5, 2]
for prod, p, c in zip(products, price, cost):
print(f'The profit of a box of {prod} is £{p-c}!')
# The profit of a box of cherry is £1.5!
# The profit of a box of strawberry is £1.5!
# The profit of a box of banana is £3!
Czy istnieje bardziej elegancki sposób na zaimplementowanie powyższego przykładu?
Etap 6: macierz transponowana
Wróćmy teraz do pytania technicznego, o którym wspomniałem na początku:
Jak otrzymać macierz transponowaną z macierzy 2*3?
Ponieważ znamy już m.in. rozpakowywanie przy pomocy znaku *, czy wyrażenia listowe, to jednolinijkowe rozwiązanie wydaje się tutaj bardzo intuicyjne.
matrix = [[1, 2, 3], [1, 2, 3]]
matrix_T = [list(i) for i in zip(*matrix)]
print(matrix_T)
# [[1, 1], [2, 2], [3, 3]]
Podsumowanie
Funkcja zip
to w Pythonie niezwykle pożyteczne narzędzie. Używanie jej w prawidłowy sposób pomoże nam w pisaniu mniejszej ilości kodu i wykonywaniu większej ilości operacji. A dla Pythona mniej znaczy więcej.
Dziękuję za uwagę!
Oryginał tekstu w języku angielskim możesz przeczytać tutaj.