Yang Zhou
Yang ZhouSoftware Engineer

Co musisz wiedzieć, zanim użyjesz funkcji zip w Pythonie

Poznaj etapy, przez które musisz przejść, zanim na poważnie zaczniesz korzystać z funkcji zip w Pythonie.
11.02.20215 min
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: 

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.

<p>Loading...</p>