Quelles sont les bonnes utilisations des «annotations de fonction» de Python3

159

Annotations de fonction: PEP-3107

Je suis tombé sur un extrait de code démontrant les annotations de fonction de Python3. Le concept est simple mais je ne vois pas pourquoi ceux-ci ont été implémentés dans Python3 ou leurs bonnes utilisations. Peut-être que SO peut m'éclairer?

Comment ça fonctionne:

def foo(a: 'x', b: 5 + 6, c: list) -> max(2, 9):
    ... function body ...

Tout ce qui suit les deux points après un argument est une «annotation», et les informations qui suivent ->sont une annotation pour la valeur de retour de la fonction.

foo.func_annotations renverrait un dictionnaire:

{'a': 'x',
 'b': 11,
 'c': list,
 'return': 9}

Quelle est la signification d'avoir cela disponible?

Agscala
la source
6
@SilentGhost: malheureusement, de nombreux liens avec les cas d'utilisation réels sont rompus. Y a-t-il un endroit où le contenu a pu être stocké ou est-il parti pour toujours?
max
16
ne devrait pas foo.func_annotations être foo.__annotations__en python3?
zhangxaochen
2
Les annotations n'ont pas de signification particulière. La seule chose que fait Python est de les mettre dans le dictionnaire d' annotations . Toute autre action dépend de vous.
N Randhawa
que veut def foo(a: 'x', b: 5 + 6, c: list) -> max(2, 9):dire?
Ali SH

Réponses:

90

Je pense que c'est vraiment génial.

Issu d'un milieu universitaire, je peux vous dire que les annotations se sont révélées inestimables pour activer des analyseurs statiques intelligents pour des langages comme Java. Par exemple, vous pouvez définir des sémantiques telles que des restrictions d'état, des threads autorisés à accéder, des limitations d'architecture, etc., et il existe de nombreux outils qui peuvent ensuite les lire et les traiter pour fournir des assurances au-delà de ce que vous obtenez des compilateurs. Vous pouvez même écrire des choses qui vérifient les conditions préalables / postconditions.

Je pense que quelque chose comme cela est particulièrement nécessaire en Python en raison de son typage plus faible, mais il n'y avait vraiment aucune construction qui rendait cela simple et faisait partie de la syntaxe officielle.

Il existe d'autres utilisations des annotations qui ne sont pas assurées. Je peux voir comment je pourrais appliquer mes outils basés sur Java à Python. Par exemple, j'ai un outil qui vous permet d'assigner des avertissements spéciaux aux méthodes, et qui vous donne des indications lorsque vous les appelez que vous devez lire leur documentation (par exemple, imaginez que vous avez une méthode qui ne doit pas être invoquée avec une valeur négative, mais c'est pas intuitif du nom). Avec des annotations, je pourrais technicall écrire quelque chose comme ça pour Python. De même, un outil qui organise des méthodes dans une grande classe basée sur des balises peut être écrit s'il existe une syntaxe officielle.

Uri
la source
34
ISTM ce sont des avantages théoriques qui ne peuvent être réalisés que si la bibliothèque standard et les modules tiers utilisent tous des annotations de fonction et les utilisent avec une signification cohérente et utilisent des systèmes d'annotations bien pensés. Jusqu'à ce jour (qui ne viendra jamais), les principales utilisations des annotations de fonctions de Python seront les utilisations ponctuelles décrites dans les autres réponses. Pour le moment, vous pouvez oublier les analyseurs statiques intelligents, les assurances du compilateur, les chaînes d'outils Java, etc.
Raymond Hettinger
4
Même sans que tout n'utilise les annotations de fonction, vous pouvez toujours les utiliser pour l'analyse statique dans le code qui les a sur ses entrées et qui appelle un autre code annoté de la même manière. Dans un projet ou une base de code plus large, cela pourrait encore être un corps de code très utile pour effectuer une analyse statique basée sur des annotations.
gps
1
AFAICT, vous pouvez faire tout cela avec des décorateurs, qui sont antérieurs aux annotations; par conséquent, je ne vois toujours pas l'avantage. J'ai une
vision
9
Avance rapide jusqu'en 2015, python.org/dev/peps/pep-0484 et mypy-lang.org commencent à prouver que tous les opposants ont tort.
Mauricio Scheffer
1
Il révèle également encore plus l'influence de Python sur Swift.
uchuugaka
92

Les annotations de fonction sont ce que vous en faites.

Ils peuvent être utilisés pour la documentation:

def kinetic_energy(mass: 'in kilograms', velocity: 'in meters per second'):
     ...

Ils peuvent être utilisés pour le contrôle de pré-condition:

def validate(func, locals):
    for var, test in func.__annotations__.items():
        value = locals[var]
        msg = 'Var: {0}\tValue: {1}\tTest: {2.__name__}'.format(var, value, test)
        assert test(value), msg


def is_int(x):
    return isinstance(x, int)

def between(lo, hi):
    def _between(x):
            return lo <= x <= hi
    return _between

def f(x: between(3, 10), y: is_int):
    validate(f, locals())
    print(x, y)


>>> f(0, 31.1)
Traceback (most recent call last):
   ... 
AssertionError: Var: y  Value: 31.1 Test: is_int

Voir également http://www.python.org/dev/peps/pep-0362/ pour un moyen d'implémenter la vérification de type.

Raymond Hettinger
la source
18
En quoi est-ce mieux qu'une docstring pour la documentation ou une vérification de type explicite dans la fonction? Cela semble compliquer la langue sans raison.
endolith
10
@endolith Nous pouvons certainement nous passer des annotations de fonction. Ils fournissent simplement un moyen standard d'accéder aux annotations. Cela les rend accessibles à l'aide () et aux info-bulles et les rend disponibles pour l'introspection.
Raymond Hettinger
4
Plutôt que de passer des nombres, vous pouvez créer des types Masset à la Velocityplace.
rightfold
1
pour démontrer pleinement cela, je devrais def kinetic_energy(mass: 'in kilograms', velocity: 'in meters per second') -> float:également montrer le type de retour. C'est ma réponse préférée ici.
Tommy
À l'aide de votre code, existe-t-il un moyen de vérifier l' returnannotation? Il ne semble pas apparaître danslocals
user189728
46

C'est une réponse tardive, mais AFAICT, la meilleure utilisation actuelle des annotations de fonction est PEP-0484 et MyPy .

Mypy est un vérificateur de type statique facultatif pour Python. Vous pouvez ajouter des indices de type à vos programmes Python en utilisant le prochain standard pour les annotations de type introduit dans Python 3.5 beta 1 (PEP 484), et utiliser mypy pour les vérifier de manière statique.

Utilisé comme ça:

from typing import Iterator

def fib(n: int) -> Iterator[int]:
    a, b = 0, 1
    while a < n:
        yield a
        a, b = b, a + b
Dustin Wyatt
la source
Voir également pytype - l'autre analyseur statique construit avec PEP-0484 à l'esprit.
gps
Malheureusement, le type n'est pas appliqué. Si je tape list(fib('a'))avec votre exemple de fonction, Python 3.7 accepte volontiers l'argument et se plaint qu'il n'y a aucun moyen de comparer une chaîne et un int.
Denis de Bernardy
@DenisdeBernardy Comme PEP-484 l'explique, Python ne fournit que des annotations de type. Pour appliquer les types, vous devez utiliser mypy.
Dustin Wyatt
23

Juste pour ajouter un exemple spécifique d'une bonne utilisation de ma réponse ici , couplé avec des décorateurs, un mécanisme simple pour plusieurs méthodes peut être fait.

# This is in the 'mm' module

registry = {}
import inspect

class MultiMethod(object):
    def __init__(self, name):
        self.name = name
        self.typemap = {}
    def __call__(self, *args):
        types = tuple(arg.__class__ for arg in args) # a generator expression!
        function = self.typemap.get(types)
        if function is None:
            raise TypeError("no match")
        return function(*args)
    def register(self, types, function):
        if types in self.typemap:
            raise TypeError("duplicate registration")
        self.typemap[types] = function

def multimethod(function):
    name = function.__name__
    mm = registry.get(name)
    if mm is None:
        mm = registry[name] = MultiMethod(name)
    spec = inspect.getfullargspec(function)
    types = tuple(spec.annotations[x] for x in spec.args)
    mm.register(types, function)
    return mm

et un exemple d'utilisation:

from mm import multimethod

@multimethod
def foo(a: int):
    return "an int"

@multimethod
def foo(a: int, b: str):
    return "an int and a string"

if __name__ == '__main__':
    print("foo(1,'a') = {}".format(foo(1,'a')))
    print("foo(7) = {}".format(foo(7)))

Cela peut être fait en ajoutant les types au décorateur comme le montre le post original de Guido , mais annoter les paramètres eux-mêmes est préférable car cela évite la possibilité d'une mauvaise correspondance des paramètres et des types.

Remarque : en Python, vous pouvez accéder aux annotations au function.__annotations__lieu de function.func_annotationsla func_*suppression du style sur Python 3.

Muhammad Alkarouri
la source
2
Application intéressante, même si j'ai peur de function = self.typemap.get(types)ne pas fonctionner lorsque des sous-classes sont impliquées. Dans ce cas, vous devrez probablement boucler en typemaputilisant isinnstance. Je me demande si @overloadcela gère correctement
Tobias Kienzler
Je pense que cela est cassé si la fonction a un type de retour
zenna
1
Le __annotations__ est un dictqui n'assure pas l'ordre des arguments, donc cet extrait de code échoue parfois. Je recommanderais de changer le types = tuple(...)à spec = inspect.getfullargspec(function)alors types = tuple([spec.annotations[x] for x in spec.args]).
xoolive
Vous avez tout à fait raison, @xoolive. Pourquoi ne modifiez-vous pas la réponse pour ajouter votre correctif?
Muhammad Alkarouri
@xoolive: J'ai remarqué. Parfois, les éditeurs utilisent une main lourde dans la gestion des modifications. J'ai modifié la question pour inclure votre correctif. En fait, j'ai eu une discussion à ce sujet, mais il n'y a aucun moyen d'annuler le rejet du correctif. Merci pour ton aide au passage.
Muhammad Alkarouri
22

Uri a déjà donné une réponse correcte, alors voici une réponse moins sérieuse: vous pouvez donc raccourcir vos docstrings.

COUP
la source
2
aimer. +1. cependant, en fin de compte, écrire des docstrings est toujours la meilleure façon de rendre mon code lisible, cependant, si vous deviez implémenter n'importe quel type de vérification statique ou dynamique, c'est bien d'avoir cela. Peut-être que je pourrais en trouver une utilité.
Warren P
8
Je ne recommande pas d'utiliser des annotations en remplacement d'une Args: section ou @param lines ou similaire dans vos docstrings (quel que soit le format que vous choisissez d'utiliser). Bien que les annotations de documentation constituent un bel exemple, elles ternissent la puissance potentielle des annotations car elles pourraient gêner d'autres utilisations plus puissantes. De plus, vous ne pouvez pas omettre d'annotations lors de l'exécution pour réduire la consommation de mémoire (python -OO) comme vous pouvez le faire avec les docstrings et les instructions assert.
gps
2
@gps: Comme je l'ai dit, c'était une réponse moins sérieuse.
JAB
2
En toute sincérité, c'est une bien meilleure façon de documenter les types que vous attendez, tout en adhérant à DuckTyping.
Marc
1
@gps Je ne suis pas sûr que la consommation de mémoire des docstrings soit un sujet de préoccupation dans 99,999% des cas.
Tommy
13

La première fois que j'ai vu des annotations, je me suis dit "génial! Enfin, je peux opter pour une vérification de type!" Bien sûr, je n'avais pas remarqué que les annotations ne sont pas réellement appliquées.

J'ai donc décidé d' écrire un simple décorateur de fonctions pour les appliquer :

def ensure_annotations(f):
    from functools import wraps
    from inspect import getcallargs
    @wraps(f)
    def wrapper(*args, **kwargs):
        for arg, val in getcallargs(f, *args, **kwargs).items():
            if arg in f.__annotations__:
                templ = f.__annotations__[arg]
                msg = "Argument {arg} to {f} does not match annotation type {t}"
                Check(val).is_a(templ).or_raise(EnsureError, msg.format(arg=arg, f=f, t=templ))
        return_val = f(*args, **kwargs)
        if 'return' in f.__annotations__:
            templ = f.__annotations__['return']
            msg = "Return value of {f} does not match annotation type {t}"
            Check(return_val).is_a(templ).or_raise(EnsureError, msg.format(f=f, t=templ))
        return return_val
    return wrapper

@ensure_annotations
def f(x: int, y: float) -> float:
    return x+y

print(f(1, y=2.2))

>>> 3.2

print(f(1, y=2))

>>> ensure.EnsureError: Argument y to <function f at 0x109b7c710> does not match annotation type <class 'float'>

Je l'ai ajouté à la bibliothèque Ensure .

tisserand
la source
J'ai la même déception après avoir été quittée en pensant que Python avait enfin une vérification de type. Devra enfin continuer avec l'implémentation de contrôle de type maison.
Hibou57
3

Cela fait longtemps que cela n'a pas été posé, mais l'exemple d'extrait de code donné dans la question est (comme indiqué ici également) de PEP 3107 et à la fin de cet exemple de PEP Des cas d'utilisation sont également donnés qui pourraient répondre à la question du point de PEP de vue ;)

Ce qui suit est cité de PEP3107

Cas d'utilisation

Au cours de la discussion des annotations, un certain nombre de cas d'utilisation ont été soulevés. Certains d'entre eux sont présentés ici, regroupés en fonction du type d'informations qu'ils véhiculent. Sont également inclus des exemples de produits et packages existants qui pourraient utiliser des annotations.

  • Fournir des informations de saisie
    • Vérification de type ([3], [4])
    • Laissez les IDE montrer quels types une fonction attend et renvoie ([17])
    • Surcharge de fonctions / fonctions génériques ([22])
    • Ponts en langues étrangères ([18], [19])
    • Adaptation ([21], [20])
    • Fonctions logiques de prédicat
    • Mappage des requêtes de base de données
    • Marshaling des paramètres RPC ([23])
  • Les autres informations
    • Documentation des paramètres et des valeurs de retour ([24])

Voir le PEP pour plus d'informations sur des points spécifiques (ainsi que leurs références)

klaas
la source
J'apprécierais vraiment que les contrevenants laissent au moins un bref commentaire sur la cause du vote défavorable. Cela aiderait vraiment (au moins moi) beaucoup à m'améliorer.
klaas
2

Python 3.X (uniquement) généralise également la définition de fonction pour permettre aux arguments et aux valeurs de retour d'être annotés avec des valeurs d'objet à utiliser dans les extensions .

Ses données META à expliquer, pour être plus explicite sur les valeurs de fonction.

Les annotations sont codées comme :valueaprès le nom de l'argument et avant une valeur par défaut, et comme ->valueaprès la liste d'arguments.

Ils sont collectés dans un __annotations__attribut de la fonction, mais ne sont pas traités autrement comme spéciaux par Python lui-même:

>>> def f(a:99, b:'spam'=None) -> float:
... print(a, b)
...
>>> f(88)
88 None
>>> f.__annotations__
{'a': 99, 'b': 'spam', 'return': <class 'float'>}

Source: Python Pocket Reference, cinquième édition

EXEMPLE:

Le typeannotationsmodule fournit un ensemble d'outils pour la vérification de type et l'inférence de type du code Python. Il fournit également un ensemble de types utiles pour annoter des fonctions et des objets.

Ces outils sont principalement conçus pour être utilisés par des analyseurs statiques tels que les linters, les bibliothèques de complétion de code et les IDE. De plus, des décorateurs pour effectuer des contrôles d'exécution sont fournis. La vérification de type à l'exécution n'est pas toujours une bonne idée en Python, mais dans certains cas, elle peut être très utile.

https://github.com/ceronman/typeannotations

Comment la saisie aide à écrire un meilleur code

La saisie peut vous aider à effectuer une analyse de code statique pour détecter les erreurs de type avant d'envoyer votre code en production et vous éviter certains bogues évidents. Il existe des outils comme mypy, que vous pouvez ajouter à votre boîte à outils dans le cadre du cycle de vie de votre logiciel. mypy peut vérifier les types corrects en exécutant partiellement ou entièrement votre base de code. mypy vous aide également à détecter des bogues tels que la vérification du type None lorsque la valeur est renvoyée par une fonction. La saisie aide à rendre votre code plus propre. Au lieu de documenter votre code à l'aide de commentaires, où vous spécifiez des types dans une docstring, vous pouvez utiliser des types sans aucun coût de performance.

Clean Python: Codage élégant en Python ISBN: ISBN-13 (pbk): 978-1-4842-4877-5

PEP 526 - Syntaxe des annotations de variables

https://www.python.org/dev/peps/pep-0526/

https://www.attrs.org/en/stable/types.html

Le Demz
la source
@BlackJack, le "pour une utilisation dans les extensions" n'était pas clair?
The Demz
Il est clair, mais ne répond pas à la question à mon humble avis. C'est comme répondre "Quels sont les bons usages des cours?" Avec "Pour une utilisation dans les programmes." C'est clair, correct, mais la partie qui fait la demande n'est pas vraiment plus sage quant à ce que sont les bonnes utilisations concrètes . La vôtre est une réponse qui ne peut pas être plus générique, avec un exemple qui est essentiellement le même que celui déjà dans la question .
BlackJack
1

Malgré toutes les utilisations décrites ici, la seule utilisation exécutoire et, très probablement, forcée des annotations sera pour les indices de type .

Cela n'est actuellement pas appliqué en aucune façon mais, à en juger par PEP 484, les futures versions de Python n'autoriseront que les types comme valeur pour les annotations.

Citations Qu'en est-il des utilisations existantes des annotations? :

Nous espérons que les indices de type finiront par devenir la seule utilisation pour les annotations, mais cela nécessitera des discussions supplémentaires et une période de dépréciation après le déploiement initial du module de saisie avec Python 3.5. Le PEP actuel aura un statut provisoire (voir PEP 411) jusqu'à ce que Python 3.6 soit publié. Le schéma imaginable le plus rapide introduirait une dépréciation silencieuse des annotations sans indication de type dans la version 3.6, une dépréciation complète dans la version 3.7, et déclarerait les indications de type comme la seule utilisation autorisée des annotations dans Python 3.8.

Bien que je n'ai pas encore vu de dépréciations silencieuses dans la version 3.6, cela pourrait très bien être ramené à 3.7 à la place.

Ainsi, même s'il peut y avoir d'autres bons cas d'utilisation, il est préférable de les garder uniquement pour les indications de type si vous ne voulez pas tout changer dans un futur où cette restriction est en place.

Dimitris Fasarakis Hilliard
la source
1

En guise de réponse un peu retardée, plusieurs de mes packages (marrow.script, WebCore, etc.) utilisent des annotations lorsqu'elles sont disponibles pour déclarer le typage (c.-à-d. Transformer les valeurs entrantes du Web, détecter quels arguments sont des commutateurs booléens, etc.). comme pour effectuer un balisage supplémentaire des arguments.

Marrow Script crée une interface de ligne de commande complète vers des fonctions et des classes arbitraires et permet de définir la documentation, la conversion et les valeurs par défaut dérivées du rappel via des annotations, avec un décorateur pour prendre en charge les anciens environnements d'exécution. Toutes mes bibliothèques qui utilisent des annotations prennent en charge les formulaires:

any_string  # documentation
any_callable  # typecast / callback, not called if defaulting
(any_callable, any_string)  # combination
AnnotationClass()  # package-specific rich annotation object
[AnnotationClass(), AnnotationClass(), …]  # cooperative annotation

La prise en charge «nue» des docstrings ou des fonctions de transtypage permet un mixage plus facile avec d'autres bibliothèques prenant en charge les annotations. (C'est-à-dire avoir un contrôleur Web utilisant la conversion de type qui se trouve également être exposé en tant que script de ligne de commande.)

Modifié pour ajouter: J'ai également commencé à utiliser le package TypeGuard en utilisant des assertions de développement pour la validation. Avantage: lors de l'exécution avec les "optimisations" activées ( -O/ PYTHONOPTIMIZEenv var) les vérifications, qui peuvent être coûteuses (par exemple récursives) sont omises, avec l'idée que vous avez correctement testé votre application en développement, donc les vérifications devraient être inutiles en production.

amcgregor
la source
-2

Les annotations peuvent être utilisées pour moduler facilement le code. Par exemple, un module pour un programme que je gère pourrait simplement définir une méthode comme:

def run(param1: int):
    """
    Does things.

    :param param1: Needed for counting.
    """
    pass

et nous pourrions demander à l'utilisateur une chose nommée "param1" qui est "Nécessaire pour compter" et devrait être un "int". En fin de compte, nous pouvons même convertir la chaîne donnée par l'utilisateur dans le type souhaité pour obtenir l'expérience la plus simple possible.

Consultez notre objet de métadonnées de fonction pour une classe open source qui aide avec cela et peut récupérer automatiquement les valeurs nécessaires et les convertir en n'importe quel type souhaité (car l'annotation est une méthode de conversion). Même les IDE affichent correctement la saisie semi-automatique et supposent que les types sont conformes aux annotations - un ajustement parfait.

Lasse Schuirmann
la source
-2

Si vous regardez la liste des avantages de Cython, l'un des principaux est la possibilité d'indiquer au compilateur le type d'un objet Python.

Je peux envisager un avenir où Cython (ou des outils similaires qui compilent une partie de votre code Python) utiliseront la syntaxe d'annotation pour faire leur magie.

boardrider
la source
Le RPython Annotator est un exemple d'approche qui se sent convenablement pythonique; après avoir généré un graphique de votre application, il peut déterminer le type de chaque variable et (pour RPython) appliquer la sécurité de type unique. OTOH, il nécessite une "boxe" ou d'autres solutions / contournements pour permettre des valeurs riches dynamiques. Qui suis-je pour forcer ma multiplyfonction à ne travailler que sur des nombres entiers, quand 'na' * 8 + ' batman!'est-il entièrement valide? ;)
amcgregor