Python 3.5:如何在给定完整文件路径的情况下动态导入模块(存在隐式兄弟导入)?

新手上路,请多包涵

问题

标准库清楚地记录 了如何直接导入源文件(给定源文件的绝对文件路径),但如果该源文件使用隐式兄弟导入,则此方法不起作用,如下例所述。

该示例如何适用于存在隐式同级导入的情况?

我已经检查了 这个这个关于该主题的其他 Stackoverflow 问题,但它们没有解决手动导入文件 的隐式同级导入。

设置/示例

这是一个说明性的例子

目录结构:

 root/
  - directory/
    - app.py
  - folder/
    - implicit_sibling_import.py
    - lib.py


app.py :

 import os
import importlib.util

# construct absolute paths
root = os.path.abspath(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
isi_path = os.path.join(root, 'folder', 'implicit_sibling_import.py')

def path_import(absolute_path):
   '''implementation taken from https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly'''
   spec = importlib.util.spec_from_file_location(absolute_path, absolute_path)
   module = importlib.util.module_from_spec(spec)
   spec.loader.exec_module(module)
   return module

isi = path_import(isi_path)
print(isi.hello_wrapper())


lib.py :

 def hello():
    return 'world'


implicit_sibling_import.py :

 import lib # this is the implicit sibling import. grabs root/folder/lib.py

def hello_wrapper():
    return "ISI says: " + lib.hello()

#if __name__ == '__main__':
#    print(hello_wrapper())


运行 python folder/implicit_sibling_import.pyif __name__ == '__main__': 块注释掉产量 ISI says: world 在 Python 3.6 中。

但是运行 python directory/app.py 产生:

 Traceback (most recent call last):
  File "directory/app.py", line 10, in <module>
    spec.loader.exec_module(module)
  File "<frozen importlib._bootstrap_external>", line 678, in exec_module
  File "<frozen importlib._bootstrap>", line 205, in _call_with_frames_removed
  File "/Users/pedro/test/folder/implicit_sibling_import.py", line 1, in <module>
    import lib
ModuleNotFoundError: No module named 'lib'

解决方法

If I add import sys; sys.path.insert(0, os.path.dirname(isi_path)) to app.py , python app.py yields world as intended, but I would like to avoid munging the sys.path 如果可能的话。

回答要求

我想 python app.py 打印 ISI says: world 我想通过修改 path_import 函数来实现。

我不确定 mangling sys.path 的含义。例如。 if there was directory/requests.py and I added the path to directory to the sys.path , I wouldn’t want import requests to start importing directory/requests.py 而不是导入我用 pip install requests 安装的 请求库

该解决方案 必须 作为 python 函数实现,该函数接受所需模块的绝对文件路径并返回 模块对象

理想情况下,解决方案不应引入副作用(例如,如果它确实修改了 sys.path ,它应该返回 sys.path 到其原始状态)。如果解决方案确实引入了副作用,它应该解释为什么不引入副作用就无法实现解决方案。


PYTHONPATH

如果我有多个项目这样做,我不想每次在它们之间切换时都必须记住设置 PYTHONPATH 。用户应该能够 pip install 我的项目并在没有任何额外设置的情况下运行它。

-m

-m 标志 是推荐的/pythonic 方法,但标准库也明确记录 了如何直接导入源文件。我想知道如何调整这种方法来处理隐式相对导入。显然,Python 的内部结构必须这样做,那么内部结构与“直接导入源文件”文档有何不同?

原文由 Pedro Cattori 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 620
2 个回答

我能想到的最简单的解决方案是在执行导入的函数中临时修改 sys.path

 from contextlib import contextmanager

@contextmanager
def add_to_path(p):
    import sys
    old_path = sys.path
    sys.path = sys.path[:]
    sys.path.insert(0, p)
    try:
        yield
    finally:
        sys.path = old_path

def path_import(absolute_path):
   '''implementation taken from https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly'''
   with add_to_path(os.path.dirname(absolute_path)):
       spec = importlib.util.spec_from_file_location(absolute_path, absolute_path)
       module = importlib.util.module_from_spec(spec)
       spec.loader.exec_module(module)
       return module

除非您同时在另一个线程中导入,否则这不会导致任何问题。否则,由于 sys.path 已恢复到以前的状态,因此应该没有不需要的副作用。

编辑:

我意识到我的回答有些不尽如人意,但是,深入研究代码表明,行 spec.loader.exec_module(module) 基本上导致 exec(spec.loader.get_code(module.__name__),module.__dict__) 被调用。这里 spec.loader.get_code(module.__name__) 只是lib.py中包含的代码。

因此,该问题的更好答案必须找到一种方法,使 import 语句的行为有所不同,只需通过 exec 语句的第二个参数注入一个或多个全局变量。然而,“无论你做什么来使导入机制在该文件的文件夹中查找,它都必须在初始导入的持续时间之后继续存在,因为当你调用它们时,来自该文件的函数可能会执行进一步的导入”,如@所述user2357112 在问题评论中。

不幸的是,改变 import 语句行为的唯一方法似乎是改变 sys.path 或在一个包中 __path__module.__dict__ 已经包含 __path__ 所以这似乎不起作用留下 sys.path (或者试图弄清楚为什么代码执行包甚至不处理虽然它有 __path____package__ … - 但我不知道从哪里开始 - 也许它与没有 __init__.py 文件有关)cb– .

此外,这个问题似乎并不特定于 importlib 而是 同级导入 的一般问题。

Edit2: 如果您不希望模块以 sys.modules 结束,则以下应该有效(请注意,在导入过程中添加到 sys.modules 的任何模块都将被 _删除_):

 from contextlib import contextmanager

@contextmanager
def add_to_path(p):
    import sys
    old_path = sys.path
    old_modules = sys.modules
    sys.modules = old_modules.copy()
    sys.path = sys.path[:]
    sys.path.insert(0, p)
    try:
        yield
    finally:
        sys.path = old_path
        sys.modules = old_modules

原文由 Jonathan von Schroeder 发布,翻译遵循 CC BY-SA 3.0 许可协议

添加到 PYTHONPATH 环境变量你的应用程序所在的路径

增加模块文件的默认搜索路径。格式与 shell 的 PATH 相同:一个或多个目录路径名由 os.pathsep 分隔(例如,Unix 上的冒号或 Windows 上的分号)。不存在的目录将被忽略。

在 bash 上是这样的:

 export PYTHONPATH="./folder/:${PYTHONPATH}"

或者直接运行:

 PYTHONPATH="./folder/:${PYTHONPATH}" python directory/app.py

原文由 ShmulikA 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题