Importe la función local desde un módulo alojado en otro directorio con importaciones relativas en Jupyter Notebook usando Python 3

126

Tengo una estructura de directorio similar a la siguiente

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

Cuando se trabaja en notebook.jpynbsi trato de usar una importación relativa a acceder a una función function()en module.pyla:

from ..project1.lib.module import function

Obtuve el siguiente error:

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

¿Hay alguna forma de hacer que esto funcione usando importaciones relativas?

Tenga en cuenta que se crea una instancia del servidor del cuaderno en el nivel del meta_projectdirectorio, por lo que debería tener acceso a la información de esos archivos.

Tenga en cuenta también que, al menos como se pretendía originalmente, project1no se pensó en un módulo y, por lo tanto, no tiene un __init__.pyarchivo, solo se pensó como un directorio del sistema de archivos. Si la solución al problema requiere tratarlo como un módulo e incluir un __init__.pyarchivo (incluso uno en blanco) está bien, pero hacerlo no es suficiente para resolver el problema.

Comparto este directorio entre máquinas y las importaciones relativas me permiten usar el mismo código en todas partes y, a menudo, utilizo cuadernos para la creación rápida de prototipos, por lo que es poco probable que las sugerencias que impliquen piratear rutas absolutas juntas sean útiles.


Editar: Esto es diferente a las importaciones relativas en Python 3 , que habla de las importaciones relativas en Python 3 en general y, en particular, de ejecutar un script desde el directorio de un paquete. Esto tiene que ver con trabajar dentro de un cuaderno jupyter tratando de llamar a una función en un módulo local en otro directorio que tiene aspectos generales y particulares diferentes.

mpacer
fuente
1
¿Hay __init__archivos en el directorio de su paquete?
Iron Fist
Sí, en el libdirectorio.
mpacer
Por favor, menciónelo en la estructura de su directorio en su pregunta
Iron Fist
Acabo de hacer esa edición tan pronto como vi tu primer comentario :). Gracias por captar eso.
mpacer
Posible duplicado de importaciones relativas en Python 3
baldr

Respuestas:

173

Tuve casi el mismo ejemplo que usted en este cuaderno en el que quería ilustrar el uso de la función de un módulo adyacente de manera SECA.

Mi solución fue decirle a Python de esa ruta de importación de módulo adicional agregando un fragmento como este al cuaderno:

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

Esto le permite importar la función deseada de la jerarquía del módulo:

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

Tenga en cuenta que es necesario agregar __init__.pyarchivos vacíos a las carpetas project1 / y lib / si aún no los tiene.

metakermit
fuente
6
Esto resuelve el problema de poder importar un paquete usando lo que es más o menos una ubicación relativa, pero solo indirectamente. Sé que Matthias Bussonier (@matt en SE) y Yuvi Panda (@yuvi en SE) están desarrollando github.com/ipython/ipynb que abordará esto de manera más directa (por ejemplo, al permitir importaciones relativas usando la sintaxis estándar una vez que su paquete es importado). Aceptaré su respuesta por ahora, y cuando su solución esté completamente lista para que otros la usen, probablemente escribiré una respuesta sobre cómo usarla o le pediré a uno de ellos que lo haga.
mpacer
gracias por señalar el init .py vacío Soy un novato en Python y tenía problemas para importar mis clases. Estaba obteniendo errores encontrados en la nota del módulo, ¡agregando init .py vacío se solucionó el problema!
Pat Grady
5
El archivo init .py vacío ya no es necesario en Python 3.
CathyQian
FYI: hay un visor para el cuaderno: nbviewer.jupyter.org/github/qPRC/qPRC/blob/master/notebook/…
thoroc
25

Vine aquí en busca de las mejores prácticas para abstraer código en submódulos cuando trabajaba en Notebooks. No estoy seguro de que exista una buena práctica. He estado proponiendo esto.

Una jerarquía de proyectos como tal:

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

Y de 20170609-Initial_Database_Connection.ipynb:

    In [1]: cd ..

    In [2]: from lib.postgres import database_connection

Esto funciona porque, de forma predeterminada, Jupyter Notebook puede analizar el cdcomando. Tenga en cuenta que esto no hace uso de la magia de Python Notebook. Simplemente funciona sin anteponer %bash.

Teniendo en cuenta que 99 de cada 100 veces estoy trabajando en Docker usando una de las imágenes de Project Jupyter Docker , la siguiente modificación es idempotente

    In [1]: cd /home/jovyan

    In [2]: from lib.postgres import database_connection
Joshua Cook
fuente
Gracias. Realmente horribles las restricciones de estas importaciones relativas.
Michael
Yo también uso en chdirlugar de agregar a la ruta, ya que estoy interesado tanto en importar desde el repositorio principal como en interactuar con algunos archivos allí.
TheGrimmScientist
Lamentablemente, lo más pirateado que hago en Python. Sin embargo, no puedo encontrar una mejor solución.
TheGrimmScientist
para idempotencia simple (permitiendo que la misma celda se ejecute varias veces y obtenga el mismo resultado) if os.path.isdir('../lib/'): os.chdir('../lib'):; o, mejor, úselo ../lib/db/con su postgres.pypara no subir accidentalmente a un directorio superior que también contenga otro lib.
Michael
1
Me gusta esta solución hasta que accidentalmente ejecuté cd ..dos veces.
minhle_r7
15

Hasta ahora, la respuesta aceptada me ha funcionado mejor. Sin embargo, mi preocupación siempre ha sido que existe un escenario probable en el que podría refactorizar el notebooksdirectorio en subdirectorios, requiriendo cambiar el module_pathen cada cuaderno. Decidí agregar un archivo de Python dentro de cada directorio de cuaderno para importar los módulos requeridos.

Así, teniendo la siguiente estructura de proyecto:

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

Agregué el archivo project_path.pyen cada subdirectorio del cuaderno ( notebooks/explorey notebooks/explain). Este archivo contiene el código para las importaciones 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)

De esta manera, solo necesito hacer importaciones relativas dentro del project_path.pyarchivo y no en los cuadernos. Los archivos de los cuadernos solo tendrían que importar project_pathantes de importar lib. Por ejemplo en 0.0-notebook.ipynb:

import project_path
import lib

La advertencia aquí es que revertir las importaciones no funcionaría. ESTO NO FUNCIONA:

import lib
import project_path

Por lo tanto, se debe tener cuidado durante las importaciones.

Gerges
fuente
3

Acabo de encontrar esta bonita solución:

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

Solo quieres algunas funciones de ese archivo

from lib.store_load import your_function_name

Si la versión de python> = 3.3 no necesita el archivo init.py en la carpeta

Víctor Callejas
fuente
3
Encontre esto muy utíl. Agregaré que se debe agregar la siguiente modificación ->if ".." not in sys.path: ... sys.path.insert(0,"..")
Yaakov Bressler
2

Investigando este tema yo mismo y después de leer las respuestas, recomiendo usar la biblioteca path.py, ya que proporciona un administrador de contexto para cambiar el directorio de trabajo actual.

Entonces tienes algo como

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

Aunque, puede omitir la isdirdeclaración.

Aquí agregaré declaraciones impresas para que sea más fácil seguir lo que está sucediendo

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 produce en este ejemplo (donde lib está en /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

Dado que la solución utiliza un administrador de contexto, se le garantiza que volverá a su directorio de trabajo anterior, sin importar en qué estado estaba su kernel antes de la celda y sin importar qué excepciones se produzcan al importar el código de su biblioteca.

marr75
fuente
Esto no funcionará en combinación con% autoreload, ya que la ruta del módulo no se encontrará en el momento de la recarga
Johannes
1

Aquí están mis 2 centavos:

importar sys

mapee la ruta donde se encuentra el archivo del módulo. En mi caso fue el escritorio

sys.path.append ('/ Usuarios / John / Escritorio')

O importe todo el módulo de mapeo PERO entonces debe usar la .notation para mapear las clases como mapeo.

import mapping # mapping.py es el nombre de mi archivo de módulo

shipit = mapping.Shipment () #Shipment es el nombre de la clase que necesito usar en el módulo de mapeo

O importar la clase específica del módulo de mapeo

desde el mapeo importar mapeo

shipit = Shipment () #Ahora no tienes que usar el .notation

Poli
fuente
0

He descubierto que python-dotenv ayuda a resolver este problema con bastante eficacia. La estructura de su proyecto termina cambiando ligeramente, pero el código en su cuaderno es un poco más simple y consistente en todos los cuadernos.

Para su proyecto, instale un poco.

pipenv install python-dotenv

Luego, el proyecto cambia a:

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

Y finalmente, su importación cambia a:

import os
import sys

from dotenv import find_dotenv


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

Un +1 para este paquete es que sus cuadernos pueden tener varios directorios de profundidad. python-dotenv encontrará el más cercano en un directorio principal y lo usará. Un +2 para este enfoque es que jupyter cargará variables de entorno desde el archivo .env al inicio. Doble golpe.

t.perk
fuente