Sugerencias de tipo Python sin importaciones cíclicas

Resuelto velis asked hace 8 años • 7 respuestas

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

main.pyarchivo:

import mymixin.py

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

mymixin.pyarchivo:

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

Ahora bien, si bien esto funciona bien, la sugerencia de tipo, MyMixin.func2por supuesto, no puede funcionar. No puedo importar main.pyporque obtendría una importación cíclica y sin la pista, mi editor (PyCharm) no puede decir qué selfes.

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?

velis avatar Sep 28 '16 14:09 velis
Aceptado

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, declarándola 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 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 annotationsimportació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 decezedescribe: crear un ABC que hereden tanto usted Maincomo sus MyMixinclases. No me sorprendería que terminaras necesitando hacer algo similar para hacer feliz al corrector de Pycharm.

Michael0x2a avatar Sep 28 '2016 20:09 Michael0x2a

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
Tomasz Bartkowiak avatar Jul 12 '2019 11:07 Tomasz Bartkowiak

El mayor problema es que, para empezar, tus tipos no están cuerdos. MyMixinhace 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, MyMixinse 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')
deceze avatar Sep 28 '2016 07:09 deceze

Desde Python 3.5, dividir tus clases en archivos separados es fácil.

De hecho, es posible utilizar importdeclaraciones 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 ay 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_CHECKINGes Truecuando 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.pyde 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: ingrese la descripción de la imagen aquí

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_CHECKINGno 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 annotationsy cite las declaraciones de tipo como se mencionó anteriormente.

Ben Mares avatar Oct 03 '2021 17:10 Ben Mares