Exploration de Python : zip et unzip

Christophe Vaudry
norsys-octogone
Published in
9 min readApr 24, 2024

Contexte

Python est un langage de programmation très abordable, que l’on démarre en programmation ou que l’on connaisse déjà d’autres langages. Néanmoins, ce n’est pas parce qu’un langage est abordable qu’il n’a pas ses propres idiomes et qu’il n’y a pas des trucs & astuces à connaître et à retenir.

Ce billet explore l’utilisation de la fonction zip en Python.

Les exemples de ce billet ont été testés avec la version 3.12 de Python.

Just zip it

L’image animée ci-après sur le fonctionnement d’une fermeture éclair illustre parfaitement l’essence de ce que fait la fonction zip. Néanmoins nous allons élaborer un peu plus.

La fonction zip permet de transformer un séquence de listes (ou de tuples) en une liste de tuples. Cela sera plus clair avec un premier exemple. Nous avons la liste des nucléotides de l'ADN (réduit à la lettre qui les représente) et dans le même ordre la liste des nucléotides de l'ARN leur correspondant. Je veux les regrouper en une liste de couple. Nous allons utiliser zip à cette fin.

>>> dna_nucleotides = ['G', 'C', 'T', 'A']
>>> rna_nucleotides = ['C', 'G', 'A', 'U']
>>> for pair in zip(dna_nucleotides, rna_nucleotides):
... print("Pair de nucléotides ADN - ARN :", pair)
...
Pair de nucléotides ADN - ARN : ('G', 'C')
Pair de nucléotides ADN - ARN : ('C', 'G')
Pair de nucléotides ADN - ARN : ('T', 'A')
Pair de nucléotides ADN - ARN : ('A', 'U')

Dans les faits zip retourne un itérateur comme on peut le voir ci-dessous à la ligne Résultat de l'application de zip sur les 2 listes : <zip object at 0x00000263A55EC340>.

>>> dna_nucleotides = ['G', 'C', 'T', 'A']
>>> rna_nucleotides = ['C', 'G', 'A', 'U']
>>> print("La liste des nucléotides de l'ADN :\n",dna_nucleotides)
La liste des nucléotides de l'ADN :
['G', 'C', 'T', 'A']
>>> print("La liste des nucléotides de l'ARN :\n", rna_nucleotides)
La liste des nucléotides de l'ARN :
['C', 'G', 'A', 'U']
>>> print("Résultat de l'application de zip sur les 2 listes :\n",zip(dna_nucleotides, rna_nucleotides))
Résultat de l'application de zip sur les 2 listes :
<zip object at 0x00000263A55EC340>

On peut donc réaliser un unpacking des valeurs de l'itérateur avec l'opérateur splat *, le convertir en liste ou en tuple ou le parcourir avec une boucle for (comme on l’a fait dans l’exemple initial) ou une compréhension.

>>> dna_nucleotides = ['G', 'C', 'T', 'A']
>>> rna_nucleotides = ['C', 'G', 'A', 'U']
>>> print("Résultat de l'application de zip sur les 2 listes après unpacking :\n",*zip(dna_nucleotides, rna_nucleotides))
Résultat de l'application de zip sur les 2 listes après unpacking :
('G', 'C') ('C', 'G') ('T', 'A') ('A', 'U')
>>> print("Résultat de l'application de zip sur les 2 listes après conversion en liste :\n",list(zip(dna_nucleotides, rna_nucleotides)))
Résultat de l'application de zip sur les 2 listes après conversion en liste :
[('G', 'C'), ('C', 'G'), ('T', 'A'), ('A', 'U')]
>>> print("Résultat de l'application de zip sur les 2 listes après conversion en tuple :\n",tuple(zip(dna_nucleotides, rna_nucleotides)))
Résultat de l'application de zip sur les 2 listes après conversion en tuple :
(('G', 'C'), ('C', 'G'), ('T', 'A'), ('A', 'U'))

Il est également intéressant de noter que comme une liste ou un tuple de pairs d’éléments peut-être converti directement en un dictionnaire, cela fonctionne également avec zip : le résultat du zip est directement transformé en dictionnaire.

>>> dna_nucleotides = ['G', 'C', 'T', 'A']
>>> rna_nucleotides = ['C', 'G', 'A', 'U']
>>> print("Résultat de l'application de zip sur les 2 listes après conversion en dictionnaire :\n",dict(zip(dna_nucleotides, rna_nucleotides)))
Résultat de l'application de zip sur les 2 listes après conversion en dictionnaire :
{'G': 'C', 'C': 'G', 'T': 'A', 'A': 'U'}

Got more examples ?

La fonction zip peut prendre en paramètres plus de 2 itérables, comme l'illustre l'exemple ci-après. Comme précisé plus haut, il ne faut pas perdre de vu que zip ne retourne pas directement une liste mais un objet itérable. Il est donc possible de l’utiliser directement dans un for ou une compréhension mais il est nécessaire de le transformer en liste ou en tuple pour avoir un affichage des éléments dans un print . Il est également possible d'utiliser l'opérateur splat * pour réaliser un unpacking de l' itérable créé par zip. L'exemple ci-après illustre ces différents cas.

houses = ["Stark", "Lannister", "Baratheon", "Greyjoy"]
seats = ["Winterfell", "Casterly Rock", "Storm's End", "Pyke"]
sigils = ["A Gray Direwolf", "A Golden Lion", "A Crowned Black Stag", "A Golden Kraken"]
words = ["Winter is coming", "Hear me roar !", "Our is the fury !", "We do not sow"]

print(f"houses : {houses}")
print(f"seats : {seats}")
print(f"sigils : {sigils}")
print(f"words : {words}")

print("\n###########\n")

print(
f"zip(houses, seats, sigils, words) en tant que liste : \n{list(zip(houses, seats, sigils, words))}",
)
print(
"\nunpacking de zip(houses, seats, sigils, words) :\n",
*zip(houses, seats, sigils, words),
)

print("\n###########\n")

print(
"Affichage de chacun des éléments de la séquence zip(houses, seats, sigils, words) via un for :"
)
for got_house_info in zip(houses, seats, sigils, words):
print(got_house_info)

print("\n###########\n")

print("Utilisation dans une compréhension")
print(
"\n".join(
[f"{house} : {word}" for house, _, _, word in zip(houses, seats, sigils, words)]
)
)

Ce script donne l’affichage suivant dans la console :

Capture d’écran du résultat de l’exécution du script précédent

Ci-après le résultat sous forme textuelle :

houses : ['Stark', 'Lannister', 'Baratheon', 'Greyjoy']
seats : ['Winterfell', 'Casterly Rock', "Storm's End", 'Pyke']
sigils : ['A Gray Direwolf', 'A Golden Lion', 'A Crowned Black Stag', 'A Golden Kraken']
words : ['Winter is coming', 'Hear me roar !', 'Our is the fury !', 'We do not sow']

###########

zip(houses, seats, sigils, words) en tant que liste :
[('Stark', 'Winterfell', 'A Gray Direwolf', 'Winter is coming'), ('Lannister', 'Casterly Rock', 'A Golden Lion', 'Hear me roar !'), ('Baratheon', "Storm's End", 'A Crowned Black Stag', 'Our is the fury !'), ('Greyjoy', 'Pyke', 'A Golden Kraken', 'We do not sow')]

unpacking de zip(houses, seats, sigils, words) :
('Stark', 'Winterfell', 'A Gray Direwolf', 'Winter is coming') ('Lannister', 'Casterly Rock', 'A Golden Lion', 'Hear me roar !') ('Baratheon', "Storm's End", 'A Crowned Black Stag', 'Our is the fury !') ('Greyjoy', 'Pyke', 'A Golden Kraken', 'We do not sow')

###########

Affichage de chacun des éléments de la séquence zip(houses, seats, sigils, words) via un for :
('Stark', 'Winterfell', 'A Gray Direwolf', 'Winter is coming')
('Lannister', 'Casterly Rock', 'A Golden Lion', 'Hear me roar !')
('Baratheon', "Storm's End", 'A Crowned Black Stag', 'Our is the fury !')
('Greyjoy', 'Pyke', 'A Golden Kraken', 'We do not sow')

###########

Utilisation dans une compréhension
Stark : Winter is coming
Lannister : Hear me roar !
Baratheon : Our is the fury !
Greyjoy : We do not sow

Where is my unzip ?

La fonction zip permet de transformer plusieurs séquences en une séquence de tuples, chaque élément de ces tuples venant d’éléments à la même position de chacune de ces listes.

Maintenant comment réaliser l'opération inverse : j'ai une liste de tuples de taille identique et je veux en obtenir autant de listes, constituées des éléments à la même position dans chacun des tuples de départ.

Facile en utilisant unzip, cela semble cohérent comme nom pour l'opération inverse !

Juste un soucis, unzip n'existe pas en Python !

Alors, comment faire ? Pas de panique, si la fonction n’existe pas en Python, il y a une bonne raison, c’est qu’on n’en a pas vraiment besoin !

Et oui, il suffit d’utiliser zip ... enfin presque, on va s’aider de l’opérateur splat déjà évoqué précédemment pour réaliser un unpacking.

Ainsi, avec * (splat operator) on peut forcer le unpacking de notre séquence de tuples, on a ainsi plusieurs tuples, plutôt qu'une liste de séquence de tuples. En appliquant zip à nouveau sur ces tuples, on crée une nouvelle séquence de tuples, cette séquence correspond à la séquence de nos listes de départ (ok, les listes ont été transformées en tuples au passage mais c'est un détail). On peut ensuite par exemple, faire le unpacking du résultat dans des variables et on retrouve nos listes de départ (transformées en tuples).

En continuant sur l’exemple du paragraphe précédent, voici ce que l’on pourrait écrire :

print("Réalisation d'un 'unzip' et retour à la situation initiale")

print(
"zip(*zip(houses, seats, sigils, words)) comme liste : ",
list(zip(*zip(houses, seats, sigils, words))),
)

print("\nunpacking de zip(*zip(houses, seats, sigils, words)) :")
houses_2, seats_2, sigils_2, words_2 = zip(*zip(houses, seats, sigils, words))

print(f"=> houses_2 : {houses_2}")
print(f"=> seats_2 : {seats_2}")
print(f"=> sigils_2 : {sigils_2}")
print(f"=> seats_2 : {words_2}")

Ce qui nous donne si on exécute ce code :

Capture d’écran du résultat de l’exécution du script précédent
Réalisation d'un 'unzip' et retour à la situation initiale
zip(*zip(houses, seats, sigils, words)) comme liste : [('Stark', 'Lannister', 'Baratheon', 'Greyjoy'), ('Winterfell', 'Casterly Rock', "Storm's End", 'Pyke'), ('A Gray Direwolf', 'A Golden Lion', 'A Crowned Black Stag', 'A Golden Kraken'), ('Winter is coming', 'Hear me roar !', 'Our is the fury !', 'We do not sow')]

unpacking de zip(*zip(houses, seats, sigils, words)) :
=> houses_2 : ('Stark', 'Lannister', 'Baratheon', 'Greyjoy')
=> seats_2 : ('Winterfell', 'Casterly Rock', "Storm's End", 'Pyke')
=> sigils_2 : ('A Gray Direwolf', 'A Golden Lion', 'A Crowned Black Stag', 'A Golden Kraken')
=> seats_2 : ('Winter is coming', 'Hear me roar !', 'Our is the fury !', 'We do not sow')

Synthèse en image

Un petit récapitulatif de ce qui vient d’être présenté : ce n’est pas compliqué mais ce n’est pas toujours très simple à visualiser.

Synthèse du fonctionnement de zip avec l’exemple précédent

A ne pas zapper

Il y a encore quelques particularités de la fonction zip que nous n'avons pas abordées.

La fonction zip peut être utilisée avec des séquences infinies : ce que produit zip est un itérable et tant que vous n'essayez pas de réaliser la séquence de manière brutale en la transformant en liste ou en essayant de faire un unpacking dessus par exemple, il n'y a pas de soucis : vous avez un itérable qui représente une séquence potentiellement infinie.

Dans l'exemple ci-après j'utilise count de itertools (dans la bibliothèque standard de Python) pour générer des listes de nombres entiers infinis et j'utilise la fonction islice de itertoolségalement pour prendre une tranche de taille définie (ici 10) de l' iterator produit par zip. En effet, on ne peut pas slicer directement un iterator avec la syntaxe [start:end:step] utilisable avec les listes, les tuples ou les chaînes.

>>> from itertools import count, islice
>>> print(list(islice(zip(count(0), count(1), count(2), count(3)), 10)))
[(0, 1, 2, 3), (1, 2, 3, 4), (2, 3, 4, 5), (3, 4, 5, 6), (4, 5, 6, 7), (5, 6, 7, 8), (6, 7, 8, 9), (7, 8, 9, 10), (8, 9, 10, 11), (9, 10, 11, 12)]

Si vous appliquez zip sur des séquences de tailles différentes, que se passe-t-il ? Eh bien, le zip s'effectue par rapport à la séquence qui a la plus petite taille, il n'y aura pas plus de tuples que d'éléments dans la plus petite des listes. Cela sera probablement plus clair avec l'exemple qui suit :

>>> numbers = [1, 2, 3, 4, 5]
>>> lower_cap_letters = ["a", "b", "c", "d"]
>>> upper_cap_letters = ["A", "B", "C"]
>>> print(list(zip(numbers, lower_cap_letters, upper_cap_letters)))
[(1, 'a', 'A'), (2, 'b', 'B'), (3, 'c', 'C')]

On obtient ainsi qu’une séquence de 3 tuples, avec les 3 premiers éléments de chaque liste, c’est la plus petite liste qui a déterminé la contrainte sur la taille de la séquence produite.

Et si je veux zipper par rapport à la liste qui a la plus grande taille ? Je fais appel à zip_longest de itertools

>>> from itertools import zip_longest
>>> numbers = [1, 2, 3, 4, 5]
>>> lower_cap_letters = ["a", "b", "c", "d"]
>>> upper_cap_letters = ["A", "B", "C"]
>>> print(list(zip_longest(numbers, lower_cap_letters, upper_cap_letters)))
[(1, 'a', 'A'), (2, 'b', 'B'), (3, 'c', 'C'), (4, 'd', None), (5, None, None)]

Cette fois-ci c’est la plus longue liste qui impose les conditions, les valeurs manquantes pour compléter les tuples sont remplacées assez logiquement par None.

Avant de partir

Les fonctions zip et unzip font parties des outils de manipulation de séquences qu'il est intéressant de connaître notamment dès qu'on souhaite manipuler des séquences d'éléments sous forme d'iterator avec des fonctions comme map, filter & co, dans des compréhensions ou avec des generators. Il est intéressant de trouver la fonction en Python directement et de pouvoir faire le unzip facilement.

Il y a bien sûr un gist avec les sources des exemples.

J’avais publié une première version de ce billet sur mon blog personnel.

Remerciement

Je remercie mes collègues Ounaïrat Anzibouddine et Thomas Verhoken pour leur relecture attentive et leurs remarques.

--

--

Christophe Vaudry
norsys-octogone

Developer working for Norsys. Programming languages explorer. Know nothing.