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 - les expressions régulières

Un conteneur dans le jargon Python désigne un objet qui contient une collection d'éléments donc une variable qui contient plusieurs valeurs.
Vous connaissez déjà les différents types de conteneurs : listes, chaines, tuples, dictionnaires, ensembles.

Il est possible qu'à ce stade de votre apprentissage de Python, vous soyez un peu perdu, que vous fassiez de nombreuses confusions.
C'est normal il faut un peu de temps pour décanter toute l'information contenue dans les précédents chapitre.

Je profite aussi de ce chapitre pour évoquer les expressions régulières en Python.
Si vous connaissez JavaScript & PHP vous connaissez cet outil qu'on retrouve dans tous les langages.

Considérez ce chapitre comme une pause dans votre apprentissage de Python.
Vous apprendrez peu de choses mais ce sera l'occasion d'y voir plus clair, de dégager une typologie des conteneurs, d'acquérir une méthodologie pour retenir sans peine et aussi pour trouver rapidement l'information.

Terminologie

Sachez que l'on peut attribuer à chaque type de conteneur des qualificatifs et qu'à partir de ces propriété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.

Exemple :

>>> montuple = 5,4,3
>>> montuple[0]
5
>>> montuple.index(5)
0
>>> del montuple[0]
TypeError: 'tuple' object doesn't support item deletion

Et si je vous dis qu'une liste est un objet séquentiel et 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 subscriptable
>>> 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]
...
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 indice. On ne peut non plus utiliser la méthode index()

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).

>>> maliste = list(range(10)
>>> del maliste[0]
>>> maliste
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> machaine ="Bonjour mes amis"
>>>machaine[0] ='b'
...
TypeError: 'str' object does not support item assignment

J'arrive à supprimer un élément d'une liste mais je n'arrive pas à remplacer un caractère d'une chaine.
Chaque conteneur de type mutable propose différentes méthodes pour ajouter, insérer, supprimer des éléments.

Parcourir une séquence avec for ... enumerate

Les conteneurs sont des structures 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)

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)
Vous constaterez que certaines méthodes sont partagées par plusieurs types de conteneurs.
Ainsi on retrouve la méthode index( chez toutes les structures séquentielles.

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 structure 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 l'objet list

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

Une liste est une séquence mutable.

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

Vous avez parfois le choix entre une fonction "built in" 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 "built in" 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]
>>> machaine ="bonjour"
>>> sorted(machaine)
['b', 'j', 'n', 'o', 'o', 'r', 'u']
>>> montuple = 5,4,3,2
>>> sorted(montuple)
[2, 3, 4, 5]

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 sur place ; l'ordre d'origine est perdu.

On peut appliquer la fonction sorted() à d'autres types de conteneurs.
Cette fonction retourne alors 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() .

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 génériques 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 le paragraphe ci-dessous j'évoque des traitements sur les chaines qui n'ont pas encore été évoqués.

Rechercher une sous-chaine dans une chaine

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

Vous pouvez aussi faire une recherche en utilisant une RE (Regular Expression), une expression régulière.

>>> 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'

Pour effectuer une recherche sur une chaine vous pouvez aussi la comparer à une expression régulière via la méthode search() du module re ("re" comme regular expression).
Voir la deuxième partie de ce chapitre.

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.
Rappel : l'indice peut être une valeur négative afin d'extraire à partir de la fin 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.

>>> alphabet ="abcdefghijklmnopqrstuvwxyz"
>>> n=2
>>> alphabet_2n =alphabet[n:] + alphabet[:n]
>>> alphabet_2n
'cdefghijklmnopqrstuvwxyzab'

Grâce au "slicing" 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 valeur numérique

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 qu'une suite de chiffres ...

>>> chaine ="1245"
>>> type(chaine)
class 'str'
>>> somme = chaine *2
>>> somme
'12451245'
>>> 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

somme = chaine * 2 : dans certains langages (JS) il y aurait conversion du contenu de "chaine" en numérique puis multiplication par 2.

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 (le séparateur décimal dans la chaine).
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 "maliste" chaque élément a été remplacé par son ancienne valeur + 2
Puis la liste est vidée (mais pas supprimée).

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

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é le contenu de la liste sans utiliser de méthodes de liste mais uniquement via des accès indexés.

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)
class 'dict'
>>> repertoire.get("damien")
'0604040404'
>>> repertoire.get("louise")
>>> repertoire["françois"] ="0605050505"
>>> rperteoire.pop("marius")
'0601010101'
>>> repertoire["émile"] = "0701010101"
>>> repertoire.clear()
>>> repertoire
{}
>>> ajout = {'alain': '0601010101', 'bernard':'0602020202'}
>>> repertoire.update(ajout)
>>> repertoire
{'alain': '0601010101', 'bernard': '0602020202'}

Pour créer un dictionnaire on utilise le format JSON (accolades, clés sous forme de chaine).

Les frozensets

Un frozenset est un set non mutable !
Donc si vous voulez un série immuable et sans doublons optez ce type de conteneur.

>>> dir(frozenset)
'copy', 'difference', 'intersection', 'isdisjoint', 'issubset', 'issuperset', 'symmetric_difference', 'union'
>>> 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'
>>> frozenset2 = frozenset({5,5,3,3})
>>> frozenset2
frozenset({3, 5})

On peut procéder aux opérations union et intersection sur un frozenset.

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 où 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 !
Cet ajout aurait été possible si la variable "b" était une simple liste.
Puis 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.

Les expressions régulières

Les RE ne sont pas propres à Python ; c'est un outil commun à de nombreux langages. Et de plus la syntaxe est quasi identique dans ces différents langages.

à quoi ça sert les expressions régulières ?

Dans le cadre d'une saisie une RE permet un contrôle de saisie efficace (remplace de nombreux tests).

Une RE permet aussi de recherche des occurences dans une chaine pour éventuellement effectuer un remplacement.

Construire des RE simples

Une RE est une chaine donc entre guillemets.
Pour une coïncidence exacte avec la chaine à laquelle elle est comparée, elle doit commencer par ^ et se terminer par $.

RE simples - exemples

On peut utiliser des caractères ordinaires.
Nous cherchons si le mot contient "bonjour" ; la RE sera : "bonjour"
Nous cherchons si le mot commence par "abc" ; la RE sera : "^abc"
Nous cherchons si le mot commence par "isme" ; la RE sera: "isme$"
Nous cherchons si le mot coïncide strictement à "bonjour" ; la RE sera : "^bonjour$".
Nous cherchons si le mot contient "salut" quelque soit la casse ; la RE sera "salut" mais il faudra rajouter un argument à la méthode search() du module re ; voir ci-dessous.

Un script où on ne tient pas compte de la casse

Le code
# expression_reguliere.py
import re
format ="chat"
while True:
    mot_saisi = input("saisir un mot  ou 999:")
    if mot_saisi =="999" :
        break
    if re.search(format,mot_saisi,re.IGNORECASE):
        print(f" {mot_saisi} contient la RE  {format}")
    else:
        print(f" {mot_saisi} ne contient pas la RE {format}")

Pour créer et manipuler des expressions régulières il faut importer le module re (comme regular expression).

...re.search(format,mot_saisi,re.IGNORECASE) : la méthode search() comprend un troisième argument qui précise qu'il ne faut pas tenir compte de la casse.

Exécution
saisir un mot  ou 999:chaton
 chaton contient l'expression  chat
saisir un mot  ou 999:CHATEAU
 CHATEAU contient l'expression  chat
saisir un mot  ou 999:cHateAU
 cHateAU contient l'expression  chat
saisir un mot  ou 999:chapeau
 chapeau ne contient pas l'expression chat
saisir un mot  ou 999:

CHATEAU & cHateAU contiennent "chat" si on ne tient pas compte de la casse.

RE - exemples complexes

Une RE complexe peut être une suite de paires "classe-quantificateur".

Les classes

On reconnait une classe car elle est entre crochets. Une classe est l'énumération de caractères autorisés sous forme d'une liste ou d'un intervalle.
Des exemples sont préférables à un long discours.

À la place de classes on peut utiliser certains raccourcis.

Les quantificateurs

Un quantificateur suit une classe et indique le nombre de caractères de la classe autorisés.

Exemples de paires "classe - quantificateur"

Les métacaractères

Les symboles *,+, . , ? , ^, $ sont utilisés dans la syntaxe de construction d'une RE.
Si vous voulez les utiliser dans une classe en tant que caractères autorisés, il faut alors les échapper.

Contrôle de saisie via RE - exemple1

Il s'agit d'une amélioration du programme "cercle.py". Voir chapite X.
Les fonctions sont identiques à la version précédente.
Ce programma autorise la saisie d'un nombre décimal.

Le code

#programme principal
import math
import re
from turtle import *
format = "^[0-9\.]{1,}$"
rayon =""
while not re.search(format,rayon): 
    rayon = input("saisir le rayon sous forme d'un nombre entier ou décimal  : ")
rayon =eval(rayon)      #conversion chaine en numérique
circonference, aire = cercle1(rayon)    # appel fonction cercle1
print ("circonférence du cercle : " , circonference)
print ("aire du cercle : " , aire)
cercle2(rayon)  #appel procédure cercle2
...

format ="^[0-9\.]{1,}$" : seuls les chiffres et le point sont autorisés dans la saisie ; au moins 1 de ces caractères
Il y a ici une classe et un quantificateur.

Exécution du script

saisir le rayon sous forme d'un nombre entier ou décimal  : 12cm
saisir le rayon sous forme d'un nombre entier ou décimal  : 12,5
saisir le rayon sous forme d'un nombre entier ou décimal  : 12.5
circonférence du cercle :  78.53981633974483
aire du cercle :  490.8738521234052

Seuls les chiffres et le point sont autorisés par la RE.
Donc si la chaine saisie via INPUT contient autre chose, on reste dans la boucle de saisie.

RE exemple 2 : contrôle de saisie d'une adresse mail

Cette fois la RE est relativement complexe.

Le programme

# expression_reguliere2.py
import re
format_mail ="^\w{1,}@\w{2,}\.[A-z]{2,4}$"
while True:
    email_saisi = input("saisir une adresse mail valide ")
    if email_saisi =="999" :
        break
    if re.search(format_mail,email_saisi):
        print(f" {email_saisi} est une adresse mail valide")
    else:
        print(f" {email_saisi} est une adresse mail invalide")

Observez la RE ; j'ai utilisé la classe abrégée \w qui équivaut à [A-z0-9_]

Exécution

saisir une adresse mail valide :  darchefreefr
darchefreefr est une adresse mail invalide
saisir une adresse mail valide :  darche@free
darche@free est une adresse mail invalide
saisir une adresse mail valide :  darche@free.fr
darche@free.fr est une adresse mail valide