Suponiendo que quiero escribir una función que acepte cualquier tipo de número en Python, puedo anotarla de la siguiente manera:
from numbers import Number def foo(bar: Number): print(bar) Llevando este concepto un paso más allá, estoy escribiendo funciones que aceptan tipos de números, es decir, int , float o numpy dtypes, como argumentos. Actualmente, estoy escribiendo:
from typing import Type def foo(bar: Type): assert issubclass(bar, Number) print(bar) Pensé que podría sustituir Type con algo como NumberType (similar a NotImplementedType y amigos, reintroducidos en Python 3.10 ), porque todos los tipos de números son subclases de Number :
from numbers import Number import numpy as np assert issubclass(int, Number) assert issubclass(np.uint8, Number) Resulta que (o al menos por lo que puedo decir), no existe un NumberType genérico en Python (3.9):
>>> type(Number) abc.ABCMeta¿Existe una forma limpia (es decir, sin controles de tiempo de ejecución) para lograr el tipo de anotación deseado?
Esta no es una respuesta a la pregunta original. ( La respuesta de Alex Waygood se seleccionó correctamente como receptiva). Sin embargo, he intentado generalizar mis propias soluciones alternativas para los bordes afilados entre los números y la escritura en Python. Esas soluciones ahora viven en numerary (después de haberlo extraído por cesárea de dyce , donde fue concebido).
No dediqué mucho tiempo a nombrar, con la esperanza de que dure poco. Los documentos están en línea. Debe considerarse experimental, pero se está acercando rápidamente a la estabilidad. Se agradecen desesperadamente los comentarios, las sugerencias y las contribuciones .
No hay una forma general de hacer esto. Para empezar, los números no están estrictamente relacionados y sus tipos son aún menos.
Mientras que numbers.Number puede parecer "el tipo de números", no es universal. Por ejemplo, decimal.Decimal no es explícitamente un numbers.Number como subclase, subtipo o subclase virtual. Específicamente para escribir, numbers.Number El número no está respaldado por PEP 484 - Sugerencias de tipo .
Para escribir indirectamente "números" de manera significativa, uno tiene que definir explícitamente qué números son en ese contexto. Esto podría ser un conjunto de tipos numéricos preexistente como int <: float <: complex , un typing.Union / TypeVar de tipos numéricos, un typing.Protocol para definir operaciones y estructura algebraica, o similar.
from typing import TypeVar from decimal import Decimal from fractions import Fraction #: typevar of rational numbers if we squint real hard Q = TypeVar("Q", float, Decimal, Fraction) Dicho todo esto, "el tipo del tipo de números" es aún menos significativo. Incluso los numbers.Number específicos . El número prácticamente no tiene ninguna característica: no se puede convertir a un tipo concreto, ni se puede instanciar en un número significativo.
En su lugar, utilice "el tipo de algún tipo de números":
from typing import Type def zero(t: Type[Q]) -> Q: return t() # all Type[Q]s can be instantiated without arguments print(zero(Fraction)) Si el único objetivo del Type es crear instancias, puede ser mejor solicitar un Callable en su lugar. Esto cubre tanto los tipos como las funciones de fábrica.
def one(t: Callable[[int], Q]) -> Q: return t(1)En general, si queremos decirle a un verificador de tipos que cualquier instancia de cierta clase (o cualquier instancia de una subclase de esa clase) debe aceptarse como argumento de una función, lo hacemos así:
def accepts_int_instances(x: int) -> None: pass class IntSubclass(int): pass accepts_int_instances(42) # passes MyPy (an instance of `int`) accepts_int_instances(IntSubclass(666)) # passes MyPy (an instance of a subclass of `int`) accepts_int_instances(3.14) # fails MyPy (an instance of `float` — `float` is not a subclass of `int`) Si, por otro lado, tenemos una clase C y queremos insinuar que la clase C en sí (o una subclase de C ) debe pasarse como argumento a una función, usamos type[C] en lugar de C (En Python <= 3.8, necesitará usar typing.Type en lugar de la función de type incorporada, pero a partir de Python 3.9 y PEP 585 , podemos parametrizar el type directamente).
def accepts_int_and_subclasses(x: type[int]) -> None: pass class IntSubclass(int): pass accepts_int_and_subclasses(int) # passes MyPy accepts_int_and_subclasses(float) # fails Mypy (not a subclass of `int`) accepts_int_and_subclasses(IntSubclass) # passes MyPy¿Cómo podemos anotar una función para decir que cualquier clase numérica debe aceptarse para un determinado parámetro?
int , float y todos los tipos numéricos numpy son subclases de numbers.Number , por lo que deberíamos poder usar type[Number] si queremos decir que todas las clases numéricas están permitidas.
Al menos, Python dice que float e int son subclases de Number :
>>> from numbers import Number >>> issubclass(int, Number) True >>> issubclass(float, Number) True Y si estamos usando una biblioteca de verificación de tipos en tiempo de ejecución como typeguard , usar type[Number] parece funcionar bien:
>>> from typeguard import typechecked >>> from fractions import Fraction >>> from decimal import Decimal >>> import numpy as np >>> >>> @typechecked ... def foo(bar: type[Number]) -> None: ... pass ... >>> foo(str) Traceback (most recent call last): TypeError: argument "bar" must be a subclass of numbers.Number; got str instead >>> foo(int) >>> foo(float) >>> foo(complex) >>> foo(Decimal) >>> foo(Fraction) >>> foo(np.int64) >>> foo(np.float32) >>> foo(np.ulonglong) >>> # etc. ¡Pero espera! Si intentamos usar type[Number] con un verificador de tipos estático , parece que no funciona. Si ejecutamos el siguiente fragmento de código a través de MyPy, genera un error para cada clase excepto fractions.Fraction :
from numbers import Number from fractions import Fraction from decimal import Decimal NumberType = type[Number] def foo(bar: NumberType) -> None: pass foo(float) # fails foo(int) # fails foo(Fraction) # succeeds! foo(Decimal) # fails Seguramente Python no nos mentiría acerca de que float e int son subclases de Number . ¿Qué está sucediendo?
type[Number] no funciona como una sugerencia de tipo estático para clases numéricas Si bien issubclass(float, Number) e issubclass(int, Number) se evalúan como True , ni float ni int son, de hecho, una subclase "estricta" de numbers.Number . numbers.Number es una clase base abstracta, y tanto int como float están registrados como "subclases virtuales" de Number . Esto hace que Python en tiempo de ejecución reconozca float e int como "subclases" de Number , aunque Number no esté en el orden de resolución del método de ninguna de ellas.
Consulte esta pregunta de StackOverflow para obtener una explicación de lo que es el "orden de resolución del método" o "mro" de una clase.
>>> # All classes have `object` in their mro >>> class Foo: pass >>> Foo.__mro__ (<class '__main__.Foo'>, <class 'object'>) >>> >>> # Subclasses of a class have that class in their mro >>> class IntSubclass(int): pass >>> IntSubclass.__mro__ (<class '__main__.IntSubclass'>, <class 'int'>, <class 'object'>) >>> issubclass(IntSubclass, int) True >>> >>> # But `Number` is not in the mro of `int`... >>> int.__mro__ (<class 'int'>, <class 'object'>) >>> # ...Yet `int` still pretends to be a subclass of `Number`! >>> from numbers import Number >>> issubclass(int, Number) True >>> #?!?!!??¿Qué es una clase base abstracta? ¿Por qué es
numbers.Numberuna clase base abstracta? ¿Qué es la "subclase virtual"?
- Los documentos para las clases base abstractas ("ABC") están aquí.
- PEP 3119, que presenta clases base abstractas, está aquí.
- Los documentos para el módulo de
numbersestán aquí.- PEP 3141, que introdujo números.
numbers.Number, está aquí.- Puedo recomendar esta charla de Raymond Hettinger, que tiene una explicación detallada del ABC y los propósitos de la subclasificación virtual.
El problema es que MyPy no comprende el mecanismo de "subclasificación virtual" que utilizan los ABC (y, tal vez, nunca lo hará ).
MyPy entiende algunos ABC en la biblioteca estándar. Por ejemplo, MyPy sabe que list es un subtipo de collections.abc.MutableSequence , aunque MutableSequence es un ABC, y list es solo una subclase virtual de MutableSequence . Sin embargo, MyPy solo entiende que list es un subtipo de MutableSequence porque le hemos estado mintiendo a MyPy sobre el orden de resolución del método para list .
MyPy, junto con todos los demás verificadores de tipos principales, utiliza los stubs que se encuentran en el repositorio tipificado para su análisis estático de las clases y módulos que se encuentran en la biblioteca estándar. Si observa el stub for list en typeshed , verá que list se proporciona como una subclase directa de collections.abc.MutableSequence . Eso no es cierto en absoluto: MutableSequence está escrito en Python puro, mientras que list es una estructura de datos optimizada escrita en C. Pero para el análisis estático, es útil que MyPy piense que esto es cierto. Otras clases de colecciones en la biblioteca estándar (por ejemplo, tuple , set y dict ) tienen mayúsculas y minúsculas de la misma manera, pero los tipos numéricos como int y float no lo son.
Si le mentimos a MyPy sobre las clases de colecciones, ¿por qué no le mentimos también a MyPy sobre las clases numéricas?
Mucha gente (¡incluyéndome a mí!) piensa que deberíamos, y las discusiones han estado en curso durante mucho tiempo sobre si se debe hacer este cambio (por ejemplo, propuesta mecanografiada, problema de MyPy ). Sin embargo, existen varias complicaciones para hacerlo.
El crédito va a @chepner en los comentarios por encontrar el enlace al problema de MyPy.
Una solución posible (aunque un poco repulsiva ) aquí podría ser usar typing.SupportsFloat .
SupportsFloat es un protocolo verificable en tiempo de ejecución que tiene un único método abstractmethod , __float__ . Esto significa que cualquier clase que tenga un método __float__ se reconoce, tanto en tiempo de ejecución como por verificadores de tipos estáticos , como subtipos de SupportsFloat , incluso si SupportsFloat no está en el orden de resolución del método de la clase.
¿Qué es un protocolo? ¿Qué es el tipeo de patos? ¿Cómo hacen los protocolos lo que hacen? ¿Por qué algunos protocolos, pero no todos los protocolos, se pueden verificar en tiempo de ejecución?
- PEP 544, que presenta la
typing.Protocoly la tipificación estructural/tipo pato , explica en detalle cómo funciona latyping.Protocol. También explica cómo los verificadores de tipos estáticos pueden reconocer clases comofloateintcomo subtipos deSupportsFloat, aunqueSupportsFloatno aparece en el orden de resolución del método paraintofloat.- Los documentos de Python para la subtipificación estructural están aquí.
- Los documentos de Python para escribir.
typing.Protocolestán aquí.- Los documentos de
typing.Protocolpara escribir. Protocolo están aquí.- Los documentos de Python para escribir.
typing.SupportsFloatestán aquí.- El código fuente para escribir.
typing.SupportsFloatestá aquí.- De forma predeterminada, los protocolos no se pueden verificar en tiempo de ejecución con
isinstanceeissubclass.SupportsFloatse puede verificar en tiempo de ejecución porque está decorado con el decorador@runtime_checkable. Lea la documentación para ese decorador aquí.
Nota: aunque los protocolos definidos por el usuario solo están disponibles en Python >= 3.8, SupportsFloat ha estado en el módulo de typing desde que se agregó el módulo a la biblioteca estándar en Python 3.5.
Ventajas de esta solución
Soporte completo* para todos los tipos numéricos principales: fractions.Fraction , decimal.Decimal , int , float , np.int32 , np.int16 , np.int8 , np.int64 , np.int0 , np.float16 , np.float32 , np.float64 , np.float128 , np.intc , np.uintc , np.int_ , np.uint , np.longlong , np.ulonglong , np.half , np.single , np.double , np.longdouble , np.csingle , np.cdouble y np.clongdouble tienen un método __float__ .
Si anotamos un argumento de función como de type[SupportsFloat] , MyPy acepta correctamente* los tipos que se ajustan al protocolo y rechaza correctamente los tipos que no se ajustan al protocolo.
Es una solución bastante general: no necesita enumerar explícitamente todos los tipos posibles que desea aceptar.
Funciona tanto con verificadores de tipos estáticos como con bibliotecas de verificación de tipos en tiempo de ejecución, como typeguard .
Desventajas de esta solución
Se siente como (y es) un truco. Tener un método __float__ no es una idea razonable para nadie de lo que define un "número" en abstracto.
Mypy no reconoce complex como un subtipo de SupportsFloat . complex , de hecho, tiene un método __float__ en Python <= 3.9. Sin embargo, no tiene un método __float__ en el código auxiliar tipificado para complex . Dado que MyPy (junto con todos los demás verificadores de tipos principales) usa stubs tipificados para su análisis estático, esto significa que no sabe que complex tiene este método. complex.__float__ probablemente se omite del stub tipográfico debido al hecho de que el método siempre genera TypeError ; por esta razón, el método __float__ se eliminó de la clase complex en Python 3.10 .
Cualquier clase definida por el usuario, incluso si no es una clase numérica, podría definir potencialmente __float__ . De hecho, incluso hay varias clases no numéricas en la biblioteca estándar que definen __float__ . Por ejemplo, aunque el tipo str en Python (que está escrito en C) no tiene un método __float__ , collections.UserString (que está escrito en Python puro) sí lo tiene. ( El código fuente de str está aquí , y el código fuente de collections.UserString está aquí ).
Ejemplo de uso
Esto pasa MyPy para todos los tipos numéricos con los que lo probé, excepto para complex :
from typing import SupportsFloat NumberType = type[SupportsFloat] def foo(bar: NumberType) -> None: pass Si queremos que se acepte complex también, un ajuste ingenuo a esta solución sería usar el siguiente fragmento en su lugar, special-casing complex . Esto satisface a MyPy para todos los tipos numéricos que se me ocurren. También incluí type[Number] en la sugerencia de tipo, ya que podría ser útil para capturar una clase hipotética que hereda directamente de los numbers.Number Número y no tiene un método __float__ . No sé por qué alguien escribiría una clase de este tipo, pero hay algunas clases que heredan directamente de numbers.Number (por ejemplo, fractions.Fraction . Fracción), y ciertamente sería teóricamente posible crear una subclase directa de Number sin un método __float__ . Number en sí es una clase vacía que no tiene métodos ; existe únicamente para proporcionar una "clase base virtual" para otras clases numéricas en la biblioteca estándar.
from typing import SupportsFloat, Union from numbers import Number NumberType = Union[type[SupportsFloat], type[complex], type[Number]] # You can also write this more succinctly as: # NumberType = type[Union[SupportsFloat, complex, Number]] # The two are equivalent. # In Python >= 3.10, we can even write it like this: # NumberType = type[SupportsFloat | complex | Number] # See PEP 604: https://www.python.org/dev/peps/pep-0604/ def foo(bar: NumberType) -> None: pass Traducido al inglés, NumberType aquí es equivalente a:
Cualquier clase, si y solo si:
- Tiene un método
__float__;- Y/O es
complex;- Y/O es una subclase de
complex;- Y/O es
numbers.Number;- Y/O es una subclase "estricta" (no virtual) de
numbers.Number. Número .
No veo esto como una "solución" al problema con el complex , es más una solución. El problema con complex es ilustrativo de los peligros de este enfoque en general. Puede haber otros tipos numéricos inusuales en bibliotecas de terceros, por ejemplo, que no subclasifican directamente numbers.Number Número o tienen un método __float__ . Sería extremadamente difícil saber cómo se verían de antemano y considerarlos como casos especiales.
¿Por qué SupportsFloat en lugar de escribir. typing.SupportsInt ?
fractions.Fraction tiene un método __float__ ( heredado de numbers.Rational ) pero no tiene un método __int__ .
¿Por qué SupportsFloat en lugar de SupportsAbs ?
¡Incluso complex tiene un método __abs__ , por lo que escribir. typing.SupportsAbs parece una alternativa prometedora a primera vista! Sin embargo, hay varias otras clases en la biblioteca estándar que tienen un método __abs__ y no tienen un método __float__ , y sería exagerado argumentar que todas son clases numéricas. ( datetime.timedelta no me parece muy parecido a un número). Si SupportsAbs en lugar de SupportsFloat , correrías el riesgo de ampliar tu red demasiado y permitir todo tipo de clases no numéricas.
¿Por qué SupportsFloat en lugar deSupportsRound ?
Como alternativa a SupportsFloat , también podría considerar usar typing.SupportsRound , que acepta todas las clases que tienen un método __round__ . Esto es tan completo como SupportsFloat (cubre todos los principales tipos numéricos que no sean complex ). También tiene la ventaja de que collection.UserString no tiene un método __round__ mientras que, como se discutió anteriormente, tiene un método __float__ . Por último, parece menos probable que las clases no numéricas de terceros incluyan un método __round__ .
Sin embargo, si optó por SupportsRound en lugar de SupportsFloat , en mi opinión, correría un mayor riesgo de excluir clases numéricas válidas de terceros que, por cualquier motivo, no definen __round__ .
"Tener un método __float__ " y "tener un método __round__ " son definiciones bastante pobres de lo que significa que una clase sea un "número". Sin embargo, el primero se siente mucho más cercano a la definición "verdadera" que el segundo. Como tal, se siente más seguro contar con clases numéricas de terceros que tengan un método __float__ que contar con que tengan un método __round__ .
Si desea estar "más seguro" cuando se trata de garantizar que su función acepte un tipo numérico de terceros válido, no veo ningún daño particular en extender NumberType aún más con SupportsRound :
from typing import SupportsFloat, SupportsRound, Union from numbers import Number NumberType = Union[type[SupportsFloat], type[SupportsRound], type[complex], type[Number]] Sin embargo, me preguntaría si es realmente necesario incluir SupportsRound , dado que es muy probable que cualquier tipo que tenga un método __round__ también tenga un método __float__ .
*...excepto complex