Journal Quizz Python : esp[èa]ce de nom

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
15
8
mar.
2023

Demat' iNal

Regarde bien dans les yeux le bout de code suivant

def foo():
  x = 1
  class bar:
    x = x + 1
  return bar
foo()

exécute le mentalement. Rien d'exceptionnel ?
Et pourtant si,

NameError: name 'x' is not defined

Maintenant essayons avec :

def foo():
  x = 1
  class bar:
    y = x + 1
  return bar
foo()

Et là… non, rien.

Intuitivement, j'imagine que le x = x + 1 rend Python tout confus, mais j'aimerai mettre le doigt sur une source officielle et je n'la trouve point. Sauras-tu faire mieux que moi ?

  • # Le moi d'après répond au moi d'avant

    Posté par  (site web personnel) . Évalué à 10.

    D'après https://docs.python.org/3/reference/executionmodel.html#resolution-of-names :

    If a name binding operation occurs anywhere within a code block, all uses of the name within the block are treated as references to the current block. This can lead to errors when a name is used within a block before it is bound. This rule is subtle.

    en gros on rentre dans le scope de la classe. Ce scope introduit une variable locale x et donc toute référence à x dans ce scope se fera en référançant la variable locale. Or lors de l'évaluation de la partie droite de l'assignation, x n'est pas encore assigné, bam NameError.

    • [^] # Re: Le moi d'après répond au moi d'avant

      Posté par  (site web personnel) . Évalué à 2. Dernière modification le 08 mars 2023 à 21:41.

      Tout à fait. Et un IDE qui ne soulignerait pas en rouge le x de x+1 serait à mon avis un très mauvais IDE pour Python.

      • [^] # Re: Le moi d'après répond au moi d'avant

        Posté par  (Mastodon) . Évalué à 9.

        un IDE qui ne soulignerait pas en rouge le x de x+1 serait à mon avis un très mauvais IDE

        Je pardonne plus facilement l'IDE que le codeur. Non mais sérieusement, c'est quoi cette horreur ?

        En théorie, la théorie et la pratique c'est pareil. En pratique c'est pas vrai.

        • [^] # Re: Le moi d'après répond au moi d'avant

          Posté par  (site web personnel) . Évalué à 4.

          J’avoue qu'en ait la question ne devrait même pas se poser. Peu importe la réponse, c'est non maintenable et ce genre de chose c'est direct rejeté en peer review ^

          Python fait des choses bizarre parfois avec la portées des variables, faut rester simples, sinon on se prends les pieds dans le tapis très, très rapidement.

  • # Et pourtant c'est simple

    Posté par  (site web personnel) . Évalué à 4.

    Comme toujours en python, enfin la plupart du temps, c'est simple

    Imagine que Python utilise un dictionnaire ou une table correspondance pour gérer ses variables

    Chaque fonction a droit a ses variables et donc sa table de correspondance.

    Si on reprend ton exemple :

    Création de la fonction foo
    => création et affectation de x a partir d'un entier
    => Création de la class bar
    ==> affectation de x à partir de x (qui n'existe pas dans la class bar …) => erreur

    Je devrais abuser de ce forum et te conseiller un excellent livre sur Python :)

    mais comme la 2eme édition ne va pas tarder à sortir … attends un peu ;)

    • [^] # Re: Et pourtant c'est simple

      Posté par  . Évalué à 5.

      Création de la fonction foo
      => création et affectation de x a partir d'un entier
      => Création de la class bar
      ==> affectation de x à partir de x (qui n'existe pas dans la class bar …) => erreur

      Pas tout à fait sinon la seconde méthode marcherait pas non plus.

      Création de la fonction foo
      => création et affectation de x a partir d'un entier
      => Création de la class bar
      ==> le bloc crée une variable x donc on retire x de la table
      ==> affectation de x à partir de x => erreur

      Du coup ça fonctionne :

      def foo():
        x = 1
        class bar:
          y = x + 1
        return bar
      foo()

      Du coup ça plante :

      def foo():
        x = 1
        class bar:
          y = x + 1
          x = 1
        return bar
      foo()

      Toutes les variables crées dans un bloque cachent les variables disponibles dès le début du bloque. Ce n'est pas la déclaration d'une variable à partir d'elle même qui pose problème, mais le fait d'utiliser une variable qui sera surchargée dans ce même bloque.

      https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll

      • [^] # Re: Et pourtant c'est simple

        Posté par  (site web personnel) . Évalué à 5. Dernière modification le 09 mars 2023 à 12:01.

        dès le début du bloque

        forcément, ça (dé-)bloque dans tout le bloc… tout comme la brique braque dans le bric à brac ;-)

      • [^] # Re: Et pourtant c'est simple

        Posté par  (site web personnel) . Évalué à 3. Dernière modification le 09 mars 2023 à 19:12.

        J'ai répondu un peu trop rapidement …
        Tu as raison d'ailleurs les dernières versions de python sont plus précises

        Traceback (most recent call last):
          File "/home/chris/dvp/env/t1.py", line 6, in <module>
            foo()
          File "/home/chris/dvp/env/t1.py", line 3, in foo
            class bar:
          File "/home/chris/dvp/env/t1.py", line 4, in bar
        ____x = x + 1
        ________^
        NameError: name 'x' is not defined

        Remarque : les underscores sont la pour respecter la cadrage

        Donc python crée une variable locale et ne va pas rechercher dans les variable "globales" la valeur de x donc la recherche ne se fait que dans la portée "locale"

        Alors que dans y = x + 1
        x fait partie des variables "globales" de la classe bar car cette class a été déclarée à l'intérieur de la fonction et donc cela ne déclenche par d'erreur

        Si en plus on demande au langage python de retourner une classe et pas une instance de classe

        par curiosité :

            def foo():
                  x = 1
                  class bar:
                    y = x + 1
                  return bar
                print(foo())

        cela retourne :
        <class 'main.foo.<locals>.bar'>

        Python version 3.11

        PS: désolé pour la présentation, je comprends pas pourquoi les backquotes ne me font pas du beau code en python …

        • [^] # Re: Et pourtant c'est simple

          Posté par  (site web personnel, Mastodon) . Évalué à 2.

          PS : C'est le Markdown de LinuxFr qui casse parfois les pieds ; t'as bien pensé à sauter une ligne avant et après ?

          “It is seldom that liberty of any kind is lost all at once.” ― David Hume

        • [^] # Re: Et pourtant c'est simple

          Posté par  (site web personnel) . Évalué à 2. Dernière modification le 10 mars 2023 à 02:44.

          PS: désolé pour la présentation, je comprends pas pourquoi les backquotes ne me font pas du beau code en python …

          l'explication est sur le wiki LinuxFr.org où il est indiqué :

          pensez à ajouter une ligne blanche avant la balise ``` ;

          PS : C'est le Markdown de LinuxFr qui casse parfois les pieds ; t'as bien pensé à sauter une ligne avant et après ?

          merci Gil< de l'explication :-) (Gil et Oumph sont de bonne facture :p)

          et puis bon, hein, si c'est pas assez clair, plaignez-vous ! (bah, je mettrai à jour la page avec vos remarques :p)

        • [^] # Re: Et pourtant c'est simple

          Posté par  . Évalué à 1.

          Et d’ailleurs on peut accéder dans le deuxième cas a y, qui a pour valeur 2

      • [^] # Re: Et pourtant c'est simple

        Posté par  (site web personnel) . Évalué à 7. Dernière modification le 09 mars 2023 à 15:03.

        le bloc crée une variable x donc on retire x de la table

        Ha mais non, on supprime rien.

        C'est plutôt que la variable x dans le namespace de la class bar "masque" la variable x dans le namespace de foo.

        Le fonctionnement global est celui ci:

        Lors de la définition d'une fonction (ou d'une class ici), un "scope" (local) est créé.

        Lors de la résolution de nom (la recherche de l’objet qui correspond à x), on cherche d'abord dans ce scope local. Si on le trouve pas et si la définition de la fonction à lieu dans un scope (par exemple à l'intérieur de l’exécution de foo()), on cherche dans ce scope "parent". Si on le trouve toujours pas, on remonte à nouveau (si on peut). Enfin, on arrive au scope global (celui du module) et si on trouve toujours pas, on arrête avec une exception NameError.

        La subtilité, c'est qu'on attend pas de faire un x = 1 pour créer la variable x dans son scope. À la définition de la fonction, python "sait" qu'on va avoir besoin d'une variable x et donc il crée le slot (vide) dans le scope dès le début de l’exécution.

        Donc lors de la résolution de nom, il trouve x dans le scope local et donc arrête de chercher dans le scopes parents. Mais comme x n'est pas encore initialisé, bam !

        Donc c'est bien l'existence de la variable x dans le scope local qui masque x dans les scopes parent même si l'initialisation de x a lieu après la ligne qui plante.

        Ça donne des trucs rigolo :

        • Ça marche de manière récursive :
        a = 5 
        def foo():
            b = 10
            def bar():
               c = 15
               def baz():
                   d = 20
                   print(a + b + c + d)
               return baz
            return bar()
        
        print(foo()()) # Affiche 50
        • La résolution de nom à lieu à l’exécution du code, ce qui peut amener des surprises :
        def foo():
            for i in range(3):
                def bar():
                    print(i)
                yield bar
        
        # Affiche 0, 1 et 2
        for f in foo():
            f()
        
        # Affiche 2, 2, 2
        functions = list(foo())
        for f in functions:
            # Lors de l'appel à `f`, la boucle dans foo a fini et donc i vaut tout le temps 2
            f()
        • Si on veut forcer la résolution des nom à la définition, il faut lire la valeur à la définition:
        def foo():
           for i in range(3):
               def bar(i=i):
                   # i est dans le scope local (c'est un argument) et ça valeur par défaut
                   # est la valeur de i (dans foo) *au moment de la définition de bar*
                   print(i)
               yield bar
        
        # Affiche 0, 1 et 2
        functions = list(foo())
        for f in functions:
            f()
        • Le mot clé nonlocal permet de dire à python de ne pas créer de slot dans le scope local mais d'utiliser celui du scope parent (et seulement celui là). global fait la même chose mais dit d'utiliser le scope global :
        def foo():
            x = 5
            def bar():
               nonlocal x
               x = 6
               print(x)
            yield bar
            def baz():
               print(x)
            yield baz
        
        # Affiche 6 et 6
        for f in foo():
            f()

        Matthieu Gautier|irc:starmad

        • [^] # Re: Et pourtant c'est simple

          Posté par  . Évalué à 7.

          Et après y'en a encore pour dire que c'est plus lisible que perl…

          perl a le

          use strict; 
          use warnings;

          qui manque cruellement…

          Il ne faut pas décorner les boeufs avant d'avoir semé le vent

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.