Sugestão de tipo Python sem importações cíclicas


108

Estou tentando dividir minha enorme classe em duas; bem, basicamente na classe "principal" e um mixin com funções adicionais, como:

main.py Arquivo:

import mymixin.py

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

mymixin.py Arquivo:

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

Agora, embora isso funcione bem, a dica de tipo MyMixin.func2obviamente não pode funcionar. Eu não posso importar main.py, porque eu obteria uma importação cíclica e sem a dica, meu editor (PyCharm) não poderia dizer o que selfé.

Estou usando Python 3.4, disposto a mudar para 3.5 se houver uma solução disponível lá.

Existe alguma maneira de dividir minha classe em dois arquivos e manter todas as "conexões" para que meu IDE ainda me ofereça preenchimento automático e todas as outras vantagens que vêm dele, sabendo os tipos?


2
Eu não acho que você deveria normalmente precisar anotar o tipo de self, já que sempre será uma subclasse da classe atual (e qualquer sistema de verificação de tipo deve ser capaz de descobrir isso por conta própria). Está func2tentando ligar func1, o que não está definido em MyMixin? Talvez devesse ser (como abstractmethod, talvez)?
Blckknght

Observe também que geralmente as classes mais específicas (por exemplo, seu mixin) devem ir para a esquerda das classes base na definição da classe, ou seja, class Main(MyMixin, SomeBaseClass)para que os métodos da classe mais específica possam sobrescrever os da classe base
Anentrópico

3
Não tenho certeza de como esses comentários são úteis, uma vez que são tangenciais à pergunta que está sendo feita. velis não estava pedindo uma revisão de código.
Jacob Lee

Dicas de tipo Python com métodos de classe importados fornecem uma solução elegante para seu problema.
Ben Mares

Respostas:


166

Receio que não haja uma maneira extremamente elegante de lidar com os ciclos de importação em geral. Suas opções são reprojetar seu código para remover a dependência cíclica ou, se não for viável, fazer algo assim:

# some_file.py

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from main import Main

class MyObject(object):
    def func2(self, some_param: 'Main'):
        ...

A TYPE_CHECKINGconstante está sempre Falseem tempo de execução, portanto, a importação não será avaliada, mas mypy (e outras ferramentas de verificação de tipo) avaliará o conteúdo desse bloco.

Também precisamos fazer a Mainanotação de tipo em uma string, efetivamente declarando-o, uma vez que o Mainsímbolo não está disponível em tempo de execução.

Se você estiver usando o Python 3.7+, podemos pelo menos pular a necessidade de fornecer uma anotação de string explícita, aproveitando o 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):
        ...

A from __future__ import annotationsimportação fará com que todas as dicas de tipo sejam strings e pulará sua avaliação. Isso pode ajudar a tornar nosso código um pouco mais ergonômico.

Dito isso, usar mixins com mypy provavelmente exigirá um pouco mais de estrutura do que você tem atualmente. Mypy recomenda uma abordagem que é basicamente o que decezeestá descrevendo - criar um ABC que tanto sua Mainquanto as MyMixinclasses herdam. Eu não ficaria surpreso se você acabasse precisando fazer algo semelhante para deixar o verificador de Pycharm feliz.


3
Obrigado por isso. Meu python 3.4 atual não tem typing, mas o PyCharm também ficou muito feliz com ele if False:.
velis

O único problema é que ele não reconhece MyObject como um Django models.Model e, portanto, reclama sobre os atributos de instância sendo definidos fora de__init__
velis

Aqui está a pep correspondente para typing. TYPE_CHECKING : python.org/dev/peps/pep-0484/#runtime-or-type-checking
Conchylicultor

24

Para pessoas que lutam com importações cíclicas ao importar classe apenas para verificação de tipo: você provavelmente desejará usar uma referência direta (PEP 484 - dicas de tipo):

Quando uma dica de tipo contém nomes que ainda não foram definidos, essa definição pode ser expressa como uma string literal, para ser resolvida posteriormente.

Então, em vez de:

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

Você faz:

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

Pode ser PyCharm. Você está usando a versão mais recente? Você já tentou File -> Invalidate Caches?
Tomasz Bartkowiak

Obrigado. Desculpe, eu havia excluído meu comentário. Ele mencionou que isso funciona, mas PyCharm reclama. Resolvi usar o hack if False sugerido por Velis . A invalidação do cache não resolveu. Provavelmente é um problema do PyCharm.
Jacob Lee

1
@JacobLee Em vez de if False:você também pode from typing import TYPE_CHECKINGe if TYPE_CHECKING:.
luckydonald

11

O maior problema é que, para começar, seus tipos não são sãos. MyMixinfaz uma suposição codificada de que ele será misturado em Main, ao passo que poderia ser misturado a qualquer número de outras classes, caso em que provavelmente seria quebrado. Se seu mixin for codificado para ser mixado em uma classe específica, você também pode escrever os métodos diretamente naquela classe em vez de separá-los.

Para fazer isso corretamente com uma digitação sã, MyMixindeve ser codificado em uma interface ou classe abstrata na linguagem 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')

1
Bem, não estou dizendo que minha solução seja ótima. É apenas o que estou tentando fazer para tornar o código mais gerenciável. Sua sugestão pode passar, mas isso significaria apenas mover toda a classe Main para a interface no meu caso específico .
velis

3

Acontece que minha tentativa original também estava bem próxima da solução. Isso é o que estou usando atualmente:

# 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
        ...

Observe a if Falseinstrução import within que nunca é importada (mas o IDE sabe disso de qualquer maneira) e usando a Mainclasse como string porque não é conhecida em tempo de execução.


Eu esperava que isso causasse um aviso sobre código morto.
Phil

@Phil: sim, na época eu estava usando o Python 3.4. Agora há digitação.TYPE_CHECKING
velis

-4

Acho que a maneira perfeita deveria ser importar todas as classes e dependências em um arquivo (como __init__.py) e depois from __init__ import *em todos os outros arquivos.

Neste caso você é

  1. evitando referências múltiplas a esses arquivos e classes e
  2. também só precisa adicionar uma linha em cada um dos outros arquivos e
  3. a terceira seria o pycharm sabendo sobre todas as classes que você pode usar.

1
isso significa que você está carregando tudo em todos os lugares, se você tiver uma biblioteca bem pesada, isso significa que para cada importação você precisa carregar a biblioteca inteira. + a referência funcionará super lento.
Omer Shacham

> significa que você está carregando tudo em todos os lugares. >>>> absolutamente não se você tiver muitos arquivos " init .py" ou outros, e evitar import *, e ainda assim você pode tirar vantagem desta abordagem fácil
Sławomir Lenart
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.