[英]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.py
, python 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
确保您的根目录位于在PYTHONPATH
明确搜索的文件夹中
使用绝对导入:
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.