Licence CC BY-SA

[Signet] Des choses sales avec le pattern matching de Python

Un intéressant article qui pousse le pattern matching assez (trop) loin en le combinant avec les classes abstraites.

Les résultats obtenus sont assez intéressant, mais relèvent probablement plus de la magie noire que de la bonne pratique !

A lire ici : CRIMES WITH PYTHON’S PATTERN MATCHING



7 commentaires

Je comprends pas trop pourquoi il qualifie tout ça de crimes. À part les cas à la fin avec des effets de bords (qui eux sont vraiment des crimes), c’est juste de l’algèbre de types et de l’émulation au runtime de structural subtyping. Ce serait plus propre (et simple) avec des @runtime_checkable sur des Protocol, mais fondamentalement c’est la raison d’être de __subclasshook__ donc encore heureux que ça fonctionne avec les match.

+1 -0

Ce qui est drôle c’est qu’on voit ce genre d’immondice seulement dans des tutoriels et peut-être jamais dans du code en production dans des sociétés tout à fait honorables. Il serait intéressant d’avoir l’avis de certains cadors (@entwanne pour ne citer que lui) sur la question.

Ces "immondices" sont la façon d’implémenter les ABC dans la lib standard. Ces implémentations tout ce qu’il y a de plus officielles et utilisées (souvent sans le savoir) par les "sociétés honorables" sont exactement pareilles que le DistanceMetric du billet de blog.

J’irai même plus loin, l’existence de __subclasshook__ vient du mauvais design du système de type de Python qui mélange allègrement les notions de type, classe, et interface tout en déclarant haut et fort faire du duck typing. De fait, le isinstance(t: T, U) porte deux significations qui sont un peu orthogonales mais que Python mélange :

  • T est un sous-type de U au sens le plus direct lorsqu’on dit que les types sont les classes : T hérite simplement de U;
  • T implémente (au moins) la même interface que U (structural subtyping), qui est en fait la sémantique que l’on veut lorsqu’on fait du duck typing : les histoires d’héritages ne sont qu’un morceau incomplet de la réponse à la question de savoir si on a effectivement un canard entre les mains.

La machinerie de __subclasshook__ est justement là pour que isinstance renvoie un résultat pertinent lorsqu’on fait du duck typing, c’est à dire lorsqu’on veut s’intéresser à l’interface effectivement exposée par les objets que l’on manipule plutôt que leur classe. Le fait par ailleurs que Python va pas râler si tu fais n’importe quoi sémantiquement (typiquement en ayant des effets de bords dans __subclasshook__) est juste que Python n’a aucun moyen de contrôler ça, et ça rejoint le contrat "on est entre adultes consentants" qui est de partout dans le langage.

Il n’y a vraiment rien de surprenant dans tout ce billet de blog, pour être franc.

+1 -0

J’ai bien aimé cet article parce qu’il permet de rappeler ce qu’il est effectivement possible de faire à l’aide du système de types de Python (__subclasshook__ n’étant qu’une simplification par le module abc de ce qui existe de base en Python avec __instancecheck__ et __subclasscheck__ dont je parle ici).

J’aime bien parce que ça montre bien que la notion d’appartenance à un type n’est pas liée à l’héritage mais de comment on définit l’interface d’un type. On trouve aussi simplement une méthode register sur les classes abstraites pour simplement déclarer que tel type doit être reconnu comme implémentation de l’interface.

Par contre je ne dirais pas que c’est une question de mauvais design : techniquement on s’en fout que tel type hérite effectivement de tel autre, tout ce qui nous intéresse est de savoir s’il peut être considéré comme tel ou non. Ça facilité l’adaptation de types existants pour coller à une interface ou la création de stubs pour des tests par exemple.
Je dirais qu’en Python l’héritage n’est que du détail d’implémentation et sert avant tout à réutiliser le code des classes parentes.

Et effectivement dans la même veine on trouve aujourd’hui les Protocol (PEP 544) du côté du typage statique.

Par contre je ne dirais pas que c’est une question de mauvais design : techniquement on s’en fout que tel type hérite effectivement de tel autre, tout ce qui nous intéresse est de savoir s’il peut être considéré comme tel ou non.

Justement, l’erreur de design n’est pas dans le fait d’avoir du structural subtyping, il est dans le fait de considérer les classes comme des types et de voir l’héritage comme du subtyping alors que c’est qu’une partie de la question. Dans un langage qui veut faire du duck typing par défaut, il n’y a aucune raison de lier les classes aux types, ni d’avoir de l’héritage. Si à la place le système de type de Python était centré sur les interfaces, il n’y aurait même pas besoin de toute cette machinerie pour pouvoir corriger le tir lorsque qu’on veut des sous-types sans héritage.

+0 -0

Je pense que c’est plus par facilité pour rester le plus simple possible dans les cas d’usage courants.

Mais dans le fond je suis d’accord avec toi : ça rend les choses plus compliquées si on veut un code ne reposant vraiment que sur des interfaces.

D’un autre côté, décorréler totalement les deux notions de l’héritage (réutilisation de code et sous-typage) ça mène un peu trop facilement à un monde de mixins à gogo qui font grossir inutilement les classes (je pense par exemple aux include de Ruby qui illustrent bien cet aspect réutilisation) alors que de la délégation est généralement préférable.
J’ai l’impression que c’est un problème qui se rencontre moins quand les deux concepts sont liés dans l’héritage, justement parce qu’on ne veut pas que les objets soient de « trop » de types à la fois (je ne dis pas qu’il n’y a pas de mixins en Python, mais que ça a l’air d’être utilisé avec plus de parcimonie qu’en Ruby par exemple).

Il y a deux points à considérer :

  • dans un système de types basé sur des interfaces, il n’y a pas besoin d’héritage pour avoir du subtyping, c’est tout l’intérêt ;
  • ça fait une bonne dizaine d’années qu’on s’est rendu compte que l’héritage n’est pas une bonne façon de réutiliser du code parce qu’on se retrouve avec des arbres d’héritages conpliqués et que les détails d’implémentations de classes parentes fuitent en permanence dans les classes filles.

Le problème fondamental dans le système de type de Python est de voir les classes comme des types en premier lieu au lieu des interfaces, puis d’utiliser l’héritage comme mécanisme "naturel" de subtyping au lieu de faire du structural subtyping.

Bon de toute façon c’est impossible à corriger, et les mécanismes dans le billet de blog sont une façon pragmatique de bricoler une solution autour des ces erreurs de design.

+2 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte