Noms et objets non immuables

Nous utilisons ici http://pythontutor.com pour visualiser pas à pas le déroulement d’un programme python et l’évolution des variables.

Un nom est une référence, pas une variable …

  • … la variable est l’objet référencé ; …

  • … et “=” ne fait donc pas de copie de variable.

En Python, il est plus juste de dire que “=relie un nom à un objet.

Malgré tout ça

  • on ne se gênera pas pour parler de variables

  • on se contentera d’être pragmatique

  • la surprise est de courte durée

Deux noms peuvent désigner la même chose…

[1]:
a = [5]
b = a
b[0] = 3

print(f"{a = }\n{b = }")
a = [3]
b = [3]
[2]:
from IPython.display import IFrame
IFrame("https://pythontutor.com/iframe-embed.html#code=a+%3D+%5B5%5D%0Ab+%3D+a%0Ab%5B0%5D+%3D+3&origin=opt-frontend.js&cumulative=false&heapPrimitives=true&drawParentPointers=false&textReferences=false&showOnlyOutputs=false&py=3&rawInputLstJSON=%5B%5D&curInstr=0&codeDivWidth=350&codeDivHeight=400",
       width='100%', height=300)
[2]:

lien

… mais pas toujours

[3]:
# Deux noms PEUVENT désigner la même chose
# mais pas pour toujours

i = 5
j = i
j = 3

print(f"{i = }\n{j = }")
i = 5
j = 3
[4]:
from IPython.display import IFrame
IFrame("https://pythontutor.com/iframe-embed.html#code=i+%3D+5%0Aj+%3D+i%0Aj+%3D+3&origin=opt-frontend.js&cumulative=false&heapPrimitives=true&drawParentPointers=false&textReferences=false&showOnlyOutputs=false&py=3&rawInputLstJSON=%5B%5D&curInstr=0&codeDivWidth=350&codeDivHeight=400",
       width='100%', height=300)
[4]:

lien

Attention aux arguments de type “list”

[5]:
# Fonction test

def somme(x) :
    "Somme x avec lui-même"
    x += x
    return x
[6]:
# Test sur un entier

a = 2

print('avant exécution : ', f"{a = }")

b = somme(a)

print('après exécution : ', f"{a = }")
print('après exécution : ', f"{b = }")
avant exécution :  a = 2
après exécution :  a = 2
après exécution :  b = 4
[7]:
# Test sur une liste

a = [1, 2]

print('avant exécution : ', f"{a = }")

b = somme(a)

print('après exécution : ', f"{a = }")
print('après exécution : ', f"{b = }")
avant exécution :  a = [1, 2]
après exécution :  a = [1, 2, 1, 2]
après exécution :  b = [1, 2, 1, 2]
[8]:
# Mais que se passe-t-il?
from IPython.display import IFrame
IFrame("https://pythontutor.com/iframe-embed.html#code=def+somme(x)+%3A%0A++++%23+On+somme+x+avec+lui-m%C3%AAme%0A++++x+%2B%3D+x%0A++++return+x%0A%0A%23+entier%0Aa+%3D+2%0Ab+%3D+somme(a)%0A%0A%23+liste%0Aa+%3D+%5B1,+2%5D%0Ab+%3D+somme(a)&origin=opt-frontend.js&cumulative=false&heapPrimitives=true&drawParentPointers=false&textReferences=false&showOnlyOutputs=false&py=3&rawInputLstJSON=%5B%5D&curInstr=0&codeDivWidth=350&codeDivHeight=400",
       width='100%', height=500)
[8]:

lien

Portée des variables

La résolution d’un nom suit la règle dite LEGB (Local > Enclosed > Global > Built-in).

Assigner un nom localement (i.e. au sein d’une fonction) ne modifie pas le niveau global.

Le mot clef ``global`` permet de modifier cette règle.

[9]:
# pas de danger à utiliser le même nom localement et globalement

X = 'toto'

def f():
    X = 42
    print(f"local : {X = }")

f()
print(f"global: {X = }")


local : X = 42
global: X = 'toto'
[10]:
# Un cas fréquent (et potentiellement dangereux) est l'utilisation d'une variable globale dans une fonction
X = 3
def triple(a):
    return a*X

triple(3)
[10]:
9
[11]:
# Catastrophe : la fonction a changé de valeur retournée.
X = 4
triple(3)
[11]:
12

conclusion : il faut assumer ses responsabilités quand on modifie un objet.

remarque : si le nom est une référence, il n’y a pas de pointeur natif en python.

Exercices

Explorer la liste liste_de_liste construite par :

x = [1, 2, 3]
y = [x, x]
liste_de_liste = [y, y]

Que se passe-t-il quand on modifie un élément de x ? de y ?

Plus de fun ! Construire une liste récursive (dont un élément est elle-même ou bien la contient).