Importação circular do Python?

98

Então, estou recebendo este erro

Traceback (most recent call last):
  File "/Users/alex/dev/runswift/utils/sim2014/simulator.py", line 3, in <module>
    from world import World
  File "/Users/alex/dev/runswift/utils/sim2014/world.py", line 2, in <module>
    from entities.field import Field
  File "/Users/alex/dev/runswift/utils/sim2014/entities/field.py", line 2, in <module>
    from entities.goal import Goal
  File "/Users/alex/dev/runswift/utils/sim2014/entities/goal.py", line 2, in <module>
    from entities.post import Post
  File "/Users/alex/dev/runswift/utils/sim2014/entities/post.py", line 4, in <module>
    from physics import PostBody
  File "/Users/alex/dev/runswift/utils/sim2014/physics.py", line 21, in <module>
    from entities.post import Post
ImportError: cannot import name Post

e você pode ver que eu uso a mesma instrução de importação mais acima e funciona? Existe alguma regra não escrita sobre a importação circular? Como faço para usar a mesma classe mais abaixo na pilha de chamadas?

CpILL
fonte

Respostas:

161

Acho que a resposta do jpmc26, embora de forma alguma errada , depende muito das importações circulares. Eles podem funcionar muito bem, se você configurá-los corretamente.

A maneira mais fácil de fazer isso é usar import my_modulesintaxe, em vez de from my_module import some_object. O primeiro quase sempre funcionará, mesmo se my_moduleincluído nos importa de volta. Este último só funciona se my_objectjá estiver definido em my_module, o que em uma importação circular pode não ser o caso.

Para ser específico para o seu caso: tente mudar entities/post.pypara fazer import physicse, em seguida, consulte em physics.PostBodyvez de apenas PostBodydiretamente. Da mesma forma, mude physics.pypara fazer import entities.poste use em entities.post.Postvez de apenas Post.

Blckknght
fonte
5
Esta resposta é compatível com as importações relativas?
Joe
17
Por que isso acontece?
Juan Pablo Santos
4
É errado dizer que a não fromsintaxe sempre funcionará. Se eu tiver class A(object): pass; class C(b.B): passno módulo a e class B(a.A): passno módulo b, a importação circular ainda é um problema e não funcionará.
CrazyCasta
1
Você está certo, quaisquer dependências circulares no código de nível superior dos módulos (como as classes básicas de declarações de classe em seu exemplo) serão um problema. Esse é o tipo de situação em que a resposta do jpmc de que você deve refatorar a organização do módulo está provavelmente 100% correta. Mova a classe Bpara o módulo aou mova a classe Cpara o módulo bpara quebrar o ciclo. Também é importante notar que mesmo se apenas uma direção do círculo tiver código de nível superior envolvido (por exemplo, se a classe Cnão existir), você pode obter um erro, dependendo de qual módulo foi importado primeiro por outro código.
Blckknght
2
@TylerCrompton: Não tenho certeza do que você quer dizer com "a importação do módulo deve ser absoluta". As importações relativas circulares podem funcionar, desde que você esteja importando módulos, não seu conteúdo (por exemplo from . import sibling_module, não from .sibling_module import SomeClass). Há um pouco mais de sutileza quando o __init__.pyarquivo de um pacote está envolvido na importação circular, mas o problema é raro e provavelmente um bug na importimplementação. Veja o bug 23447 do Python , para o qual enviei um patch (que infelizmente está enfraquecendo).
Blckknght
51

Quando você importa um módulo (ou um membro dele) pela primeira vez, o código dentro do módulo é executado sequencialmente como qualquer outro código; por exemplo, não é tratado de forma diferente que o corpo de uma função. Um importé apenas um comando como qualquer outro (atribuição, uma chamada de função, def, class). Supondo que suas importações ocorram no início do script, eis o que está acontecendo:

  • Quando você tenta importar Worldde world, o worldscript é executado.
  • O worldscript é importado Field, o que faz com que o entities.fieldscript seja executado.
  • Este processo continua até chegar ao entities.postscript porque tentou importarPost
  • O entities.postscript faz com que o physicsmódulo seja executado porque ele tenta importarPostBody
  • Finalmente, physicstenta importar Postdeentities.post
  • Não tenho certeza se o entities.postmódulo existe na memória ainda, mas realmente não importa. Ou o módulo não está na memória, ou o módulo ainda não tem um Postmembro porque não terminou de executar para definirPost
  • De qualquer forma, ocorre um erro porque Postnão existe para ser importado

Portanto, não, não está "avançando na pilha de chamadas". Este é um rastreamento de pilha de onde ocorreu o erro, o que significa que houve um erro ao tentar importar Postnessa classe. Você não deve usar importações circulares. Na melhor das hipóteses, ele tem um benefício insignificante (normalmente, nenhum benefício) e causa problemas como esse. Ele onera qualquer desenvolvedor que o mantenha, forçando-o a pisar na casca do ovo para evitar quebrá-lo. Refatore sua organização de módulo.

jpmc26
fonte
1
Deve ser isinstance(userData, Post). Independentemente disso, você não tem escolha. A importação circular não funcionará. O fato de você ter importações circulares é um cheiro de código para mim. Isso sugere que você tem alguma funcionalidade que deve ser transferida para um terceiro módulo. Eu não poderia dizer o quê sem olhar para as duas classes inteiras.
jpmc26
3
@CpILL Depois de um tempo, uma opção muito hacky me ocorreu. Se você não puder fazer isso por enquanto (devido a restrições de tempo ou o que for), poderá fazer sua importação localmente dentro do método em que está usando. Um corpo de função interno defnão é executado até que a função seja chamada, portanto, a importação não ocorreria até que você realmente chamasse a função. Nesse momento, os imports devem funcionar, pois um dos módulos teria sido completamente importado antes da chamada. Esse é um hack absolutamente nojento e não deve permanecer em sua base de código por um período de tempo significativo.
jpmc26
15
Acho que sua resposta é muito difícil para as importações circulares. As importações circulares geralmente funcionam se você fizer apenas, em import foovez de from foo import Bar. Isso porque a maioria dos módulos apenas define coisas (como funções e classes) que são executadas posteriormente. Módulos que fazem coisas significativas quando você os importa (como um script não protegido por if __name__ == "__main__") ainda podem ser problemas, mas isso não é muito comum.
Blckknght
6
@Blckknght Eu acho que você está se preparando para gastar tempo em problemas estranhos que outras pessoas terão que investigar e ficar confusos se você usar importações circulares. Eles o forçam a gastar tempo tomando cuidado para não tropeçar neles e, além disso, há um cheiro de código de que seu projeto precisa de refatoração. Posso estar errado sobre se eles são tecnicamente viáveis, mas são uma péssima escolha de design destinada a causar problemas mais cedo ou mais tarde. Clareza e simplicidade são o Santo Graal na programação, e as importações circulares violam ambas em meu livro.
jpmc26
6
Alternativamente; você dividiu demais sua funcionalidade e essa é a causa das importações circulares. Se você tem duas coisas que dependem uma da outra o tempo todo ; pode ser melhor apenas colocá-los em um arquivo. Python não é Java; nenhuma razão para não agrupar funcionalidades / classes em um único arquivo para evitar uma lógica de importação estranha. :-)
Mark Ribau
40

Para entender as dependências circulares, você precisa se lembrar de que Python é essencialmente uma linguagem de script. A execução de instruções fora dos métodos ocorre em tempo de compilação. As instruções de importação são executadas como chamadas de método e, para compreendê-las, você deve pensar nelas como chamadas de método.

Quando você faz uma importação, o que acontece depende se o arquivo que você está importando já existe na tabela do módulo. Em caso afirmativo, Python usa tudo o que está atualmente na tabela de símbolos. Caso contrário, o Python começa a ler o arquivo do módulo, compilando / executando / importando tudo o que encontrar lá. Os símbolos referenciados em tempo de compilação são encontrados ou não, dependendo se eles foram vistos ou ainda não foram vistos pelo compilador.

Imagine que você tenha dois arquivos de origem:

Arquivo X.py

def X1:
    return "x1"

from Y import Y2

def X2:
    return "x2"

Arquivo Y.py

def Y1:
    return "y1"

from X import X1

def Y2:
    return "y2"

Agora, suponha que você compile o arquivo X.py. O compilador começa definindo o método X1 e, em seguida, acessa a instrução import em X.py. Isso faz com que o compilador pause a compilação de X.py e comece a compilar Y.py. Pouco depois, o compilador acessa a instrução import em Y.py. Como o X.py já está na tabela do módulo, o Python usa a tabela de símbolos X.py incompleta existente para satisfazer todas as referências solicitadas. Todos os símbolos que aparecem antes da instrução de importação em X.py agora estão na tabela de símbolos, mas os símbolos posteriores não estão. Como X1 agora aparece antes da instrução de importação, ele foi importado com sucesso. O Python então retoma a compilação de Y.py. Ao fazer isso, ele define Y2 e termina de compilar Y.py. Em seguida, ele retoma a compilação de X.py e encontra Y2 na tabela de símbolos Y.py. A compilação eventualmente termina sem erro.

Algo muito diferente acontece se você tentar compilar Y.py a partir da linha de comando. Ao compilar Y.py, o compilador acessa a instrução import antes de definir Y2. Em seguida, ele começa a compilar o X.py. Logo ele atinge a instrução import em X.py que requer Y2. Mas Y2 é indefinido, então a compilação falha.

Observe que se você modificar o X.py para importar Y1, a compilação sempre será bem-sucedida, independentemente do arquivo que você compilar. No entanto, se você modificar o arquivo Y.py para importar o símbolo X2, nenhum arquivo será compilado.

Sempre que o módulo X ou qualquer módulo importado pelo X puder importar o módulo atual, NÃO use:

from X import Y

Sempre que você achar que pode haver uma importação circular, você também deve evitar referências em tempo de compilação a variáveis ​​em outros módulos. Considere o código de aparência inocente:

import X
z = X.Y

Suponha que o módulo X importe este módulo antes que este módulo importe X. Além disso, suponha que Y seja definido em X após a instrução de importação. Então, Y não será definido quando este módulo for importado e você obterá um erro de compilação. Se este módulo importar Y primeiro, você pode se safar. Mas quando um de seus colegas de trabalho muda inocentemente a ordem das definições em um terceiro módulo, o código falha.

Em alguns casos, você pode resolver dependências circulares movendo uma instrução de importação para baixo, abaixo das definições de símbolo necessárias para outros módulos. Nos exemplos acima, as definições antes da instrução import nunca falham. As definições após a instrução de importação às vezes falham, dependendo da ordem de compilação. Você pode até colocar instruções de importação no final de um arquivo, desde que nenhum dos símbolos importados seja necessário no momento da compilação.

Observe que mover as instruções de importação para baixo em um módulo obscurece o que você está fazendo. Compense isso com um comentário no topo do seu módulo, algo como o seguinte:

#import X   (actual import moved down to avoid circular dependency)

Em geral, essa é uma prática ruim, mas às vezes é difícil de evitar.

Gene Olson
fonte
2
Eu não acho que haja compilador ou tempo de compilação em python
pkqxdd
6
Python faz ter um compilador, e é compilado @pkqxdd, compilação é apenas normalmente escondido do usuário. Isso pode ser um pouco confuso, mas seria difícil para o autor dar essa descrição admiravelmente clara do que está acontecendo sem alguma referência ao, um tanto obscurecido, "tempo de compilação" do Python.
Hank,
Fui tentar fazer isso na minha máquina e obtive um resultado diferente. Executou X.py mas obteve o erro "não é possível importar o nome 'Y2' de 'Y'". No entanto, executei Y.py sem nenhum problema. Estou no Python 3.7.5. Você poderia ajudar a explicar qual é o problema aqui?
xuefeng huang
18

Para aqueles de vocês que, como eu, chegaram a este problema do Django, devem saber que os documentos fornecem uma solução: https://docs.djangoproject.com/en/1.10/ref/models/fields/#foreignkey

"... Para referir-se a modelos definidos em outro aplicativo, você pode especificar explicitamente um modelo com o rótulo completo do aplicativo. Por exemplo, se o modelo do fabricante acima for definido em outro aplicativo chamado produção, você precisará usar:

class Car(models.Model):
    manufacturer = models.ForeignKey(
        'production.Manufacturer',
        on_delete=models.CASCADE,
)

Esse tipo de referência pode ser útil ao resolver dependências de importação circular entre dois aplicativos. ... "

Malik A. Rumi
fonte
6
Eu sei que não devo usar comentários para dizer "obrigado", mas isso tem me atormentado por algumas horas. Obrigado, obrigado, obrigado!!!
MikeyE
Eu concordo com @MikeyE. Eu li vários blogs e Stackoverflows tentando remediar isso com PonyORM. Onde outros dizem que é uma prática ruim, ou por que você codificaria suas classes para serem circulares, bem, ORMs são exatamente onde isso acontece. Como muitos exemplos colocam todos os modelos no mesmo arquivo, e seguimos esses exemplos, exceto que usamos um modelo por arquivo, o problema não fica claro quando o Python falha na compilação. No entanto, a resposta é tão simples. Como Mike disse, muito obrigado.
trash80
4

Consegui importar o módulo dentro da função (apenas) que exigiria os objetos deste módulo:

def my_func():
    import Foo
    foo_instance = Foo()
Alexander Shubert
fonte
quão elegante de python
Yaro
2

Se você encontrar esse problema em um aplicativo bastante complexo, pode ser complicado refatorar todas as suas importações. O PyCharm oferece uma correção rápida para isso, que também altera automaticamente todo o uso dos símbolos importados.

insira a descrição da imagem aqui

Andreas Bergström
fonte
0

Eu estava usando o seguinte:

from module import Foo

foo_instance = Foo()

mas para me livrar circular referencefiz o seguinte e funcionou:

import module.foo

foo_instance = foo.Foo()
MKJ
fonte