Typage Python : pros & cons

Typage Python : pros & cons

Une nouvelle bibliothèque standard est apparue en Python 3.5 : typing. Corrélée avec une évolution de la grammaire du langage, elle permet d’ajouter au code des indications de type. La PEP « théorique » à l’origine de cette fonctionnalité de typage est la 483, ses applications plus concrètes sont exposées dans les PEP 484 et 526.

Une telle fonctionnalité, dans un langage à typage dynamique, peut être considérée comme assez paradoxale, voire inutile ; mais après quelques mois passés à développer en typant mon code, j’aimerais faire ici un retour très rapide sur ses intérêts et certains de ses inconvénients.

Typing hints: ce que c’est, ce que ce n’est pas

Il faut être immédiatement et absolument clair sur un point : typing n’ambitionne pas de faire du typage statique en Python, ni de forcer seulement certaines variables à être statiquement typées.
En fait, typer son code n’a absolument aucun impact au runtime. Il est tout à fait possible d’affecter des int à des variables déclarées en str, ou d’écrire une fonction qui retourne dict tout en typant son retour comme un tuple. L’interpréteur ne dira rien, tout se passera exactement comme si aucun type n’était précisé et c’est prévu de rester comme ça. Au cas où ça ne serait pas clair, Guido explicite dans sa PEP 484, chapitre « Non-goals » :

Python will remain a dynamically typed language, and the authors have no desire to ever make type hints mandatory, even by convention.

Guido van Rossum

Le gras n’est pas de moi. Je pense qu’on peut être rassuré sur ce point pour quelques années.

Le typage autorisé par la grammaire est donc avant tout un « hint », un indice, une aide à la compréhension du code. Il permet d’expliciter les types attendus et retournés, ces informations étant ensuite accessibles dans un attribut __annotations__ de chaque variable.

def add(a: int, b: int) -> int:
    return a + b

> add.__annotations__
{'a': int, 'b': int, 'return': int}

La bibliothèque typing, elle, apporte avant tout des définitions de types, certains correspondant à des builtins déjà présents dans le langage (Str, Dict, Tuple, …), d’autres non (Callable, Iterable, …). D’autres permettent du typage logique, comme par exemple a: List[Union[str, int]] = [42, 'foo'], ou Optional qui est un équivalent de Union[None, <something>]. Elle apporte également certains outils bien utiles comme NamedTuple.

Le typage n’a donc pas d’impact immédiat dans l’exécution d’un programme Python. Par contre, il peut être utilisé comme support à des outils qui, en exploitant les informations renseignées, peuvent contrôler le code et s’assurer que les types réels correspondent aux types déclarés. Ces outils s’utilisent soit en statique, l’exemple le plus connu étant mypy (qui a d’ailleurs inspiré la PEP 484), soit pendant le runtime avec des bibliothèques comme typeguard, enforce (assez intrusives dans le code vu les caractéristiques du langage).

Au niveau des compilateurs, Cython a rapidement été capable de comprendre la syntaxe du typage, mais l’utilisation effective de ces informations supplémentaires pour optimiser le code semble récent et incomplet, ce qui semble assez logique vu, par exemple, que des types de bases Python n’ont pas de correspondance triviale en C (un int Python doit être représenté sur combien de bits en C ?).

Pros

Du code plus lisible et autodocumenté

Chacun est juge de la quantité d’annotations qu’il veut intégrer à son code. Pour ma part je trouve que le typage est surtout utile pour expliciter des fonctions :

def pack(name, age, siblings=None):
    pack = {'name': name,
            'age': age}
    if siblings is not None:
        pack['siblings'] = siblings
    return pack

def pack(name: str, age: int, siblings: Optional[Set[Human]] = None) -> Dict[str, Any]:
    pack = {'name': name,
            'age': age}
    if siblings is not None:
        pack['siblings'] = siblings
    return pack

Si vous développez en ayant à l’esprit de délivrer le code le plus clair possible, vous devez vraisemblablement passer un temps non négligeable à simplement nommer vos variables. Préciser le type permet un supplément d’information qui n’alourdit pas les noms, contrairement à certaines conventions comme la pesante notation hongroise.

On peut craindre a priori que le typage alourdirait le code, le complexifierait et au final le rendrait moins compréhensible. Cependant, avec un peu d’habitude (et de la mesure), je trouve que les ajouts s’intègrent bien et apporte beaucoup de clarté.

Type matters

L’absence du typage en Python est une arme à double tranchant. Elle apporte une partie significative de l’immense souplesse du langage qui permet un code concis, rapide à écrire et facilite les expérimentations. Mais en même temps, elle peut favoriser une certaine négligence sur les objets qu’on manipule, le polymorphisme autorisant des comportements identiques (ou apparemment identiques) malgré des changements de types.

Ajouter des indications de typage – et vérifier leur justesse – force à passer un peu de temps à s’interroger sur les types qu’on utilise : pourquoi j’ai choisi un set ? Est-ce que ce middleware a intérêt à modifier les types des objets qu’il transmet ? Pourquoi cette fonction prend en entrée soit un int, soit une list de str ?
Le typage apporte un je-ne-sais-quoi de rigueur – purement virtuelle – que je trouve éminemment bénéfique. Je suis personnellement convaincu qu’il me rend beaucoup plus conscient de mon Ātman des types que j’utilise.

Tout ça est évidemment difficilement quantifiable de façon objective, mais je pense que ça a plusieurs intérêts, notamment :

  • Meilleures performances : les builtins types des containers Python sont codés en C et viennent avec leurs complexités. Un programme performant commence par une bonne maîtrise des bases
  • Homogénéité et cohérence des itérables
  • Code plus lisible(²) en évitant (limitant) les fonctions fourre-tout qui peuvent prendre 50 types pour faire des traitements magiques

Bien sûr, typing n’est absolument pas nécessaire pour déjà travailler ces aspects (heureusement). Mais le typage me parait un bon outil pour révéler ces problématiques, en changeant le paradigme.

[Sponsorisé] Le NamedTuple

Dans la bibliothèque typing, on trouve aussi un certain NamedTuple. Je trouve cet outil incroyablement sympa à manipuler pour définir rapidement des petites structures :

from typing import NamedTuple

class Agent(NamedTuple):
    name: str
    id: int
    ltk: bool

> a = Agent('Bond. James Bond', 007., True)
> a.dead = True
AttributeError: 'Agent' object has no attribute 'dead'
> a.name
'Bond. James Bond'
> a.name is a[0]
True

Les attributs de la classe sont devenus des properties, on ne peut pas surcharger l’instance a posteriori (comme quand on définit un __slots__ dans notre classe), les arguments peuvent être passés en positionnels ou en nommés, … bref, ce petit joyau ne paie pas de mine, mais a été fait avec amour et ça se voit. Merci NamedTuple.

Alors oui, ça existait déjà dans collections, mais la construction était quand même un peu plus rugueuse (en passant, elle peut par construction nous faire nous penser qu’il y a sûrement de la métaclasse dessous) :

import collections

Agent = collections.namedtuple('Agent', [('name', str), ('id', int), ('ltk', bool)])

Cons

En soit, on ne peut pas vraiment dire qu’il y ait des « contres » au typage : en tant qu’ajout complètement optionnel, et sans incidence au runtime (peut-être un overhead ultra négligeable au parsing), soit on est pour et on l’utilise, soit on est contre, on n’y touche pas et c’est réglé.

Par contre, il y a des différences entre ce qu’on voudrait faire avec le typage, et ce qu’on peut en faire à travers les outils qui l’exploitent. Quelques « contres » sont ainsi à mettre au crédit des outils tiers – tous encore récents, soyons indulgents -, à commencer par le principal : mypy.

mypy doit encore mûrir

mypy est un outil très utile, mais encore jeune : la plus récente version (en mai 2018) est la 0.600 – toujours pas stable donc. Il est donc encore limité, et si son analyse statique peut se révéler parfois surprenamment puissante, elle peut aussi se perdre sur des points qui semblent faciles.

Certains héritages par exemple  :

class A: pass

class B1(A):
    number: int = 1

class B2(A):
    number: int = 2

for b in (B1, B2):
    b.number

error: "Type[A]" has no attribute "number"

On pourrait croire que mypy considère (B1, B2) comme des A, mais en fait même en forçant le type de b (en déclarant en amont un b_iter: Tuple[Type[B1], Type[B2]] = (B1, B2)), on aura toujours la même erreur.

Mypy peut également parfois ne pas voir des méthodes. Prenons http.client.HTTPResponse, qui est une classe en pur Python de la bibliothèque standard. Elle possède une méthode HTTPResponse.info définie tout à fait normalement. Et pourtant :

from http.client import HTTPResponse
from urllib import request

result = request.urlopen('https://google.com')
if isinstance(result, HTTPResponse):  # juste histoire d'enfoncer le clou sur le type de 'result'
    result.info()

error: "HTTPResponse" has no attribute "info"

Tout ces petits défauts s’accumulent plus vite qu’on le croit, et au final, soit mypy est abandonné petit à petit (trop de faux positifs), soit le code se parsème de #  type: ignore (un moindre mal). Mais du temps aura été consommé à essayer toutes sortes de typages excessivement explicites / modifications lourdes du code dans l’espoir de satisfaire l’outil.

Aussi, ça peut devenir phat (mais ce n’est pas un vrai problème)

Le typage peut aussi passablement surcharger le code si on décide de devenir complètement nazi, mais bon, c’est votre choix.

from typing import Dict, List, Union, Optional, Callable, Iterable, Any, Tuple, Type

a: Tuple[str, Type, Any] = ('property', str, 'whatever value')
b: List[Dict[str, Union[str, int, Optional[List[str]]]]] = \
    [{'name': 'Doe', 'age': 13, 'children': None}, 
     {'name': 'John', 'age': 92, 'children': ['Marie', 'Chloe']}]
c: Callable[[Iterable[Optional[int]]], int] = lambda x: sum(i for i in x if i is not None)

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.