¿Cuáles son los buenos usos para "Anotaciones de funciones" de Python3?


Answers

Las anotaciones de funciones son lo que haces de ellas.

Se pueden usar para la documentación:

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

Se pueden usar para la verificación previa de condición:

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

También vea http://www.python.org/dev/peps/pep-0362/ para una forma de implementar la verificación de tipos.

Question

Anotaciones de funciones: PEP-3107

Me encontré con un fragmento de código que demuestra las anotaciones de función de Python3. El concepto es simple, pero no puedo pensar en por qué se implementaron en Python3 o en algún buen uso para ellos. Tal vez SO pueda iluminarme?

Cómo funciona:

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

Todo lo que sigue al colon después de un argumento es una 'anotación', y la información que sigue a -> es una anotación para el valor de retorno de la función.

foo.func_annotations devolvería un diccionario:

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

¿Cuál es la importancia de tener esto disponible?




Como una respuesta tardía, varios de mis paquetes (médula.script, WebCore, etc.) usan anotaciones donde están disponibles para declarar el encasillado (es decir, transformar los valores entrantes de la web, detectar qué argumentos son interruptores booleanos, etc.). como para realizar marcado adicional de argumentos.

Marrow Script construye una interfaz completa de línea de comandos para funciones y clases arbitrarias y permite definir documentación, fundición y valores por defecto derivados de la devolución de llamadas a través de anotaciones, con un decorador para soportar tiempos de ejecución más antiguos. Todas mis bibliotecas que usan anotaciones admiten los formularios:

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

El soporte "desnudo" para docstrings o funciones de encasillado permite una mezcla más fácil con otras bibliotecas que reconocen las anotaciones. (Es decir, tengo un controlador web que usa encasillado que también se ve expuesto como un script de línea de comandos).




Si nos fijamos en la lista de beneficios de Cython, uno de los principales es la capacidad de decirle al compilador qué tipo de objeto es Python.

Puedo imaginar un futuro donde Cython (o herramientas similares que compilan parte de tu código Python) usará la sintaxis de anotación para hacer su magia.




Solo para agregar un ejemplo específico de un buen uso de mi respuesta here , junto con los decoradores se puede hacer un mecanismo simple para multimedios.

# 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

y un ejemplo de uso:

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)))

Esto se puede hacer agregando los tipos al decorador como se muestra en la publicación original de Guido , pero anotar los parámetros es mejor ya que evita la posibilidad de una coincidencia incorrecta de los parámetros y tipos.

Nota : en Python, puede acceder a las anotaciones como function.__annotations__ lugar de function.func_annotations ya que el estilo func_* se eliminó en Python 3.




Python 3.X (solamente) también generaliza la definición de la función para permitir que los argumentos y los valores de retorno se anoten con los valores del objeto para usar en las extensiones .

Su META-data para explicar, para ser más explícito sobre los valores de la función.

Las anotaciones se codifican como :value después del nombre del argumento y antes de un valor predeterminado, y como ->value después de la lista de argumentos.

Se recopilan en un atributo __annotations__ de la función, pero Python no lo considera como especial:

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

Fuente: Python Pocket Reference, quinta edición

EJEMPLO:

El módulo typeannotations proporciona un conjunto de herramientas para la verificación de tipos y la inferencia de tipo de código de Python. También proporciona un conjunto de tipos útiles para anotar funciones y objetos.

Estas herramientas están diseñadas principalmente para ser utilizadas por analizadores estáticos como linters, bibliotecas de finalización de código e IDE. Además, se proporcionan decoradores para realizar comprobaciones en tiempo de ejecución. La comprobación de tipos en tiempo de ejecución no siempre es una buena idea en Python, pero en algunos casos puede ser muy útil.

https://github.com/ceronman/typeannotations




La primera vez que vi anotaciones, pensé "¡genial! ¡Finalmente puedo optar por algún tipo de verificación!" Por supuesto, no me había dado cuenta de que las anotaciones en realidad no se hacen cumplir.

Así que decidí escribir un simple decorador de funciones para aplicarlas :

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'>

Lo agregué a la biblioteca Ensure .