15. Les ‘ndarray’ : de vrais tableaux en python#
version 2022, PhL.
Chapitre où sont présentés les vrais tableaux en python : les ndarray
(tableaux) numpy
.
Les sections avec une \(\star\) sont de niveau Objectif 20.
15.1. Les tableaux#
Les types scalaires permettent de manipuler des valeurs scalaires comme les :
bool
int
float
complex
les caractères souvent dénotés de type char (absent en python).
Un type composé permet de regrouper plusieurs valeurs, de même type scalaire ou non, dans une seule variable (ou un seul objet).
Il y a plusieurs familles de types composés.
Les tableaux sont l’une d’entre elles.
15.1.1. La vision classique#
Les tableaux sont définis comme des ensembles :
de valeurs de même type
en nombre connu et fixé “une fois pour toute” lors de leur définition
et dont les valeurs sont stockées de façon contigüe en mémoire.
Le nombre de valeurs stockées dans un tableau est la taille ou la dimension de ce tableau.
Un tableau peut être 1D (linéaire) ou multidimensionnel : 2D (matrice), 3D, …
Les valeurs du tableau sont repérées par un ou des indices , entiers et consécutifs.
Il y a autant d’indices que de dimensions du tableau :
1D = 1 indice
i
: le numéroi
de la case qui contient la valeur2D = 2 indices
i
etj
: les numéros de ligne et de colonne de la case qui contient la valeuret ainsi de suite …
15.1.2. En python#
Jusque-là, les tableaux ont été représentés par des listes python. Au chapitre “Types composés”, on a vu qu’une liste python est très différente de la définition – la vision classique – des tableaux.
Voilà pourquoi nous allons étudier les ndarray
, qui définissent un type composé fourni par le module numpy
et qui correspondent aux tableaux classiques.
15.2. Les ndarray
de numpy
#
Les ndarray
de numpy
sont ainsi appelés car :
array
: tableau in english,nd
: \(n\)-dimension.
ATTENTION : il existe des array
dans la bibliothèque standard de python.
15.2.1. Préliminaire nécessaire à l’utilisation de numpy
#
Ce qui suit concerne les ndarray
du module numpy
utilisables après un import numpy
ou le classique :
import numpy as np
15.2.2. Créer un ndarray
: fonctions de création#
La création d’un ndarray
est un peu pénible au début.
Il faut utiliser un des constructeurs définis dans le tableau ci-dessous.
Le choix d’un constructeur dépend de la connaissance ou non des valeurs du ndarray
. Bien sûr, le nombre d’éléments de ndarray
, ou de façon équivalente le produit de ses dimensions, doit être connu avant la création du ndarray
.
identifiant |
Argument |
Famille |
Rôle |
---|---|---|---|
|
|
constructeur |
créé un tableau rempli de zéros et de dimension et de tailles définies comme argument |
|
|
constructeur |
créé un tableau rempli de 1 et de dimension et de tailles définies comme argument |
|
range d’ |
constructeur |
créé un tableau 1D rempli des valeurs du range défini comme argument |
On verra aussi un peu plus loin un autre constructeur qui permet de définir un ndarray
à partir de la liste python lst
de ses valeurs.
t0 = np.zeros(3)
t1 = np.ones(5)
t2 = np.arange(10)
print("Créations de ndarray :")
print(t0)
print("t1 =", t1)
print("t2 =", t2)
t34 = np.ones( (3, 4) )
print("t2D de 3 lignes et 4 colonnes : \n", t34)
t23 = np.arange(10)
print(t23)
#help(np.arange)
Créations de ndarray :
[0. 0. 0.]
t1 = [1. 1. 1. 1. 1.]
t2 = [0 1 2 3 4 5 6 7 8 9]
t2D de 3 lignes et 4 colonnes :
[[1. 1. 1. 1.]
[1. 1. 1. 1.]
[1. 1. 1. 1.]]
[0 1 2 3 4 5 6 7 8 9]
15.2.3. Choisir le type des valeurs à la création#
Le paramètre dtype
(data-type) permet de définir type des valeurs du ndarray
lors de sa création.
dtype
:
est utilisé comme paramètre nommé de certaines fonctions pour définir le type numérique des valeurs
les valeurs de
dtype
dépendent de l’environnement; les plus classiques sont :ceux de python :
int
,float
,complex
et
numpy
en propose d’autres :uint8
,int16
,int32
,int64
,float32
,float64
, …
t0 = np.zeros(3, dtype = int)
t1 = np.ones(5, dtype = float)
t11 = np.ones(5, dtype = np.float32)
t2 = np.arange(10, dtype = complex)
print("Création de ndarray :")
print("t0 = ", t0)
print("t1 =", t1)
print("t11 =", t11)
print("t2 =", t2)
Création de ndarray :
t0 = [0 0 0]
t1 = [1. 1. 1. 1. 1.]
t11 = [1. 1. 1. 1. 1.]
t2 = [0.+0.j 1.+0.j 2.+0.j 3.+0.j 4.+0.j 5.+0.j 6.+0.j 7.+0.j 8.+0.j 9.+0.j]
help(np.zeros)
Help on built-in function zeros in module numpy:
zeros(...)
zeros(shape, dtype=float, order='C', *, like=None)
Return a new array of given shape and type, filled with zeros.
Parameters
----------
shape : int or tuple of ints
Shape of the new array, e.g., ``(2, 3)`` or ``2``.
dtype : data-type, optional
The desired data-type for the array, e.g., `numpy.int8`. Default is
`numpy.float64`.
order : {'C', 'F'}, optional, default: 'C'
Whether to store multi-dimensional data in row-major
(C-style) or column-major (Fortran-style) order in
memory.
like : array_like, optional
Reference object to allow the creation of arrays which are not
NumPy arrays. If an array-like passed in as ``like`` supports
the ``__array_function__`` protocol, the result will be defined
by it. In this case, it ensures the creation of an array object
compatible with that passed in via this argument.
.. versionadded:: 1.20.0
Returns
-------
out : ndarray
Array of zeros with the given shape, dtype, and order.
See Also
--------
zeros_like : Return an array of zeros with shape and type of input.
empty : Return a new uninitialized array.
ones : Return a new array setting values to one.
full : Return a new array of given shape filled with value.
Examples
--------
>>> np.zeros(5)
array([ 0., 0., 0., 0., 0.])
>>> np.zeros((5,), dtype=int)
array([0, 0, 0, 0, 0])
>>> np.zeros((2, 1))
array([[ 0.],
[ 0.]])
>>> s = (2,2)
>>> np.zeros(s)
array([[ 0., 0.],
[ 0., 0.]])
>>> np.zeros((2,), dtype=[('x', 'i4'), ('y', 'i4')]) # custom dtype
array([(0, 0), (0, 0)],
dtype=[('x', '<i4'), ('y', '<i4')])
15.2.4. Accéder aux éléments d’un ndarray
#
On manipule les élements, les tranches de tableau avec la même syntaxe que les str
ou les lst
:
t[5]
tab[0:3, :]
…
Indices de tableaux multi-dimentionnels : les deux écritures suivantes sont possibles.
dans
t
un tableau 2D (matrice), l’élément à la ligne i et à la colonne j s’écrit :t[i, j]
out[i][j]
On pourra préférer t[i, j]
qui permet de bien distinguer les ndarray
des listes (de listes).
print(t34[1, 2] == t34[1][2])
True
15.2.5. Afficher un ndarray
: fonction d’affichage#
On aura remarqué que la fonction print()
accepte et traite correctement un argument de type ndarray
.
Et même sans avoir à préciser :
les dimensions et les tailles de l’argument
le type de ces valeurs.
numpy
propose ainsi une fonction d’affichage d’une utilisation identique au print()
de python.
Comprendre qu’il y a en fait plusieurs fonctions print()
mais qu’elles partagent le même nom.
Quizz
Quelle est la limitation d’une telle fonction
print()
?
15.2.6. Méthodes d’introspection#
Les méthodes d’introspection sont des fonctions qui retournent des caractéristiques des variables (objets) auxquelles elles s’appliquent.
Ce sont des méthodes de description.
identifiant |
Argument |
Famille |
Rôle |
---|---|---|---|
|
|
descripteur |
retourne le nombre de dimensions du tableau argument |
|
|
descripteur |
retourne, sous la forme d’un t-uple, les dimensions du tableau argument |
|
|
descripteur |
retourne le nombre d’éléments du tableau argument |
|
|
descripteur |
retourne le type des éléments du tableau argument |
La notation pointée agit comme les parenthèses d’une fonction : elle permet de nommer la variable (l’objet) à laquelle l’introspection s’applique.
print("Plus de détails : tableau 1D")
i = 3
print(type(i))
print(type(t0)) # type() est une fonction python
print()
print("Plus de détails sur le tableau 1D avec des méthodes : ")
print("t.dim avec t un nd-array:", t0.ndim, t1.ndim, t2.ndim)
print("t.shape avec t un nd-array:", t0.shape, t1.shape, t2.shape)
print("t.size avec t un nd-array:", t0.size, t2.size, t2.size)
print()
print("Et des détails sur les valeurs du tableau 1D :")
print(t0.dtype)
print(t0.itemsize)
print()
Plus de détails : tableau 1D
<class 'int'>
<class 'numpy.ndarray'>
Plus de détails sur le tableau 1D avec des méthodes :
t.dim avec t un nd-array: 1 1 1
t.shape avec t un nd-array: (3,) (5,) (10,)
t.size avec t un nd-array: 3 10 10
Et des détails sur les valeurs du tableau 1D :
int64
8
print("Plus de détails avec tableau 2D :")
print(type(t34))
print("t.dim:", t34.ndim)
print("t.shape:", t34.shape)
print("t.size:", t34.size)
print("Et des détails sur les valeurs du tableau : dtype, itemsize", t34.dtype, t34.itemsize)
print()
Plus de détails avec tableau 2D :
<class 'numpy.ndarray'>
t.dim: 2
t.shape: (3, 4)
t.size: 12
Et des détails sur les valeurs du tableau : dtype, itemsize float64 8
v = np.ones(17)
t1 = np.ones((2,3))
print(v, t1)
print("np.ones((2,3)):", t1.ndim, t1.shape, t1.size)
print()
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.] [[1. 1. 1.]
[1. 1. 1.]]
np.ones((2,3)): 2 (2, 3) 6
t2 = np.arange(2,11,0.5)
print("t2=",t2)
print("size :", t2.size)
print()
t2f = np.arange(6.0)
print("t2f=",t2f)
print("size :", t2f.size)
print()
t2= [ 2. 2.5 3. 3.5 4. 4.5 5. 5.5 6. 6.5 7. 7.5 8. 8.5
9. 9.5 10. 10.5]
size : 18
t2f= [0. 1. 2. 3. 4. 5.]
size : 6
15.2.7. Fonctions : élément-par-élément vs. tableau#
Les ndarray
bénéficient d’opérations vectorielles et matricielles ainsi que des traitements d’algèbre linéaire :
dot()
: produit scalaire (aussivdot
),dot()
ou@
: produit matriciel,transpose()
ou méthode.T
: transposition
ATTENTION Les autres fonctions travaillent élément-par-élement (element-wise) en utilisant la surcharge des opérateurs.
Ainsi ces fonctions retournent des ndarray
de forme induite par celles des opérandes.
opérateurs arithmétiques (surchargés et s’appliquant element-wise) :
+
,-
,*
,/
,//
,%
,divmod()
,**
,pow()
opérateurs logiques :
&
,^,
|,
~`comparaisons :
==
,<
,>
,<=
,>=
,!=
fonctions :
sqrt()
,exp()
, …
ATTENTION : Le produit matriciel classique est A @ B
tandis que A * B
est le produit élément-par-élement.
ATTENTION : La mise en garde précédente s’appuie sur le fait que seul le type ndarray de numpy a été introduit dans ce cours.
En particulier, le terme suivant est utilisé une et une seule fois, ici : matrix!
15.2.8. Définir des ndarray
à partir de listes python (list
)#
L’exemple suivant utilise un autre constructeur, dénoté np.array()
qui définit un tableau ndarray
à partir de la liste de ses valeurs (cf. paragraphe suivant).
vi = np.array([1,0,0])
print(vi)
print()
m23 = np.array([[1,1,1], [2,2,2]])
print(m23)
[1 0 0]
[[1 1 1]
[2 2 2]]
vi = np.array([1,0,0])
vj = np.array([0,1,0])
vk = np.array([0,0,1])
print(vi, vj, vk)
zero = np.dot(vi, vj)
autre_zero = np.vdot(vi, vj)
print("comparaison scalaire:", zero == autre_zero)
print()
print("égalité élement-élément:", vi == vj)
print("comparaison élement-élément:", vi > vj)
un_autre_vecteur = vi * vj
print(un_autre_vecteur)
print("comparaison élément-élement:", zero == un_autre_vecteur)
print()
I3 = np.array([vi, vj, vk])
print("I3="); print(I3)
I2 = I3[0:2, 0:2]
print("I2="); print(I2)
print()
K = I3[2, :]
print("tranche de tableau:", K)
print(vk == K)
[1 0 0] [0 1 0] [0 0 1]
comparaison scalaire: True
égalité élement-élément: [False False True]
comparaison élement-élément: [ True False False]
[0 0 0]
comparaison élément-élement: [ True True True]
I3=
[[1 0 0]
[0 1 0]
[0 0 1]]
I2=
[[1 0]
[0 1]]
tranche de tableau: [0 0 1]
[ True True True]
Les opérations composées (avec affectation) +=
ou *=
effectuent un traitement en place, c-a-d sans créer un nouvel objet.
15.3. Aspects plus avancés (\(\star\))#
15.3.1. Autres constructeurs et autres descripteurs#
identifiant |
Fonction ou Méthode |
Argument |
Famille |
Rôle |
---|---|---|---|---|
|
F |
|
constructeur |
créé un tableau à partir d’un argument |
|
F |
|
constructeur |
redimensionne aux dimensions définies dans le tuple argument, un tableau existant (et compatible) passé comme argument |
|
M |
|
descripteur |
retourne l’espace, mesuré en octets, entre 2 éléments consécutifs dans une direction donnée de chaque élément du tableau argument ; ces valeurs sont retournées sous la forme d’un tuple |
|
M |
|
descripteur |
retourne la taille en octets de chaque élément du tableau argument |
t12 = np.zeros(12).reshape(4,3)
print("t12=",t12)
print()
t12.reshape(6,2)
print("Attention au (no-)reshape : t12.reshape(6,2)")
print(t12.shape); print("t12=", t12)
print()
t12bis = np.reshape(t12,(6,2))
print("t12bis = t12.reshape(6,2)")
print(t12bis.shape); print("t12bis=", t12bis)
print()
print("t12 et t12bis : même tableau en mémoire ?", id(t12) == id(t12bis))
t12= [[0. 0. 0.]
[0. 0. 0.]
[0. 0. 0.]
[0. 0. 0.]]
Attention au (no-)reshape : t12.reshape(6,2)
(4, 3)
t12= [[0. 0. 0.]
[0. 0. 0.]
[0. 0. 0.]
[0. 0. 0.]]
t12bis = t12.reshape(6,2)
(6, 2)
t12bis= [[0. 0.]
[0. 0.]
[0. 0.]
[0. 0.]
[0. 0.]
[0. 0.]]
t12 et t12bis : même tableau en mémoire ? False
Attention : La forme (shape) du tableau est fixée une fois pour toute à sa définition.
reshape()
, vu comme une fonction, ne modifie pas la forme d’un tableau existant mais permet de construire si besoin un nouveau tableau structuré différemment d’un tableau existant.
Ce nouveau tableau est obtenu lors de l’affectation du résultat (de la fonction) constructeur reshape()
Difficile : En pratique, cette “remise en forme” d’un tableau pose des questions quant à son stockage contigu en mémoire. En effet, la mémoire est linéaire (1D) et ré-organiser un tableau n-D de façon contigue dans un espace 1D peut s’effectuer de façons diverses : ligne après ligne vs. colonne après colonne pour un tableau 2D par exemple, …
print("comparaison :", t12 == t12bis)
print("shapes :", t12.shape, t12bis.shape)
print("id :", id(t12), id(t12bis))
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[13], line 1
----> 1 print("comparaison :", t12 == t12bis)
2 print("shapes :", t12.shape, t12bis.shape)
3 print("id :", id(t12), id(t12bis))
ValueError: operands could not be broadcast together with shapes (4,3) (6,2)
t = np.array([1,0])
print(t)
print(type(t))
print(t.dtype)
print(t.strides)
[1 0]
<class 'numpy.ndarray'>
int64
(8,)
Id = np.array([[1,0],[0,1]])
A = np.arange(5,9).reshape(2,2)
print(Id)
print(A)
print()
reA = np.dot(Id, A)
B = Id * A
print(reA)
print(B)
[[1 0]
[0 1]]
[[5 6]
[7 8]]
[[5 6]
[7 8]]
[[5 0]
[0 8]]
import numpy as np
tc = np.array( [complex(1,2), complex(3,4)] , dtype=complex )
tc2 = np.array( [ [1,2], [3,4]] , dtype=complex )
tc3 = np.array( [ 1+2.j, 3+4.j ] , dtype=complex )
t_2d = np.array( [ [1,2], [3,4] ])
print(tc, tc.dtype)
print(tc2, tc2.dtype)
print(tc3, tc3.dtype)
print(t_2d, t_2d.dtype)
[1.+2.j 3.+4.j] complex128
[[1.+0.j 2.+0.j]
[3.+0.j 4.+0.j]] complex128
[1.+2.j 3.+4.j] complex128
[[1 2]
[3 4]] int64
Création \(\neq\) affectation.
L’affectation entre ndarray
:tab2 = tab1
ne créé pas un nouvel ndarray
tab2
.
On retrouvera ce comportement avec types mutables python : list
et dict
.
En revanche, les précédentes fonctions de création produisent l’effet attendu.
15.3.2. Autres fonctions et méthodes utiles#
En vrac des fonctions et des méthodes qui manipulent (en entrée et sortie) des ndarray
:
linspace(a,b,nb)
: fonction qui génère et retourne un tableaundarray
denb
valeurs régulièrement espacées entrea
etb
empty(shape)
génère et retourne un tableaundarray
non initialisé et de dimensionshape
Rmq. mentionné pour insister sur l’intérêt des constructeurs
zeros()
etones()
!!!
axis
: notion d’axeaxis= 0, 1, ...
désignent les première, deuxième, … dimensions dundarray
est utilisé comme paramètre nommé de certaines méthodes pour désigner où s’applique le traitement
exemple :
a23.sum(x)
retourne les sommes[ a[0,0]+a[1,0], a[0,1]+a[1,1], a[0,2]+a[1,2] ]
concerne aussi les méthodes :
.min()
,.argmin()
, …,.mean()
,.var()
,.std()
Plus de détails l’initiation et la documentation de numpy.
a23 = np.empty((2,3), dtype="float32")
print(a23, "\n on vous aura prévenu !")
print(a23.dtype)
print()
i33 = np.empty((3,3), dtype="int16")
print(i33, "\n on vous aura bien prévenu !")
print(i33[0,0].dtype)
print()
for i in range(len(i33[:, 0])):
i33[i, :] = 2*i
print(i33)
print(i33.mean(axis=0))
print(i33.mean(axis=1))
[[0. 0. 0.]
[0. 0. 0.]]
on vous aura prévenu !
float32
[[257 257 257]
[257 257 257]
[257 257 257]]
on vous aura bien prévenu !
int16
[[0 0 0]
[2 2 2]
[4 4 4]]
[2. 2. 2.]
[0. 2. 4.]
15.4. Synthèse#
ndarray
denumpy
: des vrais tableaux nDL’espace de stockage en mémoire d’un vrai tableau est fixé une fois pour toute à la création du tableau.
Ainsi, la taille d’un tableau ne peut pas changer après sa création. Il faut donc connaitre le nombre maximal de valeurs qu’il doit contenir dès cette création.
Quand les choisir ?
les
ndarray
pour du calcul numérique type algèbre linéaireles
ndarray
pour des tableaux multi-dim plein de valeurs numériquesDans ces deux cas, les préférer aux
lst
python
numpy
propose de multiples autres fonctions pour manipuler efficacement des tableaux.Se référer à quickstart numpy (en anglais).