Journal Quel pov' type

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
13
21
mar.
2024

Demat'iNal

Il m'arrive de soumettre des patch à des projets Python utilisant cette délicate innovation qu'est le système de typage pour Python. Si si, tu sais ces trucs à base de mypy qui cherchent à améliorer le monde en le rendant plus rigide.

Regardez donc cette beauté :

try:
    from yaml import CSafeLoader as SafeLoader
except ImportError:
    from yaml import SafeLoader

qui essaie d'utiliser la version native (et plus rapide) qui peut ne pas être présent sur le système, d'où l'alternative. C'est je pense assez idiomatique comme code.

Pour plaire au système de type, je vous propose ce code qui a de vieux relents de C

import typing

from yaml import SafeLoader as _SafeLoader
try:
    from yaml import CSafeLoader
    SafeLoader = typing.cast(type[_SafeLoader], CSafeLoader)
except ImportError:
    SafeLoader = _SafeLoader

Y aurait-il une meilleur approche ?

  • # type: ignore

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

    Je mettrais simplement un commentaire avec type: ignore :

    try:
        from yaml import CSafeLoader as SafeLoader
    except ImportError:
        from yaml import SafeLoader  # type: ignore # appaise mypy: 2em assignement avec un autre module                                                           

    Personnellement, les types me sont très utiles. Ils attrapent beaucoup d'erreurs qui ne se signaleraient que bien plus tard. L'effort pour ajouter des annotations, et la difficulté pour annoter certaines constructions, vaut le coût.

    • [^] # Re: type: ignore

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

      Tiens tiens, une connaissance, salut, ça fait plaisir :-)

      C'est la solution que j'ai utilisée au final, mais ce n'est pas très satisfaisant intellectuellement. C'est du typage graduel quoi Gradual_typing.

      • [^] # Re: type: ignore

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

        Salut Serge ! J'avais pas vu le nom avant de répondre :)

        Il m'est venu une autre idée :

        import yaml
        SafeLoader: type["PySafeLoader"] | type["CSafeLoader"]
        try:
            from yaml import CSafeLoader
            SafeLoader = CSafeLoader
        except ImportError:
            from yaml import SafeLoader as PySafeLoader
            SafeLoader = PySafeLoader
        
        y = yaml.load("a: 1", Loader=SafeLoader)
        assert y == {"a": 1}

        Pas de cast ou de type: ignore, et mypy peut vérifier que les opérations que tu fais sur SafeLoader sont valides à la fois avec CSafeLoader et PySafeLoader.

        Clairement c'est pas évident, c'est un exemple où le typage est plus difficile qu'il ne devrait l'être. Mais on pourrait aussi dire : pourquoi donc yaml ne détecte pas tout seul si CSafeLoader est disponible, et force l'utilisateur à faire ces acrobaties :)

        Aussi, la syntaxe blah: typ1 | typ2 est relativement nouvelle (Python 3.10 je crois?), utilise typing.Union[typ1, type2] si tu as un Python plus vieux.

        • [^] # Re: type: ignore

          Posté par  . Évalué à 2.

          Clairement c'est pas évident, c'est un exemple où le typage est plus difficile qu'il ne devrait l'être. Mais on pourrait aussi dire : pourquoi donc yaml ne détecte pas tout seul si CSafeLoader est disponible, et force l'utilisateur à faire ces acrobaties :)

          Ou pourrait implementer une classe abstraite qui servirait au typing.

  • # Double import

    Posté par  . Évalué à 2.

    Je comprends le besoin, mais du coup, c'est pas un peu dommage d'importer la bibliothèque inutilement ?

    J'avoue que parfois, j'ai trop d'obsession pour la performance…

  • # Mauvais compromis

    Posté par  . Évalué à 10.

    Je trouve que ce système de typage Python est un assez mauvais compromis au final. Ça ruine la clarté de python car c'est très verbeux et ça n'arrive pas a la cheville d'un vrai typage statique

    • [^] # Re: Mauvais compromis

      Posté par  . Évalué à 0.

      Question de goût probablement

    • [^] # Re: Mauvais compromis

      Posté par  . Évalué à 10.

      Je suis d'accord. Python perd chaque année de sa simplicité et de sa beauté. Ça va finir par ressembler à du Rust (bim deux d'un coup :D).

    • [^] # Re: Mauvais compromis

      Posté par  (site web personnel) . Évalué à 5. Dernière modification le 22 mars 2024 à 10:51.

      Complètement d'accord.

      Plus je l'utilise, plus je suis contre.

      Il faut noter que mypy est un très mauvais outil, il abandonne très vite lorsqu'il s'agit d'inférer des types complexes. Pyright (qui est développé en typescript et inclus de base avec VS Code) est mieux, mais on a toujours pas un type-checker qui soit solide.

      Le système de type que Python propose est de plus peu expressif et ne fonctionne tout simplement pas dans des cas pourtant simple, par exemple :

      type Request = Union[
        tuple[Literal["action_a"], PayloadA],
        tuple[Literal["action_b"], PayloadB],
      ]
      
      match request:
          case ("action_a", payload_a):
              pass
      
          case ("action_b", payload_b):
              pass

      Dans cet exemple, mypy ne sait pas inférer le type de payload_a ou payload_b.

      Et il sera impossible d'exprimer un type Response dont les variants dépendent du variant de Request :

      type Response = Union[
          ResponseA,
          ResponseB,
      ]

      Comment exprimer que quand j'ai une PayloadA je reçois une ResponseA ? En Python c'est pas possible.

      De plus, Python est un langage qui est de nature purement dynamique. Aucun type-checker ne pourra vérifier la magie noire que l'on peut faire avec de la magie noire à base de subclasshook. Et pourtant ce sont des choses bien utiles parfois lorsqu'il est question de génération de code au runtime.

      Au final, je deviens de l'avis suivant :

      Si tu veux un système de type, tu veux un autre langage, alors utilise un autre langage.

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

    • [^] # Re: Mauvais compromis

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

      c'est très verbeux

      Dans mon expérience, 90% du typage c'est annoter les arguments des fonctions ou les attributs de classe avec var: TypeDeVar. Je manque peut-être d'imagination :) mais je ne sais pas comment on pourrait faire plus court. Peut-être plus d'inférences : Pourquoi est-ce que je suis obligé d'ajouter -> None à toutes les routines qui ne renvoient rien du tout ? Sûrement quand j'ai un keyword argument f(var=1) mypy pourrait décider que var doit être de type int ? Pour éviter ça je me suis senti obligé de mettre l'option no-untyped-def, qui est vraiment pédante.

      def log(msg: str, level=1):
          print("%d: %s" % (level, msg))
      
      reveal_type(log)   # Revealed type is "def (msg: builtins.str, level: Any =) -> Any"

      ça n'arrive pas a la cheville d'un vrai typage statique

      C'est vrai. C'est clairement un rajout qui s'intègre moyennement bien au langage (ex: le problème de Serge). Mon problème à moi serait plutôt combien il est facile d'avoir des Any un peu partout dès qu'on oublie quelque chose (ex : juste au-dessus), ou lorsqu'on utilise une bibliothèque tierce qui n'a pas de types.

      Dans l'ensemble et malgré tout ça, je trouve que c'est très utile, et ça m'évite un grand nombre d'erreurs avant de lancer mon code, donc j'utilise.

  • # La seule bonne méthode

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

    import typing
    
    try:
        from yaml import CSafeLoader as SafeLoader
    
        if typing.TYPE_CHECKING:
            from yaml import SafeLoader as _SafeLoader
            SafeLoader = typing.cast(type[_SafeLoader], SafeLoader)
    
    except ImportError:
        from yaml import SafeLoader

    Le if typing.TYPE_CHECKING n'est entré que lors du (roulement de tambours) type-checking par mypy.

    C'est aussi utile pour les imports circulaires (quand on a des classes définies dans plusieurs modules qui se référencent mutuellement via les annotations).

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

    • [^] # Re: La seule bonne méthode

      Posté par  . Évalué à 3.

      Oui pour typing.TYPE_CHECKING, mais plutôt comme ça:

      import typing
      
      if typing.TYPE_CHECKING:
          from yaml import SafeLoader
      else:
          try:
              from yaml import CSafeLoader as SafeLoader
          except ImportError:
              from yaml import SafeLoader
      • [^] # Re: La seule bonne méthode

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

        Personnellement je préfère la méthode de Frédéric Perrin plus haut:

        SafeLoader: type["PySafeLoader"] | type["CSafeLoader"]
        Ça dit vrament ce qui se passe en réalité , l'objet est soit d'un type soit de l'autre.

Suivre le flux des commentaires

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