Pouvez-vous résoudre ces trois tâches (trompeusement) simples en Python?

Dès le début de mon parcours de développeur de logiciels, j'ai vraiment adoré creuser à l'intérieur des langages de programmation. J'ai toujours été intéressé par le fonctionnement de telle ou telle construction, le fonctionnement de telle ou telle commande, ce qui se trouve sous le capot du sucre syntaxique, etc. Récemment, je suis tombé sur un article intéressant avec des exemples de la façon dont les objets mutables et immuables en Python ne fonctionnent pas toujours de manière évidente. À mon avis, la clé est de savoir comment le comportement du code change en fonction du type de données utilisées, tout en conservant la même sémantique et les constructions de langage utilisées. Ceci est un excellent exemple de réflexion non seulement lors de l'écriture, mais aussi lors de l'utilisation. J'invite tout le monde à se familiariser avec la traduction.







Essayez de résoudre ces trois problèmes, puis vérifiez les réponses à la fin de l'article.



Conseil : les problèmes ont quelque chose en commun, alors revenez sur le premier problème lorsque vous passez au deuxième ou au troisième, ce sera plus facile pour vous.



Première tâche



Il existe plusieurs variables:



x = 1
y = 2
l = [x, y]
x += 5

a = [1]
b = [2]
s = [a, b]
a.append(5)


Qu'est-ce qui sera affiché lors de l'impression let s?



Deuxième tâche



Définissez une fonction simple:



def f(x, s=set()):
    s.add(x)
    print(s)


Que se passe-t-il si vous appelez:



>>f(7)
>>f(6, {4, 5})
>>f(2)


Troisième tâche



Définissons deux fonctions simples:



def f():
    l = [1]
    def inner(x):
        l.append(x)
        return l
    return inner

def g():
    y = 1
    def inner(x):
        y += x
        return y
    return inner


Qu'obtient-on après avoir exécuté ces commandes?



>>f_inner = f()
>>print(f_inner(2))

>>g_inner = g()
>>print(g_inner(2))


Êtes-vous confiant dans vos réponses? Vérifions votre cas.



La solution au premier problème



>>print(l)
[1, 2]

>>print(s)
[[1, 5], [2]]


Pourquoi la deuxième liste réagit-elle à une modification de son premier élément a.append(5), alors que la première liste ignore complètement la même modification x+=5?



Solution du deuxième problème



Voyons ce qui se passe:



>>f(7)
{7}

>>f(6, {4, 5})
{4, 5, 6}

>>f(2)
{2, 7}


Attendez, le dernier résultat ne devrait-il pas être {2}?



La solution au troisième problème



Le résultat sera comme ceci:



>>f_inner = f()
>>print(f_inner(2))
[1, 2]

>>g_inner = g()
>>print(g_inner(2))
UnboundLocalError: local variable ‘y’ referenced before assignment


Pourquoi g_inner(2)pas 3? Pourquoi f()la fonction interne se souvient de la portée externe, mais la fonction interne g()ne se souvient pas? Ils sont presque identiques!



Explication



Et si je vous disais que tous ces comportements étranges ont à voir avec la différence entre les objets mutables et immuables en Python?



Les objets mutables tels que les listes, les ensembles ou les dictionnaires peuvent être modifiés sur place. Les objets immuables, tels que les valeurs numériques et de chaîne, les tuples, ne peuvent pas être modifiés; leur «changement» conduira à la création de nouveaux objets.



Explication de la première tâche



x = 1
y = 2
l = [x, y]
x += 5

a = [1]
b = [2]
s = [a, b]
a.append(5)

>>print(l)
[1, 2]

>>print(s)
[[1, 5], [2]]


Puisqu'elle est ximmuable, l'opération x+=5ne modifie pas l'objet d'origine, mais en crée un nouveau. Mais le premier élément de la liste se réfère toujours à l'objet d'origine, donc sa valeur ne change pas.



Car un objet mutable, la commande a.append(5)change l'objet d'origine (plutôt que d'en créer un nouveau), et la liste s«voit» les changements.



Explication de la deuxième tâche



def f(x, s=set()):
    s.add(x)
    print(s)

>>f(7)
{7}

>>f(6, {4, 5})
{4, 5, 6}

>>f(2)
{2, 7}


Tout est clair avec les deux premiers résultats: la première valeur est 7ajoutée à l'ensemble initialement vide et il s'avère {7}; puis la valeur est 6ajoutée à l'ensemble {4, 5}et il s'avère {4, 5, 6}.



Et puis les bizarreries commencent. La valeur est 2ajoutée non pas à l'ensemble vide, mais à {7}. Pourquoi? La valeur initiale du paramètre optionnel n'est sévaluée qu'une seule fois: lors du premier appel, s sera initialisé comme un ensemble vide. Et comme il est mutable, f(7)il sera changé en place après avoir été appelé . Le deuxième appel f(6, {4, 5})n'affectera pas le paramètre par défaut: il est remplacé par un ensemble {4, 5}, c'est-à-dire qu'il s'agit d' {4, 5}une variable différente. Le troisième appel f(2)utilise la même variablesqui a été utilisé lors du premier appel, mais il n'est pas réinitialisé en tant qu'ensemble vide, mais est à la place pris à partir de sa valeur précédente {7}.



Par conséquent, vous ne devez pas utiliser d'arguments mutables comme arguments par défaut. Dans ce cas, la fonction doit être modifiée:



def f(x, s=None):
    if s is None:
        s = set()
    s.add(x)
    print(s)


Explication de la troisième tâche



def f():
   l = [1]
   def inner(x):
       l.append(x)
       return l
   return inner

def g():
   y = 1
   def inner(x):
       y += x
       return y
   return inner

>>f_inner = f()
>>print(f_inner(2))
[1, 2]

>>g_inner = g()
>>print(g_inner(2))
UnboundLocalError: local variable ‘y’ referenced before assignment


Nous avons affaire ici à des fermetures: les fonctions internes se souviennent de ce à quoi ressemblait leur espace de noms externe quand elles ont été définies. Ou du moins ils devraient s'en souvenir, mais la deuxième fonction fait l'interface de poker et se comporte comme si elle n'avait pas entendu parler de son espace de noms externe.



Pourquoi cela arrive-t-il? Lorsque nous exécutons l.append(x), l'objet mutable créé lorsque la fonction a été définie change. Mais la variable lfait toujours référence à l'ancienne adresse en mémoire. Cependant, une tentative de modification d'une variable immuable dans la deuxième fonction y += xconduit au fait que y commence à se référer à une autre adresse en mémoire: le y d'origine sera oublié, ce qui entraînera une erreur UnboundLocalError.



Conclusion



La différence entre les objets mutables et immuables en Python est très importante. Évitez le comportement étrange décrit dans cet article. Surtout:



  • .
  • - .



All Articles