【问题标题】:Python 3.5+: How to dynamically import a module given the full file path (in the presence of implicit sibling imports)?Python 3.5+:如何在给定完整文件路径的情况下动态导入模块(在存在隐式同级导入的情况下)?
【发布时间】:2017-06-11 05:08:06
【问题描述】:

问题

标准库清楚地记录了how to import source files directly(给定源文件的绝对文件路径),但如果源文件使用下面示例中描述的隐式同级导入,则此方法不起作用。

如果存在隐式同级导入,该示例如何适应工作?

我已经查看了 thisthis other 关于该主题的 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())

if __name__ == '__main__': 块被注释掉的情况下运行 python folder/implicit_sibling_import.py 在 Python 3.6 中产生 ISI says: world

但运行 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'

解决方法

如果我将import sys; sys.path.insert(0, os.path.dirname(isi_path)) 添加到app.pypython app.py 按预期生成world,但如果可能,我想避免使用sys.path

回答要求

我想让python app.py 打印ISI says: world,我想通过修改path_import 函数来完成此操作。

我不确定修改 sys.path 的含义。例如。如果有directory/requests.py 并且我将directory 的路径添加到sys.path,我不希望import requests 开始导入directory/requests.py,而不是导入我使用pip install requests 安装的requests library

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

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


PYTHONPATH

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

-m

-m flag 是推荐的/pythonic 方法,但标准库也清楚地记录了How to import source files directly。我想知道如何调整这种方法来应对隐式相对导入。显然,Python 的内部必须这样做,那么内部和“直接导入源文件”文档有何不同?

【问题讨论】:

  • 就 Python 而言,这种“隐式同级导入”是普通的绝对导入,绝对不是隐式相对导入。 Python 3 不再支持隐式相对导入。
  • 更改sys.path 可能是您最好的选择。无论你做什么来让导入机制在该文件的文件夹中查找,它都必须在初始导入的持续时间之后徘徊,因为当你调用它们时,来自该文件的函数可能会执行进一步的导入。
  • @user2357112 确实,PEP 8 确实说在 Python 3 中禁用了相对隐式导入。但我想知道:如果上面的示例不是相对隐式导入,那是什么?你有例子吗?
  • @user2357112 import lib 行没有指定任何包名,也没有使用 . 。那么,唯一不会使隐式相对导入不是在包中完成的事实吗?
  • @DarrickHerwehe 正如问题中所述,修改sys.path 是可以的,只要你能证明它是合理的。在这种情况下,只需要“解释为什么修改 sys.path 是最佳选择”。

标签: python python-3.x import python-importlib


【解决方案1】:
  1. 确保您的根目录位于PYTHONPATH 中明确搜索的文件夹中

  2. 使用绝对导入:

    from root.folder import implicit_sibling_import # called from app.py

【讨论】:

  • 如问题所述,我正在寻找一种可重复(无需每次都重新配置)的纯 python 解决方案,所以这行不通。
  • @Pedro 重复该配置是否比您提出的解决方案更可取(这显然看起来非常不符合 Python 风格)?
【解决方案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 中包含的代码。

因此,要更好地回答这个问题,必须找到一种方法,通过简单地通过 exec 语句的第二个参数注入一个或多个全局变量来使 import 语句表现不同。但是,如@问题 cmets 中的 user2357112。

不幸的是,更改import 语句行为的唯一方法似乎是更改sys.path 或包__path__module.__dict__ 已经包含 __path__ 所以这似乎不起作用留下 sys.path (或者试图弄清楚为什么 exec 不将代码视为一个包,即使它有 __path____package__ .. . - 但我不知道从哪里开始 - 也许这与没有__init__.py 文件有关)。

此外,此问题似乎并非特定于 importlib,而是 sibling imports 的一般问题。

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

【讨论】:

  • 我认为 sys.modules 在运行时也会受到影响......但不确定如何不产生这种副作用
  • 这似乎是真的 - 但是我不明白为什么这会不受欢迎/有问题? - 毕竟模块已经加载了。
  • 也许他们可以通过提供同名模块来替换当前命名空间中的模块
  • 它不会修改当前命名空间,除非您执行类似m = path_import(...) 的操作,然后我相信它的行为类似于import m,这似乎是合理的。 sys.modules 的工作方式是一个查找表,用于将来导入 docs.python.org/3/reference/import.html#the-module-cache- In any case you should be able to replace sys.modules` 并复制然后恢复它(就像我对 sys.path 所做的那样
  • 是的 :) 没错,感谢您的意见,非常有帮助
【解决方案3】:

试试:

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

或直接运行:

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

确保您的根目录位于PYTHONPATH 中明确搜索的文件夹中。使用绝对导入:

from root.folder import implicit_sibling_import #called from app.py

【讨论】:

    【解决方案4】:

    OP 的想法很棒,这仅适用于本示例,通过向 sys.modules 添加具有正确名称的兄弟模块,我会说这与添加 PYTHONPATH 相同。经过测试并使用 3.5.1 版。

    import os
    import sys
    import importlib.util
    
    
    class PathImport(object):
    
        def get_module_name(self, absolute_path):
            module_name = os.path.basename(absolute_path)
            module_name = module_name.replace('.py', '')
            return module_name
    
        def add_sibling_modules(self, sibling_dirname):
            for current, subdir, files in os.walk(sibling_dirname):
                for file_py in files:
                    if not file_py.endswith('.py'):
                        continue
                    if file_py == '__init__.py':
                        continue
                    python_file = os.path.join(current, file_py)
                    (module, spec) = self.path_import(python_file)
                    sys.modules[spec.name] = module
    
        def path_import(self, absolute_path):
            module_name = self.get_module_name(absolute_path)
            spec = importlib.util.spec_from_file_location(module_name, absolute_path)
            module = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(module)
            return (module, spec)
    
    def main():
        pathImport = PathImport()
        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')
        sibling_dirname = os.path.dirname(isi_path)
        pathImport.add_sibling_modules(sibling_dirname)
        (lib, spec) = pathImport.path_import(isi_path)
        print (lib.hello())
    
    if __name__ == '__main__':
        main()
    

    【讨论】:

      【解决方案5】:

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

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

      在 bash 上是这样的:

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

      或直接运行:

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

      【讨论】:

      • 我已经用更精确的解决方案要求更新了我的答案。不幸的是,因为我想要一个纯 python 的解决方案,所以这行不通。
      • 请问您原来的要求是什么?尤其是哪个方面使得它只能用纯python解决方案来解决。
      猜你喜欢
      • 2022-11-30
      • 2021-10-04
      • 1970-01-01
      • 1970-01-01
      • 2011-02-08
      相关资源
      最近更新 更多