Aller au contenu

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 les tuple 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 :

liste[position] = nouvelle_valeur

exemple :

l = [1, 2, 3]
print(l)
l[0] = 5
print(l)

Affiche :

[1, 2, 3]
[5, 2, 3]

Exercice

Complétez la fonction suivante pour qu'elle ajoute v à tous les éléments de la liste passée en paramètre

def ajouter(liste, v):
    """Retourne liste où chaque élément a recu +v"""
    for i in range(len(liste)):
        #complétez ici
    return liste
def ajouter(liste, v):
    """Retourne liste où chaque élément a recu +v"""
    for i in range(len(liste)):
        liste[i] = liste[i] + v
    return liste

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 :

la_list.append(element)

Par exemple :

l = [1, 2, 3]
l.append(0)
print(l)

Affiche

[1, 2, 3, 0]

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.

def filtrage_inferieur(liste, v):
    """liste une list de nombres. 
    v un nombre

    Retourne une list qui comprends uniquement les élements de liste strictement inférieurs à v."""
    copie = #complétez ici
    for e in liste:
        #complétez ici
    return copie
def filtrage_inferieur(liste, v):
    """liste une list de nombres. 
    v un nombre

    Retourne une list qui comprends uniquement les élements de liste strictement inférieurs à v."""
    copie = [] ##liste vide pour commencer
    for e in liste:
        if e < v:
            copie.append(e)
    return copie

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 :

la_list.insert(position, element)

Par exemple,

l = ["a", "b", "c", "d", "e"]
l.insert(2, "z")
print(l)

Affiche :

["a", "b", "z", "c", "d", "e"]

Exercice

Complétez la fonction suivante pour qu'elle retourne une copie de liste où les élements sont en ordre inverse.

def inverse(liste):
    """liste une list. 

    Retourne une list des élements de liste en ordre inverse"""
    copie = #complétez ici
    for e in liste:
        #complétez ici
    return copie
def inverse(liste):
    """liste une list. 

    Retourne une list des élements de liste en ordre inverse"""
    copie = []
    for e in liste:
        copie.insert(0, e)
    return copie

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:

la_liste.remove(valeur)

Exemple :

l = [1, 2, 3, 2, 5]
l.remove(2)
print(l)

Affiche :

[1, 3, 2, 5]

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.

def supprimer_tous(liste, v):
    """liste une list. 
    v une valeur

    Retourne une list qui contient tous les éléments de liste sauf ceux égaux à v"""
    while  ... : #complétez les ...
        # completez ici
    return liste

Rappel : pour vérifier si un élément est présent dans une list, vous pouvez utiliser l'opérateur in.

1 in [1, 2, 3] # vaut True
5 in [1, 2, 3] # vaut False

Tant qu'il y a une valeur égale à v dans liste, on retire la première valeur égale à v de liste.

def supprimer_tous(liste, v):
    """liste une list. 
    v une valeur

    Retourne une list qui contient tous les éléments de liste sauf ceux égaux à v"""

    while v in liste : #complétez les ...
        liste.remove(v)
    return 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 :

l1 = [1, 2, 3]
l2 = l1
l1[2] = 0 #On modifie le 3eme élément de l1
print(l1)
print(l2)

affiche

[1, 2, 0]
[1, 2, 0]

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 :

def ajout(l):
    l.append(0)

l = [1,2,3]
ajout(l)
print(l)

Affiche

[1, 2, 3, 0]

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

l = [1, 2, 3]
ajout(l)
print(l)

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.

def ajout(l):
    c = l.copy()
    c.append(0)
    return c


l = [1, 2, 3]
l = ajout(l)
print(l)

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 :

("Alex Emple", "alexemple@unmail.fr")

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

Pour chaque élément e de la liste : 
    si le nom de e est égal au nom recherché, 
        alors on retourne e
si aucun élément ne corresponds (on est sorti de la boucle),
    on retourne None
Solution
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.
    """
    for i in contacts:
        if nom == i[0]:
            return i
    return None

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

Solution
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.
    """
return str(contacts)

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 :

def copie(l):
    """l une list
    retourne une copie de l
    """
    pass

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

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

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.