Les pièges du langage Python

Le langage Python dispose d’une syntaxe extrêmement simple qui avec le temps devenu une de ses principales forces. Rappelons qu’historiquement ce langage avait été conçu pour enseigner la programmation aux enfants ! Mais derrière cette syntaxe enfantine se cache, comme tous les autres langages, des pièges loin d’être évidents. Nous présentons dans ce chapitre les pièges les plus courants faisant généralement perdre pas mal de temps aux développeurs.

Conflit entre variables globales et locales

Dans le corps d’une fonction, si aucune variable locale ne porte le nom recherché, alors on recherche une variable globale portant ce nom. Voici un exemple :

a = 5
def fnt1():
    a = 6       # définition d'une variable locale valant 6 masquant la variable globale
    print(a)    # >> 6
def fnt2():
    print(a)    # absence de variable locale, on consulte la variable globale

La syntaxe : a = … dans une fonction crée, par convention, une variable locale. Si une variable globale porte le même nom, elle est donc masquée par cette nouvelle variable locale. Cependant, si vous cherchez à modifier une variable globale en écrivant a=6, vous n’allez pas y arriver de cette façon. Pour modifier une variable globale depuis l’intérieur d’une fonction, il faut utiliser le mot clef global :

Pour pouvoir accéder à une variable globale en écriture à l’intérieur d’une fonction, il faut en début de fonction utiliser le mot clef global suivi du nom de la variable.

Voici un exemple :

a = 4
def fnt3():
    global a    # relie le nom "a" à la variable globale nommée "a"
    a = 6       # la variable globale vaut maintenant 6
fnt3()
print(a)       # >> 6

Passage

Le tout objet

Aussi étrange que cela puisse paraître, en Python tout est objet. Ainsi la notion de type de base (int, float) que l’on rencontre dans les autres langages comme Java/C/C++ n’existe pas. Un moyen de s’en rendre compte est de travailler sur de grands entiers :

a = 10
for i in range(5):
   a = a * a
   print(a,type(a))

>> 100 <class 'int'>
>> 10000 <class 'int'>
>> 100000000 <class 'int'>
>> 10000000000000000 <class 'int'>
>> 100000000000000000000000000000000 <class 'int'>

Si nous avions travaillé en C/C++/Java, pour stocker ces résultats, il aurait fallu utiliser un int, puis un __int64 et finalement un BigInteger. Ici, tout est transparent à notre niveau, la classe int Python s’est chargée de cela à notre place.

Avertissement

Les numériques sont immutables en Python, on ne peut modifier leur valeur. Dans cette logique, l’opérateur ++ et – n’existent pas en Python. Cependant il est possible d’écrire a = a + 4 car on associe la variable a à un nouvel objet, on ne modifie pas son contenu !

Passage par affectation

Dans le langage Python, lors de l’appel d’une fonction, les arguments sont passés par affectation. Ainsi, pour chaque paramètre est créé une variable locale faisant référence à l’objet passé en argument. Ce mode de passage correspond au passage par adresse en C/C++ (par pointeur) et au passage par référence en Java/C#. Il ne s’agit pas du passage par référence du C++ car l’argument et le paramètre, en Python, sont deux variables distinctes. Pour tester cela, nous allons utiliser la fonction id() qui retourne le numéro d’identification unique de chaque objet.

En conclusion :

Une affectation effectuée sur un paramètre de la fonction ne modifie pas l’objet initial passé en argument.

Cas des valeurs numériques

a = 6

def fnt(b):              # la variable T désigne la liste [1,2,3]
   print(id(a),id(b))    # 914 914, à ce niveau les variables a et b désignent le même objet
   print(a,b)            # 6 6
   b = 7                 # b est associé à un nouvel objet, cette affectation ne modifie pas la variable originale a
   print(a,b)            # 6 7
   print(id(a),id(b))    # 914 2298 à ce niveau les variables a et b désignent des objets différents

fnt(a)

Avertissement

Où est le piège ? Si l’on ignore cette logique, on est tenté de penser, par analogie avec les autres langages, que Python effectue un passage par valeur sur les types numériques, ce qui n’est pas le cas…

Cas des listes

L = [ 1 , 2 , 3 ]

def fnt(T):            # la variable T désigne la liste [1, 2, 3]
   T[0] = 7            # cette affectation modifie une valeur dans la liste originale

   print(id(L),id(T))  # 59912 59912  => L et T désignent la même liste

   T = [1,2]           # T est une variable locale, on peut l'affecter à une autre liste comme toute variable !
                       # cette affectation ne modifie en rien la liste L

   print(id(L),id(T))  # 59912 186    => L et T désignent des listes différentes

fnt(L)
print(L)               # [7, 2, 3]  l'affectation T[0] = 7 a modifié L mais pas l'affectation T = [1,2]

Ici, si l’on oublie le fonctionnement interne de Python, on peut ne plus rien comprendre à ce qu’il se passe car on observe deux comportements différents pour l’affectation :

  • L’écriture T[0] = 7 modifie un élément de la liste L, car l’affectation se produit sur le premier élément de la liste

  • L’écriture T = [1,2] ne change pas la liste L car ici on associe la variable T à une autre liste, on ne change pas le contenu de L

Modifier un entier passé en argument

Nous venons de voir qu’il n’est pas possible de modifier un entier passé en paramètre depuis l’intérieur d’une fonction. Pour effectuer cela, on peut utiliser le mécanisme de retour d’une fonction :

x, y = 1, 2

def fnt1(a, b):
    a += 1            # a et b sont des variables locales
    b += 1
    print(x,y)        # 1 2  car modifier a et b n'a pas changé x et y
    return a,b        # retour des résultats

x, y = fnt1(x, y)     # récupération des résultats et affection de x et y dans la foulée
print(x,y)            # 2 3

L’autre astuce consiste à stocker les valeurs dans une liste qui permet la modification de ses valeurs à l’intérieur de la fonction :

X = [1,2]

def fnt2(L):
    L[0] += 1         # La liste L correspond à la liste X
    L[1] += 1
    print(X)          # 2 3 => les valeurs dans la liste originale ont été modifiées

fnt2(X)
print(X)
>> 2 3

Clonage

Problématique

Nous avons vu que si L est une liste, alors l’écriture T=L crée une variable T désignant la liste initiale L, il n’y a pas création d’une deuxième liste. Si vous ne comprenez pas ce mécanisme, vous ne comprendrez pas pourquoi en modifiant les valeurs dans T, la liste L est aussi modifiée ! Voici un exemple :

L = [1, 2, 3]
T = L
T[0] += 1        # la modification sur T se fait sur la liste originale L
print(L)
>> [2, 2, 3]

Effectuer une copie superficielle

Pour cela, il faut utiliser la fonction copy() pour effectuer une duplication :

L = [1, 2, 3]
T = L.copy()
T[0] += 1

print(T)
>> [2, 2, 3]
print(L)
>> [1, 2, 3]

Mais attention, il s’agit d’une copie superficielle, voir le code ci-dessous, les éléments à l’intérieur de la liste ne sont pas dupliqués !

def copy(L):
   T = []
   for e in L:
      T.append(e)
   return T

Si vous avez des immutables dans votre liste, des numériques par exemples, cela ne posera pas problème. Mais dans le cas général, la fonction copy() sera insuffisante :

L = [ [1,2], [3,4] ]
T = L.copy()
T.append([5,6])

print(L)
>> [ [1,2], [3,4] ]
print(T)
>> [ [1,2], [3,4] , [5,6] ]

# T et L sont deux listes indépendantes, mais les listes qu'elles contiennent sont liées :

L[0][0] = 9
L[1][1] = 8
print(L)
>> [ [9,2], [3,8] ]
print(T)
>> [ [9,2], [3,8] , [5,6] ]   # oups c'est l'accident, en modifiant L on a modifié T !!

Effectuer une copie profonde

Pour dupliquer un objet en construisant une copie profonde de manière récursive, il faut utiliser la fonction copy.deepcopy() :

import copy
L = [ [1,2], [3,4] ]
T = copy.deepcopy(L)
L[0][0] = 9
L[1][1] = 8

print(L)
>> [[9, 2], [3, 8]]
print(T)
>> [[1,2], [3,4]]    Les listes présentes dans la liste T sont indépendantes de celles de la liste L

Pour chaque affirmation, indiquez si elle est vraie ou fausse.

Si on modifie les éléments d’une liste, l’identificateur de la liste change (id)

Le mot clef global permet d’avoir un accès en écriture vers une variable globale

Pour effectuer une copie profonde, il faut utiliser la fonction copy()

Une affectation effectuée sur un paramètre de fonction modifie l’objet passé en argument

Les listes sont de type mutable

Les tuples sont de type mutable

Les int sont de type mutable

L’opérateur ++ n’existe pas en Python