Annotations de type en python – Annoter ou convertir?

Lors d’une revue de code, un collègue, Zachary Paden, me demandait pourquoi j’appelais la fonction typing.cast sur mes variables plutôt que de créer des variables temporaires simplement pour typer. Eh bien, tout comme il ignorait l’existence de cast, j’ignorais que cette approche fonctionnait. En bon nerdz que nous sommes, il a décidé de mesurer la performance de chacune des approches.

Tout d’abord, voici une fonction qui n’est pas typée proprement.

def func(data: dict) -> dict:
    return data["subdata"]

Mypy lance le message d’erreur suivant: Returning Any from function declared to return "Dict[Any, Any]"

Première solution: convertir.

from typing import cast

def casting(data: dict) -> dict:
    return cast(dict, data["subdata"])

Deuxième solution, extraire le sous dictionnaire dans une variabl qui est annotée adéquatement.

def hinting(data: dict) -> dict:
    subdata: dict = data["subdata"]
    return subdata

Ces deux approches sont parfaitement valides, et pourtant, l’une d’elle est ~10x plus rapide que l’autre. Laquelle?

Pour mesurer, nous avons utilisé iPython et la fonction %timeit. Pour le cas de la solution de conversion, l’importation est exclue des calculs.

In [1]: from typing import cast
   ...:
   ...: def casting(data: dict) -> dict:
   ...:     return cast(dict, data["subdata"])
   ...:

In [2]: def hinting(data: dict) -> dict:
   ...:     subdata: dict = data["subdata"]
   ...:     return subdata
   ...:

In [3]: data = {"subdata": {"more" : "data"}}

In [4]: %timeit casting(data)
52.1 ns ± 0.0996 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

In [5]: %timeit hinting(data)
33.9 ns ± 0.0581 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

On observe que la version annotée est ~35% plus rapide. Mais, lors de son expérience, Zach avait un différence de 90%. Pourquoi? Eh bien, son code typait plus précisément que celui ci-dessus. Voici une nouvelle version avec des annotations plus précises.

In [6]: def casting(data: dict) -> dict:
   ...:     return cast(dict[str, str], data["subdata"])
   ...:

In [7]: def hinting(data: dict) -> dict:
   ...:     subdata: dict[str, str] = data["subdata"]
   ...:     return subdata
   ...:

In [8]: %timeit casting(data)
116 ns ± 0.105 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

In [9]: %timeit hinting(data)
33.9 ns ± 0.0523 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

Tien tiens, cette fois la version annotée est ~71% plus rapide que la version qui converti. Ou plutôt, la version qui converti est ~2.27 fois plus lente qu’avant. Alourdissons le type pour voir le nouvel impact sur la performance.

In [10]: data = {"subdata": {"way" : {"more" : "data"}}}

In [11]: def casting(data: dict) -> dict:
    ...:     return cast(dict[str, dict[str, str]], data["subdata"])
    ...:

In [12]: def hinting(data: dict) -> dict:
    ...:     subdata: dict[str, dict[str, str]] = data["subdata"]
    ...:     return subdata
    ...:

In [13]: %timeit casting(data)
182 ns ± 0.0948 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

In [14]: %timeit hinting(data)
33.9 ns ± 0.0621 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

Constats:

  1. La version qui converti est ~1.57 fois plus lente que précédemment.
  2. La performance de la version qui annote est stable. Très stable.
  3. La performance de la version qui converti est sujette à la complexité du type.
  4. Je suis vraiment nerd pour prendre le temps d’écrire un blog sur des nanosecondes de performance.

😂

Leave a Reply

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