Participer au site avec un Tip
Rechercher
 

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 :

Vous êtes un professionnel et vous avez besoin d'une formation ? Coder avec une
Intelligence Artificielle
Voir le programme détaillé

Copies, vues et effets de bord avec NumPy

Indexing, slicing et subsetting Changer la forme des tableaux


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

Introduction

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.

Une affectation ne copie pas le tableau

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)
Une affectation ne produit pas de copie

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 slicing produit généralement une vue

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)
Modification d'une vue NumPy

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.

Différence avec les listes Python

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)
Slicing sur une liste Python

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.

Forcer une vraie copie avec copy

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)
Utilisation de la méthode copy

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.

Tester le partage de mémoire

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))
Test de partage de mémoire

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)
Utilisation de la propriété 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.

Effets de bord sur une matrice

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)
Vue sur une partie d'une matrice

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.

Les cas qui produisent plutôt une copie

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))
Indexation avancée et copie

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))
Indexation booléenne et copie

Le résultat confirme que l'on manipule une copie.

[10 20 30 40 50]
[3000   40   50]
False

Quelques règles pratiques

Pour conclure, voici les règles à garder en mémoire lorsque vous manipulez des tableaux NumPy.

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.

Indexing, slicing et subsetting Changer la forme des tableaux




Vous êtes un professionnel et vous avez besoin d'une formation ? Mise en oeuvre d'IHM
avec Qt et PySide6
Voir le programme détaillé