Python 3.5+:如何在给定完整文件路径的情况下动态导入模块(存在隐式同级导入)?
题
标准库清楚地说明了如何直接导入源文件(给定源文件的绝对文件路径),但是如果该源文件使用隐式同级导入(如以下示例中所述),则此方法不起作用。
在隐式同级导入的情况下,该示例如何适应工作?
我已经签出这个和这个其他的话题#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())
python folder/implicit_sibling_import.py
使用该if __name__ ==
'__main__':
块运行注释掉了ISI says: world
Python 3.6中的收益。
但是运行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
如预期,但我想避免改写(munging)的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中包含的代码。因此,对该问题的更好答案将必须找到一种方法
import
,只需通过exec语句的第二个参数简单地注入一个或多个全局变量,即可使该语句具有不同的行为。但是,“
@所做的任何使导入机制在该文件的文件夹中显示的操作,都必须持续超出初始导入的持续时间,因为调用该文件时该文件中的函数可能会执行进一步的导入”,如@问题评论中的user2357112。不幸的是,更改
import
语句行为的唯一方法似乎是更改sys.path
或打包__path__
。module.__dict__
已经包含__path__
让似乎并没有工作,这叶子sys.path
(或者试图找出为什么执行不会代码当作一个包,即使它有__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