Note
Le type tableau présenté ici (numpy.array
)
n’est pas le plus courant en Python,
qui préfère l’utilisation des listes.
Il présente cependant un certain nombre d’intérêts pédagogiques,
dont celui d’être plus proche des types tableaux disponibles dans
d’autres langages de programmation.
Un tableau est une liste ordonnée de n valeurs du même type. On appelle n la taille du tableau, et les valeurs qu’ils contient sont ses éléments. Chaque élément est repéré dans le tableau par son indice, un nombre entier compris entre 0 et n-1 (inclus).
Pré-requis
Afin d’utiliser des tableaux en Python, il est nécessaire :
d’installer le module Numpy,
d’inclure la ligne suivante dans tous les programmes utilisant les tableaux :
from numpy import *
Un tableau peut-être déclaré de plusieurs manière :
>> from numpy import *
>>> array([5,3,2,1,1]) # crée un tableau à partir de ses éléments
array([5, 3, 2, 1, 1])
>>> zeros(3, float) # crée un tableau rempli de 0
array([ 0., 0., 0.])
>>> empty(4, int) # crée un tableau non initialisé
array([ 0, 8826784, 31983376, 0])
On passe à array
la liste des éléments du tableau à créer ;
on note que les valeurs doivent être encadrées par des crochets [...]
en plus des parenthèses.
On passe à zeros
et empty
la taille du tableau et le type de ses éléments.
Notons cependant que le nom empty
prête à confusion :
il ne crée pas un tableau vide (le tableau contient des éléments),
mais il n’initialise pas ses éléments, donc ceux-ci ont une valeur aléatoire.
Lorsqu’on utilise empty
,
il est donc impératif d’initialiser ensuite chaque élément du tableau,
sans quoi la suite de l’algorithme risque de donner des résultats aléatoires.
Les opérations disponibles sur les tableaux sont très similaires aux opérations disponibles sur les chaînes de caractères :
>>> a = array([5,3,2,1,1])
>>> len(a) # longueur
5
>>> a[0] # premier élément
5
>>> a[:3] # sous-tableau correspondant aux 3 premiers éléments
array([5, 3, 2])
>>> a[3:] # sous-tableau correspondant aux éléments à partir du 4ème
array([1, 1])
On peut par ailleurs modifier un élément d’un tableau (identifié par son indice) en utilisant une affectation :
>>> a = array([5,3,2,1,1])
>>> a[1] = 9 # modification du 2ème élément du tableau
>>> a
array([5, 9, 2, 1, 1])
Toutes les opérations portant sur les types de données vus précédemment (entier, flottant, booléen et chaîne [1]) produisent une nouvelle valeur à partir des valeurs des opérandes. Chaque affectation d’une variable remplace donc la valeur de cette variable par une autre valeur.
Une conséquence est que la modification d’une variable ne peut pas avoir d’impact sur une autre variable. Par exemple :
>>> a = 42
>>> b = a # b prend la même valeur que a, c.à.d. 42
>>> a = a+1 # a+1 produit la valeur 43, qui remplace 42 dans la variable a
>>> a
43
>>> b # b, en revanche, contient toujours 42
42
Avec les tableaux, l’affectation d’un élément modifie l’état du tableau sans remplacer ce dernier par un autre tableau. On dit que les tableaux sont des objets mutables (c’est à dire modifiable; le terme anglais est également mutable). Il en résulte que, si deux variables font référence au même tableau, une modification sur l’une des variables sera répercutée sur l’autre :
>>> a = array([5,3,2,1,1])
>>> b = a # a et b font maintenant référence au MÊME tableau
>>> b
array([5, 3, 2, 1, 1])
>>> a[1] = 9
>>> a # l'état du tableau a été modifié...
array([5, 9, 2, 1, 1])
>>> b # ... ce qui se voit aussi sur b, puisqu'il s'agit du même tableau
array([5, 9, 2, 1, 1])
De plus, les sous-tableaux produits par les opérations a[:i]
et a[i:]
partagent également l’état du tableau qui a servi à les produire.
Ce ne sont pas des copies partielles,
mais des vues restreintes du tableau global.
Ainsi, si on modifie un élément du sous-tableau,
le tableau global est également modifié (et inversement) :
>>> a = array([5,3,2,1,1])
>>> b = a[3:]
>>> b
array([1, 1])
>>> b[1] = 7
>>> b
array([1, 7])
>>> a
array([5, 3, 2, 1, 7]) # la modification de b est répercutée sur a
>>> a[3] = 8
>>> a
array([5, 3, 2, 8, 7])
>>> b
array([8, 7]) # la modification de a est répercutée sur b
La notion d’objet mutable demande d’étendre légèrement la notion de problème telle qu’on l’a définie au chapitre Problème.
Considérons par exemple le problème consistant à diviser par deux tous les éléments d’un tableau de flottants. On peut spécifier ce problème ainsi :
:entrée a: tableau de flottants
:pré-cond: Ø
:sortie b: tableau de flottants
:post-cond: len(b) == len(a), et
pour tout i entre 0 et len(a)-1, b[i] == a[i]/2
Un algorithme résolvant ce problème devra donc créer un nouveau tableau b, et l’initialiser en fonction des valeurs des éléments de a, tandis que a ne sera pas modifié.
Mais comment spécifier le problème de sorte que l’algorithme modifie directement les éléments de a, sans créer un nouveau tableau ?
Dans un tel problème, a serait à la fois paramètre d’entrée
(puisque les valeurs initiales de ses éléments conditionnent
la résolution du problème)
et paramètre de sortie
(puisque les valeurs finales de ses éléments constituent la solution).
On propose donc un nouveau type de paramètre :
les paramètres d’entrée-sortie.
Ces paramètres contiennent nécessairement des objets mutables,
dont l’état initial décrit le problème (ou une partie du problème),
et dont l’état final décrit (une partie de) la solution.
Dans la spécification,
on les déclarera par le texte :entrée/sortie x:
(où x
est le nom de ce paramètre).
Reste le problème d’exprimer la post-condition : le nom du paramètre ne suffit plus, puisqu’il faut distinguer sont état initial et son état final. Pour cela, on ajoutera au nom un e (entrée) en indice pour l’état initial, et un s (sortie) en indice pour l’état final.
On peut donc spécifier ainsi la variante du problème ci-dessus :
:entrée/sortie a: tableau de flottants
:pré-cond: Ø
:post-cond: pour tout i entre 0 et len(a)-1, aₛ[i] == aₑ[i]/2
On remarque que ce problème n’a aucun paramètre
qui soit strictement un paramètre de sortie.
Or seuls les paramètres de sortie sont retournés (avec le mot-clé return
) ;
les paramètres d’entrée-sortie ne le sont pas
(leurs modifications sont accessibles directement
dans l’objet passé en paramètres).
L’algorithme qui résout le problème ci-dessus sera donc
une procédure :
il ne retourne aucune valeur.
Nous avons vu précédemment que l’opérateur ==
permet de
vérifier l’égalité deux deux éléments.
Cela dit, cet opérateur a un comportement pour le moins étonnant lorsqu’on l’utilise avec des tableaux. Considérez le fragment de code suivant.
taba = array([1,2,3])
tabb = array([1,2,3])
tabc = array([1,2,4])
tabd = array([1,2,4,5])
print(taba == tabb)
print(taba == tabc)
print(taba == tabd)
L’opérateur ==
sur des tableaux numpy va retourner :
Par conséquent, comme le tableau résultat n’a pas de valeur logique unique [2], on ne peut pas écrire :
if taba == tabb:
print("les tableaux sont égaux")
Comme nous venons de le voir, l’opérateur ==
permet de
vérifier, élément par élément, l’égalité de deux tableaux.
Mais dans certains cas, on peut vouloir vérifier que deux
noms de variables représentent le même objet “tableau”.
Pour cela, on va utiliser l’opérateur is
, qui permet
de vérifier l’identité des objets (rassurez-vous, vous en
apprendrez plus long sur les objets au prochain semestre).
Considérez l’exemple suivant :
>>> taba = array([1,2,3])
>>> tabb = array([1,2,3])
>>> tabc = taba
>>> print(taba == tabb)
[ True True True]
>>> print(taba == tabc)
[ True True True]
>>> print(taba is tabb)
False
>>> print(taba is tabc)
True
>>> taba[2] = 8
>>> print(taba)
[1 2 8]
>>> print(tabb)
[1 2 3]
>>> print(tabc)
[1 2 8]
Vous constatez bien que taba
et tabb
sont
égaux car toutes leurs valeurs sont égales une à une.
En revanche, taba
et tabb
sont deux objets
différents (si l’on modifie taba
, tabb
ne sera
pas modifié) tandis que taba
et tabc
sont
deux noms différents pour le même objet (si l’on
modifie taba
, tabc
sera modifié).
Les tableaux de chaînes de caractères nécessitent quelques précautions, car la taille maximale de leurs éléments doit être connue à la création du tableau.
Considérons les instructions suivantes :
>>> a = array(["un", "deux"])
>>> a[0] = "autre chose"
>>> a
>>> a[0]
'autr'
Lorsqu’on crée un tableau de chaînes avec array
,
la taille maximale des éléments est fixée à
la longueur du plus grand élément fourni.
Il faut être très attentif à cela, car on voit qu’ensuite,
les valeurs affectées au tableau
sont silencieusement tronquées à cette taille.
Par ailleurs, si l’on souhaite créer
un tableau de chaîne de caractères avec zeros
ou empty
et que l’on passe str
comme type de données,
la taille maximale des éléments sera fixée à un caractère.
Si l’on souhaite un tableau de chaînes de caractères de longueur supérieure,
il faudra passer en guise de type un chaîne de caractères formée du préfixe
<U
suivi de la longueur souhaitée. Par exemple, la ligne suivante :
a = zeros(10, "<U1000")
créera un tableau de 10 chaînes de caractères de longueur maximale 1000.
Notons enfin que, pour les chaînes de caractères,
zeros
initialise les éléments du tableau par une chaîne vide (''
).
Notes de bas de page
[1] | Dans d’autres langages (comme le C), seul le type caractère est un type élémentaire ; les chaînes de caractères sont représentées par un tableau de caractères, et donc mutable (contrairement aux chaînes de Python). |
[2] | Cela dit, numpy nous propose une méthode pour tester
l’égalité de deux tableaux : numpy.array_equal(a, b) |