7. Les types composés#

Mis à jour : May 15, 2025, lecture : 15 minutes minimum, PhL.

Les types composés ou conteneurs de python sont introduits dans ce chapitre :

  • les types non mutables de python : tuple et str

  • les types mutables de python : list, set et dict

Rmq de vocabulaire :

J’utilise les termes mutable et non mutable.
La version française de la documentation de python préfère les termes muable et immuable.

7.1. Les types composés ou conteneurs en python#

Les types scalaires suivants permettent de manipuler des valeurs scalaires :

  • bool

  • int

  • float

  • complex

  • les caractères (souvent dénotés de type char mais absent en python)

Un type composé permet de regrouper plusieurs valeurs, de même type ou non, scalaire ou non, dans une seule variable (ou un seul objet).

Vocabulaire. Ces types composés sont appelés des conteneurs en python. Ce sont aussi des premiers exemples de structures de données en algorithmique.

Exercice. Vous en connaissez déjà au moins un. Lequel ?

7.1.1. Les questions abordées dans ce chapitre#

  • Comment créer un conteneur ?

  • Comment accéder à une valeur dans un conteneur ?

  • Comment modifier un conteneur ?

    • Comment modifier une de ses valeurs ?

    • Comment ajouter ou supprimer une valeur ?

  • Comment parcourir (toutes) les valeurs d’un conteneur ?

Les réponses à ces questions dépendent :

  • du type de conteneur,

  • de son caratère mutable ou non,

  • de son caractère itérable ou non,

  • de son caractère séquençable ou non.

(\(\star\)) Les deux dernières caractéristiques (itérable, séquençable) seront abordées en seconde lecture.

(\(\star\)) On analysera aussi la complexité des ces différents traitements selon les principaux types composés.

7.1.2. La vision classique#

Dans la plupart des langages de programmation, les types composés classiques sont les suivants.

Les tableaux, déjà revus dans ce chapitre, sont définis comme des ensembles de valeurs de même type et de nombre connu et fixé “une fois pour toute” lors de leur définition (ce nombre est la taille ou la dimension du tableau) et dont les valeurs sont ainsi stockées de façon contigüe en mémoire.
- les valeurs sont accédées par un ou des indice indiqué entre un ou plusieurs []

Les enregistrements ou structures sont définis comme un ensemble de valeurs de types quelconques. Le nombre de valeurs est défini “une fois pour toutes” mais sans hypothèse sur le mode de stockage.
- les enregistrements sont composés de champs nommés aussi appelés attributs
- la valeur stockée dans le champ d’un enregistrement est accessible en notation pointée : id_de_variable.nom_du_champ.

Aucun type composé natif de python ne correspond exactement aux 2 types composés classiques tableau et enregistrement. Le module numpy définit “de vrais” tableaux, les ndarray, que nous abordons dans cette annexe.

Exemple : Une structure etudiant pourrait être définie à partir des 4 champs suivants :

  • etudiant.numero_etud,

  • etudiant.nom,

  • etudiant.prenom et

  • etudiant.diplome.

7.1.3. Les conteneurs python#

Vocabulaire python : un conteneur est une variable (un objet) de type composé

Les conteneurs python sont répartis en 2 familles.

  1. Les conteneurs non mutables dont la forme et les valeurs sont fixées une fois pour toute lors de leur définition :

    • les chaines de caractères : str écrits entre des "  " ou des '  '

    • les t-uplets ou tuple : des valeurs écrites entre (  ) et séparées par des ,

    • les ensembles ou set : des valeurs écrites avec des {   } et séparées par des ,

  2. Les conteneurs mutables qui peuvent changer de forme et de valeurs.

    • des listes ou list : des valeurs écrits entre [  ] et séparées par des ,

    • des dictionnaires (tableaux associatifs) ou dict : des couples de valeurs écrits entre { } et séparées par des ,

  • les fichiers de texte

Les fichiers (le type file) sont aussi des conteneurs. Ils ne sont pas abordés ici et font l’objet de ce chapitre.

7.2. Ce qui est commun aux conteneurs python#

7.2.1. Opérations communes aux conteneurs python#

itération sur les valeurs d’un conteneur :

for val in conteneur:

  • pythonerie utile en pratique pour parcourir les valeurs d’un conteneur sans se soucier de son “indice”

  • cette itération s’effectue indépendamment de l’existence ou non d’un ordre sur les valeurs du conteneur, ie. séquence ou non séquence

    • on reviendra sur cette notion, en particulier pour les dictionnaires

  • tous les conteneurs sont ainsi des itérables.

accès à l’élément par indexation pour les séquences et dictionnaires :

v[1],m[3][2], st[-1], tuple[-len(tuple)], d[clé]

  • L‘“indice” est indiqué entre [ ]

  • Cet indice est un/des entier/s sauf pour les dictionnaire : notion de “clé”

  • Un conteneur ordonné par des indices entiers est appelé une séquence.

tranche (slice) d’un conteneur séquence :

conteneur[i:j] est composé des valeurs de l’indice i à j-1

Des fonctions de base qui s’appliquent à une variable conteneur :

  • len() qui renvoie le nombre de valeurs du conteneur

  • min(), max(), sorted() si une relation d’ordre s’applique aux éléments d’un conteneur (itérable)

    • Rmq: sorted() créé un nouveau conteneur list.

Exercice. Que pensez-vous de la variable v définie par v = range(10) par exemple ?

v = range(10)
print(v)

for val in v:
    print(val, end=",")
print()

print(type(v), type(val))
range(0, 10)
0,1,2,3,4,5,6,7,8,9,
<class 'range'> <class 'int'>

Réponse : v peut être vue comme un ensemble ordonné itérable de valeurs entières. Ici le terme ensemble est à comprendre au sens mathématique ; il ne s’agit pas des ensembles set de python présentés plus loin.

L’exemple suivant illustre la différence entre un tableau et un conteneur de type list.

from math import exp

en_vrac = ['a','z','e','r','t', 1, 2, 3, 3.14159, exp(1.0)]
print(type(en_vrac), len(en_vrac))
<class 'list'> 10

Et maintenant quelques manipulations avec les opérations de base sur ces conteneurs.

print(en_vrac)

# conversion de type : list -> tuple
des_float = tuple(en_vrac[-2:]) 

# len, min, max
print(des_float, len(des_float), min(des_float), max(des_float), end="\n")
['a', 'z', 'e', 'r', 't', 1, 2, 3, 3.14159, 2.718281828459045]
(3.14159, 2.718281828459045) 2 2.718281828459045 3.14159
# accès par indexation et tranche
print(en_vrac)
print(en_vrac[0], en_vrac[-3])
print(des_float)
print(des_float[1], des_float[1:3])
#print(type(en_vrac[0]), type(en_vrac[-3]), type(des_float[1]), type(des_float[1:3]))
['a', 'z', 'e', 'r', 't', 1, 2, 3, 3.14159, 2.718281828459045]
a 3
(3.14159, 2.718281828459045)
2.718281828459045 (2.718281828459045,)
# les tranches de conteneurs sont souvent très utiles.
print(en_vrac[1:-1])
print(des_float[1:3])
['z', 'e', 'r', 't', 1, 2, 3, 3.14159]
(2.718281828459045,)
# conversion en `str` et concaténations
print(en_vrac)
st = ''
for c in en_vrac[0:-2]:
    #st = st + c     # non : normal !
    st = st + str(c)  # concatenation de chaines de caractères
print(st, type(st))
['a', 'z', 'e', 'r', 't', 1, 2, 3, 3.14159, 2.718281828459045]
azert123 <class 'str'>

Le caractère non mutable d’une str explique le comportement suivant.

# sorted sur `str` retourne une `list` 
print(st)
print(sorted(st), type(sorted(st)))

st2 = str(sorted(st))
print(st2, type (st2))

print( len(sorted(st)), len(st2) )
azert123
['1', '2', '3', 'a', 'e', 'r', 't', 'z'] <class 'list'>
['1', '2', '3', 'a', 'e', 'r', 't', 'z'] <class 'str'>
8 40
sorted(en_vrac)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[8], line 1
----> 1 sorted(en_vrac)

TypeError: '<' not supported between instances of 'int' and 'str'

Exercice. Expliquer le résultat du dernier affichage.

Il y a 40 caractères dans st2. Le premier est un [], puis un ', … On retrouve 40 avec 8 répétitions de 5 caractères (les et les [ et ] donnent 8 motifs de même longueur 5)

S’en convaincre en tapant par exemple : print(st2[0])

7.2.2. Conteneurs itérables et séquences#

Tous les conteneurs sont (des) itérables.

  • la construction suivante a du sens et permet le parcours des valeurs du conteneur

for val in cont:
   ...

Mais tous les conteneurs ne sont pas des séquences.

Une séquence est un conteneur indexé par des entiers

  • types python : tuple, list et str

  • les valeurs sont ainsi ordonnées dans le conteneur

    • attention : on n’a pas écrit que les valeurs étaient ordonnées selon leur valeur

Les dictionnaires dict et les ensembles set sont itérables mais pas des séquences.

Fonctions supplémentaires

cont est un conteneur.

  • concaténation : cont1 + cont2

  • duplication (concaténations répétées) : cont * cst

  • reversed(cont) renvoie le conteneur itérable en ordre inverse

La concaténation et la duplication peuvent être des opérations coûteuses, en espace mémoire en particulier, selon le caractère mutable ou non de la variable concernée.

Et aussi les 2 fonctions-méthodes suivantes dont on n’oubliera pas qu’elle peuvent aussi “coûter très cher” :

  • correspondance valeur -> indice : cont.index(val) retourne le plus petit (ou premier) indice d’une valeur argument présente dans le conteneur cont, génère une exception si la valeur est absente

  • décompte : cont.count(val) retourne le nombre d’occurrences de val dans le conteneur cont

print(en_vrac[0:-2]) 
['a', 'z', 'e', 'r', 't', 1, 2, 3]
st = ''
for c in reversed(en_vrac[0:-2]):
    st = st + str(c)  # concatenation de chaines de caractères
print(st, sorted(st))
321treza ['1', '2', '3', 'a', 'e', 'r', 't', 'z']
st1 = st + st
st2 = st * 2
print(st, st1, st1 == st2)

print(st.count('a'))
print(st.index('a'))
321treza 321treza321treza True
1
7

7.3. Les tuple#

tuple ( ou t-uplets ) : conteneur non mutable de valeurs écrites entre (  ) , séparées par des ,, et indexées de 0 à len(tuple)-1.

(3, “bonjour”, 7, 1.32)

  • Bien identifier les 4 aspects qui définissent un tuple

  • En pratique, les parenthèses sont facultatives

    • donc un tuple singleton peut être noté : a,

    • Ne pas oublier la ,!!

t = (3,)
print(type(t))

# ATTENTION !
t = (3)
print(type(t))
t = ('3')
print(type(t))
t = 3,  
print(type(t))
<class 'tuple'>
<class 'int'>
<class 'str'>
<class 'tuple'>

On préférera donc écrire les tuples systématiquement entre parenthèses.

# premières manipulations de tuple
c = (1,2,3)
c2 = c + c
print(c2)
d1 = c * 3
c = c + (4,)
print(c, d1)
(1, 2, 3, 1, 2, 3)
(1, 2, 3, 4) (1, 2, 3, 1, 2, 3, 1, 2, 3)

Attention.

  • Les tuple sont non mutables donc modifier c (ici par concaténation à l’avant-dernière ligne) créé en fait une nouvelle variable c.

  • L’exécution pythontutor illustre bien ce point important.

Exercice. Que penser de la fonction à la spécification incomplète suivante ?

def permuter(a : int, b : int) :
    '''il manque la spécification de la sortie'''
    return b, a

x, y = 3, 7
print(x, y, type(x), type(y))
x, y = permuter(x, y)
print(x, y, type(x), type(y))
(u, v) = permuter(x, y)
print(u, v, type(u), type( (u, v) ), type( (u, v)[0] ) )
3 7 <class 'int'> <class 'int'>
7 3 <class 'int'> <class 'int'>
3 7 <class 'int'> <class 'tuple'> <class 'int'>
#d1[2] = 7 lève TypeError: 'tuple' object does not support item assignment
# manipulations de tuple avec les fonctions de base
print(c, d1)
print(d1[1], d1.index(3))
print(d1.count(3))
d1 = c * 2
print(c, d1)
print(d1[1], d1.index(3))
print(d1.count(3))
(1, 2, 3, 4) (1, 2, 3, 1, 2, 3, 1, 2, 3)
2 2
3
(1, 2, 3, 4) (1, 2, 3, 4, 1, 2, 3, 4)
2 2
2

Le code suivant :

print("on a dit _non_ mutable !")
c[1] = 11

déclenche l’erreur suivante:

TypeError                                 Traceback (most recent call last)
<ipython-input-10-b869b5ab11d8> in <module>
      1 print("on a dit _non_ mutable !")
----> 2 c[1] = 11

TypeError: 'tuple' object does not support item assignment

7.4. Les listes list#

list : conteneur mutable de valeurs écrites entre [  ], séparées par des ,, et indexées de 0 à len(lst)-1

[3, “bonjour”, 7, 1.32]

  • Les valeurs sont de type quelconque – non nécessairement unique.

En pratique, les listes python sont des structures de données sophistiquées, très souples d’utilisation.

7.4.1. Créer une liste#

Une liste peut être construite comme suit :

  • à partir d’une liste vide notée : []

  • explicitement (ou aussi dit en extension) avec des valeurs séparées par des , et entre [ ]: [a], [a, b, c]

  • en compréhension : [x for x in iterable]

  • avec le constructeur de type : list() ou list(iterable)

# une liste explicite
liste = [1, 2, 3, 4, 5, 6, 7]
sous_liste = liste[:3]
un_a_trois = (1, 2, 3)
autre = list(un_a_trois)

print(liste, sous_liste, autre)
print(un_a_trois == autre, autre == sous_liste)
[1, 2, 3, 4, 5, 6, 7] [1, 2, 3] [1, 2, 3]
False True
# Des listes en compréhension
# les carres des elements
les_carres = [x ** 2 for x in liste]
print(les_carres)

# Afficher les nombres pairs
print([x for x in liste if x % 2 == 0])
# Plus simple que filtrer, egalement :)

#Affiche les carres pairs (combinaison des deux)
print([x**2 for x in liste if x ** 2 % 2 == 0])
# ou print([x for x in [a ** 2 for a in liste] if x % 2 == 0])
  
[1, 4, 9, 16, 25, 36, 49]
[2, 4, 6]
[4, 16, 36]

7.4.2. Méthodes sur des listes#

Rappel : Une méthode est une fonction qui s’applique à une variable (un objet) avec une notation pointée – et non des parenthèses.

Compléments :

  • Une méthode peut être une fonction de plusieurs paramètres.

  • Le paramètre indispensable est celui mis devant le . de la notation pointée obligatoire.

  • Les paramètres supplémentaires sont des arguments entre parenthèses en plus de la notation pointée.

  • Remarquer que les méthodes suivantes modifient la liste à laquelle elles s’appliquent.

Syntaxe

Effet

une_lst.append(val)

ajoute la valeur val à la fin de une_lst

une_lst.pop()

supprime la dernière valeur de une_lst (s’utilise comme procédure)

une_lst.extend(seq)

ajoute la séquence seq à la fin de une_lst

une_lst.insert(idx,val)

insère la valeur val en position idx dans une_lst

une_lst.remove(val)

supprime la valeur val de une_lst

une_lst.pop(idx)

supprime la valeur d’indice idx de une_lst

une_lst.sort()

trie une_lst en place (s’utilise comme procédure)

une_lst.reverse()

inverse une_lst en place (s’utilise comme procédure)

Rmq.

  • Les 2 premières (.append() et .pop()) sont les plus fondamentales.

  • Les accès, modifications (ajout/suppression de valeur) et parcours d’une liste sont des opérations simples (et optimisées) grâce à ces méthodes.

  • Ainsi comme déjà indiqué, les listes en python sont des structures de données qui cachent une importante sophistication.

les_daltons = ["Jo", "Jack", "William", "Averell"]
print(les_daltons)
['Jo', 'Jack', 'William', 'Averell']
les_daltons[0] = "le méchant"
print(les_daltons)
['le méchant', 'Jack', 'William', 'Averell']
les_daltons.append("Ma Dalton")
print(les_daltons)
['le méchant', 'Jack', 'William', 'Averell', 'Ma Dalton']
# Voir les différences de comportement des 2 cas suivants
#chien_stupide = ("rantanplan",)   # c'est un tuple
chien_stupide = ("rantanplan")    # c'est une str

print(type(chien_stupide))
les_daltons.append(chien_stupide)
print(les_daltons)
<class 'str'>
['le méchant', 'Jack', 'William', 'Averell', 'Ma Dalton', 'rantanplan']
print("Attention :")
lst = list(chien_stupide)
print("lst:",lst)
Attention :
lst: ['r', 'a', 'n', 't', 'a', 'n', 'p', 'l', 'a', 'n']
print("on corrige :", les_daltons)
while les_daltons[-1] != "Averell":
    les_daltons.pop()
print(les_daltons)
les_daltons.pop(0)
print(les_daltons)
on corrige : ['le méchant', 'Jack', 'William', 'Averell', 'Ma Dalton', 'rantanplan']
['le méchant', 'Jack', 'William', 'Averell']
['Jack', 'William', 'Averell']
les_daltons.reverse()
print(les_daltons)
les_daltons.sort() 
print(les_daltons)
['Averell', 'William', 'Jack']
['Averell', 'Jack', 'William']

7.4.3. Affectation entre listes#

ATTENTION : l’affectation liste_destination = liste_source ne crée pas une nouvelle liste_destination.

  • L’effet de cette affectation est la création d’un nouveau nom de liste liste_destination qui désigne le même ensemble de valeurs en mémoire que celui désigné par l’autre nom liste_source. On dit aussi que ces deux identifiants référencent le même espace en mémoire.

  • Ainsi la modification de ces valeurs, qui est possible ici car les listes sont mutables, en utilisant un des deux noms (identifiant) source ou destination, agira indifféremment de l’identifiant utilisé : il n’y a qu’un seul ensemble de valeurs stockées en mémoire.

  • Il est bien sûr possible de dupliquer ces valeurs et les désigner effectivement avec deux identificateurs différents. Plusieurs solutions existent en python dont :

    • la fonction copy du module copy

    • l’affectation de tranches complètes.

Ce comportement, commun aux types composés mutables et qui peut surprendre, sera étudié en détail dans le chapitre “Fonctions : aspects avancés”.

Si besoin, l’utilisation de copy.copy (importé du module copy) sera la solution.

Cette particularité est en lien avec deux notions importantes suivantes qui sont classiques en programmation – au delà de python.

  • Notion d’effet de bord : la modification par un identifiant agit sur les valeurs “de” l’autre identifiant.

  • Notion de référence : l’identifiant d’une variable (mutable) est une référence vers un espace de mémoire.

Illustration avec python tutor

liste1 = [1, 1, 2]
liste2 = liste1
liste2[0] = 3
print(liste1 == liste2)
print(liste1, liste2)
print(id(liste1), id(liste2))
print()
True
[3, 1, 2] [3, 1, 2]
4359518912 4359518912
import copy 

print("duplication avec copy.copy : ")
new_liste = copy.copy(liste1)
print(liste1, new_liste)

new_liste[0] = 999
print(new_liste == liste1)
print(liste1, new_liste)
print(id(liste1), id(new_liste))
duplication avec copy.copy : 
[3, 1, 2] [3, 1, 2]
False
[3, 1, 2] [999, 1, 2]
4359518912 4359453120
print("duplication par affectation de tranche complète : ")
liste3 = liste1[:]
liste1[0] = -5
print(liste3 == liste1)
print(liste1, liste3)
print(id(liste1), id(liste3))
duplication par affectation de tranche complète : 
False
[-5, 1, 2] [3, 1, 2]
4359518912 4354242368

7.5. Les dictionnaires dict#

Les dictionnaires python sont des tableaux associatifs.

7.5.1. Tableau associatif#

Structure de données classique (hors python)

  • type de donnée composé

  • formé par les couples clé -> valeur

  • la clé est unique dans le tableau associatif :

    • clé permet de désigner (d’atteindre) valeur dans le dictionnaire

    • Dans un tableau associatif, la clé joue même rôle que l’indice dans un tableau.

  • cette clé n’induit pas un ordre sur les valeurs du tableau associatif – même si la clé est un entier !

7.5.2. Dictionnaire dict en python#

Syntaxe.

  • des couples clé : valeur – bien noter la séparation avec :

  • écrits entre {  } et

  • séparées par des ,

{clé1: val1, clé2: val2, ...}

  • Mutable, itérable mais non séquençable

  • Accès à la valeur : un_dict[clé]

    • similaire à l’accès à la valeur dans une liste, dans une chaîne de caractères, …

h_lever_WE = {'samedi': 8, 'dimanche': 9 }
print(h_lever_WE['samedi'] > h_lever_WE['dimanche'])
False

Rmq.

  • Un dictionnaire n’est pas “ordonné par ses indices” :

    • Cette absence d’ordre justifie le caractère non séquençable de cette structure.

    • Différent des str, tuple et list qui sont des conteneurs séquençables.

  • Les valeurs peuvent être de type quelconque

    • voir exemple suivant

  • L’accès clé -> valeur s’effectue en temps constant

    • très court en pratique : technique de hachage

jour = 2
res = "toto"
message = ''

# ce dictionnaire utilise comme clés les valeurs des 3 variables précédentes
d = {jour:3, message:"bonjour", res:1.32}

print(d)
{2: 3, '': 'bonjour', 'toto': 1.32}

Opérateurs, fonctions et méthodes.

  • len() : retourne le nombre de valeurs de l’argument

les_daltons = { "le_mechant": "Jo", '2': "Jack",  3: "William", "le_cretin": "Averell"}
print(len(les_daltons), les_daltons)
non = (les_daltons["le_mechant"] == les_daltons[3])
print("non:", non)
4 {'le_mechant': 'Jo', '2': 'Jack', 3: 'William', 'le_cretin': 'Averell'}
non: False

Rmq.

  • Les clés d’un dictionnaire sont de type quelconque et non constant.

  • En pratique, préférer des clés qui donnent un sens à la valeur correspondante et au dictionnaire.

Parcourir un dictionnaire

  • l’itération sur un contain : for cle in un_dict: appartenance d’une clé

    • permet un parcours “naturel” selon les clés du dictionnaire.

    • méthode privilégiée pour parcourir tout un dictionnaire

  • On peut itérer sur les`valeurs du dictionnaire mais dans un ordre aléatoire.

    • iter(un_dict) retourne un tuple (séquence) qui sert d’itérateur

print("Attention : l'itération simple suivante parcourt les clés")
for c in les_daltons:
    print(c)
Attention : l'itération simple suivante parcourt les clés
le_mechant
2
3
le_cretin
print("Celle-ci aussi mais accède aux valeurs en parcourant les clés")
for c in les_daltons:    
    print(les_daltons[c])
Celle-ci aussi mais accède aux valeurs en parcourant les clés
Jo
Jack
William
Averell

Méthodes introspectives

  • .keys() : fonction-méthode qui retourne la liste des clé de l’argument

  • .values() : fonction-méthode qui retourne la liste des val de l’argument

  • .items() : fonction-méthode qui retourne la liste des couples clé:val de l’argument

Rmq. Noter le pluriel des identifiants de ces méthodes.

print(les_daltons)
{'le_mechant': 'Jo', '2': 'Jack', 3: 'William', 'le_cretin': 'Averell'}
les_cles = les_daltons.keys()
print(les_cles)
#print(type(les_cles))
# type(les_cles[0]) produit TypeError: 'dict_keys' object is not subscriptable
dict_keys(['le_mechant', '2', 3, 'le_cretin'])
les_noms = les_daltons.values()
print(les_noms, type(les_noms))
dict_values(['Jo', 'Jack', 'William', 'Averell']) <class 'dict_values'>
tout = les_daltons.items()
print(tout, type(tout))
dict_items([('le_mechant', 'Jo'), ('2', 'Jack'), (3, 'William'), ('le_cretin', 'Averell')]) <class 'dict_items'>

Ce qui permet d’introduire d’autres modes de parcours d’un dictionnaire.

Rmq. C’est rendu possible grâce à la fonction iter().

for c in iter(les_daltons.keys()):
    print(c)
print("--")
for v in iter(les_daltons.values()):
    print(v)
print("--")
for v in iter(les_daltons.items()):
    print(v, type(v)) 
le_mechant
2
3
le_cretin
--
Jo
Jack
William
Averell
--
('le_mechant', 'Jo') <class 'tuple'>
('2', 'Jack') <class 'tuple'>
(3, 'William') <class 'tuple'>
('le_cretin', 'Averell') <class 'tuple'>

Exercice. Quelle construction est inutile ?

Modifier un dictionnaire

  • .pop(cle) : efface (cle:val) et retourne val

  • del un_dic[cle] détruit l’entrée cle : val du dictionnaire un_dic

  • .update(autre_dict) : ajout ou maj avec un autre_dict (et écrasement si clé commune)

  • .clear() : efface le dictionnaire

print("mutable : ajout, retrait, modification")
les_daltons["la_maman"] = "Ma Dalton" #ajout
print(les_daltons)
mutable : ajout, retrait, modification
{'le_mechant': 'Jo', '2': 'Jack', 3: 'William', 'le_cretin': 'Averell', 'la_maman': 'Ma Dalton'}
del les_daltons["le_mechant"] #retrait
print(les_daltons)
{'2': 'Jack', 3: 'William', 'le_cretin': 'Averell', 'la_maman': 'Ma Dalton'}
print("le_mechant" in les_daltons, len(les_daltons))
print() 
False 4
animaux_fideles = {"le_cretin":"Rantanplan", "la_classe": "Jolly Jumper"}
animaux_fideles.pop("la_classe") #retrait
print(animaux_fideles)
{'le_cretin': 'Rantanplan'}
les_daltons.update(animaux_fideles) # ajout-réduction de dictionnaires
print("changement de cretin : ", les_daltons)
changement de cretin :  {'2': 'Jack', 3: 'William', 'le_cretin': 'Rantanplan', 'la_maman': 'Ma Dalton'}

7.6. Les chaînes de caractères str#

Leur importance pratique mériterait presque un chapitre dédié. Bien relire cette section plusieurs fois.

str (string) ou chaîne de caractères : type de donnée non mutable composé d’une suite ordonnée de caractères (séquence)

  • syntaxe : s’écrit indifféremment avec ' ' et " "

7.6.1. Opérations#

  • len(), concaténation, répétition

Accès

  • par indexation : s[i], s[0], s[-1]

  • par extraction de sous-chaînes ou tranches de str : s[i:j], s[:], s[i:], s[-2:], s[1:10:2]

hello = 'bonjour !'
print(hello[-1], len(hello))
s = hello[0:len(hello):2]
print(s, len(s))
! 9
bnor! 5

Modification

  • Une str est non mutable

  • Toute modification de sa valeur

    • créé une nouvelle str

    • et détruit la précédente ;

    • ce qui justifie que la nouvelle str puisse être désignée par le même identifiant.

    • Se souvenir des illustrations pythontutor déjà vues (copie de list, concaténation de str).

s = 'abc'
print(s, id(s))

t = s
print(t, id(t))

s = s + 'def'
print(s, id(s))
abc 4301674064
abc 4301674064
abcdef 4354282672

7.6.2. Fonctions-méthodes#

Ces fonctions-méthodes spécifiques aux str sont utiles en pratique.
Savoir qu’elles existent est suffisant dans le cadre de ce cours.
Les identifiants sont en général bien explicites.

On distingue surtout deux types de fonctions-méthodes.

Celles qui retournent un bool:

  • .isupper(), .istitle(),

  • .isalnum() (comprendre is alpha and num), .isalpha(), .isdigit(), .isspace(),

  • .startswith(), .endswith()

Celles qui modifient l’argument arg :

  • la casse des caractères : .upper(), .lower(), .capitalize(), .swapcase()

  • la justification : .expantabs(nb), .center(width), .ljust(), .rjust(), .zfill(width) (comprendre zero fill),

ou aussi :

  • .replace(old, new, count)count est le nombre de remplacements à effectuer,

  • arg.join(cont) concatène les chaînes du conteneur cont en intercalant arg,

  • arg.split(seps) partage la chaîne arg selon seps et retourne le tout sous forme de list de str ;

    • très utile en pratique pour manipuler les mots d’un texte par exemple

    • arg.split() (sans argument seps) partage la chaîne arg selon le (ou les) caractères espaces qu’elle comporte, la liste retournée ne comporte aucun mot vide.

    • Plus de détail avec help(str.split).

On finit en mentionnant cette dernière méthode de recherche :

  • arg.find(substr, deb, fin) qui retourne l’indice de substr dans arg[deb,fin].

hello = 'bonjour !'

t0 = hello.split()
print(t0)

t = hello.split('o')
print(t)
print('z' in t, 'b' in t)
['bonjour', '!']
['b', 'nj', 'ur !']
False True
plus_fort = hello.upper()
print(hello, hello.isupper())
print(plus_fort, plus_fort.isupper(), plus_fort.isalpha(), '\n')
bonjour ! False
BONJOUR ! True False 
liste_de_deux = plus_fort.split()
print(".split:", liste_de_deux, type(liste_de_deux), type(liste_de_deux[0]), '\n')
.split: ['BONJOUR', '!'] <class 'list'> <class 'str'> 
liste_de_deux = '**!!**'.join(liste_de_deux)
print(".join:", liste_de_deux, type(liste_de_deux), '\n')
.join: BONJOUR**!!**! <class 'str'> 
deux_fois_plus_fort = liste_de_deux.replace('!', '@@'+str(liste_de_deux)+'@@', 1)
print(".replace:", deux_fois_plus_fort)
.replace: BONJOUR**@@BONJOUR**!!**!@@!**!
deux_fois_plus_fort = liste_de_deux.replace('!', '@'+str(liste_de_deux)+'@', 2)
print(".replace:", deux_fois_plus_fort)
.replace: BONJOUR**@BONJOUR**!!**!@@BONJOUR**!!**!@**!

Rappel sur les fonctions-méthodes qui “modifient” une str

  • Le caractère non mutable des str n’est pas mis en défaut par les méthodes de modification

  • Celles-ci créent de nouvelles str

hello = 'bonjour !'
print(id(hello))

bjr = hello
print(id(bjr))

plus_fort = hello.upper()
print(id(plus_fort))

hello = plus_fort
print(id(hello))
4359573168
4359573168
4359505392
4359505392

7.6.3. Les caractères spéciaux ou caractères/séquences d’échappement#

Caractère d’échappement : “backslash” \

Caractères spéciaux ou séquences d’échappement : l’association du “backslash” \ et de certains symboles (ou suite de symboles) a un sens particulier dans une chaîne de caractères.
Cela permet par exemple d’interpréter différemment les symboles ' ou " ou .

Quelques exemples (\(\star\)) à connaitre :

  • \' ou \" ou \\ ou \/ : les symboles ' ou " ou \ ou /

  • \b : backspace

  • \t \v: tabulation horizontale ou verticale

  • \n : new line saut de ligne

  • \f : form feed saut de page

  • \r : retour en début de ligne

Remarques.

  • Cette notion se retrouve dans d’autres langages de programmation : C, …

  • On parle aussi de séquence d’échappement mais retenons que l’association \ suivie d’un caractère est “comptée” comme un seul caractère : un caractère spécial.

Exemples.

s1 = 'bonjour'
s2 = '\tbonjour'
print(s1)
print(s2)
bonjour
	bonjour
print('s1:', len(s1))
print('s2:', len(s2))
print('[', s1[0], ']', sep='')
print('[', s2[0], ']', sep='')
s1: 7
s2: 8
[b]
[	]

Comment utiliser le symbole \ sans qu’il s’évalue comme l’annonce d’une séquence d’échappement ?

  • avec lui même !

  • ainsi \\ s’évalue comme le caractère \

  • et \\n comme la chaîne de 2 caractères non évalués \n (piégeux !).

print(len('\n'), len('\\n'))
1 2
print("\\ texte avec '\\' au début et à la fin \\")
print()

print("Bo\njour")
print("Bo\\njour")
print("Bo\\n\b\bnjour")
\ texte avec '\' au début et à la fin \

Bo
jour
Bo\njour
Bo\nnjour

Ainsi le \ évite d’évaluer “normalement” le symbole qui le suit.

Rmq.
Dans l’exemple suivant, expliquer pourquoi va (aux lignes affichées 2 et 4) n’est pas à la même colonne entre le premier et le second print ?

print('Bonjour \"mon ami\" ! \n \t \t  Ca va ?')
print('Bonjour \"mon ami\" ! \n \t \t  Ca va ? \r Comment')
Bonjour "mon ami" ! 
 	 	  Ca va ?
Bonjour "mon ami" ! 
 	 	  Ca va ? 
 Comment

Réponse :

  1. Analyse du premier print() : on s’intéresse à l’interprétation par le print() de la chaîne de caractère argument.

    • \n et \t sont resp. les symboles du saut à la ligne et de la tabulation

    • la tabulation est interprétée dans mon environnement comme 7 (caractères) espaces

    • lors de l’affichage, la chaîne " \t \t  Ca va ?" à afficher à partir du \n est donc interprétée comme :

      • 1 espace

      • 7 espaces (1 tabulation)

      • 1 espace

      • 7 espaces (1 tabulation)

      • 2 espaces

      • les 7 caractères Ca va ?.

    • Soit donc au total 25 caractères affichés sur la seconde ligne (après le \n).

  1. Analyse de second print() : on va expliciter la valeur de la chaîne de caractères sans se soucier dans un premier temps de son interprétation à l’affichage, cad. lors de l’exécution du print().

    • \t est une chaîne de caractères de longueur 1, cad. un caractère :

      • essayer type('\t') et len('\t')

    • à partir du \n, la chaîne de caractères \t \t  Ca va ? est de longueur 13

      • essayer len(' \t \t  Ca va ?')

    • \r revient en début de ligne dans cette chaîne de caractères

      • cad. juste après le \n

      • et donc “recule” (la position d’entrée) de 13 caractères sur cette ligne

    • Comment est un chaîne de caractères de longueur 8

      • elle est placée aux 8 premiers caractères à partir de la position du curseur, cad. du début de la ligne

      • et ainsi écrase les 8 premiers caractères de la chaîne \t \t  Ca va ?

      • ces 8 premiers caractères sont : \t \t  Ca

    • donc la chaîne de caractètes à afficher devient, à partir du \n: Comment va ?

      • on peut vérifier que len(" Comment va ?") retourne 13.

    • L’affichage obtenu est conforme à l’analyse.

print('[\t]')
tab = '[' + 'x'*7 + ']'  # sept x entre crochets
print(tab)
print()
print('Bonjour \"collègue\" ! \n \t \t  Ca va ?')
s = '.'*25
print(s)
print('Bonjour \"collègue\" ! \n \t \t  Ca va ? \r Comment')
s = '.'*13
print(s)
[	]
[xxxxxxx]

Bonjour "collègue" ! 
 	 	  Ca va ?
.........................
Bonjour "collègue" ! 
 	 	  Ca va ? 
 Comment
.............
print(len(' Comment va ?'))
13

7.7. Les ensembles set et frozenset#

set (ensemble) : type composé de valeurs uniques écrites entre { } et séparées par des ,.

{3, “bonjour”, 7, 1.32}

  • set: ensemble mutable

  • (\(\star\)) frozenset : ensemble non mutable

  • La fonction set() permet de créer un ensemble à partir d’un tuple ou d’une chaîne de caractères

  • Et de façon similaire, la fonction frozenset()

bof = {'b', 'o', 'f'}
voyelles = set('aeiouy')
aie = set('aie')

print(voyelles, len(voyelles))
{'o', 'u', 'y', 'e', 'i', 'a'} 6
f_set = frozenset("non mutable")

print(f_set, len(f_set))
frozenset({'o', 'n', 'l', 'u', ' ', 'e', 'm', 'b', 'a', 't'}) 10
print("aie :", aie)
print("voyelles :", voyelles)
aie : {'i', 'a', 'e'}
voyelles : {'o', 'u', 'y', 'e', 'i', 'a'}
print('a' in voyelles)
print('a' in bof)
True
False

Opérations :

  • opérations ensemblistes (des maths) :

    • in et not in : appartenance ou non

    • | : union

    • & : intersection

    • -: différence (habituellement notée \ : a - b = { x \(\in\) a | x \(\notin\) b})

    • ^: différence symétrique (complémentaire de l’intersection)

    • <= ou < : inclusion ensembliste, resp. est un sous-ensemble de vs. est inclus strictement dans; a < b == a <= b and a != b (et de même : >= >)

Rappel. Si besoin : différence et différence symétrique

print("union :", voyelles | aie)
print("autre union :", voyelles | bof)
print("intersection :", voyelles & aie)
print("difference : ", aie - voyelles, bof - voyelles)
print("difference symétrique: ", aie ^ voyelles)
print("inclusions :", aie < voyelles, aie <= voyelles, aie >= voyelles, bof < voyelles)
 
#print(aie[1])  # déclenche une erreur : l'accès indexé n'est pas possible dans un ensemble
union : {'o', 'u', 'y', 'e', 'i', 'a'}
autre union : {'o', 'u', 'y', 'f', 'e', 'b', 'i', 'a'}
intersection : {'i', 'a', 'e'}
difference :  set() {'b', 'f'}
difference symétrique:  {'o', 'u', 'y'}
inclusions : True True False False

Rmq. On n’accède pas à la valeur d’un set avec la notation indicée [ ].

7.8. Spécifier des fonctions avec des types composés#

Les fonctions peuvent manipuler des arguments de type composé.

On a déjà vu beaucoup d’exemples avec les tableaux. Ces derniers ayant été représentés en python par le type (composé) liste, on sait expliciter le type de tels paramètres à l’aide de :

  • list depuis python > 3.9

  • List (avec une majuscule) du module typing pour les versions antérieures.

On va étendre cette approche aux type composés vus dans ce chapitre.

Depuis python 3.9, il est possible de paramètrer les types prédéfinis avec des [ ] de façon similaire à celle décrite dans la section 2 de cette partie.

Il n’est donc plus nécessaire de passer par les types “avec majuscule” de typing.

7.8.1. Paramètres de type composé prédéfini#

Les types composés prédéfinis de python sont renvoyés par la fonction type() (appliquée à une variable).

  • list

  • dict

  • tuple

  • set

  • str

unTuple = (1,2,3)
uneListe = [1, 'a', 1.3]
unDictionnaire = { "le_mechant":"Jo", '2':"Jack",  3:"William", "le_cretin": "Averell"}
unEnsemble = {'b', 'o', 'f'}
uneChaineDeCaracteres = "bof"

for v in (unTuple, uneListe, unDictionnaire, unEnsemble, uneChaineDeCaracteres):
    print(type(v))
<class 'tuple'>
<class 'list'>
<class 'dict'>
<class 'set'>
<class 'str'>

Solution minimale. On peut donc utiliser ces types prédéfinis pour spécifier le type des paramètres d’une fonction.

Par la suite, on utilisera au moins cette solution minimale pour toutes les spécifications de fonction avec des paramètres de type composé.

def nb_caracteres(s : str) -> int:
    return len(s)

def nb_entrees(d : dict) -> int:
    return len(d)

def card(s : set) -> int:
    return len(s)

(\(\star\)) Surcharge de fonctions.

2 fonctions d’identifiant identiques mais de signature différentes grâce au typage de ses paramètres formels. On appelle aussi cette homonymie de la surcharge de fonctions.

def nb_val(l : list) -> int:
    return len(s)

def nb_val(t : tuple) -> int:
    return len(s)
  
print(nb_caracteres(uneChaineDeCaracteres))
print(nb_entrees(unDictionnaire))
print(card(unEnsemble))
for v in (uneListe, unTuple):
    print(nb_val(v))    
3
4
3
13
13

Rmq. Dans l’exemple précédent, on a 2 fonctions d’identifiant identique nb_val() mais de signatures différentes grâce au typage de leurs paramètres formels.

On a par exemple déjà signalé cette caractéristique avec les différents sens du symbole + selon qu’il s’applique à des entiers, des flottants, des chaînes de caractères, …

7.8.2. (\(\star\)) Mieux expliciter le type composé#

Comme on l’a fait pour mieux expliciter les tableaux, on peut expliciter “l’intérieur” d’un type composé, cad. le type de ses composants.

Par exemple, le type d’un tableau 2D de valeurs entières (type composé qui regroupe des valeurs de même type) a été spécifié (depuis python>3.9) par :

list[list[int]]

On a ainsi explicité :

  • la dimension du tableau : ici 2 et la représentation comme une liste de listes

  • le type de ses valeurs : ici des entiers, ie. des int

python > 3.9

On peut directement faire de même pour les types composés :

list , dict, tuple, set

Ces types permettent d’expliciter “l’intérieur” des arguments de type composé. La syntaxe s’appuie sur les crochets [ ] déjà rencontrés pour les tableaux-listes.

def nb_entrees(d : dict[str, str]) -> int:
    return len(d)

def card(s : set[str]) -> int:
    return len(s)

d0 = {'1':"a", '2':"b"}
res = nb_entrees(d0)
print(d0, res)

s0 = set(d0)
res = card(s0)
print(s0, res)
{'1': 'a', '2': 'b'} 2
{'1', '2'} 2

(\(\star\)) Union pour la surcharge de fonctions

Le module typing définit Union :

  • Union[int, float] est un paramètre de type int ou float.

  • Il convient par exemple pour écrire une seule version de la fonction nb_val().

# exemple d'Union pour la surcharge
from typing import Union
def nb_val(cont : Union[list[int], tuple[int]]) -> int:
    return len(cont)

unTuple = (1,2,3)
uneListe = [1, 'a', 1.3]
unDictionnaire = { "le_mechant":"Jo", '2':"Jack",  3:"William", "le_cretin": "Averell"}
unEnsemble = {'b', 'o', 'f'}
uneChaineDeCaracteres = "bof"

print(nb_entrees(unDictionnaire))
print(card(unEnsemble))
for v in (uneListe, unTuple):
    print(nb_val(v))    

# autre exmple avec Union
def maxListe( l : list[Union[int, float]]) -> int:
    m = l[0]
    for i in range(len(l)):
        if l[i]> m:
            m = l[i] 
    return m

resI = maxListe([1, 0, 1, 2, 2, 2])
resF = maxListe([1.0, 1.2, 2.2])

print(resI, resF)
4
3
3
3
2 2.2

python < 3.9.

Le module typing définit des types similaires mais qui commencent avec une majuscule :

List , Dict, Tuple, Set

Ainsi par exemple :

  • Tuple[int, float, str] est un tuple triplet composé d’un entier, d’un flottant et d’une chaîne de caractères.

  • Dict[str, int] est un dictionnaire de couples clés:valeur de types str:int

from typing import List

def maxTab2D( t : List[List[int]], d1 : int, d2 : int) -> int:
    m = t[0][0]
    for i in range(d1):
        for j in range(d2):
            if t[i][j] > m:
                m = t[i][j] 
    return m

print(maxTab2D([[1,0],[1,2]], 2, 2))
2

Consulter la documentation pour d’autres constructions plus avancées et très utiles.

7.9. Synthèse#

  • Conteneurs non mutables : str, tuple (et frozenset)

  • Conteneurs mutables : list, dict, set

  • Création, accès aux valeurs, parcours et modification

  • Des fonctions (pour les non mutables) et des fonction-méthodes

  • Attention : la modification de non mutable cache une recopie en mémoire

  • Attention : bien que très simples à utiliser, certaines fonctions-méthodes sont d’un coût algorithmique élevé (complexité non constante)

  • Quels mutables choisir ?

    • les list pour des tableaux 1D de valeurs de type qcq et où le traitement demande de la souplesse

    • les dict pour des clés str

    • (\(\star\)) mais aussi les ndarray pour du calcul numérique type algèbre linéaire ou les tableaux multi-dimensionnels de valeurs numériques – voir cette annexe.

  • La spécification d’arguments de type composé avec le module typing ou plus directement en python>=3.9

7.9.1. Avoir les idées claires#

Distinguer les caractéristiques des structures de données : tuple, liste, dictionnaire, ensemble, et chaîne de caractères – aussi appelés type composé ou conteneur en python :

  • pour une représentation,un stockage des données et des traitements adaptés au problème

  • pour effectuer l’accès aux valeurs, le parcours (séquences, itérateur), et la modification ou la copie (mutabilité) de ces SD

  • (\(\star\)) Connaître les complexités de ces traitements

7.9.2. Savoir faire#

  • Connaitre la syntaxe python de définition et manipulation de ces SD

  • Créer des listes et dictionnaires en compréhension

  • Connaître les méthodes (prédéfinies) utiles au traitement, en particulier des chaînes de caractères

  • Définir des spécifications de fonctions avec un ou des paramètres de type composé