Accès rapide :
Introduction
Une affectation ne copie pas le tableau
Le slicing produit généralement une vue
Différence avec les listes Python
Forcer une vraie copie avec copy
Tester le partage de mémoire
Effets de bord sur une matrice
Les cas qui produisent plutôt une copie
Quelques règles pratiques
Dans le chapitre précédent, nous avons vu comment extraire des valeurs d'un tableau NumPy via l'indexation, le slicing et quelques autres techniques de subsetting. Il nous reste maintenant à comprendre un point très important : une extraction ne produit pas toujours une copie des données.
Avec NumPy, certaines opérations produisent une vue. Une vue est simplement une autre manière d'observer les mêmes données en mémoire. C'est très performant, car cela évite de dupliquer inutilement les tableaux. Mais cela peut aussi provoquer des effets de bord si vous modifiez la vue en pensant manipuler une copie indépendante.
Commençons par le cas le plus simple. Comme pour les listes Python, une affectation de variable ne duplique pas le tableau : on obtient simplement une seconde variable qui référence le même objet.
1 2 3 4 5 6 7 8 9 10 |
import numpy as np v1 = np.array([10, 20, 30, 40, 50]) v2 = v1 v2[2] = 3000 print(v1) print(v2) print(v1 is v2) |
Et voici le résultat produit par ce programme.
[ 10 20 3000 40 50] [ 10 20 3000 40 50] True
Comme vous le constatez, la modification réalisée via v2 est visible via
v1. Il n'y a pas deux tableaux NumPy dans cet exemple, mais bien un seul tableau
manipulé par deux variables différentes.
Le point le plus important concerne le slicing. Sur une liste Python, un slicing produit une nouvelle liste. Sur un tableau NumPy, un slicing produit généralement une vue sur les données d'origine. Cela signifie qu'une modification réalisée sur la vue peut modifier le tableau initial.
1 2 3 4 5 6 7 8 9 |
import numpy as np v = np.array([10, 20, 30, 40, 50]) part = v[1:4] part[0] = 2000 print(v) print(part) |
L'exécution produit le résultat suivant.
[ 10 2000 30 40 50] [2000 30 40]
La valeur 20 du tableau d'origine a bien été remplacée par 2000.
La variable part ne contient donc pas une copie des valeurs 20,
30 et 40. Elle contient une vue sur une partie du tableau v.
Note : ce comportement est volontaire. NumPy manipule parfois des tableaux très volumineux. Produire des copies à chaque extraction serait souvent trop coûteux en temps d'exécution et en mémoire.
Pour bien mesurer la différence, voici le même type d'opération réalisé sur une liste Python.
1 2 3 4 5 6 7 |
data = [10, 20, 30, 40, 50] part = data[1:4] part[0] = 2000 print(data) print(part) |
Le résultat est cette fois-ci différent.
[10, 20, 30, 40, 50] [2000, 30, 40]
La liste initiale n'a pas été modifiée, car le slicing a produit une nouvelle liste. Il faut donc être prudent lorsque l'on passe de listes Python à des tableaux NumPy : la syntaxe ressemble beaucoup, mais le comportement mémoire n'est pas toujours le même.
Si vous souhaitez modifier un sous-tableau sans impacter le tableau d'origine, il faut demander
explicitement une copie via la méthode copy.
1 2 3 4 5 6 7 8 9 |
import numpy as np v = np.array([10, 20, 30, 40, 50]) part = v[1:4].copy() part[0] = 2000 print(v) print(part) |
Cette fois-ci, le tableau initial reste inchangé.
[10 20 30 40 50] [2000 30 40]
Le fait d'utiliser copy() rend votre intention explicite : vous voulez travailler sur
des données indépendantes. Cela consomme plus de mémoire, mais cela évite les effets de bord
involontaires.
NumPy fournit la fonction np.shares_memory pour savoir si deux tableaux partagent le même
espace mémoire. C'est souvent plus clair que d'essayer de raisonner uniquement à partir du code.
1 2 3 4 5 6 7 8 9 |
import numpy as np v = np.array([10, 20, 30, 40, 50]) view = v[1:4] copy = v[1:4].copy() print(np.shares_memory(v, view)) print(np.shares_memory(v, copy)) |
Voici le résultat obtenu.
True False
Dans le premier cas, view partage bien la mémoire du tableau v. Dans le
second cas, copy possède son propre espace de stockage.
Vous pouvez aussi inspecter la propriété base. Si un tableau est une vue, cette propriété
permet souvent de retrouver le tableau qui porte effectivement les données.
1 2 3 4 5 6 7 8 9 |
import numpy as np v = np.array([10, 20, 30, 40, 50]) view = v[1:4] copy = v[1:4].copy() print(view.base is v) print(copy.base) |
L'exécution produit le résultat suivant.
True None
Attention : sur des enchaînements plus complexes, base peut pointer
vers un tableau intermédiaire et pas forcément vers la variable que vous avez en tête. Pour un
simple test de partage mémoire entre deux tableaux, np.shares_memory est souvent plus lisible.
Le même principe s'applique bien entendu aux matrices et, plus généralement, aux tableaux NumPy à plusieurs dimensions. Le code suivant extrait un bloc de valeurs dans une matrice, puis modifie ce bloc.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import numpy as np m = np.array([ [10, 20, 30], [40, 50, 60], [70, 80, 90] ]) bloc = m[:2, 1:] bloc[0, 0] = 2000 print(m) print(bloc) |
Voici le résultat produit.
[[ 10 2000 30] [ 40 50 60] [ 70 80 90]] [[2000 30] [ 50 60]]
La cellule m[0, 1] a été modifiée en passant par bloc. C'est exactement
le même phénomène que pour un vecteur : la vue ne possède pas ses propres données.
Toutes les extractions ne produisent pas des vues. L'indexation avancée, par exemple avec une liste d'indices, produit une copie. Il en est de même pour l'indexation booléenne.
1 2 3 4 5 6 7 8 9 10 |
import numpy as np v = np.array([10, 20, 30, 40, 50]) part = v[[1, 3]] part[0] = 2000 print(v) print(part) print(np.shares_memory(v, part)) |
Dans ce cas, le tableau initial n'est pas modifié.
[10 20 30 40 50] [2000 40] False
Voici le même type de constat avec une indexation booléenne.
1 2 3 4 5 6 7 8 9 10 |
import numpy as np v = np.array([10, 20, 30, 40, 50]) part = v[v >= 30] part[0] = 3000 print(v) print(part) print(np.shares_memory(v, part)) |
Le résultat confirme que l'on manipule une copie.
[10 20 30 40 50] [3000 40 50] False
Pour conclure, voici les règles à garder en mémoire lorsque vous manipulez des tableaux NumPy.
copy().np.shares_memory.Ces points sont particulièrement importants lorsque vous passez des tableaux à des fonctions. Si une fonction reçoit une vue et la modifie, elle modifie aussi le tableau d'origine. C'est parfois exactement ce que l'on souhaite, mais cela doit rester un choix conscient.
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :