Journal écrire du code dans le corps d'une classe python

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
12
11
oct.
2024

Découverte que je viens de faire à l'instant : il est possible d'écrire du code dans le corps d'une classe python, et ce code est exécuté automatiquement au chargement du module.

Exemple :

import datetime

class MyClass:
    if datetime.datetime.now().isoweekday() == 5:
        current_day = "trolldi"
        for i in range(10):
            print("TODAY IS", current_day, "!!!!!!!!")
    else:
        current_day = "pas trolldi"

print("current_day:", MyClass.current_day)
$ python3 bla.py
TODAY IS trolldi !!!!!!!!
TODAY IS trolldi !!!!!!!!
TODAY IS trolldi !!!!!!!!
TODAY IS trolldi !!!!!!!!
TODAY IS trolldi !!!!!!!!
TODAY IS trolldi !!!!!!!!
TODAY IS trolldi !!!!!!!!
TODAY IS trolldi !!!!!!!!
TODAY IS trolldi !!!!!!!!
TODAY IS trolldi !!!!!!!!
current_day: trolldi

Voila, c'est tout. Bisous et bon weekend à tous !

  • # Petite bizarrerie à noter

    Posté par  (site web personnel, Mastodon) . Évalué à 6 (+5/-0). Dernière modification le 11 octobre 2024 à 18:41.

    >>> class C:
    ...     x = 0
    ...     def meth(self):
    ...             return x
    ... 
    >>> C().meth()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 4, in meth
    NameError: name 'x' is not defined. Did you mean: 'self.x'?
    

    J'ai toujours trouvé ça déconcertant, mais il paraît que c'est intentionnel.

    • [^] # Re: Petite bizarrerie à noter

      Posté par  (site web personnel) . Évalué à 6 (+4/-0).

      Oui mais :

      >>> x=0
      >>> C().meth()
      0

      Adhérer à l'April, ça vous tente ?

    • [^] # Re: Petite bizarrerie à noter

      Posté par  (site web personnel) . Évalué à 10 (+10/-0).

      Python n'a que 2 espaces de noms : global et local.

      Donc :

      # global
      
      class Foo:
          # local
      
          def meth(self):
              # local (mais un nouveau local, pas celui au dessus)
              ...

      Quand une variable n'est pas définie dans l'espace de nom "local", Python le cherche dans l'espace de nom "global". Dans ton exemple, x n'existe pas dans "global", d'où l'erreur.

      Le corps de la classe est exécuté lors de la création de la classe, mais le corps de la méthode est exécuté que lorsqu'elle est appelée, l'espace de nom du corps de la classe n'existe donc plus à ce moment là.

      Il faut imaginer que ce qu'il se passe c'est ça :

      def Foo_meth(self):
          return x
      
      
      def make_Foo():
          return type("Foo", tuple(), {"x": 0, "meth": Foo_meth})
      
      
      Foo = make_Foo()
      foo = Foo()
      foo.meth()

      https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg

      • [^] # Re: Petite bizarrerie à noter

        Posté par  (site web personnel, Mastodon) . Évalué à 4 (+3/-0).

        Merci, mais je comprends ce qui se passe, je suis juste déconcerté que cette décision ait été prise.

        Python n'a que 2 espaces de noms : global et local.

        Ah non :

        >>> def f():
        ...     x = 0
        ...     def g():
        ...             return x
        ...     return g
        ... 
        >>> f()()
        0
        

        L'espace de nom d'une fonction est disponible dans une fonction définie à l'intérieur d'elle, c'est pour ça que je suis surpris que les classes n'aient pas le même comportement.

        • [^] # Re: Petite bizarrerie à noter

          Posté par  (site web personnel) . Évalué à 10 (+8/-0).

          Tu as bien 2 espaces de nom "global" (accessible via la fonction builtin globals() et "local" (accessible via la fonction builtin locals().

          On vois ici que x ne fait partie ni de "global" ni de "local" :

          >>> def foo():
          ...     x = 0
          ...     def g():
          ...             print(globals())
          ...             print(locals())
          ...     return g
          ...
          >>> foo()()
          {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'foo': <function foo at 0x000001F518DA7F60>}
          {}
          >>>
          

          Mais alors, d'où il vient ?

          Réponse courte : Du locals() de la fonction f().

          Réponse longue : Pour bien comprendre ce qu'il se passe, voici le code désassemblé :

          >>> def f():
          ...     x = 0
          ...     def g():
          ...             return x
          ...     return g
          ...
          >>> import dis
          >>> dis.dis(f)
                        0 MAKE_CELL                1 (x)
          
            1           2 RESUME                   0
          
            2           4 LOAD_CONST               1 (0)
                        6 STORE_DEREF              1 (x)
          
            3           8 LOAD_CLOSURE             1 (x)
                       10 BUILD_TUPLE              1
                       12 LOAD_CONST               2 (<code object g at 0x00000243D97258F0, file "<stdin>", line 3>)
                       14 MAKE_FUNCTION            8 (closure)
                       16 STORE_FAST               0 (g)
          
            5          18 LOAD_FAST                0 (g)
                       20 RETURN_VALUE
          
          Disassembly of <code object g at 0x00000243D97258F0, file "<stdin>", line 3>:
                        0 COPY_FREE_VARS           1
          
            3           2 RESUME                   0
          
            4           4 LOAD_DEREF               0 (x)
                        6 RETURN_VALUE
          

          Premièrement, dans la fonction f on créé la variable x avec l'opcode MAKE_CELL. Et on l'initialise à 0 avec LOAD_CONST et STORE_DEREF.

          Vient ensuite le MAKE_FUNCTION qui va créer la fonction g() en utilisant un "code object" (le corps de la fonction g compilé), ainsi qu'un tuple qui va contenir l'espace mémoire pour des variables libres.

          Ce tuple est créé avec l'opcode BUILD_TUPLE 1 (tuple de 1 élément), le tuple va être créé en prenant un élément sur la pile. Cet élément c'est une référence vers x qui est chargée avec LOAD_CLOSURE (python 3.11, depuis python 3.13 c'est LOAD_FAST).

          Enfin, dans le code de la fonction g(), on a COPY_FREE_VARS qui va introduire dans la "stack frame" actuelle les variables référencées par le tuple avec lequel on a construit la "closure".

          Démontrant bien qu'il n'existe que 2 espaces de noms : global et local. La fonction g porte avec elle une copie des variables qu'elle capture. Après tout, g n'est pas une fonction mais une "closure" (fonction + environnement).

          En pseudo-code python, voici à peu près ce qu'il se passe :

          def f():
              x = 0
              g = make_g(x)
              return g
          
          
          class make_g:
              # rien ne nous oblige a appeler `self` "self" ;)
              def __init__(free_vars, x):
                  free_vars.x = x
          
              def __call__(free_vars):
                  return free_vars.x

          https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg

    • [^] # Re: Petite bizarrerie à noter

      Posté par  (site web personnel) . Évalué à 3 (+0/-0).

      Ce n’est pas déconcertant, tu as fait une erreur : x n’est pas défini dans la fonction mais dans la classe C.

      L’objet de classe C créé est passé en paramètre sous le nom de self, et donc pour retourner le membre x de self il faut retourner self.x.

      In [3]: class C:
         ...:  x = 0
         ...:  def meth(self):
         ...:   return self.x
         ...: C().meth()
      Out[3]: 0
      

      C’est un autre comportement qui serait déconcertant.

      Le nom self n’est qu’une convention, tu peux faire ça si tu veux:

      In [4]: class C:
         ...:  x = 0
         ...:  def meth(toto):
         ...:   return toto.x
         ...: C().meth()
      Out[4]: 0
      

      Le fonction meth ne connait que l’objet créé de classe C, quelque soit le nom donné à cet objet.

      C’est tout à fait normal qu’il faille accéder à cet objet de classe C pour accéder à x.

      ce commentaire est sous licence cc by 4 et précédentes

  • # hello

    Posté par  . Évalué à 2 (+1/-0).

    Pour le x is not defined, python est quand meme pas trop mal fait, il te suggere comment corriger le pb

    Sinon, rapport a ces variables de classe, je trouve pas toujours ca tres clair, par exemple:
    ```
    class MyClass:
    my_class_var = "osef"

    a = MyClass()
    print(a.my_class_var)

    b = MyClass()
    b.my_class_var = "osef encore"
    print(b.my_class_var)
    print(a.my_class_var)

    class MyOtherClass:
    my_class_var = []

    z = MyOtherClass()
    z.my_class_var.append('a')
    print(z.my_class_var)

    y = MyOtherClass()
    y.my_class_var.append('b')
    print(y.my_class_var)
    print(z.my_class_var)
    ```
    $ python3 .py
    osef
    osef encore
    osef
    ['a']
    ['a', 'b']
    ['a', 'b']

    Avec un string, on pourrait croire que ca se comporte exactement comme une variable d'instance, mais avec une list, on constate bien que non. Ca me perturbe toujours autant!

    ++
    Gi)

    • [^] # Re: hello

      Posté par  (site web personnel) . Évalué à 4 (+2/-0).

      Lors de l'instanciation de la classe, les variables de classes sont copiées dans l'instance (self).

      Les collections en Python sont des références, c'est donc la référence qui est copiée.

      C'est comme en C au final :

      struct foo {
        int a;
        int *p;
      };
      
      int n = 42;
      struct foo x = { .a = 23, .p = &n };
      struct foo y;
      
      memcpy(&y, &x, sizeof(struct foo));
      
      *y.p = 0;
      assert(*x.p == 0);

      https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg

  • # what

    Posté par  (site web personnel) . Évalué à 2 (+2/-1).

    Sinon je suis le seul à penser que c'est juste horrible comme comportement ?

    • [^] # Re: what

      Posté par  (site web personnel) . Évalué à 5 (+4/-1).

      Python est un langage dynamique, la création de classe est donc également dynamique. C'est un aspect de Python qui le rend très puissant, et la plupart des ORMs Python utilisent ce comportement.

      Je ne vois rien de choquant ici.

      https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg

      • [^] # Re: what

        Posté par  (site web personnel) . Évalué à 2 (+2/-1). Dernière modification le 11 octobre 2024 à 23:17.

        Dans l'absolu, c'est vrai, mais purée, les comportements imbitables que ça produit…

        • [^] # Re: what

          Posté par  (site web personnel) . Évalué à 9 (+8/-1).

          "Imbitable" ça insinue que cela serait impossible a comprendre.

          Hors cela suit des règles simples et précises et d'autant plus logique si on tient compte de la nature et la philosophie du langage.

          https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg

          • [^] # Re: what

            Posté par  . Évalué à 4 (+1/-0).

            Les règles compréhensibles peuvent provoquer des comportements qui le sont moins quand on passe à l'échelle. Les règles du jeu de la vie sont simples et compréhensible. Prévoir l'évolution du système est indécidable.

        • [^] # Re: what

          Posté par  . Évalué à 6 (+4/-0).

          C'est surtout que quand tu viens du monde rigoureux (rigide ?) du typage statique c'est une chose de plus de déconcertante

          • [^] # Re: what

            Posté par  . Évalué à 4 (+1/-0).

            C'est pas "que" déconcertant, c'est que ça peut faire facilement péter les hypothèses que pourra faire un code utilisant la classe, dans une bibliothèque par exemple. Tu supposes que le code va marcher partout, manque de bol lors de l'import c'est quasi une classe différente que tu utilises en important la même biblio.

            En typage statique c'est aussi possible de faire ce genre de truc, avec des macros en C par exemple, mais potentiellement si t'as changé la signature d'une fonction ça passera pas la compilation. En typage dynamique tu fais péter tous les gardes fous et tu peux faire vraiment sauter toutes les hypothèses que va faire le code client, et ça peut sauter à la figure n'importe quand à l'exécution. Ça rejoint un peu le genre de critique qu'on peut faire à l'Aspect Oriented Programming, ou a la fin tout peut être modifié et tu n'as plus vraiment de garantie de savoir ce qui va être exécutés, les spécifications du code que t'utilise peuvent être changées un peu n'importe comment.

            • [^] # Re: what

              Posté par  (site web personnel) . Évalué à 2 (+0/-0).

              Je comprends que l'on trouver certains comportements de langages déconcertants.
              Chaque langage a ses forces et ses faiblesses.

              Mais l'informatique n'est pas une science exacte, cela se saurait …, et surtout rien ne vaut une bonne phase de test … avec un environnement et un jeu de données probants.

              • [^] # Re: what

                Posté par  . Évalué à 4 (+1/-0).

                Une vérification de certaines propriétés en phase de compilation n'est pas incompatible avec ça et permettra de concentrer les tests sur des comportements de plus haut niveau.

                Par ailleurs si tu codes une librairie, par exemple, un cas d'utilisation de ce type de comportements, tu portes la responsabilité de tester ta bibliothèque sur l'utilisateur vu que tu ne connais pas nécessairement son environnement d'exécution.

  • # Utilité

    Posté par  . Évalué à 4 (+3/-0). Dernière modification le 13 octobre 2024 à 16:18.

    Oui, Python est conçu pour permettre l'exécution en tant que script exécutable indépendamment.

    Les classes sont typiquement écrites pour être importé ou exécuté en mode interactif, par exemple pour des tests unitaires. Pour cela on détecte comme suit si on est dans l'environnement d'exécution principal :

        if __name__ == '__main__':
            # environnement d'exécution principal
            ...

    Doc officielle sur le sujet.

Envoyer un commentaire

Suivre le flux des commentaires

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