36

问题

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

在存在隐式同级导入的情况下,该示例如何适应工作?

我已经检查了这个这个关于该主题的其他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 3.6 中运行注释掉python folder/implicit_sibling_import.py的块会产生收益。if __name__ == '__main__':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.py,按预期python app.py产生world,但我想尽可能避免修改sys.path

回答要求

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

我不确定 mangling 的含义sys.path。例如。如果directory/requests.py并且我添加directory了. _ _sys.pathimport requestsdirectory/requests.pypip install requests

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

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


PYTHONPATH

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

-m

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

4

5 回答 5

20

我能想出的最简单的解决方案是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(或者试图弄清楚为什么 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
于 2017-01-27T23:14:11.113 回答
6

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

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

在 bash 上是这样的:

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

或直接运行:

PYTHONPATH="./folder/:${PYTHONPATH}" python directory/app.py
于 2017-01-25T21:13:31.770 回答
1
  1. 确保您的根目录位于在PYTHONPATH

  2. 使用绝对导入:

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

于 2017-01-25T21:49:24.033 回答
1

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()
于 2017-02-03T06:10:35.783 回答
1

尝试:

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

或直接运行:

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

确保您的根目录位于在PYTHONPATH. 使用绝对导入:

from root.folder import implicit_sibling_import #called from app.py
于 2017-02-03T14:33:22.160 回答