Accueil

Traduction

Tutoriel Python - sommaire

Tutoriel Python - recherche

L'auteur : Patrick Darcheville

Vous pouvez me contacter via Facebook pour questions & suggestions : Page Facebook relative à mon site

Python : le point sur les conteneurs - des conteneurs moins connus

Un conteneur dans le jargon Python désigne un objet qui contient une collection d'autres objets.
Vous connaissez déjà les différents types de "containers" : listes, chaines, tuples, dictionnaires, ensembles ainsi que les "range".

Il est possible qu'à ce stade de votre apprentissage de Python, vous soyez un peu perdu et que vous vous posiez légitimmment certaines questions.
Pourquoi une telle diversité de conteneurs ?
Quels traitements pour chaque type de conteneur ?

Dans ce chapitre je vais traiter deux types de conteneurs qui n'ont pas encore été évoqués : les frozensets et les arrays. Ces derniers ne sont pas à confondre avec les "numpy.array" ...

Terminologie

Sachez que l'on peut attribuer à chaque type de conteneur des qualificatifs. Et qu'à partir des qualités vous saurez ce qu'on peut faire (ou pas) pour chaque type de conteneur.
Je prends un premier exemple ; on dit qu'un tuple est un conteneur séquentiel mas non mutable.

Autre exemple : on dit qu'un ensemble (ou 'set') est un objet non séquentiel et mutable.

Un "set" à une autre qualité : les doublons dans cette série sont impossibles.

Et si je vous dis qu'une liste est un objet séquentiel mutable. Qu'en déduisez vous ???

Comparaison des différents conteneurs

Les points communs

Tous les conteneurs sont des structures itérables : peuvent être parcourues par une boucle.

Pour tous les conteneurs on peut appliquer la fonction générique len() qui retourne la longeur du conteneur.

On peut appliquer à n'importe quel type de conteneur l'opérateur d'appartenance "in" (ou "not in").

Conteneurs séquentiels et non séquentiels

Une séquence est un conteneur ordonné et indexable.
Les listes, les chaines, les tuples et les "range" sont des séquences.
Par contre les ensembles et les dictionnaires ne sont pas des séquences.

>>> maliste = list(range(10))
>>> len(maliste)
10
>>> maliste[-1]
9
>>> 8 in maliste
True
>>> monset = set(range(10))
>>> len(monset)
10
>>> 7 in monset
True
>>> monset[-1]
...
TypeError: 'set' object is not >>> maliste = list(range(10)
... )
>>> len(maliste)
10
>>> maliste[-1]
9
>>> 8 in maliste
True
>>> maliste.index(0)
0
>>> monset = set(range(10))
>>> len(monset)
10
>>> 7 in monset
True
>>> monset[-1]
Traceback (most recent call last):
  File "", line 1, in 
TypeError: 'set' object is not subscriptable
>>> monset.index(0)
...
AttributeError: 'set' object has no attribute 'index'

Un "set" n'est pas "subscriptable" (indexable) ; on ne peut accéder à un élément via son index. On ne peut non plus faire de recherche d'index à partir de la valeur : .index() non disponible.

Conteneurs mutables et immuables

Il existe des conteneurs que l'on peut modifier après leur création et d'autres non modifiables après leur création.
Un tuple, une chaine & un 'range' sont immuables : on ne peut pas modifier leur contenu.
Les autres conteneurs (liste, dictionnaire, ensemble) sont mutables : on peut modifier leur contenu (ajout, suppression, insertion).

>>> del maliste[0]
>>> maliste
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> machaine ="Bonjour mes amis"
>>> del machaine[-1]
...
TypeError: 'str' object doesn't support item deletion
>>>del machaine
>>> machaine
...
NameError: name 'machaine' is not defined

J'arrive à supprimer un élément d'une liste mais je n'arrive pas à supprimer un caractère d'une chaine.
Chaque conteneur de type mutable propose différentes méthodes pour ajouter, insérer, supprimer des éléments.
J'ai supprime "machaine" avec la fonction del(nomConteneur).

Parcourir une séquence avec for ... enumerate

Les conteneurs sont des séquences itérables : peuvent être parcourues par une boucle.
Dans une boucle basique : for ... in série l'indice n'apparait pas. Par contre l'index(ou indice) apparait dans la boucle for ...in enumerate(nomConteneur)

Parcourir un objet "range"

>>> impairs =range(1,11,2)
>>> impairs
range(1, 11, 2)  #affichage d'un objet range
>>> for indice, ele in enumerate(impairs):
...     print(indice,ele)
...
0 1
1 3
2 5
3 7
4 9

Donc emploi de trois fonctions natives (ou génériques) de Python : list(), tuple(), set()

Parcourir une liste

>>> liste = list(range(1,10,2))
>>> liste
[1, 3, 5, 7, 9]
>>> for index, valeur in enumerate(liste):
...     print(index, valeur)
...
0 1
1 3
2 5
3 7
4 9

J'espère que vous notez bien l'apport de for ... in enumerate() par rapport à for ... in

Je peux appliquer for ... enumerate à une chaine ou à un tuple ou un ensemble.

Parcourir une chaine

>>> chaine ="Bonjour"
>>> for index,car in enumerate(chaine):
...     print(index,car)
...
0 B
1 o
2 n
3 j
4 o
5 u
6 r

Parcourir un tuple

>>> tuple = tuple(range(5))
>>> tuple
(0, 1, 2, 3, 4)
>>> for index,ele in enumerate(tuple) :
...     print(index,ele)
...
0 0
1 1
2 2
3 3
4 4

Découvrir les méthodes d'une classe

Quelles méthodes puis-je employer pour chaque type de conteneur ?
Inutile d'apprendre par coeur les méthodes pour chaque type de collection ; il suffit de faire appel à l'aide Python.
Pour connaitre les méthodes d'une classe il suffit de taper dans la console de Python : dir(nomClasse) OU help(nomClasse)

La commande help(nomClasse)

A l'exception des méthodes magiques (évoquées plus tard) la classe tuple ne propose que deux méthodes ...
Ce qui est assez logique puisqu'il s'agit d'une série immuable.

Commande dir(nomClasse)

Revenons à la commande dir(nomClasse) qui offre l'avantage de donner une information synthétique.
Pour chaque type d'objet je n'affiche que les méthodes standards.

Méthodes de l'objet range

>>> dir(range)
 'count', 'index', 'start', 'step', 'stop'
>>> impairs =range(1,11,2)
>>> del impairs[-1]
...
TypeError: 'range' object doesn't support item deletion

Un objet 'range' est une séquence non mutable.

Méthodes de liste

>>> dir(list)
'append', 'clear', 'copy', 'count', 'extend',
 'index', 'insert', 'pop', 'remove', 'reverse', 'sort'

Une liste est une séquence mutable d'où les nombreuses méthodes qui modifient le contenu de la liste : .append(), .clear(), .insert(), .pop(), .remove(), .sort(), .reverse()
Tous les conteneurs mutables proposent la méthode .clear()
Tous les conteneurs séquentiels proposent la méthode .index()

Méthodes d'ensemble

>>> dir(set)
 'add', 'clear', 'copy', 'difference', 'difference_update', 
'discard', 'intersection', 'intersection_update', 
'isdisjoint', 'issubset', 'issuperset', 'pop', 'remove', 
'symmetric_difference', 'symmetric_difference_update', 
'union', 'update'
>>> monset
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
>>> monset.discard(5)
>>> monset
{0, 1, 2, 3, 4, 6, 7, 8, 9}
>>> del monset[0]
...
TypeError: 'set' object doesn't support item deletion

un ensemble est une collection d'éléments non ordonnés ; c'est donc un conteneur non séquentiel mais mutable.
Pour supprimer un élément on doit utiliser la méthode .discard(élement). On ne peut utiliser la fonction nomConteneur.del[index]

Méthodes de tuple

>>> dir(tuple)
'count', 'index'

Un tuple est une séquence non mutable d'où l'absence de méthodes pour modifier son contenu.

Méthodes de dictionnaire

>>> dir(dict)
'clear', 'copy', 'fromkeys', 'get', 'items',
 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values'
>>>

Un dictionnaire est un conteneur non séquentiel ; c'est un conteneur mutable.

Méthodes de chaine

>>> dir(str)
 'capitalize', 'casefold', 'center', 'count', 'encode', 
 'endswith', 'expandtabs', 'find', 'format',
 'format_map', 'index', 
 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit',
 'isidentifier', 'islower', 'isnumeric', 
 'isprintable', 'isspace', 
 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 
 'maketrans', 'partition', 'removeprefix', 
 'removesuffix', 'replace',
 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 
 'split', 'splitlines', 'startswith',
 'strip', 'swapcase', 'title',  'translate', 'upper', 'zfill'
>>>
>>> machaine
'Bonjour mes amis'
>>> machaine.upper()
'BONJOUR MES AMIS'
>>> machaine
'Bonjour mes amis'
>>> machaine = machaine.upper()  # on écrase l'ancien contenu de la variable "machaine"
>>> machaine
'BONJOUR MES AMIS'

Une chaine est une séquence non mutable.

La liste des méthode disponible est impressionnante alors qu'il s'agit d'une série non mutable ...
Mais ces méthodes se contentent de modifier l'affichage de la chaine à moins d'écraser l'ancien contenu d'origine de la variable chaine.

Méthodes de classe et fonctions génériques

Comme je disais plus haut, vous avez parfois le choix entre une fonction générique ou une méthode de classe pour certains traitements.

Trier une liste par ordre croissant

Si vous voulez trier une liste, vous avez le choix entre la méthode .sort() de la classe "list" et la fonction native sorted()

>>> maliste =list(range(11,1,-2))
>>> maliste
[11, 9, 7, 5, 3]
>>> sorted(maliste)
[3, 5, 7, 9, 11]
>>> maliste
[11, 9, 7, 5, 3]
>>> maliste.sort()
>>> maliste
[3, 5, 7, 9, 11]

La fonction générique sorted() se contente d'afficher la liste triée mais ne modifie pas la liste d'origine.
Par contre la méthode .sort() modifie l'ordre des éléments dans la liste.

On peut appliquer la fonction sorted() à d'autres types de conteneurs.
Cette fonction affiche toujours en retour une liste.

Trier une liste par ordre décroissant

>>> pairs = list(range(0,16,2))
>>> pairs
[0, 2, 4, 6, 8, 10, 12, 14]
>>> sorted(pairs,reverse = True)
[14, 12, 10, 8, 6, 4, 2, 0]
>>> pairs
[0, 2, 4, 6, 8, 10, 12, 14]
>>> pairs.sort(reverse = True)
>>> pairs
[14, 12, 10, 8, 6, 4, 2, 0]
>>>

Pour trier effectivement une liste par ordre "descending" (ou décroissant) il faut utiliser l'argument reverse = True avec la méthode .sort() .

Trier une chaine

>>> machaine ="CBAcba"
>>> chaine_triee = sorted(machaine)
>>> machaine
'CBAcba'
>>> chaine_triee
['A', 'B', 'C', 'a', 'b', 'c']
>>> type(chaine_triee)
class 'list'
>>> machaine = sorted(machaine)
>>> machaine
['A', 'B', 'C', 'a', 'b', 'c']

La fonction sorted() affiche la chaine sous forme d'une liste.
C'est une façon de produire une liste à partir d'une chaine si vous affecter le tri à une variable OU de convertir une chaine en liste si vous écrasez l'ancien contenu.
Les lettres majuscules apparaissent en tête car leur code ASCII est plus petit que les minuscules.

Les fonctions génériques peuvent être incontournables

Pour certaines manipulations sur les conteneurs, il n'y a pas de méthodes ; il faut utiliser les fonctions génériques. Vous pensez déjà aux fonctions type() et len() mais il y en a d'autres ...

>>> unset = set(range(1,11,2))
>>> unset
{1, 3, 5, 7, 9}

>>> max(unset)
9
>>> min(unset)
1
>>> sum(unset)
25
>>> unechaine ="Salut"
>>> len(unechaine)
5
>>> max(unechaine)
'u'
>>> min(unechaine)
'S'
>>>
>>> sum(unechaine)
...
TypeError: unsupported operand type(s) for +: 'int' and 'str'
>>> del (unset)
>>> del (unechaine)

Ci-dessus j'ai appliqué les fonctions len(), max(),min() & sum() à un objet "set" et un objet "str". Les méthodes de classe pour effectuer de tels traitements n'existent pas !

La commande del nomConteneur permet de supprimer un conteneur. Ne confondez pas avec la méthode .clear() qui existe pour les séries mutables et qui vide le conteneur (sans le supprimer).

Retour sur les chaines (instances de la classe 'str')

L'image est grivoise mais le discours qui suit est des plus sérieux.
Dans ce paragraphe j'évoque des traitements sur les chaines qui n'ont pas encore été évoqués.

Rechercher une sous-chaine dans une chaine

Pour réchercher un caractère (ou plusieurs) dans une chaine, vous pensez à index() mais il existe aussi find(), rindex() et rfind(), startswith(), endswidth() .

>>> machaine ="avec cesar"
>>> machaine.index('a')
0
>>> machaine.index('y')
ValueError: substring not found
>>> machaine.find('y')
-1
>>> machaine.rindex('a')
8
>>> machaine.rfind('a')
8
>>> machaine.rindex('y')
ValueError: substring not found
>>> machaine.rfind('y')
-1
>>> "y" in machaine
False
>>> "y" not in machaine
True
>>> "a" in machaine
True
>>> chaine1 ='bonjour'
>>> chaine1.startswith('bon')
True
>>> chaine1.endswith('soir')
False
>>>
>>> machaine_cryptee = machaine.replace('a','1').replace('e','2')
>>> machaine_cryptee
'1v2 c2s1r'
>>> machaine
'avec cesar'

Supprimer les espaces dans une chaine

>>> machaine = "     bon jour    "
>>> machaine.strip()
'bon jour'
>>> machaine.lstrip()
'bon jour    '
>>> machaine.rstrip()
'     bon jour'
>>> machaine.strip().replace(" ","")
'bonjour'
>>> machaine    # commande 5
'     bon  jour    '
>>> machaine2 = machaine.strip().replace(" ","") #commande 6
>>> machaine2
'bonjour'
>>> machaine = machaine.strip().replace(" ","") #commande 7
>>> machaine
'bonjour'
>>>

lstrip() efface les espaces en début de chaine, rstrip() efface les espaces en fin de chaine ;
strip() équivaut à lstrip().rstrip().
Pour effacer les espaces en milieu de chaine il pouvez utiliser .replace() de la façon suivante : replace(" " ."") (remplacer espace par rien).

Un objet string est immuable ; vous ne pouvez pas modifier le contenu d'une chaine.
L'astuce consiste donc à créer une nouvelle variable : machaine2 = machaine.strip() ... OU écraser le contenu d'origine de la variable : machaine = machaine.strip() ...

Obtenir une chaine débarassée de symboles et espaces

Les méthodes strip(), lstrip() & lstrip() sans arguments effacent les espaces. Mais vous pouvez argumenter ces méthodes. pour vous débarasser d'autres caractères.

>>> machaine =" *  Bonjour * - "
>>> machaine = machaine.lstrip(" *-").rstrip(" * -")
>>> machaine
'Bonjour'
>>>

Pour effacer des caractères parasites en milieu de chaine l'astuce consiste à utiliser replace() avec en deuxième argument une chaine vide.

>>> chainebizarre ="B*- on*-jour"
>>> chainecorrigee = chainebizarre.replace(" ","").replace("*","").replace("-","")
>>> chainecorrigee
'Bonjour'

Le "slicing" (ou découpage) d'une chaine

Le 'slicing" a déjà été évoqué. Une simple expression entre crochets signifie beaucoup de choses. Aussi la compréhension de cette expression n'est pas toujours facile.
Sachez que l'indice peut être une valeur négative afin d'extraire à partir de la fin (ou droite de la chaine).

Exemple de "slicing" (découpage)

Rappelez vous, dans le chapitre 6 j'ai évoqué le code de César : un système de cryptage simple reposant sur un décalage de n lettres.

Grâce au découpage j'obtiens la table de conversion ("a" devient "c" , "b" devient "d" ... "z" devient "b") dans l'hypothèse d'un décalage de 2 (valeur de n).

Convertir une chaine en nombre

Voilà une problèmatique que vous allez rencontrer souvent en programmation Python.
En effet la fonction input() retourne toujours une donnée de type 'str' même si vous n'avez saisi que des chiffres.

>>> chaine ="1245"
>>> int(chaine)
1245
>>> chaine2 ="1245.55"
>>> int(chaine2)
ValueError: invalid literal for int() with base 10: '1245.55'
>>> nombre =eval(chaine2)
>>> type(nombre)
class 'float'
>>> nombre
1245.55

La fonction int(chaine) convertit une chaine en entier si celle-ci ne contient qu'une suite de chiffres mais "plante" si le format de la chaine est celle d'un flottant.
La fonction eval(chaine) convertit une chaine numérique en flottant. La fonction float(chaine) fait la même chose !

Retour sur les listes

Une liste est mutable ; on peut modifier son contenu initial.
C'est pour cette raison que les méthodes de liste sont nombreuses et beaucoup visent à modifier le contenu de la séquence.

Modification d'une liste via le "slicing"

>>> maliste = list(range(10))
>>> maliste
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> maliste[0:] = range(2,12)
>>> maliste
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
>>> del(maliste[0:])
>>> maliste
[]

Dans la liste chaque élément remplacé par son ancienne valeur + 2
Puis la liste est vidée.

Rappels sur les principales méthodes applicables à un objet 'list'

>>> maliste =[2,3,2,5,7,9]
>>> maliste.append(11)
>>> maliste
[2, 3, 2, 5, 7, 9, 11]
>>> maliste.reverse()
>>> maliste
[11, 9, 7, 5, 2, 3, 2]
>>> maliste.sort()
>>> maliste
[2, 2, 3, 5, 7, 9, 11]
>>> maliste.remove(11)
>>> maliste.pop(0)
>>> maliste
[2, 3, 5, 7, 9]
>>> maliste.clear()
>>> maliste
[]

Il existe aussi les méthodes extend() pour ajout N éléments en fin de liste et insert(position, valeur) pour insérer un élément au milieu d'une liste.

Ci-dessous je vous montre que vous pouvez modifier le contenu d'une liste sans recourir à des méthodes de liste.

>>> maliste =list(range(10))
>>> maliste
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> maliste[0] = 11
>>> maliste
[11, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> del maliste[-1]
>>> maliste
[11, 1, 2, 3, 4, 5, 6, 7, 8]

J'ai modifié la valeur du premier élément puis j'ai supprimé le dernier élément.

Inconvénients des listes

On ne peut pas vraiment comparer une liste Python avec un tableau indicé dans d'autres langages.
En effet rien n'interdit dans une liste de mélanger des entiers avec des flottants ou des booléens ou des chaines.
Or en Informatique un tableau indicé est une série de données de même natures (uniquement des entiers, par exemple).

Vous verrez plus loin dans ce chapitre que le module array permet de créer de véritables tableaux indicés en Python.

Retour sur les dictionnaires

Un dictionnaire (tableau associatif dans d'autres langages) est un conteneur qui comprend non pas des éléments mais des paires "clé : valeur" appelées items.

 
>>> repertoire = {"marius":"0601010101", "émile":"0602020202",
"bernard": "0603030303","damien":"0604040404"}
>>> type(repertoire)

>>> repertoire.get("damien")
'0604040404'
>>> repertoire.get("louise")
>>> repertoire["françois"] ="0605050505"
>>> rperteoire.pop("marius")
'0601010101'
>>> repertoire["émile"] = "0701010101"
>>> repertoire.popitem()
('françois', '0605050505')
>>> repertoire.clear()
>>> repertoire
{}
>>> ajout = {'alain': '0601010101', 'bernard':'0602020202'}
>>> type(ajout)

>>> repertoire.update(ajout)
>>> repertoire
{'alain': '0601010101', 'bernard': '0602020202'}
>>> repertoire_copie = repertoire
>>> repertoire_copie
{'alain': '0601010101', 'bernard': '0602020202'}

Les frozensets

Un frozenset est un set non mutable !

>>> monfrozenset = frozenset(range(10))
>>> monfrozenset
frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
>>> monfrozenset.add(10)
...
AttributeError: 'frozenset' object has no attribute 'add'
>>> type(monfrozenset)
class 'frozenset'
>>> dir(frozenset)
...
 'copy', 'difference', 'intersection', 'isdisjoint', 'issubset', 'issuperset',
 'symmetric_difference', 'union']
>>>

On peut procéder aux opérations union et intersection sur un frozenset.
Les méthodes .add(), .discard(), .clear() ne sont pas , et c'est logique, disponibles.

Les tableaux en Python

En programmation, un tableau est une collection d'éléments du même type.
Or rien n'interdit dans une liste Python de mélanger des entiers, des chaines, des flottants, etc.

Le module array de Python permet de créer de véritables tableaux indicés au sens informatique du terme c'est à dire des séries dont tous les éléments ont le même type.

Création d'un tableau d'entiers

>>> from array import array
>>> b = array('i', range(10))
>>> b
array('i', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> b[0] = 10  # changer une valeur 
>>> b
array('i', [10, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> b.append(10.5)
...
TypeError: integer argument expected, got float
>>> b.append(11)
>>> b
array('i', [10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11])
>>> type(b)
class 'array.array'
>>> for i,element in enumerate(b):
...     print(i, element)
...
0 10
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
10 11
>>> sorted(b)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
>>> del(b[0:])
>>> b
array('i')
>>>

b = array('i', range(10)) : création d'un tableau indicé ne comprenant que des entiers.
Le constructeur "array" est obligatoire ainsi que le type de contenu :

J'ai tenté d'ajouté un nombre décimal au tableau : échec !
J'ai utilisé l'accès indexé pour modifier un élément puis pour vider tout le tableau.
Je vous montre aussi qu'un "array" est itérable.

"array.array et numpy.array"

Dans le chapitre précédent je vous ai présenté la bibliothèque numpy qui permet de créer des tableaux. Et maintenant je vous parle des tableaux créés avec le module array. Il y a de quoi s'y perdre.

En fait la bibliothèque numpy permet de créer des tableaux au sens mathématique du terme c'est à dire des matrices.
Alors que les "array" du module du même nom n'ont aucune vocation mathématique ; ce sont simplement des listes typées ; on ne peut faire de calculs matriciels sur des "array.array".
Par ailleurs les "numpy.array" sont immuables alors que "array.array" sont mutables.

Conclusion

J'espère qu'à l'issue de ce chapitre vous avez désormais les idées plus claires.
Vous avez aussi découvert deux types de conteneurs que je n'avais pas encore évoqué : les frozensets et les arrays.
Si vous savez qu'un conteneur est séquentiel (ou pas), mutable (ou pas) vous en déduirez les traitements autorisés et ceux qui ne le sont pas.
Prenons l'exemple d'une chaine : c'est une séquence non mutable donc les méthodes .index() & .count() sont disponibles et on peut pratiquer l'accès indexé. Par contre aucune méthode de modifification n'est disponible.
Autre exemple la liste qui est un séquence mutable. Donc de nombreuses méthodes de modification sont présentes ; .append(), .insert(), .pop(), .remove(), .extend(), .clear()

N'importe quel conteneur (mutable ou immuable) peut être supprimé avec la commande del(nomConteneur).