Importer une fonction locale à partir d'un module hébergé dans un autre répertoire avec des importations relatives dans Jupyter Notebook à l'aide de Python 3

127

J'ai une structure de répertoires similaire à la suivante

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

Lorsque vous travaillez dans notebook.jpynbsi je tente d'utiliser une importation par rapport à accéder à une fonction function()dans module.pyavec:

from ..project1.lib.module import function

J'obtiens l'erreur suivante:

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-t-il un moyen de faire fonctionner cela en utilisant des importations relatives?

Notez que le serveur notebook est instancié au niveau du meta_projectrépertoire, il doit donc avoir accès aux informations de ces fichiers.

Notez également qu'au moins, comme prévu à l'origine, project1n'a pas été pensé comme un module et n'a donc pas de __init__.pyfichier, il était simplement conçu comme un répertoire de système de fichiers. Si la solution au problème nécessite de le traiter comme un module et d'inclure un __init__.pyfichier (même vide), c'est bien, mais cela ne suffit pas pour résoudre le problème.

Je partage ce répertoire entre les machines et les importations relatives me permettent d'utiliser le même code partout, et j'utilise souvent des blocs-notes pour le prototypage rapide, donc les suggestions qui impliquent de pirater ensemble des chemins absolus sont peu susceptibles d'être utiles.


Edit: Ceci est différent des importations relatives dans Python 3 , qui parle d'importations relatives dans Python 3 en général et - en particulier - d'exécuter un script à partir d'un répertoire de package. Cela a à voir avec le travail dans un notebook jupyter en essayant d'appeler une fonction dans un module local dans un autre répertoire qui a à la fois des aspects généraux et particuliers différents.

mpacer
la source
1
y a-t-il des __init__fichiers dans votre répertoire de package?
Iron Fist
Oui, dans l' libannuaire.
mpacer
S'il vous plaît, mentionnez-le dans votre structure de répertoire dans votre question
Iron Fist
Je viens de faire cette modification dès que j'ai vu votre premier commentaire :). Merci de le saisir.
mpacer
Double

Réponses:

174

J'ai eu presque le même exemple que vous dans ce cahier où je voulais illustrer l'utilisation de la fonction d'un module adjacent de manière DRY.

Ma solution était d'indiquer à Python ce chemin d'importation de module supplémentaire en ajoutant un extrait de code comme celui-ci au notebook:

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

Cela vous permet d'importer la fonction souhaitée à partir de la hiérarchie des modules:

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

Notez qu'il est nécessaire d'ajouter des __init__.pyfichiers vides aux dossiers project1 / et lib / si vous ne les avez pas déjà.

metakermit
la source
6
Cela résout le problème de la possibilité d'importer un package en utilisant ce qui est plus ou moins un emplacement relatif, mais seulement indirectement. Je sais que Matthias Bussonier (@matt sur SE) et Yuvi Panda (@yuvi sur SE) développent github.com/ipython/ipynb qui abordera cela plus directement (par exemple, en autorisant les importations relatives en utilisant la syntaxe standard une fois leur package est importé). J'accepterai votre réponse pour le moment, et lorsque leur solution sera complètement prête à être utilisée par d'autres, j'écrirai probablement une réponse sur la façon de l'utiliser ou je demanderai à l'un d'entre eux de le faire.
mpacer
merci d'avoir signalé le fichier init vide .py Je suis un novice en python et j'avais du mal à importer mes classes. J'obtenais des erreurs de note de module trouvées, l'ajout d' init vide .py a résolu le problème!
Pat Grady
5
Le fichier init .py vide n'est plus nécessaire dans Python 3.
CathyQian
FYI: il y a une visionneuse pour notebook: nbviewer.jupyter.org/github/qPRC/qPRC/blob/master/notebook/…
thoroc
26

Je suis venu ici à la recherche des meilleures pratiques en matière de résumé du code dans des sous-modules lorsque vous travaillez dans des blocs-notes. Je ne suis pas sûr qu'il existe une meilleure pratique. J'ai proposé ceci.

Une hiérarchie de projet en tant que telle:

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

Et de 20170609-Initial_Database_Connection.ipynb:

    In [1]: cd ..

    In [2]: from lib.postgres import database_connection

Cela fonctionne car par défaut, le bloc-notes Jupyter peut analyser la cdcommande. Notez que cela n'utilise pas la magie Python Notebook. Cela fonctionne simplement sans pré-ajouter %bash.

Étant donné que 99 fois sur 100 je travaille dans Docker en utilisant l'une des images Project Jupyter Docker , la modification suivante est idempotente

    In [1]: cd /home/jovyan

    In [2]: from lib.postgres import database_connection
Joshua Cook
la source
Merci. Vraiment horribles les restrictions de ces importations relatives.
Michael
Moi aussi, j'utilise chdirplutôt que d'ajouter au chemin, car je suis à la fois intéressé par l'importation à partir du référentiel principal ainsi que par l'interfaçage avec certains fichiers.
TheGrimmScientist
Malheureusement, la chose la plus piratée que je fasse en python. Pourtant, je ne trouve pas de meilleure solution.
TheGrimmScientist
pour une idempotence simple (permettant à la même cellule de s'exécuter plusieurs fois et d'obtenir le même résultat) if os.path.isdir('../lib/'): os.chdir('../lib'):; ou, mieux, utilisez ../lib/db/avec votre postgres.pyafin de ne pas chdir accidentellement vers un répertoire supérieur contenant également un autre lib.
michael
1
J'aime cette solution jusqu'à ce que j'exécute accidentellement cd ..deux fois.
minhle_r7
15

Jusqu'à présent, la réponse acceptée a fonctionné le mieux pour moi. Cependant, ma préoccupation a toujours été qu'il existe un scénario probable où je pourrais refactoriser le notebooksrépertoire en sous-répertoires, nécessitant de changer le module_pathdans chaque bloc-notes. J'ai décidé d'ajouter un fichier python dans chaque répertoire de notebook pour importer les modules requis.

Ainsi, ayant la structure de projet suivante:

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

J'ai ajouté le fichier project_path.pydans chaque sous-répertoire de notebook ( notebooks/exploreet notebooks/explain). Ce fichier contient le code des importations relatives (depuis @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)

De cette façon, j'ai juste besoin de faire des importations relatives dans le project_path.pyfichier, et non dans les cahiers. Les fichiers de notebooks auraient alors juste besoin d'être importés project_pathavant l'importation lib. Par exemple dans 0.0-notebook.ipynb:

import project_path
import lib

La mise en garde ici est que l'annulation des importations ne fonctionnerait pas. CELA NE FONCTIONNE PAS:

import lib
import project_path

Il faut donc être prudent lors des importations.

Gerges
la source
3

Je viens de trouver cette jolie solution:

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

Vous voulez juste quelques fonctions de ce fichier

from lib.store_load import your_function_name

Si la version python> = 3.3, vous n'avez pas besoin du fichier init.py dans le dossier

Victor Callejas
la source
3
J'ai trouvé cela très utile. J'ajouterai que la modification suivante devrait être ajoutée ->if ".." not in sys.path: ... sys.path.insert(0,"..")
Yaakov Bressler
2

En recherchant moi-même ce sujet et après avoir lu les réponses, je recommande d'utiliser la bibliothèque path.py car elle fournit un gestionnaire de contexte pour modifier le répertoire de travail actuel.

Vous avez alors quelque chose comme

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

Cependant, vous pouvez simplement omettre la isdirdéclaration.

Ici, je vais ajouter des instructions d'impression pour vous permettre de suivre facilement ce qui se passe

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())

qui sort dans cet exemple (où lib est à /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

Étant donné que la solution utilise un gestionnaire de contexte, vous êtes assuré de revenir à votre répertoire de travail précédent, quel que soit l'état dans lequel se trouvait votre noyau avant la cellule et quelles que soient les exceptions levées en important le code de votre bibliothèque.

marr75
la source
Cela ne fonctionnera pas en combinaison avec% autoreload, car le chemin du module ne sera pas trouvé au moment du rechargement
Johannes
1

Voici mes 2 cents:

import sys

mappez le chemin où se trouve le fichier du module. Dans mon cas, c'était le bureau

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

Soit importer tout le module de mappage MAIS alors vous devez utiliser le .notation pour mapper les classes comme mappage.

import mapping # mapping.py est le nom de mon fichier de module

shipit = mapping.Shipment () #Shipment est le nom de la classe que je dois utiliser dans le module de cartographie

Ou importez la classe spécifique depuis le module de mappage

depuis le mappage importation mappage

shipit = Shipment () #Maintenant, vous n'avez pas besoin d'utiliser le .notation

Policier
la source
0

J'ai trouvé que python-dotenv aide à résoudre ce problème assez efficacement. La structure de votre projet finit par changer légèrement, mais le code de votre bloc-notes est un peu plus simple et cohérent entre les blocs-notes.

Pour votre projet, faites une petite installation.

pipenv install python-dotenv

Ensuite, le projet se transforme en:

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

Et enfin, votre import se transforme en:

import os
import sys

from dotenv import find_dotenv


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

Un +1 pour ce package est que vos blocs-notes peuvent contenir plusieurs répertoires. python-dotenv trouvera le plus proche dans un répertoire parent et l'utilisera. Un +2 pour cette approche est que jupyter chargera les variables d'environnement à partir du fichier .env au démarrage. Double coup dur.

t.perk
la source