繁体   English   中英

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

[英]Python 3.5+: How to dynamically import a module given the full file path (in the presence of implicit sibling imports)?

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

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

我已经签出这个这个其他的话题#1的问题,但他们没有解决手工导入的文件隐含的兄弟进口。

设置/示例

这是一个说明性的例子

目录结构:

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__': block commented out 运行python folder/implicit_sibling_import.py产生ISI says: world 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)) 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安装的请求库

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

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


PYTHONPATH

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

-m

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

我能想到的最简单的解决方案是在执行导入的函数中临时修改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语句的行为有所不同。 但是,“无论您做什么使导入机制在该文件的文件夹中查找,它都必须在初始导入的持续时间之后逗留,因为当您调用它们时,来自该文件的函数可能会执行进一步的导入”,如@所述问题评论中的 user2357112。

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

此外,这个问题似乎不是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

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

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

在 bash 上是这样的:

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

或直接运行:

PYTHONPATH="./folder/:${PYTHONPATH}" python directory/app.py
  1. 确保您的根目录位于在PYTHONPATH明确搜索的文件夹中

  2. 使用绝对导入:

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

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

尝试:

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

或直接运行:

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM