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

时间:2017-01-25 20:50:25

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

问题

标准库清楚地记录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())

使用python folder/implicit_sibling_import.py块运行if __name__ == '__main__':注释掉了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

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

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

PYTHONPATH

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

-m

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

5 个答案:

答案 0 :(得分:12)

我能想到的最简单的解决方案是在执行导入的函数中临时修改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语句的行为不同。但是,无论您如何使导入机器查看该文件的文件夹,它都必须延迟超过初始导入的持续时间,因为该文件中的函数可能会执行进一步的导入。你打电话给他们&#34;,如问题评论中的@ user2357112所述。

不幸的是,更改import语句行为的唯一方法似乎是更改sys.path或包__path__module.__dict__已经包含__path__,因此似乎无法正常工作sys.path(或者试图找出为什么exec不会将代码视为包,即使它有{ {1}}和__path__ ... - 但我不知道从哪里开始 - 也许它与没有__package__文件有关。

此外,此问题似乎并非针对__init__.py,而是sibling imports的一般问题。

Edit2 :如果您不希望模块以importlib结尾,则以下内容应该有效(请注意,导入过程中添加到sys.modules的所有模块已删除):

sys.modules

答案 1 :(得分:6)

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

  

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

关于bash,就像这样:

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

或直接运行:

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

答案 2 :(得分:1)

  1. 确保您的root位于PYTHONPATH
  2. 中显式搜索的文件夹中
  3. 使用绝对导入:

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

答案 3 :(得分:1)

OP的想法很棒,这个工作只针对这个例子,通过添加具有正确名称的兄弟模块到sys.modules,我会说它是添加PYTHONPATH的SAME。测试并使用版本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()

答案 4 :(得分:1)

尝试:

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

或直接运行:

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

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

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