¿Cuáles son los buenos usos de las “anotaciones de funciones” de Python3?

Anotaciones de funciones: PEP-3107

Me encontré con un fragmento de código que demuestra las anotaciones de funciones de Python3. El concepto es simple pero no se me ocurre por qué se implementaron en Python3 ni por ningún otro buen uso para ellos. Tal vez así me puede iluminar?

Cómo funciona:

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

Todo lo que sigue a los dos puntos 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 el significado de tener esto disponible?

Creo que esto es realmente genial.

Desde una formación académica, puedo decirles que las anotaciones han demostrado ser invaluables para habilitar los analizadores estáticos inteligentes para lenguajes como Java. Por ejemplo, podría definir semánticas como restricciones de estado, subprocesos que pueden acceder, limitaciones de architecture, etc., y hay bastantes herramientas que pueden leerlas y procesarlas para proporcionar garantías más allá de lo que obtiene de los comstackdores. Incluso podrías escribir cosas que verifiquen condiciones previas / postcondiciones.

Siento que algo así es especialmente necesario en Python debido a su tipificación más débil, pero realmente no hubo construcciones que hicieran esto sencillo y parte de la syntax oficial.

Hay otros usos para las anotaciones más allá de la seguridad. Puedo ver cómo podría aplicar mis herramientas basadas en Java a Python. Por ejemplo, tengo una herramienta que le permite asignar advertencias especiales a los métodos y le da indicaciones cuando las llama de que debería leer su documentación (por ejemplo, imagine que tiene un método que no debe invocarse con un valor negativo, pero es no intuitivo desde el nombre). Con las anotaciones, podría técnicamente escribir algo como esto para Python. De manera similar, una herramienta que organiza métodos en una clase grande basada en tags se puede escribir si hay una syntax oficial.

Las anotaciones de funciones son lo que ustedes hacen de ellas.

Se pueden utilizar para la documentación:

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

Se pueden utilizar para la comprobación previa a la 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.

Esta es una respuesta tardía, pero, AFAICT, el mejor uso actual de las anotaciones de funciones es PEP-0484 y MyPy .

Mypy es un verificador de tipo estático opcional para Python. Puede agregar sugerencias de tipo a sus progtwigs de Python usando el próximo estándar para anotaciones de tipo introducidas en Python 3.5 beta 1 (PEP 484), y usar mypy para escribirlas de forma estática.

Utilizado como tal:

 from typing import Iterator def fib(n: int) -> Iterator[int]: a, b = 0, 1 while a < n: yield a a, b = b, a + b 

Solo para agregar un ejemplo específico de un buen uso de mi respuesta aquí , junto con los decoradores, se puede hacer un mecanismo simple para los métodos múltiples.

 # 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 muestra la publicación original de Guido , pero anotar los parámetros en sí mismos es mejor, ya que evita la posibilidad de una coincidencia incorrecta de 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.

Uri ya ha dado una respuesta adecuada, así que aquí hay una pregunta menos seria: para que puedas hacer tus cadenas de documentos más cortas.

La primera vez que vi anotaciones, pensé “¡genial! ¡Finalmente, puedo optar por una comprobación de tipos!” Por supuesto, no me había dado cuenta de que las anotaciones en realidad no se hacen cumplir.

Así que decidí escribir un decorador de función simple para hacerlos cumplir :

 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  does not match annotation type  

Lo agregué a la biblioteca Asegurar .

Hace mucho tiempo que no se solicitó esto, pero el fragmento de ejemplo dado en la pregunta es (como se indica allí también) de PEP 3107 y al final de este ejemplo de PEP. También se dan casos de uso que pueden responder a la pregunta desde el punto de PEP. vista;)

Lo siguiente está citado de PEP3107

Casos de uso

En el curso de la discusión de las anotaciones, se han planteado una serie de casos de uso. Algunos de estos se presentan aquí, agrupados por el tipo de información que transmiten. También se incluyen ejemplos de productos y paquetes existentes que podrían hacer uso de anotaciones.

  • Proporcionar información de mecanografía
    • Comprobación de tipos ([3], [4])
    • Permite que los IDE muestren qué tipos espera y devuelve una función ([17])
    • Función de sobrecarga / funciones genéricas ([22])
    • Puentes en idiomas extranjeros ([18], [19])
    • Adaptación ([21], [20])
    • Predicar funciones lógicas
    • Mapeo de consulta de base de datos
    • Ajuste de parámetros de RPC ([23])
  • Otra información
    • Documentación para parámetros y valores de retorno ([24])

Consulte el PEP para obtener más información sobre puntos específicos (así como sus referencias)

A pesar de todos los usos descritos aquí, el uso forzoso y, muy probablemente, forzado de anotaciones será para sugerencias de tipo .

Actualmente, esto no se aplica de ninguna manera pero, a juzgar por PEP 484, las versiones futuras de Python solo permitirán tipos como el valor de las anotaciones.

Cotización ¿Qué pasa con los usos existentes de las anotaciones? :

Esperamos que las sugerencias de tipo se conviertan en el único uso de las anotaciones, pero esto requerirá una discusión adicional y un período de desaprobación después de la implementación inicial del módulo de mecanografía con Python 3.5. El PEP actual tendrá un estado provisional (ver PEP 411) hasta que se libere Python 3.6. El esquema más rápido concebible introduciría la eliminación silenciosa de las anotaciones de sugerencia no tipo en 3.6, la depreciación completa en 3.7, y declararía las sugerencias de tipo como el único uso permitido de anotaciones en Python 3.8.

Aunque no he visto ninguna disminución silenciosa en 3.6 aún, esto podría ser superado a 3.7, en su lugar.

Por lo tanto, aunque podría haber otros buenos casos de uso, es mejor mantenerlos solo para sugerencias de tipo si no desea cambiar todo en un futuro donde existe esta restricción.

Python 3.X (solo) también generaliza la definición de funciones para permitir que los argumentos y valores de retorno se anoten con valores de objeto para su uso en extensiones .

Sus datos META para explicar, para ser más explícitos 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 recogen en un atributo __annotations__ de la función, pero Python no las trata de manera especial:

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

Fuente: Python Pocket Reference, Quinta edición.

EJEMPLO:

El módulo de typeannotations proporciona un conjunto de herramientas para la verificación de tipos y la inferencia de tipos del código 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ódigos e IDE. Además, se proporcionan decoradores para hacer verificaciones 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

Como un poco de una respuesta retrasada, varios de mis paquetes (marrow.script, WebCore, etc.) usan anotaciones donde están disponibles para declarar el encasillado (es decir, la transformación de los valores entrantes de la web, la detección de qué argumentos son interruptores booleanos, etc.) En cuanto a realizar marcado adicional de argumentos.

Marrow Script construye una interfaz de línea de comandos completa para funciones y clases arbitrarias y permite definir la documentación, la conversión y los valores predeterminados derivados de la callback a través de anotaciones, con un decorador para admitir los tiempos de ejecución anteriores. Todas mis bibliotecas que usan anotaciones soportan 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 las cadenas de documentos o las funciones de encasillamiento permite una mezcla más fácil con otras bibliotecas que tienen en cuenta las anotaciones. (Es decir, tiene un controlador web que usa typecasting que también se expone como un script de línea de comandos).

Editado para agregar: También he empezado a utilizar el paquete TypeGuard utilizando aserciones en tiempo de desarrollo para la validación. Beneficio: cuando se ejecutan con “optimizaciones” habilitadas ( -O / PYTHONOPTIMIZE env var) se PYTHONOPTIMIZE las verificaciones, que pueden ser costosas (por ejemplo, recursivas), con la idea de que haya probado correctamente su aplicación en desarrollo, por lo que las verificaciones no deberían ser necesarias. en producción.

Las anotaciones se pueden utilizar para modularizar fácilmente el código. Por ejemplo, un módulo para un progtwig que mantengo podría definir un método como:

 def run(param1: int): """ Does things. :param param1: Needed for counting. """ pass 

y podríamos pedirle al usuario una cosa llamada “param1” que es “Necesario para contar” y debería ser un “int”. Al final, incluso podemos convertir la cadena dada por el usuario al tipo deseado para obtener la mejor experiencia sin problemas.

Vea nuestro objeto de metadatos de función para una clase de código abierto que ayuda con esto y puede recuperar automáticamente los valores necesarios y convertirlos a cualquier tipo deseado (porque la anotación es un método de conversión). Incluso los IDE muestran autocompletaciones y asumen que los tipos son de acuerdo con las anotaciones, un ajuste perfecto.

Si observa la lista de beneficios de Cython, uno de los más importantes es la capacidad de decirle al comstackdor qué tipo de objeto es Python.

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