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

159

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 puedo pensar por qué se implementaron en Python3 o cualquier buen uso para ellos. ¿Quizás SO puede iluminarme?

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

agscala
fuente
66
@SilentGhost: desafortunadamente, muchos de los enlaces con los casos de uso reales están rotos. ¿Hay algún lugar donde el contenido podría haberse almacenado o se ha ido para siempre?
máximo
16
no debería foo.func_annotations estar foo.__annotations__en python3?
zhangxaochen
2
Las anotaciones no tienen significación especial. Lo único que hace Python es ponerlos en el diccionario de anotaciones . Cualquier otra acción depende de usted.
N Randhawa
que def foo(a: 'x', b: 5 + 6, c: list) -> max(2, 9):significa
Ali SH

Respuestas:

90

Creo que esto es realmente genial.

Viniendo de una formación académica, puedo decirle que las anotaciones han demostrado ser invaluables para permitir analizadores estáticos inteligentes para lenguajes como Java. Por ejemplo, podría definir la semántica como restricciones de estado, subprocesos a los que se les permite acceder, limitaciones de arquitectura, etc., y hay bastantes herramientas que pueden leerlas y procesarlas para proporcionar garantías más allá de lo que obtiene de los compiladores. Incluso podría escribir cosas que verifiquen las condiciones previas / posteriores.

Siento que algo como esto es especialmente necesario en Python debido a su escritura más débil, pero realmente no hubo construcciones que lo hicieran sencillo y parte de la sintaxis oficial.

Hay otros usos para las anotaciones más allá de la seguridad. Puedo ver cómo puedo 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 para que lea su documentación (por ejemplo, imagine que tiene un método que no debe invocarse con un valor negativo, pero es no intuitivo por el nombre). Con anotaciones, podría escribir técnicamente algo así para Python. Del mismo modo, se puede escribir una herramienta que organice métodos en una clase grande basada en etiquetas si hay una sintaxis oficial.

Uri
fuente
34
ISTM estos son beneficios teóricos que solo se pueden obtener solo si la biblioteca estándar y los módulos de terceros usan anotaciones de funciones y las usan con un significado consistente y usan un sistema de anotaciones bien pensado. Hasta ese día (que nunca llegará), los usos principales de las anotaciones de función de Python serán los usos únicos descritos en las otras respuestas. Por el momento, puede olvidarse de los analizadores estáticos inteligentes, las garantías del compilador, las cadenas de herramientas basadas en Java, etc.
Raymond Hettinger
44
Incluso sin que todo use anotaciones de funciones, puede usarlas para el análisis estático dentro del código que las tiene en sus entradas y está llamando a otro código que está anotado de manera similar. Dentro de un proyecto o base de código más grande, este podría ser un cuerpo de código significativamente útil para realizar análisis estáticos basados ​​en anotaciones.
GPS
1
AFAICT, puede hacer todo esto con decoradores, que son anteriores a las anotaciones; por lo tanto, todavía no veo el beneficio. Tengo una opinión ligeramente diferente sobre esta pregunta: stackoverflow.com/questions/13784713/…
allyourcode del
9
Avance rápido hasta 2015, python.org/dev/peps/pep-0484 y mypy-lang.org están comenzando a demostrar que todos los detractores están equivocados.
Mauricio Scheffer
1
También revela la influencia de Python en Swift aún más.
uchuugaka
92

Las anotaciones de funciones son lo que haces de ellas.

Se pueden usar para documentación:

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

Se pueden usar para la verificació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

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

Raymond Hettinger
fuente
18
¿Cómo es esto mejor que una cadena de documentación para la documentación, o la comprobación explícita de tipos en la función? Esto parece complicar el lenguaje sin ninguna razón.
endolito el
10
@endolith Ciertamente podemos prescindir de las anotaciones de funciones. Simplemente proporcionan una forma estándar de acceder a las anotaciones. Eso los hace accesibles a help () y a información sobre herramientas y los pone a disposición para la introspección.
Raymond Hettinger
44
En lugar de pasar números, podría crear tipos Massy, en su Velocitylugar.
derecha el
1
para demostrar completamente esto, también tendría def kinetic_energy(mass: 'in kilograms', velocity: 'in meters per second') -> float:que mostrar el tipo de retorno. Esta es mi respuesta favorita aquí.
Tommy
Usando su código, ¿hay alguna forma de verificar la returnanotación? No parece aparecer enlocals
user189728
46

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 programas de Python utilizando el próximo estándar para anotaciones de tipo introducido en Python 3.5 beta 1 (PEP 484), y use mypy para verificarlas de forma estática.

Usado así:

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
fuente
También vea pytype : el otro analizador estático que se está construyendo con PEP-0484 en mente.
gps
Lamentablemente, el tipo no se aplica. Si escribo list(fib('a'))con su función de ejemplo, Python 3.7 acepta felizmente el argumento y se queja de que no hay forma de comparar una cadena y un int.
Denis de Bernardy
@DenisdeBernardy Como explica PEP-484, Python solo proporciona anotaciones de tipo. Para imponer tipos, debe usar mypy.
Dustin Wyatt
23

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

Nota : en Python puede acceder a las anotaciones en function.__annotations__lugar de eliminar function.func_annotationsel func_*estilo en Python 3.

Muhammad Alkarouri
fuente
2
Aplicación interesante, aunque me temo function = self.typemap.get(types)que no funcionará cuando se incluyan subclases. En ese caso, probablemente tengas que recorrerlo typemapusando isinnstance. Me pregunto si @overloadmaneja esto correctamente
Tobias Kienzler
Creo que esto se rompe si la función tiene un tipo de retorno
zenna
1
El __annotations__ es un dictque no garantiza el orden de los argumentos, por lo que este fragmento a veces falla. Yo recomendaría cambiar el types = tuple(...)a spec = inspect.getfullargspec(function)entonces types = tuple([spec.annotations[x] for x in spec.args]).
xoolive
Tienes toda la razón, @xoolive. ¿Por qué no edita la respuesta para agregar su solución?
Muhammad Alkarouri
@xoolive: me di cuenta. A veces, los editores utilizan una mano dura en la gestión de las ediciones. He editado la pregunta para incluir su solución. En realidad, he tenido una discusión sobre esto, pero no hay forma de rechazar la solución. Por cierto, gracias por la ayuda.
Muhammad Alkarouri
22

Uri ya ha dado una respuesta adecuada, así que aquí hay una respuesta menos seria: para que pueda acortar sus cadenas de documentos.

PINCHAZO
fuente
2
quiéralo. +1. sin embargo, al final, escribir docstrings sigue siendo la forma número uno de hacer que mi código sea legible, sin embargo, si tuviera que implementar algún tipo de comprobación estática o dinámica, es bueno tener esto. Quizás pueda encontrar un uso para ello.
Warren P
8
No recomiendo usar anotaciones como reemplazo de una sección Args: o @param o similar en sus cadenas de documentos (cualquier formato que elija usar). Si bien las anotaciones de documentación son un buen ejemplo, empaña el poder potencial de las anotaciones, ya que podría obstaculizar otros usos más potentes. Además, no puede omitir anotaciones en tiempo de ejecución para reducir el consumo de memoria (python -OO) como puede hacerlo con cadenas de documentos y declaraciones de afirmación.
GPS
2
@gps: Como dije, fue una respuesta menos seria.
JAB
2
Con toda seriedad, es una forma mucho mejor de documentar los tipos que espera, sin dejar de cumplir con DuckTyping.
Marc
1
@gps No estoy seguro de que el consumo de memoria de las cadenas de documentos sea algo de qué preocuparse en el 99.999% de los casos.
Tommy
13

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 no se aplican realmente.

Así que decidí escribir un decorador de funciones simple para hacerlas 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 <function f at 0x109b7c710> does not match annotation type <class 'float'>

Lo agregué a la biblioteca Asegurar .

tejedor
fuente
Tengo la misma decepción después de que salí creyendo que Python finalmente tiene la verificación de tipo. Finalmente tendrá que continuar con la implementación de verificación de tipo hecha en casa.
Hibou57
3

Ha pasado mucho tiempo desde que se hizo esto, pero el fragmento de ejemplo dado en la pregunta es (como también se indica allí) de PEP 3107 y al final de este ejemplo de PEP También se dan casos de uso que podrían responder a la pregunta desde el punto de PEP vista;)

Lo siguiente se cita de PEP3107

Casos de uso

En el curso de la discusión de las anotaciones, se han planteado varios 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
    • Verificación de tipo ([3], [4])
    • Deje que los IDE muestren qué tipos espera y devuelve una función ([17])
    • Sobrecarga de funciones / funciones genéricas ([22])
    • Puentes de lenguas extranjeras ([18], [19])
    • Adaptación ([21], [20])
    • Funciones lógicas predicadas
    • Asignación de consulta de base de datos
    • Clasificación de parámetros RPC ([23])
  • Otra información
    • Documentación para parámetros y valores de retorno ([24])

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

klaas
fuente
Realmente me gustaría saber si los votantes negativos dejan al menos un breve comentario sobre la causa del voto negativo. Esto realmente ayudaría (al menos a mí) a mejorar mucho.
klaas
2

Python 3.X (solo) también generaliza la definición de funciones para permitir que los argumentos y los 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 :valuedespués del nombre del argumento y antes de un valor predeterminado, y ->valuedespués de la lista de argumentos.

Se recopilan en un __annotations__atributo de la función, pero Python no los trata como algo 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 typeannotationsmódulo proporciona un conjunto de herramientas para la verificación de tipos y la inferencia de tipos de 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ódigo e IDE. Además, se proporcionan decoradores para realizar comprobaciones de 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

Cómo escribir ayuda a escribir un mejor código

Escribir puede ayudarlo a hacer análisis de código estático para detectar errores de tipo antes de enviar su código a producción y evitar algunos errores obvios. Existen herramientas como mypy, que puede agregar a su caja de herramientas como parte de su ciclo de vida del software. mypy puede verificar los tipos correctos ejecutando su base de código parcial o totalmente. mypy también lo ayuda a detectar errores como verificar el tipo Ninguno cuando el valor es devuelto por una función. Escribir ayuda a que su código sea más limpio. En lugar de documentar su código usando comentarios, donde especifica tipos en una cadena de documentos, puede usar tipos sin ningún costo de rendimiento.

Clean Python: Codificación elegante en Python ISBN: ISBN-13 (pbk): 978-1-4842-4877-5

PEP 526 - Sintaxis para anotaciones variables

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

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

El demz
fuente
@BlackJack, el "para uso en extensiones" no estaba claro?
El Demz
Está claro, pero no responde la pregunta en mi humilde opinión. Es como si respondiera “¿Cuáles son los buenos usos de clases?” Con “Para su uso en los programas.” Está claro, correcto, pero el partido pidiendo en realidad no es más prudente en cuanto a qué diablos buena concretos usos son. La suya es una respuesta que no puede ser más genérica, con un ejemplo que es esencialmente el mismo que ya está en la pregunta .
BlackJack
1

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

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

Citando ¿Qué pasa con los usos existentes de las anotaciones? :

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

Aunque todavía no he visto ninguna depreciación silenciosa en 3.6, esto podría muy bien pasar a 3.7.

Por lo tanto, a pesar de que puede haber algunos otros buenos casos de uso, es mejor mantenerlos únicamente para sugerencias de tipo si no desea cambiar todo en un futuro donde esta restricción esté vigente.

Dimitris Fasarakis Hilliard
fuente
1

Como una respuesta un poco retrasada, varios de mis paquetes (marrow.script, WebCore, etc.) usan anotaciones donde están disponibles para declarar la conversión de texto (es decir, transformar los valores entrantes desde la web, detectar qué argumentos son modificadores booleanos, etc.) como para realizar un marcado adicional de argumentos.

Marrow Script crea una interfaz de línea de comandos completa para funciones y clases arbitrarias y permite definir documentación, conversión y valores predeterminados derivados de la devolución de llamada mediante anotaciones, con un decorador para admitir 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 las cadenas de documentos o las funciones de conversión de texto permite una mezcla más fácil con otras bibliotecas que reconocen las anotaciones. (Es decir, tengo un controlador web que usa la conversión de texto que también está expuesto como un script de línea de comandos).

Editado para agregar: también he comenzado a utilizar el paquete TypeGuard utilizando aserciones de tiempo de desarrollo para la validación. Beneficio: cuando se ejecuta con "optimizaciones" habilitadas ( -O/ PYTHONOPTIMIZEenv var), se omiten las comprobaciones, que pueden ser costosas (por ejemplo, recursivas), con la idea de que ha probado adecuadamente su aplicación en desarrollo, por lo que las comprobaciones deberían ser innecesarias en la producción.

amcgregor
fuente
-2

Las anotaciones se pueden usar para modularizar fácilmente el código. Por ejemplo, un módulo para un programa que estoy manteniendo 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 algo llamado "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 experiencia más libre de 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 los autocompletos correctamente y suponen que los tipos están de acuerdo con las anotaciones, un ajuste perfecto.

Lasse Schuirmann
fuente
-2

Si observa la lista de beneficios de Cython, una de las principales es la capacidad de decirle al compilador qué tipo de objeto es Python.

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

jinete
fuente
El RPython Annotator es un ejemplo de un enfoque que se siente adecuadamente Pythonic; después de generar un gráfico de su aplicación, puede calcular el tipo de cada variable y (para RPython) hacer cumplir la seguridad de un solo tipo. OTOH requiere "boxeo" u otras soluciones / soluciones para permitir valores dinámicos ricos. ¿Quién soy yo para forzar mi multiplyfunción a trabajar solo contra enteros, cuando 'na' * 8 + ' batman!'es completamente válida? ;)
amcgregor