[PYTHON] La vengeance des types: la vengeance des types

Cet article a été écrit par Armin Ronacher ([@mitsuhiko](https: // twitter.)) Le dimanche 24 août 2014, après une discussion de la communauté Python qui a eu lieu lorsque la proposition d'ajouter des annotations de type à Python a été faite. Ceci est une traduction de l'article écrit par M. com / mitsuhiko)).

Si vous êtes intéressé par les annotations de type que vous envisagez d'introduire dans Python 3.5, veuillez vous reporter à ce qui suit.

Je ne suis moi-même pas familier avec le système de typage et les autres langages, donc je pense qu'il y a des parties non traduites, des malentendus et des erreurs de traduction. Si vous trouvez une telle erreur, il serait utile que vous puissiez nous envoyer une demande de modification.

Type de vengeance

Ceci est la partie 2 sur "Python I Want". Sur la base de discussions récentes, nous explorerons un peu le système de types de Python. Une partie de cet article fait référence à Article précédent sur le slot .. Comme dans l'article précédent, cet article est une réflexion pour les futurs concepteurs de langage du langage Python, et plongera dans le monde des interprètes CPython.

En tant que l'un des programmeurs Python, les types sont un peu délicats. Les types existent et agissent différemment les uns des autres, mais la plupart du temps, vous remarquerez l'existence d'un type uniquement lorsque le type ne se comporte pas comme prévu et que l'exception ou l'exécution échoue.

Python était fier de la façon dont il gérait la saisie. Je me souviens avoir lu une FAQ dans cette langue il y a plusieurs années, qui disait que taper du canard était génial. Le typage de canard est également une excellente solution en termes de praticité car il est juste. Il ne combat fondamentalement pas les systèmes de types et ne limite pas ce que vous voulez faire, il implémente donc une bonne API. Les choses que vous faites le plus souvent sont très faciles en Python.

La plupart des API que j'ai conçues pour Python ne fonctionnent pas dans d'autres langages. Même une interface très simple comme l'interface générique de click ne fonctionne toujours pas dans d'autres langues. La raison principale en est de lutter constamment contre la moisissure.

Il y a eu un débat récent sur l'ajout du typage statique à Python. Je suis sûr que le train quittera la gare et partira loin et ne reviendra jamais. Voici mes réflexions sur les raisons pour lesquelles j'espère que Python ne s'adapte pas au typage explicite parce que c'est intéressant.

Qu'est-ce qu'un système de type?

Un système de types est une règle d'interaction des types. Il y a même un domaine en informatique qui ne traite que du type entier. Le moule lui-même est très impressionnant. Cependant, il est difficile d'ignorer les systèmes de type, même si vous n'êtes pas particulièrement intéressé par l'informatique théorique.

Je ne veux pas entrer dans le système de type pour deux raisons: La première raison est que je ne comprends guère les systèmes de types. La deuxième raison est que la compréhension n'est pas si importante pour «réaliser» les conséquences logiques d'un système de types. C'est important pour moi comment le type se comporte, ce qui affecte la façon dont l'API est conçue. Par conséquent, considérez cet article comme une introduction de base à une meilleure API de mes illusions, plutôt qu'une introduction au type correct.

Un système de type a de nombreuses caractéristiques, mais la chose la plus importante pour distinguer un type est la quantité d'informations qu'il fournit lorsque l'on tente de l'expliquer.

Utilisons Python comme exemple. Python a des types. Lorsqu'on lui a demandé le type du nombre «42», Python répond qu'il s'agit d'un type entier. Il a de nombreuses implications et permet à l'interpréteur de définir des règles sur la manière dont les types entiers interagissent avec d'autres types entiers.

Cependant, il y a une chose que Python n'a pas. C'est un type complexe. Tous les types Python sont primitifs. Cela signifie essentiellement qu'un seul type fonctionne à la fois. Le contraire du type de base est le composite. De temps en temps, vous verrez des types composites Python dans différents contextes.

Le type complexe le plus simple que la plupart des langages de programmation ont est une structure. Python n'a pas de structure directement, mais il y a souvent des situations où vous devez définir votre propre structure dans un rallye. Par exemple, les modèles ORM Django et SQLAlchemy sont essentiellement des structures. Les colonnes de chaque base de données sont représentées par des descripteurs Python, qui à leur tour correspondent aux champs de la structure. Si vous appelez la clé primaire ʻidet ʻIntegerField (), cela définit le modèle comme un type composite.

Les types complexes ne sont pas limités aux structures. Par exemple, si vous souhaitez utiliser plusieurs entiers, utilisez une collection comme un tableau. Python fournit des listes et les éléments individuels de la liste peuvent être de n'importe quel type. Cela contraste avec une liste définie en spécifiant un type (comme une liste de types entiers).

On peut dire qu'une "liste de type entier" n'est pas une liste. Vous pouvez affirmer que vous pouvez déterminer le type en parcourant cette liste, mais vous aurez des problèmes avec une liste vide. Je ne connais pas son type lors de l'utilisation d'une liste sans élément en Python.

Le même problème en Python est causé par une référence nulle (None). Si vous passez un objet utilisateur à une fonction et que cet objet utilisateur peut être «Aucun», vous ne savez pas si l'argument est un objet utilisateur.

Donc, y a-t-il une solution? Pour avoir un tableau typé explicite sans références nulles. Haskell est, bien sûr, une langue que tout le monde sait avoir, mais il y en a d'autres qui ne semblent pas inappropriées. Par exemple, Rust est un langage bien connu qui ressemble en grande partie au C ++, mais il apporte un système de types très puissant à ses tables.

Alors, comment dire "l'utilisateur n'existe pas" s'il n'y a pas de références nulles? Par exemple, la réponse dans Rust est de type option. «Option » signifie soit «Certains (utilisateur)» ou «Aucun». Le premier est une énumération balisée qui encapsule une valeur (un utilisateur spécifique). Maintenant que la variable a une valeur ou n'existe pas, tout le code traitant de cette variable ne sera pas compilé à moins que vous ne gériez explicitement le cas «Aucun».

Je ne peux pas dire le futur

Dans le passé, le monde était divisé en langages interprétés clairement typés dynamiquement et en langages compilés typés pré-statiquement. Cela change avec l'émergence de nouvelles tendances.

Le premier signe que nous avons commencé à nous diriger vers ce territoire inexploré était C #. C'est un langage compilé statiquement, et au départ, il était très similaire à Java. À mesure que le langage s'améliorait, de nombreuses nouvelles fonctionnalités liées au système de type ont été ajoutées. Le plus important est l'introduction de génériques qui fournissent un typage fort pour les collections, listes, dictionnaires, etc. autres que ceux fournis par le compilateur. Depuis lors, C # est allé dans la direction opposée du typage statique, et il est maintenant possible d'arrêter de taper statiquement pour chaque variable individuelle et de la faire typer dynamiquement. C'est ridiculement pratique. Cela est particulièrement vrai dans le cadre du travail avec des données fournies par des services Web (JSON, XML, etc.). Avec le typage dynamique, vous pouvez essayer de faire quelque chose qui peut ne pas être de type sécurisé, détecter une erreur de type due à des données d'entrée incorrectes et l'afficher à l'utilisateur.

Les systèmes de type C # d'aujourd'hui ont des génériques très puissants pour les spécifications de co-modification et d'anti-dégénérescence. Non seulement cela, mais de nombreux supports au niveau du langage pour traiter les types Nullables ont également été développés. Par exemple, l'opérateur de fusion nul (`` ?? '') a été introduit pour fournir des valeurs par défaut pour les objets représentés comme null. C # est arrivé trop tard pour supprimer null du langage, mais il vous permet de contrôler les dommages causés par null.

Dans le même temps, d'autres langages traditionnellement précompilés statiquement explorent également de nouveaux domaines. C ++ est toujours typé statiquement, mais il est toujours pris en compte pour l'inférence de type à de nombreux niveaux. Les jours de MyType <X, Y> <:: const_iterator iter sont révolus. De nos jours, dans la plupart des situations, le simple remplacement du type par ʻauto` amènera le compilateur à incorporer le type à la place.

Rust en tant que langage prend également en charge une bonne inférence de type pour écrire des programmes typés statiquement sans définition de type complètement arbitraire.

Rust


    use std::collections::HashMap;
    
    fn main() {
        let mut m = HashMap::new();
        m.insert("foo", vec!["some", "tags", "here"]);
        m.insert("bar", vec!["more", "here"]);
    
        for (key, values) in m.iter() {
            println!("{} = {}", key, values.connect("; "));
        }
    }

Je pense que nous nous dirigeons vers l'avenir avec un système de typage puissant. Je ne pense pas que ce sera la fin du typage dynamique, mais il semble qu'il y ait une tendance marquée à accepter un typage statique fort avec une inférence de type local.

Explicitement typé avec Python

Donc, il n'y a pas si longtemps, quelqu'un a convaincu les gens autour d'eux lors de conférences que la saisie statique devrait être une excellente fonctionnalité de langage. Je ne sais pas exactement quel était l'argument, mais le résultat final a été déclaré que la combinaison du module de type de mypy et de la syntaxe d'annotation de type de Python 3 deviendrait la norme pour taper en Python.

Si vous n'avez pas encore vu la suggestion, quelque chose comme celui-ci a été proposé:

Python3


    from typing import List
    
    def print_all_usernames(users: List[User]) -> None:
        for user in users:
            print(user.username)

Pour être honnête, je ne pense pas que ce soit une bonne décision du tout pour de nombreuses raisons. La raison principale en est que Python souffre de ne pas avoir un bon système de type. La signification de cette langue dépend de la façon dont vous la regardez.

Pour que le typage statique ait un sens, le système de type doit être bon. Étant donné deux types, c'est un système de types qui vous permet de savoir comment les types sont liés les uns aux autres. Python ne le fait pas.

Sémantique de type Python

Si vous lisez l'article sur le système de slot que j'ai écrit plus tôt, vous vous souviendrez que les types Python ont des significations différentes selon qu'ils sont implémentés du côté du langage C ou du côté Python. C'est une caractéristique assez inhabituelle de cette langue et ne se trouve généralement pas dans de nombreuses autres langues. Il est vrai que de nombreux langages ont des types implémentés au niveau de l'interpréteur à des fins de bootstrap, mais ils sont généralement considérés comme des types de base et traités spécialement.

Python n'a pas de vrai type "fondamental". Cependant, il existe de nombreux types implémentés du côté du langage C. Celles-ci ne sont pas du tout limitées aux primitives ou aux types fondamentaux, elles apparaissent partout sans aucune logique. Par exemple, collections.OrderedDict est un type implémenté côté Python, tandis que collections.defaultdict dans le même module est un type implémenté côté langage C.

Cela pose en fait pas mal de problèmes avec PyPy. En effet, nous devons imiter le type d'origine autant que possible afin d'obtenir une API similaire où ces différences ne sont pas perceptibles. Il est très important de comprendre ce que signifie cette différence diverse entre le code de l'interpréteur de niveau de langage C et le reste du langage.

A titre d'exemple, signalons le module re jusqu'à Python 2.7. (Ce comportement a finalement changé dans le module re, mais il y a encore des problèmes divers avec les interprètes qui fonctionnent en dehors de la langue.)

Le module re offre la possibilité de compiler une expression régulière dans un modèle d'expression régulière ( compile). Il prend une chaîne et renvoie un objet modèle. Cela ressemble à ceci:

Python2.7


    >>> re.compile('foobar')
    <_sre.SRE_Pattern object at 0x1089926b8>

Comme vous pouvez le voir, cet objet pattern est à l'intérieur du module _sre, mais il peut être utilisé universellement:

Python2.7


    >>> type(re.compile('foobar'))
    <type '_sre.SRE_Pattern'>

Malheureusement, c'était un peu un mensonge. Le module _sre ne contient pas réellement ce type.

Python2.7


    >>> import _sre
    >>> _sre.SRE_Pattern
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'module' object has no attribute 'SRE_Pattern'

Oui c'est vrai. Ce n'est pas la première fois de mentir si le type n'est pas là, et en tout cas c'est un type interne. Alors passez à la suivante. Nous savons que le type de cet objet pattern est _sre.SRE_Pattern. C'est lui-même une sous-classe de ʻobject:

Python2.7


    >>> isinstance(re.compile(''), object)
    True

Comme nous le savons, tous les objets implémentent des méthodes courantes. Par exemple, tous les objets implémentent «repr».

Python2.7


    >>> re.compile('').__repr__()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: __repr__

Ahh. Qu'est-ce qui s'est passé? Eh bien, la réponse est assez bizarre. Je ne sais pas pourquoi, mais en interne, l'objet de modèle SRE avait un emplacement personnalisé tp_getattr jusqu'à Python 2.7. Cet emplacement avait un processus de découverte d'attributs personnalisés qui permettait d'accéder aux méthodes et attributs personnalisés. Si vous regardez réellement l'objet avec dir (), vous remarquerez que de nombreuses fonctionnalités manquent.

Python2.7


    >>> dir(re.compile(''))
    ['__copy__', '__deepcopy__', 'findall', 'finditer', 'match',
     'scanner', 'search', 'split', 'sub', 'subn']

De plus, le fonctionnement de ce type invite à une aventure vraiment étrange. Voici ce qui se passe.

Le type de type prétend qu'il s'agit d'une sous-classe de ʻobject. C'est vrai dans le monde des interpréteurs CPython, mais pas dans le langage Python. C'est dommage que ce ne soit pas la même chose, mais c'est un cas courant. Le type ne correspond pas à l'interface de ʻobject sur la couche Python. Les appels via l'interpréteur fonctionnent, mais les appels via le langage Python échouent. Autrement dit, type (x) réussit, tandis que x .__ class__ échoue.

Qu'est-ce qu'une sous-classe

L'exemple ci-dessus montre que Python peut avoir une autre sous-classe qui ne correspond pas au comportement de la classe de base. Ceci est particulièrement problématique quand on parle de typage statique. Dans Python 3, par exemple, vous ne pouvez pas implémenter une interface de type dict à moins que vous n'écriviez le type du côté du langage C. La raison est que le type garantit un certain comportement de l'objet de vue, ce qui n'est pas facile à implémenter. Que voulez-vous dire?

Ainsi, lors de l'annotation statique d'une fonction qui reçoit un dictionnaire de clés de chaîne et d'objets entiers, il n'est pas tout à fait clair s'il s'agit d'un objet tel que dict ou dict, ou s'il autorise les sous-classes du dictionnaire.

Comportement indéfini

Le comportement étrange de l'objet pattern précédent a changé dans Python 2.7, mais le problème sous-jacent persiste. Comme le comportement d'instance de dict mentionné précédemment, le langage se comporte différemment selon la façon dont le code est écrit. Et il est impossible de comprendre pleinement la signification rigoureuse des systèmes de types.

Un cas très étrange dans un tel interpréteur est la comparaison de types, par exemple en Python 2. Ce cas particulier n'existe pas dans Python 3 en raison des changements d'interface, mais le problème de base peut être trouvé à différents niveaux.

Prenons un tri de type d'ensemble comme exemple. Le type d'ensemble de Python est utile, mais l'opération de comparaison est assez étrange. Dans Python 2, il existe une fonction appelée cmp () qui renvoie un nombre qui indique lequel des deux types donnés est le plus grand. Les valeurs inférieures à 0 signifient que le premier argument est inférieur au deuxième argument, 0 signifie qu'ils sont égaux, les nombres positifs signifient que le deuxième argument est supérieur au premier argument.

Voici ce qui se passe lorsque vous comparez des ensembles:

Python2.7


    >>> cmp(set(), set())
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: cannot compare sets using cmp()

Pourquoi? Je ne sais honnêtement pas la chose exacte. Probablement parce que l'opérateur de comparaison définit le sous-ensemble et qu'il ne fonctionne pas avec cmp (). Mais par exemple, le type frozenset fonctionne.

Python2.7


    >>> cmp(frozenset(), frozenset())
    0

Il échouera si l'un de ces ensembles figés n'est pas vide. Je me demande pourquoi? La réponse à cela est qu'il s'agit d'une optimisation de l'interpréteur CPython, pas d'une fonctionnalité de langage. frozenset interne une valeur commune. Un frozenset vide a toujours la même valeur (il est immuable et ne peut pas être ajouté), donc un frozenset vide est le même objet. Si deux objets ont la même adresse de pointeur, «cmp» renvoie normalement «0». En raison de la logique de comparaison complexe dans Python 2, je ne comprends pas immédiatement pourquoi cela se produit, mais il existe plusieurs chemins de code dans la routine de comparaison qui peuvent provoquer ce résultat.

Le fait est que plutôt que ce soit un bogue, Python n'a pas la signification appropriée de la façon dont les types interagissent réellement. Pendant très longtemps, le comportement du système de types était "à la merci de CPython".

Dans PyPy, vous trouverez une myriade d'ensembles de modifications qui ont essayé de reconstruire le comportement de CPython. Étant donné que PyPy est écrit en Python, c'est un problème très intéressant pour le langage. Si le langage Python avait complètement défini le comportement de la partie Python du langage, PyPy aurait eu beaucoup moins de problèmes.

Comportement au niveau de l'instance

Supposons que vous ayez un Python virtuel qui résout tous les problèmes mentionnés ci-dessus. Pourtant, le typage statique ne convient pas bien à Python. La raison principale est qu'au niveau du langage Python, les types ont traditionnellement peu de sens en ce qui concerne l'interaction des objets.

Par exemple, un objet datetime peut généralement être comparé à d'autres objets, mais les paramètres de fuseau horaire doivent être compatibles lors de la comparaison avec d'autres objets datetime. De même, les résultats de nombreuses opérations ne sont apparents que lorsque la main examine l'objet. La combinaison des deux chaînes dans Python 2 crée un objet unicode ou bytestring. L'API de codage et de décodage du système de codec renvoie n'importe quel objet.

Python en tant que langage est trop dynamique pour fonctionner avec des annotations. Pensez à l'importance des générateurs pour votre langue. Cependant, le générateur peut effectuer différentes conversions de type à chaque itération.

Les annotations de type peuvent être bonnes en partie, mais elles peuvent avoir un impact négatif sur la conception de l'API. Sauf si vous supprimez l'annotation de type au moment de l'exécution, elle sera au moins lente. Sauf si vous avez transformé Python en autre chose que Python, vous ne pourrez jamais implémenter un langage qui compile efficacement et statiquement.

Ce qui a été obtenu et la théorie du sens

Ce que je pense personnellement de Python, c'est que les langages sont ridiculement complexes. Python est un langage qui souffre de ces interactions complexes entre différents types sans spécification de langage. Il semble que cela ne se réunira jamais. Il y a tellement de comportements mystérieux et un peu étranges que lorsque vous essayez de créer une spécification de langage, vous vous retrouvez avec juste une transcription de l'interpréteur CPython.

Je pense que cela n'a pas de sens de mettre des annotations de type sur cette base.

Si, à l'avenir, quelqu'un développe un autre langage typé dynamiquement, plus d'efforts devraient être faits pour définir clairement le fonctionnement des types. JavaScript fonctionne plutôt bien à cet égard. Toutes les sémantiques étranges mais intégrées sont clairement définies. Je pense que c'est généralement une bonne chose. Une fois que vous avez une définition claire du fonctionnement de cette sémantique, il y a de la place pour une optimisation ou un typage statique facultatif plus tard.

Garder un langage clair et bien défini vaut bien le défi. Les futurs concepteurs de langage ne doivent jamais commettre les erreurs de PHP, Python ou Ruby. Là, il se termine par la conclusion que le comportement de la langue est "à la merci de l'interprète".

Ce que je pense de Python ne changera probablement pas à ce stade. Le temps et les efforts nécessaires pour nettoyer la langue et les interprètes l'emportent sur la valeur que vous obtenez.

© Copyright 2014 by Armin Ronacher. Content licensed under the Creative Commons attribution-noncommercial-sharealike License.

Recommended Posts

La vengeance des types: la vengeance des types
Le début de cif2cell
Le sens de soi
Types de communication inter-processus
le zen de Python
L'histoire de sys.path.append ()
Qu'est-ce qu'un moteur de recommandation? Résumé des types
Aligner la version de chromedriver_binary
Grattage du résultat de "Schedule-kun"
10. Compter le nombre de lignes
L'histoire de la construction de Zabbix 4.4
Vers la retraite de Python2
Attrapez plusieurs types d'exceptions
Comparez les polices de jupyter-themes
Obtenez le nombre de chiffres
Expliquez le code de Tensorflow_in_ROS
Résumé des types de distribution Linux
Réutiliser les résultats du clustering
GoPiGo3 du vieil homme
Changer le thème de Jupyter
La popularité des langages de programmation
Changer le style de matplotlib
Visualisez la trajectoire de Hayabusa 2
À propos des composants de Luigi
Composants liés du graphique
Filtrer la sortie de tracemalloc
À propos des fonctionnalités de Python
Simulation du contenu du portefeuille
Le pouvoir des pandas: Python
Les spécifications de pytz ont changé
Tester la version du module argparse
Trouvez la définition de la valeur de errno
jour de course des dockers (note)
Tracez la propagation du nouveau virus corona
L'histoire de Python et l'histoire de NaN
Élever la version de pyenv elle-même
Obtenez le nombre de vues de Qiita
[Python] La pierre d'achoppement de l'importation
J'ai résumé 11 types de systèmes d'exploitation
First Python 3 ~ Le début de la répétition ~
Traduction japonaise du manuel e2fsprogs
L'histoire de la participation à AtCoder
La probabilité de précipitation est-elle correcte?
J'ai étudié le mécanisme de connexion flask!
Comprendre le contenu du pipeline sklearn
Le monde des livres d'ingénierie de contrôle
Prenez le journal d'exécution du céleri
Tester l'adéquation de la distribution
Existence du point de vue de Python
Calcul du nombre d'associations de Klamer
pyenv-changer la version python de virtualenv
À propos de la valeur de retour de pthread_mutex_init ()
Obtenir les attributs d'un objet
Résoudre le retard d'observation de l'interféromètre
Discrimination de la forme agari du mahjong
Simulation Python du modèle épidémique (modèle Kermack-McKendrick)