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.