Solution du problème Add¶
Vous trouverez ici la solution pour l'exercice suivant:
Exercice de base¶
Il va falloir faire des sommes dans deux tableaux a deux dimensions.
L'approche la moins pythonienne serait de trouver la taille des matrices et d'utiliser deux index pour iterer sur les deux matrices à la fois. C'est surement ce qu'on ferait en C++, mais nous sommes en python, et iterer sur un iterable en python ne se fait pas avec des index.
Il aurait aussi été possible d'utiliser des modules complémentaires dont le but est justement de faire ce genre de traitements. C'est le cas par exemple de numpy et de pandas. Vous pouvez les regarder, ils sont très interressants.
Donc, revenons à notre problème. Juste pour le fun, on peut quand même le faire avec des indices:
def add(matrix1, matrix2):
result = []
for i in range(len(matrix1)):
row = []
for j in range(len(matrix1[i])):
row.append(matrix1[i][j] + matrix2[i][j])
result.append(row)
return result
Maintenant essayons de le faire à la mode Python. Pour cela il faudrait pouvoir iterer sur les deux matrices en même temps.
Et pour cela, python fournit une fonction qui colle parfaitement à notre besoin: zip.
Cette fonction prend des iterables et retourne un seul iterable de tuple de chacun des éléments des iterables en entrée.
Voila ce que ca donne sur un exemple simple:
>>> list(zip(("a", "b", "c"), (1, 2, 3)))
[('a', 1), ('b', 2), ('c', 3)]
NB: zip m'a de prime à bord déconcerté. Pourquoi nommer une fonction telle que celle là, comme un format de compression bien connu? En fait, je me trompais, il faut plutôt la voir comme le zip de la fermeture éclaire: en effet, elle prend deux pans et les mélange pour n'en faire qu'un...

Dans notre cas, nous allons pouvoir iterer en une fois sur nos deux matrices, d'abord sur les lignes, puis sur les elements de chacune des lignes:
def add(matrix1, matrix2):
result = list()
for row1, row2 in zip(matrix1, matrix2):
row = list()
for i1, i2 in zip(row1, row2):
row.append(i1 + i2)
result.append(row)
return result
list Comprehension¶
Histoire d'être encore plus orientée Python, notre solution va maintenant utiliser quelque chose de très commun en python: les comprehension de liste.
Si nous avons du code qui s'écrit de cette façon:
result = list()
for i in range(50):
result.append(i**2)
result = [i**2 for i in range(50)]
result = [
i**2
for i in range(50)
]
On appelle ça la comprehension de liste (list comprehension en anglais).
Si on applique ce principe pour les lignes de nos matrices:
def add(matrix1, matrix2):
result = list()
for row1, row2 in zip(matrix1, matrix2):
result.append([i1 + i2 for i1, i2 in zip(row1, row2)])
return result
On peut tout autant le faire pour la boucle qui reste:
def add(matrix1, matrix2):
return [[i1 + i2 for i1, i2 in zip(row1, row2)] for row1, row2 in zip(matrix1, matrix2)]
Pas très lisible sur une seule ligne; donc écrivons sur plusieurs lignes pour y voir (bien) plus clair:
def add(matrix1, matrix2):
return [
[i1 + i2 for i1, i2 in zip(row1, row2)]
for row1, row2 in zip(matrix1, matrix2)
]
Et voila qui resoud l'exercice de base.
Bonus 1¶
zip permet de prendre n'import quel nombre de parametres, afin de pouvoir zipper autant d'iterables que necessaire; nous souhaitons faire pareil ici, mais comment s'y prendre?
* devant un parametre de fonction¶
Python permet de definir des parametres spéciaux qui autorisent a avoir une fonction qui prend autant de parametres que l'on souhaite:
def echo(*args, **kargs):
print("Arguments:", args)
print("Named Arguments:", kargs)
Les noms sont ici donnés de manière indicative et peuvent être changés selon vos besoins. *args indique à python de faire un tuple avec les parametres donnés de manière positionelle; et **kargs un dictionnaire avec les parametres nommés.
Pour notre cas, il n'y a besoin que de *args pour obtenir toutes les matrices données en parametre:
def add(*matrices):
result = list()
(...)
return result
Nous avons fait la moitié du chemin, il faut encore pouvoir passer toutes les matrices à zip, comme si nous les avions données parametre par parametre dans le code.
* comme operateur¶
Pour cela il va falloir utiliser l'opérateur * comme cela: zip(*matrices). Si matrices contient m1, m2, m3, alors zip(*matrices) est equivalent à zip(m1, m2, m3). C'est donc pleinement adapté à ce qu'il nous faut.
Resolution du Bonus 1¶
Mais comment faire pour ajouter tous les éléments?
-
Premiere solution revenir à une version avec des boucles, et une valeur qui stock la somme:
def add(*matrices): result = list() for rows in zip(*matrices): row = list() for values in zip(*rows): v = 0 for value in values: v += value row.append(v) result.append(row) return result -
Seconde solution: en trouvant une fonction qui retourne la somme des éléments passés en parametre. En python il existe
sum()qui fait cela, et la on peut réutiliser la comprehension de liste:def add(*matrices): return [ [sum(items) for items in zip(*rows)] for rows in zip(*matrices) ]
Bonus 2¶
Il va falloir traiter le cas de matrices non homogènes en taille de manière à lever une exception dans le cas ou les matrices ne peuvent pas être additionnées.
Vérification dur¶
Première possibilité: calculer les dimensions de toutes les matrices et vérifier qu'elles sont identiques.
def get_matrix_shape(matrix):
return (len(matrix), {len(r) for r in matrix})
def is_shape_valid(matrix_shape):
return len(matrix_shape[1]) == 1
def have_matrices_same_shape(matrices):
base_shape = get_matrix_shape(matrices[0])
if not is_shape_valid(base_shape):
return False
for m in matrices:
if base_shape != get_matrix_shape(m):
return False
return True
def add(*matrices):
if not have_matrices_same_shape(matrices):
raise ValueError("")
return [
[sum(items) for items in zip(*rows)]
for rows in zip(*matrices)
]
Vérification plus intelligeante¶
Nous pouvons utiliser des éléments Python pour simplifier cette fonction.
En effet, zip itère sur les différents itérateurs qui lui sont donnés, mais s'arrête au plus petit des itérateur.
A contrario, zip_longest du module itertools, propose la même fonctionalité, mais, en iterant jusqu'au plus long des itérateur, en utilisant une valeur par défaut (None, si on ne donne pas de valeur) pour les itérateurs qui n'ont pas assez d'elements.
Dans notre cas, cela va nous permettre de gerer des tailles de matrices differentes, mais par quelle magie cela va faire lever une exception?
Tout simplement parce que None, ne va pas fonctionner pour des opérations que nous faisons:
sumva lever une exception de typeTypeErrorsiNoneintervient dans l'addition.zip_longestva lever une exception de typeTypeErrorsiNoneintervient dans les itérateurs à zipper.
Il va donc suffir de remplacer zip par zip_longest, et mettre tout notre code dans un block protégé par try: ... except ....
Dans le cas de ValueError, nous n'avons qu'a lever une autre exception de type ValueError.
En python dans le code:
from itertools import zip_longest
def add(*matrices):
try:
return [
[sum(items) for items in zip_longest(*rows)]
for rows in zip_longest(*matrices)
]
except TypeError as e:
raise ValueError("Given matrices are not the same shape.") from e
Derniére note avant la fin: raise ... from e est apparu en Python 3, et permet d'avoir une backtrace plus lisible, car l'exception connais la raison de sa création, qui est ici une autre exception levée plus bas dans le code.
Voila, merci de votre lecture et à bientôt!