.. NB: on triche ci-dessous en écrivant "elément" (sans accent sur le 1er e) plutôt que "élément", sinon dans l'index, le mot ne se retrouve pas trié correctement. C'est un bug connu de Sphinx: https://bitbucket.org/birkenfeld/sphinx/issue/736/invalid-sort-in-index-for-characters-with .. index:: single: tableau single: elément single: indice Tableaux ======== .. 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). .. rubric:: 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 : .. code-block:: python from numpy import * Déclaration d'un tableau ++++++++++++++++++++++++ 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. Opérations sur les tableaux +++++++++++++++++++++++++++ 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.size # autre manière d'obtenir la longueur d'un tableau 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]) Paramètres de type tableau ++++++++++++++++++++++++++ Pour déclarer qu'un paramètre d'entrée ou de sortie est un tableau, on indiquera le type d'élément du tableau entre crochet :: def double(tab: [float]) -> [float]: """ :post-cond: retourne un tableau dont les éléments valent le double de ceux de tab """ ret = empty(len(tab)) for i in range(len(tab)): ret[i] = 2*tab[i] return ret .. index:: mutable Tableaux et mutabilité ++++++++++++++++++++++ Toutes les opérations portant sur les types de données vus précédemment (entier, flottant, booléen et chaîne\ [#chaînes_immutables]_) 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 .. index:: ! paramètre d'entrée-sortie see: entrée-sortie; paramètre d'entrée-sortie .. _def-param-es: Paramètre d'entrée-sortie ------------------------- 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 `probleme`:doc:. 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: [float] :pré-cond: Ø :sortie b: [float] :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: [float] :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`:term: : il ne retourne aucune valeur, et a pour `effet de bord`:term: la modification du paramètre d'entrée-sortie. Mutabilité, égalité et identité ------------------------------- 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. .. code-block:: python 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 : * False si les tableaux sont de taille différentes * Un tableau de booléens comparant les éléments un a un Par conséquent, comme le tableau résultat n'a pas de valeur logique unique\ [#egalite_numpy]_, on ne peut pas écrire : .. code-block:: python # ⚠ ce code ne fonctionne pas 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é). Tableaux de chaînes de caractères +++++++++++++++++++++++++++++++++ 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 ``