Sugerencias de tipo Python sin importaciones cíclicas
Estoy intentando dividir mi enorme clase en dos; bueno, básicamente en la clase "principal" y un mixin con funciones adicionales, así:
main.py
archivo:
import mymixin.py
class Main(object, MyMixin):
def func1(self, xxx):
...
mymixin.py
archivo:
class MyMixin(object):
def func2(self: Main, xxx): # <--- note the type hint
...
Ahora bien, si bien esto funciona bien, la sugerencia de tipo, MyMixin.func2
por supuesto, no puede funcionar. No puedo importar main.py
porque obtendría una importación cíclica y sin la pista, mi editor (PyCharm) no puede decir qué self
es.
Estoy usando Python 3.4, pero estoy dispuesto a pasar a 3.5 si hay una solución disponible allí.
¿Hay alguna manera de dividir mi clase en dos archivos y conservar todas las "conexiones" para que mi IDE todavía me ofrezca la función de autocompletar y todas las demás ventajas que se derivan de conocer los tipos?
Me temo que no existe una forma muy elegante de manejar los ciclos de importación en general. Sus opciones son rediseñar su código para eliminar la dependencia cíclica o, si no es factible, hacer algo como esto:
# some_file.py
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from main import Main
class MyObject(object):
def func2(self, some_param: 'Main'):
...
La TYPE_CHECKING
constante siempre está False
en tiempo de ejecución, por lo que la importación no se evaluará, pero mypy (y otras herramientas de verificación de tipos) evaluarán el contenido de ese bloque.
También necesitamos convertir la Main
anotación de tipo en una cadena, declarándola efectivamente hacia adelante ya que el Main
símbolo no está disponible en tiempo de ejecución.
Si está utilizando Python 3.7+, al menos podemos evitar tener que proporcionar una anotación de cadena explícita aprovechando PEP 563 :
# some_file.py
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from main import Main
class MyObject(object):
# Hooray, cleaner annotations!
def func2(self, some_param: Main):
...
La from __future__ import annotations
importación hará que todas las sugerencias de tipo sean cadenas y omitirá evaluarlas. Esto puede ayudar a que nuestro código aquí sea un poco más ergonómico.
Dicho todo esto, el uso de mixins con mypy probablemente requerirá un poco más de estructura de la que tiene actualmente. Mypy recomienda un enfoque que es básicamente lo que deceze
describe: crear un ABC que hereden tanto usted Main
como sus MyMixin
clases. No me sorprendería que terminaras necesitando hacer algo similar para hacer feliz al corrector de Pycharm.
Para las personas que luchan con las importaciones cíclicas cuando importan clases solo para verificación de tipo: probablemente querrán usar una referencia directa (PEP 484 - Sugerencias de tipo):
Cuando una sugerencia de tipo contiene nombres que aún no se han definido, esa definición puede expresarse como una cadena literal, que se resolverá más adelante.
Entonces en lugar de:
class Tree:
def __init__(self, left: Tree, right: Tree):
self.left = left
self.right = right
tú haces:
class Tree:
def __init__(self, left: 'Tree', right: 'Tree'):
self.left = left
self.right = right
El mayor problema es que, para empezar, tus tipos no están cuerdos. MyMixin
hace una suposición codificada de que se mezclará con Main
, mientras que podría mezclarse con cualquier cantidad de otras clases, en cuyo caso probablemente se rompería. Si su mixin está codificado para ser mezclado en una clase específica, también puede escribir los métodos directamente en esa clase en lugar de separarlos.
Para hacer esto correctamente con una escritura sensata, MyMixin
se debe codificar en una interfaz o clase abstracta en el lenguaje de Python:
import abc
class MixinDependencyInterface(abc.ABC):
@abc.abstractmethod
def foo(self):
pass
class MyMixin:
def func2(self: MixinDependencyInterface, xxx):
self.foo() # ← mixin only depends on the interface
class Main(MixinDependencyInterface, MyMixin):
def foo(self):
print('bar')
Desde Python 3.5, dividir tus clases en archivos separados es fácil.
De hecho, es posible utilizar import
declaraciones dentro de un class ClassName:
bloque para importar métodos a una clase. Por ejemplo,
class_def.py
:
class C:
from _methods1 import a
from _methods2 import b
def x(self):
return self.a() + " " + self.b()
En mi ejemplo,
C.a()
será un método que devuelve la cadenahello
C.b()
será un método que devuelvehello goodbye
C.x()
así regresaráhello hello goodbye
.
Para implementar a
y b
, haga lo siguiente:
_methods1.py
:
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from class_def import C
def a(self: C):
return "hello"
Explicación : TYPE_CHECKING
es True
cuando el verificador de tipos está leyendo el código. Dado que el verificador de tipos no necesita ejecutar el código, las importaciones circulares están bien cuando ocurren dentro del if TYPE_CHECKING:
bloque. La __future__
importación permite anotaciones pospuestas . Esto es opcional; sin él, debe citar las anotaciones de tipo (es decir def a(self: "C"):
).
Definimos _methods2.py
de manera similar:
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from class_def import C
def b(self: C):
return self.a() + " goodbye"
En VS Code, puedo ver el tipo detectado al self.a()
pasar el cursor:
Y todo funciona como se esperaba:
>>> from class_def import C
>>> c = C()
>>> c.x()
'hello hello goodbye'
Notas sobre versiones anteriores de Python
Para versiones de Python ≤3.4, TYPE_CHECKING
no está definido, por lo que esta solución no funcionará.
Para las versiones de Python ≤3.6, las anotaciones pospuestas no están definidas. Como solución alternativa, omita from __future__ import annotations
y cite las declaraciones de tipo como se mencionó anteriormente.