Immutabilité, mutabilité
Ecriture en cours
Ce chapitre est en cours d'écriture
Le chapitre précédent s'est terminé sur ce qui est peut-être le plus gros cliffhanger de ce cours de NSI :
Les
list
peuvent être modifiées, alors que lestuple
ne peuvent pas l'être.
Ce chapitre explicite cette différence, et ses implications.
Les tuples et les chaines sont en Python des valeurs dites immuables (en anglais immutable), c'est à dire qu'on ne peut pas les modifier. C'est aussi le cas, d'une certaine manière, pour les entiers, les booléens et les flottants. Dans la suite du cours, on utilisera la françisation des termes anglais, on parlera donc d'immutabilité et de son opposé, la mutabilité.
Les lists sont des valeurs mutables. On peut donc les modifier à l'aide d'opérateurs et de méthodes.
Modifier une list
Modification d'un élément
On peut modifier une valeur à la position i
dans une list en utilisant l'opérateur []
avec la syntaxe suivante :
exemple :
Affiche :
Exercice
Complétez la fonction suivante pour qu'elle ajoute v à tous les éléments de la liste passée en paramètre
Ajout d'un élément
On peut ajouter un élément à une list en utilisant la méthode append
. L'élément est ajouté à la fin de la list :
Par exemple :
Affiche
Exercice
Complétez la fonction suivante pour qu'elle retourne une copie de liste où seuls les éléments inférieurs à v ont été gardés.
Insertion d'un élémént
On peut insérer un élément à une position dans une list, avec la méthode insert
. L'élément à la position et les éléments suivants sont décalés :
Par exemple,
Affiche :
Exercice
Complétez la fonction suivante pour qu'elle retourne une copie de liste où les élements sont en ordre inverse.
Suppression d'un élément
La méthode remove
de list supprime le premier élément égal à une valeur. Au moins un tel élément doit être présent dans la list. Voici la syntaxe:
Exemple :
Affiche :
Exercice
Complétez la fonction suivante pour qu'elle retourne une list qui est une copie de liste où tous les élements égaux à v ont étés supprimés.
Rappel : pour vérifier si un élément est présent dans une list, vous pouvez utiliser l'opérateur in
.
Tant qu'il y a une valeur égale à v
dans liste
, on retire la première valeur égale à v
de liste
.
Effet de bord
La possibilité de modifier les lists a une conséquence qui peut sembler étrange en Python.
Observons ce que fait le code suivant :
affiche
La valeur de l2
a été impactée par la modification de la valeur de la list de l1
.
En fait, quand on fait l2 = l1
, et que l1
contient une list, la boite de la liste de l1
n'est pas copiée, et à la place, l2
devient aussi une manière d'accéder à la boîte de la list. On dit que l2
et l1
sont des références vers la list. Le site propriétaire pythontutor.com permet de visualiser l'organisation de la mémoire par Python.
Il en va de même dans les fonctions :
Affiche
L'effet de bord rends en général les programmes plus compliqués à comprendre, puisque regarder la valeur de retour d'une fonction ne suffit plus à savoir ce qu'elle fait.
Ainsi, dans le code ci-dessus, quand on fait
On pourraît s'attendre à ce que print
affiche [1, 2, 3]
. Mais comme ajout
peut modifier l
, pour vraiment prédire ce que va faire print
, on doit d'abord connaître les modifications que l
peut faire sur l
.
On va donc souvent préférer copier la list, avec la méthode copy
puis la retourner comme une valeur.
Performances
Mais alors, si l'effet de bord est si mal, pourquoi on s'en sert ? Parce que souvent, pour deux programmes avec le même comportement, celui écrit sans effet de bord moins performant.
Copier une list peut prendre beaucoup de temps, alors que copier une référence prends très peu de temps. Et souvent, on a pas besoin de copier une
TP répertoire
On va créer un programme qui permet de rechercher un contact dans un répertoire, et d'ajouter des contacts au répertoire.
On va représenter un contact sous forme d'un tuple de deux chaînes. La première chaîne sera le nom et prénom de contact, et la seconde sera son adresse mail. Par exemple le contact nommé Alex Emple, et ayant pour adresse alexemple@unmail.fr, sera représenté par le tuple suivant :
La liste des contacts est modélisée par une list de contacts, donc une liste de tuples. Voici une liste par exemple de trois contacts:
contacts3 = [
("Alex Emple", "alexemple@unmail.fr"),
("Daryl Ustration", "dust@autremail.fr"),
("Clara Perçu", "thumbnail@mail.fr")
]
Vous trouverez dans le dépliant suivant le support TP sous forme d'un code a trous :
Code support du TP
lancer_tests = True ##Mettez a True pour lancer les tests
lancer_main = True ##Mettez a True pour lancer la fonction main
## Vous devez implémenter cette fonction
def recherche(contacts, nom):
'''contacts une list de tuple (str,str) représentant des contacts. nom une chaine.
retourne le tuple du contact appelé nom si le contact correspondant existe, None sinon.
>>> type(recherche([], 'marcel'))
<class 'NoneType'>
>>> recherche([('a','b'),('c','d'),('e','f')], 'e')
('e', 'f')
>>> type(recherche([('a','b'),('c','d'),('e','f')], 'g'))
<class 'NoneType'>
'''
pass
## Vous devez implémenter cette fonction
def affichage(contacts):
'''contacts une liste de tuples (str,str) représentant des contacts.
retourne une chaine de caractère représentant le répertoire.
'''
pass
## Vous devez implémenter cette fonction
def ajout(contacts, c):
'''contacts une list de tuple (str,str) représentant des contacts. c un tuple (str,str) représentant un contact.
Si un contact du même nom que c est déjà présent dans contacts, retourne une copie de contacts
Sinon, retourne une copie de contacts dans laquelle c a été ajouté
>>> ajout([], ('marcel', 'marcel@mail.fr'))
[('marcel', 'marcel@mail.fr')]
>>> ajout([('a','b'),('c','d'),('e','f')], ('g', 'h'))
[('a', 'b'), ('c', 'd'), ('e', 'f'), ('g', 'h')]
>>> ajout([('a','b'),('c','d'),('e','f')], ('c', 'h'))
[('a', 'b'), ('c', 'd'), ('e', 'f')]
'''
pass
## Vous devez implémenter cette fonction
def suppression(contacts, n):
'''Contacts une list de tuple (str,str) représentant des contacts, n une chaine représentant le nom d'un contact
Si un contact de nom n est présent dans contacts, retourne une copie de contacts où le contact de nom n a été supprimé
Sinon, retourne une copie de contacts.
>>> suppression([], 'e')
[]
>>> suppression([('a','b'),('c','d'),('e','f')], 'e')
[('a', 'b'), ('c', 'd')]
>>> suppression([('a','b'),('c','d'),('e','f')], 'g')
[('a', 'b'), ('c', 'd'), ('e', 'f')]
'''
pass
## Vous devez implémenter cette fonction
def modification(contacts, c):
'''Contacts une list de tuple (str,str) représentant des contacts, c un tuple (str,str) représentant un contact.
Si un contact du même nom que c est déjà présent dans contacts,
retourne une copie de contacts où le contact a été mis à jour avec c
Sinon retourne une copie de contacts
>>> modification([], ('e', 'f'))
[]
>>> modification([('a','b'),('c','d'),('e','f')], ('c', 'h'))
[('a', 'b'), ('e', 'f'), ('c', 'h')]
>>> modification([('a','b'),('c','d'),('e','f')], ('g', 'h'))
[('a', 'b'), ('c', 'd'), ('e', 'f')]
'''
pass
## Les fonctions ci dessous sont fournies et n'ont pas besoin d'être implémentées
def rechercher(contacts):
'''Contact une list de tuple (str,str)
demande un nom de contact à l'utilisateur, et affiche le contact si il existe
'''
nom = input('Nom du contact : ')
r = recherche(contacts, nom)
if r == None:
print(f'Contact {nom} inexistant')
else :
print(f'{r[0]} : {r[1]}')
def ajouter(contacts):
'''Contacts une list de tuple (str,str)
Demande un nom et une adresse mail de contact à l'utilisateur.
Si le contact a ce nom est déjà présent dans contacts, affiche une erreur et retourne une copie de contacts
sinon, retourne une copie de contacts dans laquelle le contact est ajouté
'''
c = contacts.copy()
nom = input('Nom du contact : ')
adresse = input('Adresse mail du contact : ')
ancienne_longueur = len(c)
c = ajout(c, (nom, adresse))
nouvelle_longueur = len(c)
if nouvelle_longueur == ancienne_longueur:
print(f'Le contact {nom} existe déjà !')
else:
print('Contact ajouté !')
return c
def modifier(contacts):
'''Contacts une list de tuple (str,str)
Demande un nom n et une adresse mail m de contact à l'utilisateur.
Si le contact à ce nom est présent dans contacts,
retourne une copie de contacts où l'adresse mail du contact au nom n est mise à m
Sinon, affiche un message d'erreur et retourne une copie de contacts
'''
c = contacts.copy()
nom = input('Nom du contact : ')
adresse = input('Nouvelle adresse du contact : ')
r = recherche(c, nom)
c = modification(c, (nom, adresse))
if r != None:
print('Contact mis à jour !');
else:
print(f"Le contact {nom} n'existe pas !")
return c
def supprimer(contacts):
'''Contacts une list de tuple (str,str)
Demande un nom n à l'utilisateur
Si le contact à ce nom est présent dans contacts,
retourne une copie de contacts dans laquelle contact de nom m a été supprimée
Sinon, affiche un message d'erreur et retourne une copie de contacts.
'''
c = contacts.copy()
nom = input('Nom du contact : ')
ancienne_longueur = len(c)
c = suppression(contacts, nom)
nouvelle_longueur = len(c)
if ancienne_longueur == nouvelle_longueur:
print(f"Le contact {nom} n'existe pas")
else:
print('Contact supprimé')
return c
def demander_choix(q, c):
'''q une chaine, la question a poser, c une list non vide de chaines non vides, les réponses acceptées
affiche la question et les choix, et demande une réponse à l'utilisateur.
redemande tant que la réponse ne fait pas partie des choix.
retourne le choix valide fait par l'utilisateur.
'''
assert c # non vide
assert not ('' in c)
choix = ''
while not (choix in c):
choix = input(f'{q} {c}: ');
return choix
def main():
contacts = []
choix = ''
while choix != 'stop':
choix = demander_choix('entrez une commande', ['rechercher', 'afficher', 'ajouter', 'modifier', 'supprimer', 'stop'])
if choix == 'rechercher':
rechercher(contacts)
elif choix == 'afficher':
print(affichage(contacts))
elif choix == 'ajouter':
contacts = ajouter(contacts)
elif choix == 'modifier':
contacts = modifier(contacts)
elif choix == 'supprimer':
contacts = supprimer(contacts)
if __name__ == '__main__' and lancer_tests:
import doctest
doctest.testmod()
if __name__ == '__main__' and lancer_main:
main()
TODO ajouter un descriptif de comment utiliser le programme
Le code du main
et de l'interface texte est fourni, il vous faut simplement implémenter quelques fonctions.
Recherche
La fonction recherche
est la première à implémenter, parce que vous allez sans doute l'utiliser par la suite dans les autres fonctions.
Avant d'implanter la fonction, ajoutez dans sa docstring quelques exemples qui illustrent son comportement.
Aide 1
La méthode find
du type list ne vous aidera sans doute pas...
Aide 2
Voici le descriptif de l'algorithme. Il peut être fait avec un while ou un for
Ajout
La fonction ajout
vient ensuite, puisque vous allez pouvoir l'utiliser pour construire une list de contact pour tester à la main l'application.
Avant d'implanter la fonction, ajoutez dans sa docstring quelques exemples qui illustrent son comportement.
Aide 1
N'oubliez pas de copier la list de contacts !
Aide 2
Réutilisez la fonction recherche
pour vérifier si le contact est déjà présent...
Aide 3
Utilisez la méthode append
du type list pour ajouter le contact à la list.
Solution
def ajout(contacts, c):
"""contacts une list de tuple (str,str) représentant des contacts. c un tuple (str,str) représentant un contact.
Si un contact du même nom que c est déjà présent dans contacts, retourne une copie de contacts
Sinon, retourne une copie de contacts dans laquelle c a été ajouté
"""
if recherche(contacts, c[0]) == None:
contacts = contacts.copy()
contacts.append(c)
return contacts
else:
return contacts.copy()
Affichage
Avant d'implanter la fonction, ajoutez dans sa docstring quelques exemples qui illustrent son comportement.
Attention, cette fonction n'est pas couverte par les tests automatiques !
Aide
La fonction str
peut vous aider :)
Suppression
Avant d'implanter la fonction, ajoutez dans sa docstring quelques exemples qui illustrent son comportement.
Aide 1
La méthode remove
du type list
peut vous aider.
Aide 2
Pour utiliser remove
, vous devez avoir le contact complet, c'est à dire le tuple.
Aide 3
Pour avoir accès au contact complet à partir de son nom, vous pouvez utiliser la fonction recherche
que vous avez crée plus tôt.
Solution
def suppression(contacts, n):
"""Contacts une list de tuple (str,str) représentant des contacts, n une chaine représentant le nom d'un contact
Si un contact de nom n est présent dans contacts, retourne une copie de contacts où le contact de nom n a été supprimé
Sinon, retourne une copie de contacts.
"""
contact = recherche(contacts, n)
if contact == None:
return contacts.copy()
else:
c = contacts.copy()
c.remove(contact)
return c
Modification
Avant d'implanter la fonction, ajoutez dans sa docstring quelques exemples qui illustrent son comportement.
Attention, les tests ne couvrent qu'une des implémentation possible. Il se peut que votre code soit fonctionnel, même si les tests ne passent pas. Si vous avez confiance en votre code, testez à la main.
Aide 1
N'oubliez pas de vérifier si le contact existe avant de le modifier !
Aide 2
Modifier un élément, c'est le supprimer puis ajouter sa nouvelle version...
Solution
def modification(contacts, c):
"""Contacts une list de tuple (str,str) représentant des contacts, c un tuple (str,str) représentant un contact.
Si un contact du même nom que c est déjà présent dans contacts,
retourne une copie de contacts où le contact a été mis à jour avec c
Sinon retourne une copie de contacts
"""
ancien = recherche(contacts, c[0])
if ancien == None:
return contacts.copy()
else:
contacts = contacts.copy()
contacts = suppression(contacts, c[0])
contacts = ajout(contacts, c)
return contacts
Extension (Difficile) : effets de bord
Les tests actuels ne vérifient pas que l'on effectue bel et bien une copie de la liste de contacts dans les fonctions.
Voici la spécification d'une fonction qui très simple qui résume le problème :
Proposez, en français, ou à l'aide de schémas, une méthode pour tester que la fonction copie
fait bel et bien une copie de la list
l
passée en paramètre.
Votre méthode doit discriminer correctement les deux implémentations suivantes de la fonction copie
. C'est à dire décider si une implémentation est valide ou non..
Eventuellement, proposez une implémentation en Python de votre méthode, qui illustre le cas où copie
Solution en français
Pour tester que la copie effectue bien une copie, on va :
- Créer une liste l
- La passer l à la fonction
copie
et la récupérer le résultat r - Modifier r
- Vérifier que l n'a pas été affecté par les modifications
Si l n'a pas été copié, alors l et r pointent vers la même list, et donc modifier l modifiera aussi r.
Solution Python
def copie_valide(l):
"""l une list
retourne une copie de l
"""
return l.copy()
def copie_invalide(l):
"""l une list
retourne une copie de l
"""
return l
if __name__ == "__main__":
#on crée une list
l = [1,2,3]
#on la copie
r = copie_valide(l)
#on modifie r
r[2] = 0
#on vérifie que l n'a pas changé :
if l == [1,2,3]:
print("l a été copiée")
else:
print("l a n'a pas été copiée")
Extension (Difficile) : refactoring
Les fonctions d'interface supprimer
, modifier
et ajouter
qui vous ont étés fournies ont recours à des méthodes étranges pour vérifier si le contact est présent ou non au moment de l'opération. Chacune de ces fonction fait appel à une fonction que vous avez implémenté, respectivement suppression
, modification
et ajout
.
Modifier chaque couple de fonction (par exemple ajout
/ajouter
) de manière à ce que la vérification de la présence ou non d'un contact dans la list de contacts soit géré uniquement par les fonctions d'interface, sans changer le comportement du programme.
Attention, en faisant celà, les tests automatiques ne seront plus valables ! Il vous appartient donc de tester le programme à la main.
A votre avis, est-il préférable que la vérification de la présence ou non d'un contact lors de ces opérations soit géré par les fonctions d'interface ? Justifiez votre réponse.