Importações relativas em Python 3

714

Eu quero importar uma função de outro arquivo no mesmo diretório.

Às vezes funciona para mim, from .mymodule import myfunctionmas às vezes eu recebo um:

SystemError: Parent module '' not loaded, cannot perform relative import

Às vezes funciona from mymodule import myfunction, mas às vezes também recebo:

SystemError: Parent module '' not loaded, cannot perform relative import

Não entendo a lógica aqui e não consegui encontrar nenhuma explicação. Isso parece completamente aleatório.

Alguém poderia me explicar qual é a lógica por trás de tudo isso?

John Smith Opcional
fonte
76
Isso significa que você está executando um módulo dentro do pacote como um script. Execute apenas scripts de fora do pacote.
Martijn Pieters
3
Provavelmente, você deve definir as condições que possui, às vezes que menciona. Entendo que você não quer dizer que tenha erros aleatórios.
Joaquin
15
@MartijnPieters: bem, infelizmente, este módulo precisa estar dentro do pacote e também precisa ser executável como um script, às vezes. Alguma idéia de como eu poderia conseguir isso?
John Smith Opcional
22
@JohnSmithOptional: A mistura de scripts dentro de pacotes é complicada e deve ser evitada, se possível. Use um script de wrapper que importe o pacote e execute a função 'scripty'.
Martijn Pieters
3
Parece lamentável. Criei um módulo principal com classes / métodos que podem analisar / analisar um determinado tipo de arquivo e também tenho (principalmente para mim) módulos e scripts secundários separados que o importam - eles podem massagear / converter esses arquivos. Mas também gosto de poder entregar esse arquivo de núcleo único (não um pacote completo) ao usuário final, para que ele possa facilmente colocá-lo ao lado do arquivo e executá-lo. Nesse "modo de script", ele analisa e analisa o arquivo e a codificação, registra vários campos / valores / caracteres especiais e fornece um relatório. Mas na verdade não modifica o arquivo. Anti-padrão?
Jon Coombs

Respostas:

528

infelizmente, este módulo precisa estar dentro do pacote e, às vezes, também pode ser executado como um script. Alguma idéia de como eu poderia conseguir isso?

É bastante comum ter um layout como este ...

main.py
mypackage/
    __init__.py
    mymodule.py
    myothermodule.py

... com um mymodule.pyassim ...

#!/usr/bin/env python3

# Exported function
def as_int(a):
    return int(a)

# Test function for module  
def _test():
    assert as_int('1') == 1

if __name__ == '__main__':
    _test()

... um myothermodule.pyassim ...

#!/usr/bin/env python3

from .mymodule import as_int

# Exported function
def add(a, b):
    return as_int(a) + as_int(b)

# Test function for module  
def _test():
    assert add('1', '1') == 2

if __name__ == '__main__':
    _test()

... e algo main.pyassim ...

#!/usr/bin/env python3

from mypackage.myothermodule import add

def main():
    print(add('1', '1'))

if __name__ == '__main__':
    main()

... que funciona bem quando você executa main.pyou mypackage/mymodule.py, mas falha com mypackage/myothermodule.py, devido à importação relativa ...

from .mymodule import as_int

O jeito que você deve executá-lo é ...

python3 -m mypackage.myothermodule

... mas é um pouco detalhado e não combina bem com uma linha shebang como #!/usr/bin/env python3.

A correção mais simples para esse caso, assumindo que o nome mymoduleseja globalmente exclusivo, seria evitar o uso de importações relativas e usar apenas ...

from mymodule import as_int

... embora, se não for exclusivo, ou a sua estrutura de pacotes for mais complexa, você precisará incluir o diretório que contém o diretório de pacotes PYTHONPATHe fazê-lo desta maneira ...

from mypackage.mymodule import as_int

... ou se você quiser que ele funcione "fora da caixa", você pode copiar o PYTHONPATHcódigo primeiro com isso ...

import sys
import os

PACKAGE_PARENT = '..'
SCRIPT_DIR = os.path.dirname(os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser(__file__))))
sys.path.append(os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT)))

from mypackage.mymodule import as_int

É meio doloroso, mas há uma pista do porquê em um e-mail escrito por um certo Guido van Rossum ...

Eu sou -1 sobre isso e sobre quaisquer outras modificações propostas do __main__ maquinário. O único caso de uso parece estar executando scripts que vivem dentro do diretório de um módulo, que eu sempre vi como um antipadrão. Para me fazer mudar de idéia, você teria que me convencer de que não é.

Se a execução de scripts dentro de um pacote é um antipadrão ou não é subjetiva, mas, pessoalmente, acho que é realmente útil em um pacote que possua alguns widgets wxPython personalizados, para que eu possa executar o script para qualquer um dos arquivos de origem exibir wx.Frameapenas um contendo esse widget para fins de teste.

Aya
fonte
7
Uma maneira melhor de obter o SCRIPTDIR é fornecida em um comentário de Importar um módulo de um caminho relativo, como os.path.realpath(os.path.dirname(inspect.getfile(inspect.currentframe())))se você estivesse confiante de que seu módulo sempre tenha um bom funcionamento, filevocê também pode usar os.path.realpath(os.path.dirname(__file__)).
marcz
2
Você pode expandir seu PYTHONPATH aplicando um snippet de código mais curto e legível: sys.path.append( os.path.join( os.path.dirname(__file__), os.path.pardir ) )
Alex-Bogdanov
12
...which I've always seen as an antipattern.Não vejo como é um anti-padrão ... Parece que seria super conveniente simplesmente fazer com que as importações relativas funcionassem intuitivamente. Eu só quero poder importar coisas que eu sei que estão no mesmo diretório. Gostaria de saber qual era o seu raciocínio
YungGun 12/09/19
9
Guido ataca de novo: misturando coisas que poderiam ter sido úteis. Bem, isso não vai mais acontecer.
Javadba 29/10/19
4
Esta é a coisa mais triste que eu já vi sobre Python.
AtilioA 16/01
263

Explicação

Do PEP 328

As importações relativas usam o atributo __name__ de um módulo para determinar a posição desse módulo na hierarquia de pacotes. Se o nome do módulo não contiver nenhuma informação do pacote (por exemplo, está definido como '__main__') , as importações relativas serão resolvidas como se o módulo fosse um módulo de nível superior , independentemente de onde o módulo esteja realmente localizado no sistema de arquivos.

Em algum momento, o PEP 338 entrou em conflito com o PEP 328 :

... importações relativas dependem de __name__ para determinar a posição do módulo atual na hierarquia de pacotes. Em um módulo principal, o valor de __name__ é sempre '__main__' , portanto as importações relativas explícitas sempre falham (pois funcionam apenas para um módulo dentro de um pacote)

e para resolver o problema, o PEP 366 introduziu a variável de nível superior __package__:

Ao adicionar um novo atributo no nível do módulo, esse PEP permite que importações relativas funcionem automaticamente se o módulo for executado usando a opção -m . Uma pequena quantidade de clichê no próprio módulo permitirá que as importações relativas funcionem quando o arquivo for executado pelo nome. [...] Quando [o atributo] estiver presente, as importações relativas serão baseadas nesse atributo, e não no módulo __name__ . [...] Quando o módulo principal é especificado por seu nome de arquivo, o atributo __package__ será definido como None . [...] Quando o sistema de importação encontra uma importação relativa explícita em um módulo sem __package__ definido (ou com ele definido como None), calcula e armazena o valor correto (__name __. rpartition ('.') [0] para módulos normais e __name__ para módulos de inicialização de pacotes)

(ênfase minha)

Se __name__for '__main__', __name__.rpartition('.')[0]retorna uma string vazia. É por isso que existe uma string vazia literal na descrição do erro:

SystemError: Parent module '' not loaded, cannot perform relative import

A parte relevante da PyImport_ImportModuleLevelObjectfunção do CPython :

if (PyDict_GetItem(interp->modules, package) == NULL) {
    PyErr_Format(PyExc_SystemError,
            "Parent module %R not loaded, cannot perform relative "
            "import", package);
    goto error;
}

O CPython gera essa exceção se não puder encontrar package(o nome do pacote) em interp->modules(acessível como sys.modules). Como sys.modulesé "um dicionário que mapeia nomes de módulos para módulos que já foram carregados" , agora está claro que o módulo pai deve ser explicitamente importado de forma absoluta antes de executar a importação relativa .

Nota: O patch da edição 18018 adicionou outro ifbloco , que será executado antes do código acima:

if (PyUnicode_CompareWithASCIIString(package, "") == 0) {
    PyErr_SetString(PyExc_ImportError,
            "attempted relative import with no known parent package");
    goto error;
} /* else if (PyDict_GetItem(interp->modules, package) == NULL) {
    ...
*/

Se package(o mesmo que acima) for uma string vazia, a mensagem de erro será exibida.

ImportError: attempted relative import with no known parent package

No entanto, você verá isso apenas no Python 3.6 ou mais recente.

Solução 1: execute seu script usando -m

Considere um diretório (que é um pacote Python ):

.
├── package
│   ├── __init__.py
│   ├── module.py
│   └── standalone.py

Todos os arquivos no pacote começam com as mesmas 2 linhas de código:

from pathlib import Path
print('Running' if __name__ == '__main__' else 'Importing', Path(__file__).resolve())

Estou incluindo essas duas linhas apenas para tornar a ordem das operações óbvia. Podemos ignorá-los completamente, pois eles não afetam a execução.

__init__.py e module.py contêm apenas essas duas linhas (ou seja, elas estão efetivamente vazias).

standalone.py tenta adicionalmente importar module.py por meio de importação relativa:

from . import module  # explicit relative import

Estamos bem cientes de que /path/to/python/interpreter package/standalone.pyirá falhar. No entanto, podemos executar o módulo com a -mopção de linha de comando que "procurará sys.patho módulo nomeado e executará seu conteúdo como o __main__módulo" :

vaultah@base:~$ python3 -i -m package.standalone
Importing /home/vaultah/package/__init__.py
Running /home/vaultah/package/standalone.py
Importing /home/vaultah/package/module.py
>>> __file__
'/home/vaultah/package/standalone.py'
>>> __package__
'package'
>>> # The __package__ has been correctly set and module.py has been imported.
... # What's inside sys.modules?
... import sys
>>> sys.modules['__main__']
<module 'package.standalone' from '/home/vaultah/package/standalone.py'>
>>> sys.modules['package.module']
<module 'package.module' from '/home/vaultah/package/module.py'>
>>> sys.modules['package']
<module 'package' from '/home/vaultah/package/__init__.py'>

-mfaz todo o material de importação para você e define automaticamente __package__, mas você pode fazer isso sozinho no

Solução 2: Defina __package__ manualmente

Trate-o como uma prova de conceito e não como uma solução real. Não é adequado para uso em código do mundo real.

O PEP 366 tem uma solução alternativa para esse problema, no entanto, é incompleto, porque a configuração __package__sozinha não é suficiente. Você precisará importar pelo menos N pacotes anteriores na hierarquia do módulo, em que N é o número de diretórios-pai (relativos ao diretório do script) que serão pesquisados ​​pelo módulo que está sendo importado.

Portanto,

  1. Adicione o diretório pai do enésimo predecessor do módulo atual aosys.path

  2. Remova o diretório do arquivo atual de sys.path

  3. Importe o módulo pai do módulo atual usando seu nome completo

  4. Defina __package__com o nome completo de 2

  5. Executar a importação relativa

Vou emprestar arquivos da Solução 1 e adicionar mais alguns subpacotes:

package
├── __init__.py
├── module.py
└── subpackage
    ├── __init__.py
    └── subsubpackage
        ├── __init__.py
        └── standalone.py

Dessa vez, o standalone.py importará o module.py do pacote do pacote usando a seguinte importação relativa

from ... import module  # N = 3

Precisamos preceder essa linha com o código padrão, para fazê-la funcionar.

import sys
from pathlib import Path

if __name__ == '__main__' and __package__ is None:
    file = Path(__file__).resolve()
    parent, top = file.parent, file.parents[3]

    sys.path.append(str(top))
    try:
        sys.path.remove(str(parent))
    except ValueError: # Already removed
        pass

    import package.subpackage.subsubpackage
    __package__ = 'package.subpackage.subsubpackage'

from ... import module # N = 3

Ele nos permite executar standalone.py por nome de arquivo:

vaultah@base:~$ python3 package/subpackage/subsubpackage/standalone.py
Running /home/vaultah/package/subpackage/subsubpackage/standalone.py
Importing /home/vaultah/package/__init__.py
Importing /home/vaultah/package/subpackage/__init__.py
Importing /home/vaultah/package/subpackage/subsubpackage/__init__.py
Importing /home/vaultah/package/module.py

Uma solução mais geral envolvida em uma função pode ser encontrada aqui . Exemplo de uso:

if __name__ == '__main__' and __package__ is None:
    import_parents(level=3) # N = 3

from ... import module
from ...module.submodule import thing

Solução 3: use ferramentas e importações absolutas

Os passos são -

  1. Substituir importações relativas explícitas por importações absolutas equivalentes

  2. Instale packagepara torná-lo importável

Por exemplo, a estrutura de diretórios pode ser a seguinte

.
├── project
│   ├── package
│   │   ├── __init__.py
│   │   ├── module.py
│   │   └── standalone.py
│   └── setup.py

onde setup.py é

from setuptools import setup, find_packages
setup(
    name = 'your_package_name',
    packages = find_packages(),
)

O restante dos arquivos foi emprestado da Solução 1 .

A instalação permitirá que você importe o pacote independentemente do seu diretório de trabalho (assumindo que não haverá problemas de nomeação).

Podemos modificar o standalone.py para usar esta vantagem (etapa 1):

from package import module  # absolute import

Altere seu diretório de trabalho para projecte execute /path/to/python/interpreter setup.py install --user( --userinstala o pacote no diretório de pacotes do site ) (etapa 2):

vaultah@base:~$ cd project
vaultah@base:~/project$ python3 setup.py install --user

Vamos verificar se agora é possível executar standalone.py como um script:

vaultah@base:~/project$ python3 -i package/standalone.py
Running /home/vaultah/project/package/standalone.py
Importing /home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/__init__.py
Importing /home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/module.py
>>> module
<module 'package.module' from '/home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/module.py'>
>>> import sys
>>> sys.modules['package']
<module 'package' from '/home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/__init__.py'>
>>> sys.modules['package.module']
<module 'package.module' from '/home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/module.py'>

Nota : Se você decidir seguir esse caminho, seria melhor usar ambientes virtuais para instalar pacotes isoladamente.

Solução 4: use importações absolutas e algum código padrão

Francamente, a instalação não é necessária - você pode adicionar algum código padrão ao seu script para fazer com que as importações absolutas funcionem.

Vou pegar emprestados arquivos da Solução 1 e alterar standalone.py :

  1. Adicionar o diretório pai do pacote para sys.path antes de tentar importar qualquer coisa de pacote usando importações absolutos:

    import sys
    from pathlib import Path # if you haven't already done so
    file = Path(__file__).resolve()
    parent, root = file.parent, file.parents[1]
    sys.path.append(str(root))
    
    # Additionally remove the current file's directory from sys.path
    try:
        sys.path.remove(str(parent))
    except ValueError: # Already removed
        pass
  2. Substitua a importação relativa pela importação absoluta:

    from package import module  # absolute import

standalone.py é executado sem problemas:

vaultah@base:~$ python3 -i package/standalone.py
Running /home/vaultah/package/standalone.py
Importing /home/vaultah/package/__init__.py
Importing /home/vaultah/package/module.py
>>> module
<module 'package.module' from '/home/vaultah/package/module.py'>
>>> import sys
>>> sys.modules['package']
<module 'package' from '/home/vaultah/package/__init__.py'>
>>> sys.modules['package.module']
<module 'package.module' from '/home/vaultah/package/module.py'>

Sinto que devo avisá-lo: tente não fazer isso, principalmente se o seu projeto tiver uma estrutura complexa.


Como observação lateral, o PEP 8 recomenda o uso de importações absolutas, mas afirma que, em alguns cenários, importações relativas explícitas são aceitáveis:

As importações absolutas são recomendadas, pois geralmente são mais legíveis e tendem a se comportar melhor (ou pelo menos fornecer melhores mensagens de erro). [...] No entanto, as importações relativas explícitas são uma alternativa aceitável às importações absolutas, especialmente quando se trata de layouts complexos de pacotes nos quais o uso de importações absolutas seria desnecessariamente detalhado.

vaultah
fonte
3
É possível definir __package__manualmente se o nome está __main__para resolver o problema?
Paulo Scardine
Obrigado, boas respostas! Consegui carregar o módulo usando o impmódulo e defini-lo __package__adequadamente, mas o resultado é claramente um antipadrão.
Paulo Scardine
Eu recebo o erro AttributeError: 'PosixPath' object has no attribute 'path'.
usuário
Obrigado pela resposta rápida. Estou usando o pacote nltk, estou recebendo um erro: `File" /usr/local/lib/python3.5/dist-packages/nltk/__init__.py ", linha 115, no <module> do nltk.decorators decorador de importação, memorize o arquivo "/usr/local/lib/python3.5/dist-packages/nltk/decorators.py", linha 23, em <module> sys.path = [p para p em sys.path se "nltk "not in p] File" /usr/local/lib/python3.5/dist-packages/nltk/decorators.py ", linha 23, em <listcomp> sys.path = [p para p em sys.path se" nltk "not in p] TypeError: o argumento do tipo 'PosixPath' não é iterável`
user
1
Você também pode importar um arquivo pelo caminho do arquivo (em relação também): docs.python.org/3/library/…
Ctrl-C
86

Coloque isso dentro do arquivo __init__.py do seu pacote :

# For relative imports to work in Python 3.6
import os, sys; sys.path.append(os.path.dirname(os.path.realpath(__file__)))

Supondo que seu pacote seja assim:

├── project
   ├── package
      ├── __init__.py
      ├── module1.py
      └── module2.py
   └── setup.py

Agora use importações regulares no seu pacote, como:

# in module2.py
from module1 import class1

Isso funciona nos python 2 e 3.

am5
fonte
1
faz este trabalho se nós empacotá-lo como weel
Alex Punnen
1
Eu também acho que isso merece mais votos. Colocar isso em todos __init__.pybasicamente resolverá todos os erros de importação relativos.
frankliuao
3
Não posso falar pelos outros, mas tendem a evitar modificações, sys.pathporque estou preocupado que isso possa afetar outro código. (Parcialmente isso é porque eu não sei os meandros de como ele funciona.)
pianoJames
@pianoJames Eu sei o que você quer dizer, essa (aparentemente, depois de muita brincadeira) correção mágica parece um pouco fácil demais. Mas funciona. Estaria interessado em não saber daqueles que sabem se isso tem efeitos colaterais negativos.
31419 Jon
Estou usando isso agora: e até agora tudo bem.
javadba 19/03
37

Eu me deparei com esse problema. Uma solução alternativa de hack é importada por meio de um bloco if / else da seguinte maneira:

#!/usr/bin/env python3
#myothermodule

if __name__ == '__main__':
    from mymodule import as_int
else:
    from .mymodule import as_int


# Exported function
def add(a, b):
    return as_int(a) + as_int(b)

# Test function for module  
def _test():
    assert add('1', '1') == 2

if __name__ == '__main__':
    _test()
goffer
fonte
29
essa não é uma solução muito boa. Além disso, nu except:é ruim. use em except ImportError:vez disso!
ThiefMaster 16/03/2015
6
Está SystemErroraqui. (Py 3.4)
Avi
8
Essa não é uma idéia terrível, mas seria melhor detectar qual importação usar em vez de tentar / exceto. Algo como if __name__ == '__main__': from mymod import as_int; else: from .mymod import as_int.
Perkins
@ Perkins Bem ... na maioria dos casos, não . Acho que as importações relativas podem ser a exceção.
Wizzwizz4
8

Felizmente, isso será de valor para alguém por aí - passei por meia dúzia de postagens de stackoverflow tentando descobrir importações relativas semelhantes às postadas acima aqui. Configurei tudo como sugerido, mas ainda estava batendoModuleNotFoundError: No module named 'my_module_name'

Desde que eu estava apenas desenvolvendo localmente e brincando, não havia criado / executado um setup.pyarquivo. Aparentemente, eu também não tinha definido o meu PYTHONPATH.

Percebi que, quando eu executava meu código como quando os testes estavam no mesmo diretório que o módulo, não conseguia encontrar meu módulo:

$ python3 test/my_module/module_test.py                                                                                                               2.4.0
Traceback (most recent call last):
  File "test/my_module/module_test.py", line 6, in <module>
    from my_module.module import *
ModuleNotFoundError: No module named 'my_module'

No entanto, quando especifiquei explicitamente o caminho em que as coisas começaram a funcionar:

$ PYTHONPATH=. python3 test/my_module/module_test.py                                                                                                  2.4.0
...........
----------------------------------------------------------------------
Ran 11 tests in 0.001s

OK

Portanto, no caso de alguém ter tentado algumas sugestões, acredita que seu código está estruturado corretamente e ainda se encontra em uma situação semelhante à minha, tente uma das seguintes opções se você não exportar o diretório atual para o seu PYTHONPATH:

  1. Execute seu código e inclua explicitamente o caminho da seguinte maneira: $ PYTHONPATH=. python3 test/my_module/module_test.py
  2. Para evitar chamadas PYTHONPATH=., crie um setup.pyarquivo com conteúdo como o seguinte e execute python setup.py developmentpara adicionar pacotes ao caminho:
# setup.py
from setuptools import setup, find_packages

setup(
    name='sample',
    packages=find_packages()
)
LaCroixed
fonte
6

Eu precisava executar python3 no diretório principal do projeto para fazê-lo funcionar.

Por exemplo, se o projeto tiver a seguinte estrutura:

project_demo/
├── main.py
├── some_package/
   ├── __init__.py
   └── project_configs.py
└── test/
    └── test_project_configs.py

Solução

Eu rodaria o python3 dentro da pasta project_demo / e executaria uma

from some_package import project_configs
Arthur
fonte
4

Para evitar esse problema, criei uma solução com o pacote de reembalagem , que funcionou para mim por algum tempo. Ele adiciona o diretório superior ao caminho da lib:

import repackage
repackage.up()
from mypackage.mymodule import myfunction

A reembalagem pode fazer importações relativas que funcionam em uma ampla variedade de casos, usando uma estratégia inteligente (inspecionando a pilha de chamadas).

fralau
fonte
De longe a solução mais fácil! Obrigado!
CodingInCircles
1
Obrigado! Em vez de tentar dar a melhor resposta, tentei dar uma resposta útil :-)
fralau
3

se os dois pacotes estiverem no seu caminho de importação (sys.path) e o módulo / classe que você deseja estiver em example / example.py, para acessar a classe sem a importação relativa, tente:

from example.example import fkt
Salt999
fonte
1

Eu acho que a melhor solução é criar um pacote para o seu módulo: Aqui estão mais informações sobre como fazê-lo.

Depois de ter um pacote, você não precisa se preocupar com a importação relativa, basta fazer importações absolutas.

Taras Kucherenko
fonte
0

Eu tive um problema semelhante: eu precisava de um serviço Linux e de um plugin cgi que usassem constantes comuns para cooperar. A maneira 'natural' de fazer isso é colocá-los no init .py do pacote, mas não consigo iniciar o plugin cgi com o parâmetro -m.

Minha solução final foi semelhante à Solução 2 acima:

import sys
import pathlib as p
import importlib

pp = p.Path(sys.argv[0])
pack = pp.resolve().parent

pkg = importlib.import_module('__init__', package=str(pack))

A desvantagem é que você deve prefixar as constantes (ou funções comuns) com o pkg:

print(pkg.Glob)
Thomas Heckmann
fonte