Importar função local de um módulo alojado em outro diretório com importações relativas no Jupyter Notebook usando Python 3

126

Eu tenho uma estrutura de diretório semelhante à seguinte

meta_project
    project1
        __init__.py
        lib
            module.py
            __init__.py
    notebook_folder
        notebook.jpynb

Ao trabalhar em notebook.jpynbse tento usar uma importação relativa para acessar uma função function()em module.pycom:

from ..project1.lib.module import function

Estou tendo o erro a seguir:

SystemError                               Traceback (most recent call last)
<ipython-input-7-6393744d93ab> in <module>()
----> 1 from ..project1.lib.module import function

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

Existe alguma maneira de fazer isso funcionar usando importações relativas?

Observe que o servidor do notebook é instanciado no nível do meta_projectdiretório, portanto, ele deve ter acesso às informações nesses arquivos.

Observe, também, que pelo menos como originalmente pretendido, project1não foi pensado como um módulo e, portanto, não tem um __init__.pyarquivo, mas sim um diretório do sistema de arquivos. Se a solução para o problema requer tratá-lo como um módulo e incluir um __init__.pyarquivo (mesmo um em branco), tudo bem, mas fazer isso não é o suficiente para resolver o problema.

Eu compartilho este diretório entre máquinas e importações relativas me permitem usar o mesmo código em todos os lugares, e eu freqüentemente uso notebooks para prototipagem rápida, então sugestões que envolvem hackear caminhos absolutos provavelmente não serão úteis.


Editar: isso é diferente das importações relativas em Python 3 , que fala sobre importações relativas em Python 3 em geral e - em particular - a execução de um script de dentro de um diretório de pacote. Isso tem a ver com trabalhar dentro de um bloco de notas jupyter tentando chamar uma função em um módulo local em outro diretório que tem aspectos gerais e particulares diferentes.

mpacer
fonte
1
existe algum __init__arquivo no diretório do seu pacote?
Iron Fist
Sim, no libdiretório.
mpacer
Por favor, mencione isso em sua estrutura de diretório em sua pergunta
Iron Fist
Só fiz essa edição assim que vi seu primeiro comentário :). Obrigado por apanhar isso.
mpacer
Possível duplicata de importações relativas em Python 3
baldr

Respostas:

173

Tive quase o mesmo exemplo que você neste notebook, em que queria ilustrar o uso da função de um módulo adjacente de maneira SECA.

Minha solução foi informar ao Python esse caminho de importação de módulo adicional adicionando um snippet como este ao bloco de notas:

import os
import sys
module_path = os.path.abspath(os.path.join('..'))
if module_path not in sys.path:
    sys.path.append(module_path)

Isso permite que você importe a função desejada da hierarquia do módulo:

from project1.lib.module import function
# use the function normally
function(...)

Observe que é necessário adicionar __init__.pyarquivos vazios às pastas project1 / e lib / se você ainda não os tiver.

metakermit
fonte
6
Isso resolve o problema de poder importar um pacote usando o que é mais ou menos uma localização relativa, mas apenas indiretamente. Acontece que eu sei que Matthias Bussonier (@matt no SE) e Yuvi Panda (@yuvi no SE) estão desenvolvendo github.com/ipython/ipynb que tratará disso mais diretamente (por exemplo, permitindo importações relativas usando a sintaxe padrão uma vez que seu pacote é importado). Aceitarei sua resposta por enquanto e, quando a solução deles estiver completamente pronta para uso de outros, provavelmente escreverei uma resposta sobre como usá-la ou pedirei a um deles que o faça.
mpacer
obrigado por apontar o init .py vazio . Sou um novato em Python e estava tendo problemas para importar minhas classes. Eu estava recebendo erros do módulo note found, adicionar init .py vazio corrigiu o problema!
Pat Grady
5
O arquivo init .py vazio não é mais necessário no Python 3.
CathyQian
Para sua informação: há um visualizador para notebook: nbviewer.jupyter.org/github/qPRC/qPRC/blob/master/notebook/…
thoroc
25

Vim aqui em busca de melhores práticas na abstração de código para submódulos ao trabalhar em Notebooks. Não tenho certeza se existe uma prática recomendada. Eu tenho proposto isso.

Uma hierarquia de projeto como:

├── ipynb
   ├── 20170609-Examine_Database_Requirements.ipynb
   └── 20170609-Initial_Database_Connection.ipynb
└── lib
    ├── __init__.py
    └── postgres.py

E de 20170609-Initial_Database_Connection.ipynb:

    In [1]: cd ..

    In [2]: from lib.postgres import database_connection

Isso funciona porque, por padrão, o Jupyter Notebook pode analisar o cdcomando. Observe que isso não faz uso da mágica do Python Notebook. Ele simplesmente funciona sem prefixar %bash.

Considerando que 99 em 100 vezes estou trabalhando no Docker usando uma das imagens do Docker do Projeto Jupyter , a seguinte modificação é idempotente

    In [1]: cd /home/jovyan

    In [2]: from lib.postgres import database_connection
Joshua Cook
fonte
Obrigado. Realmente horríveis as restrições a essas importações relativas.
Michael
Eu também uso em chdirvez de adicionar ao caminho, já que estou interessado em importar do repositório principal e também em fazer a interface com alguns arquivos lá.
TheGrimmScientist
Infelizmente, a coisa mais hackeada que faço em python. No entanto, não consigo encontrar uma solução melhor.
TheGrimmScientist
para idempotência simples (permitindo que a mesma célula execute várias vezes e obtenha o mesmo resultado) if os.path.isdir('../lib/'): os.chdir('../lib'):; ou, melhor, use ../lib/db/com seu postgres.pypara não acidentalmente mudar para um diretório superior que também contenha outro lib.
Michael
1
Eu gosto dessa solução até que eu acidentalmente executei cd ..duas vezes.
minhle_r7
15

Até agora, a resposta aceita funcionou melhor para mim. No entanto, minha preocupação sempre foi que há um cenário provável em que eu possa refatorar o notebooksdiretório em subdiretórios, exigindo a alteração module_pathem cada bloco de notas. Decidi adicionar um arquivo python em cada diretório de notebook para importar os módulos necessários.

Assim, possuindo a seguinte estrutura de projeto:

project
|__notebooks
   |__explore
      |__ notebook1.ipynb
      |__ notebook2.ipynb
      |__ project_path.py
   |__ explain
       |__notebook1.ipynb
       |__project_path.py
|__lib
   |__ __init__.py
   |__ module.py

Eu adicionei o arquivo project_path.pyem cada subdiretório de notebook ( notebooks/exploree notebooks/explain). Este arquivo contém o código para importações relativas (de @metakermit):

import sys
import os

module_path = os.path.abspath(os.path.join(os.pardir, os.pardir))
if module_path not in sys.path:
    sys.path.append(module_path)

Dessa forma, eu só preciso fazer importações relativas dentro do project_path.pyarquivo, e não nos blocos de notas. Os arquivos dos blocos de notas só precisariam ser importados project_pathantes da importação lib. Por exemplo em 0.0-notebook.ipynb:

import project_path
import lib

A ressalva aqui é que reverter as importações não funcionaria. ISSO NÃO FUNCIONA:

import lib
import project_path

Portanto, deve-se ter cuidado durante as importações.

Gerges
fonte
3

Acabei de encontrar esta solução bonita:

import sys; sys.path.insert(0, '..') # add parent folder path where lib folder is
import lib.store_load # store_load is a file on my library folder

Você só quer algumas funções desse arquivo

from lib.store_load import your_function_name

Se python version> = 3.3 você não precisa do arquivo init.py na pasta

Victor Callejas
fonte
3
Eu achei isso muito útil. Acrescentarei que a seguinte modificação deve ser adicionada ->if ".." not in sys.path: ... sys.path.insert(0,"..")
Yaakov Bressler
2

Pesquisando este tópico e tendo lido as respostas, recomendo usar a biblioteca path.py pois ela fornece um gerenciador de contexto para alterar o diretório de trabalho atual.

Você então tem algo como

import path
if path.Path('../lib').isdir():
    with path.Path('..'):
        import lib

Embora, você possa simplesmente omitir a isdirdeclaração.

Aqui, adicionarei instruções de impressão para facilitar o acompanhamento do que está acontecendo

import path
import pandas

print(path.Path.getcwd())
print(path.Path('../lib').isdir())
if path.Path('../lib').isdir():
    with path.Path('..'):
        print(path.Path.getcwd())
        import lib
        print('Success!')
print(path.Path.getcwd())

que resulta neste exemplo (onde lib está em /home/jovyan/shared/notebooks/by-team/data-vis/demos/lib):

/home/jovyan/shared/notebooks/by-team/data-vis/demos/custom-chart
/home/jovyan/shared/notebooks/by-team/data-vis/demos
/home/jovyan/shared/notebooks/by-team/data-vis/demos/custom-chart

Visto que a solução usa um gerenciador de contexto, você tem a garantia de voltar ao seu diretório de trabalho anterior, não importa em que estado seu kernel estava antes da célula e não importa quais exceções são lançadas ao importar o código da sua biblioteca.

marr75
fonte
Isso não funcionará em combinação com% autoreload, já que o caminho do módulo não será encontrado no momento da recarga
Johannes,
1

Aqui estão meus 2 centavos:

import sys

mapeie o caminho onde o arquivo do módulo está localizado. No meu caso foi o desktop

sys.path.append ('/ Users / John / Desktop')

Importe todo o módulo de mapeamento, MAS então você terá que usar o .notation para mapear as classes como mapping.Shipping ()

import mapping # mapping.py é o nome do meu arquivo de módulo

shipit = mapping.Shipment () #Shipment é o nome da classe que preciso usar no módulo de mapeamento

Ou importe a classe específica do módulo de mapeamento

de mapeamento importação Mapeamento

shipit = Shipment () #Agora você não precisa usar o .notation

Bobby
fonte
0

Descobri que o python-dotenv ajuda a resolver esse problema de maneira bastante eficaz. A estrutura do seu projeto acaba mudando um pouco, mas o código em seu notebook é um pouco mais simples e consistente em todos os notebooks.

Para o seu projeto, instale um pouco.

pipenv install python-dotenv

Então, o projeto muda para:

├── .env (this can be empty)
├── ipynb
   ├── 20170609-Examine_Database_Requirements.ipynb
   └── 20170609-Initial_Database_Connection.ipynb
└── lib
    ├── __init__.py
    └── postgres.py

E, finalmente, sua importação muda para:

import os
import sys

from dotenv import find_dotenv


sys.path.append(os.path.dirname(find_dotenv()))

Um +1 para este pacote é que seus blocos de notas podem ter vários diretórios de profundidade. python-dotenv encontrará o mais próximo em um diretório pai e o usará. Um +2 para essa abordagem é que o jupyter carregará as variáveis ​​de ambiente do arquivo .env na inicialização. Golpe duplo.

t.perk
fonte