¿Cómo especifico que el tipo de retorno de un método es el mismo que la clase misma?

410

Tengo el siguiente código en Python 3:

class Position:

    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    def __add__(self, other: Position) -> Position:
        return Position(self.x + other.x, self.y + other.y)

Pero mi editor (PyCharm) dice que la posición de referencia no se puede resolver (en el __add__método). ¿Cómo debo especificar que espero que el tipo de retorno sea de tipo Position?

Editar: Creo que esto es realmente un problema de PyCharm. Realmente usa la información en sus advertencias y la finalización del código

Pero corrígeme si me equivoco y necesito usar otra sintaxis.

Michael van Gerwen
fuente

Respuestas:

576

TL; DR : si está utilizando Python 4.0, simplemente funciona. A partir de hoy (2019) en 3.7+, debe activar esta función usando una declaración futura ( from __future__ import annotations); para Python 3.6 o inferior, use una cadena.

Supongo que tienes esta excepción:

NameError: name 'Position' is not defined

Esto se Positiondebe a que debe definirse antes de poder usarlo en una anotación a menos que esté usando Python 4.

Python 3.7+: from __future__ import annotations

Python 3.7 presenta PEP 563: evaluación pospuesta de anotaciones . Un módulo que usa la declaración futura from __future__ import annotationsalmacenará anotaciones como cadenas automáticamente:

from __future__ import annotations

class Position:
    def __add__(self, other: Position) -> Position:
        ...

Esto está programado para convertirse en el predeterminado en Python 4.0. Dado que Python todavía es un lenguaje de tipo dinámico, por lo que no se realiza ninguna verificación de tipo en tiempo de ejecución, las anotaciones de escritura no deberían tener un impacto en el rendimiento, ¿verdad? ¡Incorrecto! Antes de Python 3.7, el módulo de mecanografía solía ser uno de los módulos de Python más lentos en el núcleo, por lo import typingque si verá un aumento de rendimiento de hasta 7 veces cuando actualice a 3.7.

Python <3.7: usa una cadena

De acuerdo con PEP 484 , debe usar una cadena en lugar de la clase en sí:

class Position:
    ...
    def __add__(self, other: 'Position') -> 'Position':
       ...

Si usa el marco de Django, esto puede resultarle familiar, ya que los modelos de Django también usan cadenas para referencias directas (definiciones de clave externa donde el modelo externo está selfo no declarado aún). Esto debería funcionar con Pycharm y otras herramientas.

Fuentes

Las partes relevantes de PEP 484 y PEP 563 , para ahorrarle el viaje:

Reenviar referencias

Cuando una sugerencia de tipo contiene nombres que aún no se han definido, esa definición puede expresarse como un literal de cadena, que se resolverá más adelante.

Una situación en la que esto ocurre comúnmente es la definición de una clase contenedor, donde la clase que se define ocurre en la firma de algunos de los métodos. Por ejemplo, el siguiente código (el inicio de una implementación de árbol binario simple) no funciona:

class Tree:
    def __init__(self, left: Tree, right: Tree):
        self.left = left
        self.right = right

Para abordar esto, escribimos:

class Tree:
    def __init__(self, left: 'Tree', right: 'Tree'):
        self.left = left
        self.right = right

El literal de cadena debe contener una expresión Python válida (es decir, compilar (iluminado, '', 'evaluar') debe ser un objeto de código válido) y debe evaluar sin errores una vez que el módulo se haya cargado completamente. El espacio de nombres local y global en el que se evalúa debe ser el mismo espacio de nombres en el que se evaluarían los argumentos predeterminados para la misma función.

y PEP 563:

En Python 4.0, las anotaciones de funciones y variables ya no se evaluarán en el momento de la definición. En cambio, se conservará una forma de cadena en el __annotations__diccionario respectivo . Los verificadores de tipo estático no verán diferencias en el comportamiento, mientras que las herramientas que usan anotaciones en tiempo de ejecución deberán realizar una evaluación pospuesta.

...

La funcionalidad descrita anteriormente se puede habilitar a partir de Python 3.7 utilizando la siguiente importación especial:

from __future__ import annotations

Cosas que quizás tengas la tentación de hacer

A. Define un muñeco Position

Antes de la definición de clase, coloque una definición ficticia:

class Position(object):
    pass


class Position(object):
    ...

Esto eliminará NameErrore incluso puede verse bien:

>>> Position.__add__.__annotations__
{'other': __main__.Position, 'return': __main__.Position}

¿Pero es?

>>> for k, v in Position.__add__.__annotations__.items():
...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                  
return is Position: False
other is Position: False

B. Parche de mono para agregar las anotaciones:

Es posible que desee probar un poco de magia de metaprogramación de Python y escribir un decorador para parchear la definición de clase para agregar anotaciones:

class Position:
    ...
    def __add__(self, other):
        return self.__class__(self.x + other.x, self.y + other.y)

El decorador debe ser responsable del equivalente de esto:

Position.__add__.__annotations__['return'] = Position
Position.__add__.__annotations__['other'] = Position

Al menos parece correcto:

>>> for k, v in Position.__add__.__annotations__.items():
...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                  
return is Position: True
other is Position: True

Probablemente demasiados problemas.

Conclusión

Si está utilizando 3.6 o menos, use un literal de cadena que contenga el nombre de la clase, use 3.7 from __future__ import annotationsy simplemente funcionará.

Paulo Scardine
fuente
2
Correcto, esto es menos un problema de PyCharm y más un problema de Python 3.5 PEP 484. Sospecho que obtendrás la misma advertencia si la ejecutaste a través de la herramienta de tipo mypy.
Paul Everitt
23
> si estás usando Python 4.0, solo funciona, ¿has visto a Sarah Connor? :)
scrutari
@JoelBerkeley Lo acabo de probar y los parámetros de tipo me funcionaron en 3.6, simplemente no olvide importar desde typingya que cualquier tipo que use debe estar dentro del alcance cuando se evalúa la cadena.
Paulo Scardine
ah, mi error, solo estaba dando la ''vuelta a la clase, no a los parámetros de tipo
joelb
55
Nota importante para cualquiera que use from __future__ import annotations: esto debe importarse antes que todas las demás importaciones.
Artur
16

Especificar el tipo como cadena está bien, pero siempre me molesta un poco que básicamente eludamos el analizador. Por lo tanto, es mejor que no escriba mal ninguna de estas cadenas literales:

def __add__(self, other: 'Position') -> 'Position':
    return Position(self.x + other.x, self.y + other.y)

Una ligera variación es usar un typevar encuadernado, al menos entonces debe escribir la cadena solo una vez al declarar el typevar:

from typing import TypeVar

T = TypeVar('T', bound='Position')

class Position:

    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    def __add__(self, other: T) -> T:
        return Position(self.x + other.x, self.y + other.y)
vbraun
fuente
8
Desearía que Python tuviera typing.Selfque especificar esto explícitamente.
Alexander Huszagh
2
Vine aquí para ver si typing.Selfexistía algo así . La devolución de una cadena codificada no devuelve el tipo correcto al aprovechar el polimorfismo. En mi caso, quería implementar un método de clase deserializado . Me decidí a devolver un dict (kwargs) y llamar some_class(**some_class.deserialize(raw_data)).
Scott P.
Las anotaciones de tipo utilizadas aquí son apropiadas al implementar esto correctamente para usar subclases. Sin embargo, la implementación regresa Position, y no la clase, por lo que el ejemplo anterior es técnicamente incorrecto. La implementación debería reemplazarse Position(por algo así self.__class__(.
Sam Bull
Además, las anotaciones dicen que el tipo de retorno depende other, pero lo más probable es que realmente dependa de self. Por lo tanto, necesitaría poner la anotación selfpara describir el comportamiento correcto (y tal vez otherdebería ser solo Positionpara mostrar que no está vinculado al tipo de retorno). Esto también se puede usar para casos en los que solo está trabajando self. Por ejemplodef __aenter__(self: T) -> T:
Sam Bull
15

El nombre 'Posición' no está disponible en el momento en que se analiza el cuerpo de la clase. No sé cómo está utilizando las declaraciones de tipo, pero el PEP 484 de Python, que es lo que la mayoría de los modos debería usar si usa estas sugerencias de escritura, dice que puede simplemente poner el nombre como una cadena en este punto:

def __add__(self, other: 'Position') -> 'Position':
    return Position(self.x + other.x, self.y + other.y)

Verifique https://www.python.org/dev/peps/pep-0484/#forward-references : las herramientas que se ajusten a eso sabrán desenvolver el nombre de la clase desde allí y usarlo. (Siempre es importante tener Tenga en cuenta que el lenguaje Python en sí mismo no hace nada de estas anotaciones, generalmente están destinadas al análisis de código estático, o uno podría tener una biblioteca / marco para la verificación de tipos en tiempo de ejecución, pero debe configurarlo explícitamente).

actualización Además, a partir de Python 3.8, marque pep-563 - a partir de Python 3.8 es posible escribir from __future__ import annotationspara diferir la evaluación de las anotaciones - las clases de referencia directa deberían funcionar de manera directa.

jsbueno
fuente
9

Cuando una sugerencia de tipo basada en cadenas es aceptable, el __qualname__elemento también se puede usar. Contiene el nombre de la clase, y está disponible en el cuerpo de la definición de clase.

class MyClass:
    @classmethod
    def make_new(cls) -> __qualname__:
        return cls()

Al hacer esto, cambiar el nombre de la clase no implica modificar las sugerencias de tipo. Pero personalmente no esperaría que los editores de códigos inteligentes manejen bien este formulario.

Yvon DUTAPIS
fuente
1
Esto es especialmente útil porque no codifica el nombre de la clase, por lo que sigue trabajando en subclases.
Florian Brucker
No estoy seguro de si esto funcionará con la evaluación pospuesta de las anotaciones (PEP 563), así que hice una pregunta para eso .
Florian Brucker