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.

Ci-après, une petite note d’apprentissage de Python, sur la fonction zip. Il faut noter que c’est un billet relativement court, je l’ai un peu compressé !

Ce qui m’a donné matière à faire ce billet, c’est une remarque dans A Whirlwind Tour of Python dans la chapitre 10 sur les itérateurs

Using this trick lets us answer the age-old question that comes up in Python learners’ forums: why is there no unzip() function which does the opposite of zip()? If you lock yourself in a dark closet and think about it for a while, you might realize that the opposite of zip() is… zip()!

J’ai voulu creuser la question pour bien visualiser les choses et être sûr que j’avais bien compris. C’est parti regardons de plus près ce à quoi correspond cette fonction zip et son pendant unzip.

Woodbine9, CC BY-SA 4.0 , via Wikimedia Commons

NB : c’est du Python 3 qui est utilisé.

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.

DemonDeLuxe (Dominique Toussaint), CC BY-SA 3.0 , via Wikimedia Commons

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 pouvoir utiliser zip

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
dna_nucleotides = ['G', 'C', 'T', 'A']
rna_nucleotides = ['C', 'G', 'A', 'U']

print("La liste des nucléotides de l'ADN :\n",
      dna_nucleotides)
print("La liste des nucléotides de l'ARN :\n", rna_nucleotides)
print("Résultat de l'application de zip sur les 2 listes :\n",
      zip(dna_nucleotides, rna_nucleotides))
print("Résultat de l'application de zip sur les 2 listes après unpacking :\n",
      *zip(dna_nucleotides, rna_nucleotides))
print("Résultat de l'application de zip sur les 2 listes après conversion en liste :\n",
      list(zip(dna_nucleotides, rna_nucleotides)))
print("Résultat de l'application de zip sur les 2 listes après conversion en tuple :\n",
      tuple(zip(dna_nucleotides, rna_nucleotides)))
print("Résultat de l'application de zip sur les 2 listes après conversion en dictionnaire :\n",
      dict(zip(dna_nucleotides, rna_nucleotides)))

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

for pair in zip(dna_nucleotides, rna_nucleotides):
    print("Pair de nucléotides ADN - ARN :", pair)

Ce qui nous donne dans la console quand on exécute le script :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
La liste des nucléotides de l'ADN :
 ['G', 'C', 'T', 'A']
La liste des nucléotides de l'ARN :
 ['C', 'G', 'A', 'U']
Résultat de l'application de zip sur les 2 listes :
 <zip object at 0x00000176EA4918C0>
Résultat de l'application de zip sur les 2 listes après unpacking :
 ('G', 'C') ('C', 'G') ('T', 'A') ('A', 'U')
Résultat de l'application de zip sur les 2 listes après conversion en liste :
 [('G', 'C'), ('C', 'G'), ('T', 'A'), ('A', 'U')]
Résultat de l'application de zip sur les 2 listes après conversion en tuple :
 (('G', 'C'), ('C', 'G'), ('T', 'A'), ('A', 'U'))
Résultat de l'application de zip sur les 2 listes après conversion en dictionnaire :
 {'G': 'C', 'C': 'G', 'T': 'A', 'A': 'U'}

###########

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')

Comme on peut le voir à la ligne Résultat de l'application de zip sur les 2 listes : <zip object at 0x0000022A411419C0>, la fonction zip retourne un itérateur. On peut donc réaliser un unpacking des valeurs de l’itérateurs avec l’opérateur splat * (Je vous recommande le billet L’opérateur splat (l’étoile: *) en Python sur ce sujet), le convertir en liste ou en tuple, le parcourir avec une boucle for (et cela fonctionnerait bien sûr avec une compréhension).

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, comme l’illustre le dernier exemple avec dict(zip(dna_nucleotides, rna_nucleotides))) dans lequel le résultat de notre zip est directement transformé en dictionnaire.

Got more examples ?

La fonction zip peut prendre en paramètres plus de 2 itérables, comme l’illustre l’exemple ci-après. Il faut également noter au passage que zip ne retourne pas directement une liste mais un objet itérable. Il est donc nécessaire de le transformer en liste ou en tuple pour l’afficher directement dans un print ou bien sûr de l’utiliser dans un for ou une compréhension). Il est également possible d’utiliser l’opérateur splat * pour réaliser un unpacking de l’itérable créer par zip. L’exemple ci-après illustre ces différents cas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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']
seats : ['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) :
('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')
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 éléments à la même position de chacune de ces listes. Maintenant si je veux faire l’opération inverse : j’ai une liste de tuples de taille identique et je veux en obtenir autant de listes, constitué 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 !

Where is my unzip

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

You know nothing Daenerys!

Et oui, il va suffire d’utiliser zip … enfin presque, on va être aider par l’opérateur de unpacking.

Avec * 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éé 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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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 :

1
2
3
4
5
6
7
8
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 en image

Si vous trouvez l’image trop petite pour une lecture confortable la voici en [PDF|XLSX|ODF]

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é 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, il n’y a pas de soucis : vous avez un itérable qui représente une séquence potentiellement infini. Dans l’exemple ci-après j’utilise count de itertools 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 avec la syntaxe [start🔚step] utilisable avec les listes, les tuples ou les chaînes.

1
2
3
4
from itertools import count, islice

print("zip avec des séquences infinies")
print(list(islice(zip(count(0), count(1), count(2), count(3)), 10)))
1
2
zip avec des séquences infinies
[(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 :

1
2
3
4
5
6
7
8
print("zip avec des séquences de tailles différentes")
numbers = [1,2,3,4,5]
lower_cap_letters = ['a', 'b', 'c', 'd']
upper_cap_letters = ['A', 'B', 'C']
print(numbers)
print(lower_cap_letters)
print(upper_cap_letters)
print("Les 3 listes précédentes 'zippées' avec la fonction zip :", list(zip(numbers, lower_cap_letters, upper_cap_letters)))

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.

1
2
3
4
5
zip avec des séquences de tailles différentes
[1, 2, 3, 4, 5]
['a', 'b', 'c', 'd']
['A', 'B', 'C']
Les 3 listes précédentes 'zippées' avec la fonction zip : [(1, 'a', 'A'), (2, 'b', 'B'), (3, 'c', 'C')]

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

1
2
3
4
5
6
7
from itertools import zip_longest

print("utilisation de zip_longest de itertools")
print(numbers)
print(lower_cap_letters)
print(upper_cap_letters)
print("Les 3 listes précédentes 'zippées' avec la fonction itertools.zip_longest :", list(zip_longest(numbers, lower_cap_letters, upper_cap_letters)))

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.

1
2
3
4
5
utilisation de zip_longest de itertools
[1, 2, 3, 4, 5]
['a', 'b', 'c', 'd']
['A', 'B', 'C']
Les 3 listes précédentes 'zippées' avec la fonction itertools.zip_longest : [(1, 'a', 'A'), (2, 'b', 'B'), (3, 'c', 'C'), (4, 'd', None), (5, None, 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 et companie, 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.

Les exemples du billet sont disponibles sous forme de gist.

Références

Livres

Articles et billets

Images

Les images de fermetures éclair sont des images sous licence Creative Commons Attribution-ShareAlike