Sugerencias de tipo Python sin importaciones cíclicas

110

Estoy tratando de dividir mi gran clase en dos; bueno, básicamente en la clase "principal" y un mixin con funciones adicionales, así:

main.py expediente:

import mymixin.py

class Main(object, MyMixin):
    def func1(self, xxx):
        ...

mymixin.py expediente:

class MyMixin(object):
    def func2(self: Main, xxx):  # <--- note the type hint
        ...

Ahora, aunque esto funciona bien, la sugerencia de tipo, por MyMixin.func2supuesto, 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é selfes.

Estoy usando Python 3.4, dispuesto a pasar a 3.5 si hay una solución disponible allí.

¿Hay alguna forma de que pueda dividir mi clase en dos archivos y mantener todas las "conexiones" para que mi IDE todavía me ofrezca la finalización automática y todas las demás ventajas que se obtienen conociendo los tipos?

velis
fuente
2
No creo que normalmente deba anotar el tipo de self, ya que siempre será una subclase de la clase actual (y cualquier sistema de verificación de tipos debería poder resolver eso por sí solo). ¿Está func2intentando llamar func1, que no está definido en MyMixin? ¿Quizás debería ser (como abstractmethod, quizás)?
Blckknght
también tenga en cuenta que las clases generalmente más específicas (por ejemplo, su mezcla) deben ir a la izquierda de las clases base en la definición de la clase, es decir, class Main(MyMixin, SomeBaseClass)para que los métodos de la clase más específica puedan anular los de la clase base
Anentropic
3
No estoy seguro de la utilidad de estos comentarios, ya que son tangenciales a la pregunta que se hace. Velis no estaba solicitando una revisión del código.
Jacob Lee
Las sugerencias de tipo Python con métodos de clase importados proporcionan una solución elegante a su problema.
Ben Mares

Respuestas:

168

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_CHECKINGconstante siempre está Falseen 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 Mainanotación de tipo en una cadena, declarando efectivamente hacia adelante ya que el Mainsímbolo no está disponible en tiempo de ejecución.

Si está utilizando Python 3.7+, al menos podemos omitir 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 annotationsimportación hará que todos los tipos de sugerencias sean cadenas y omitirá su evaluación. Esto puede ayudar a que nuestro código aquí sea ligeramente 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 decezeestá describiendo - para crear un ABC que ambos sus Mainy MyMixinlas clases heredan. No me sorprendería que terminaras necesitando hacer algo similar para hacer feliz al corrector de Pycharm.

Michael0x2a
fuente
4
Gracias por esto. Mi Python 3.4 actual no lo tiene typing, pero PyCharm también estaba bastante contento if False:.
Velis
El único problema es que no reconoce MyObject como un modelo de Django y, por lo tanto, se queja de que los atributos de instancia se definen fuera de__init__
velis
Aquí está el impulso correspondiente para typing. TYPE_CHECKING : python.org/dev/peps/pep-0484/#runtime-or-type-checking
Conchylicultor
25

Para las personas que luchan con las importaciones cíclicas al importar la clase solo para la verificación de tipo: es probable que desee utilizar 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ú lo haces:

class Tree:
    def __init__(self, left: 'Tree', right: 'Tree'):
        self.left = left
        self.right = right
Tomasz Bartkowiak
fuente
Podría ser PyCharm. ¿Estás usando la versión más reciente? ¿Usted ha intentado File -> Invalidate Caches?
Tomasz Bartkowiak
Gracias. Lo siento, borré mi comentario. Había mencionado que esto funciona, pero PyCharm se queja. Resolví usando el truco if False sugerido por Velis . La invalidación de la caché no lo resolvió. Probablemente sea un problema de PyCharm.
Jacob Lee
1
@JacobLee En lugar de if False:tú también puedes from typing import TYPE_CHECKINGy if TYPE_CHECKING:.
luckydonald
11

El mayor problema es que, para empezar, tus tipos no están cuerdos. MyMixinhace una suposición codificada de que se mezclará Main, mientras que podría mezclarse con cualquier número de otras clases, en cuyo caso probablemente se rompería. Si su mixin está codificado para mezclarse 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 sana, MyMixindebe codificarse contra 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')
deceze
fuente
1
Bueno, no estoy diciendo que mi solución sea excelente. Es lo que intento hacer para que el código sea más manejable. Su sugerencia podría pasar, pero esto en realidad significaría simplemente mover toda la clase Main a la interfaz en mi caso específico .
Velis
3

Resulta que mi intento original también estuvo bastante cerca de la solución. Esto es lo que estoy usando actualmente:

# main.py
import mymixin.py

class Main(object, MyMixin):
    def func1(self, xxx):
        ...


# mymixin.py
if False:
    from main import Main

class MyMixin(object):
    def func2(self: 'Main', xxx):  # <--- note the type hint
        ...

Tenga en cuenta la if Falsedeclaración de importación interna que nunca se importa (pero IDE lo sabe de todos modos) y el uso de la Mainclase como cadena porque no se conoce en tiempo de ejecución.

velis
fuente
Espero que esto provoque una advertencia sobre el código inactivo.
Phil
@Phil: sí, en ese momento estaba usando Python 3.4. Ahora hay typing.TYPE_CHECKING
Velis
-4

Creo que la forma perfecta debería ser importar todas las clases y dependencias en un archivo (como __init__.py) y luego from __init__ import *en todos los demás archivos.

En este caso eres

  1. evitar múltiples referencias a esos archivos y clases y
  2. Además, solo tienes que agregar una línea en cada uno de los otros archivos y
  3. el tercero sería que el pycharm conociera todas las clases que podría usar.
AmirHossein
fuente
1
significa que está cargando todo en todas partes, si tiene una biblioteca bastante pesada, significa que para cada importación necesita cargar toda la biblioteca. + la referencia funcionará muy lento.
Omer Shacham
> significa que está cargando todo en todas partes. >>>> absolutamente no si tiene muchos de " init .py" u otros archivos, y evite import *, y aún así puede aprovechar este enfoque fácil
Sławomir Lenart