Python:更改导入文件类型的优先级(.so 之前的 .py)

jmd*_*_dk 5 python import python-import python-3.x python-importlib

如果我import A在包含A.py和的目录中执行A.so.so操作,则将导入该文件。我有兴趣更改导入文件类型的顺序,因此它.py优先于.so,尽管只是暂时的,即在代码行ij. 这当然可以通过一些 importlib魔法来实现吗?

目前,我通过将 复制.py到一个单独的目录中来解决这个问题,将这个目录放在前面sys.path然后进行导入,这太糟糕了。

为什么需要?

这些.so文件是文件的 cython 编译版本.py。我正在 cython 之上进行一些自定义代码转换,.py即使.so存在“等效项” ,我也需要导入源代码。

测试设置

下面是一个简单的测试设置。

# A.py
import B
Run Code Online (Sandbox Code Playgroud)
# B.py
import C
print('hello from B')
Run Code Online (Sandbox Code Playgroud)
# C.py
pass
Run Code Online (Sandbox Code Playgroud)

运行python A.py成功会打印出来自 的消息B.py。现在添加B.so(因为.so文件的内容无关紧要,B.so真正成为文本文件就可以了):

# B.so
this is a fake binary
Run Code Online (Sandbox Code Playgroud)

现在python A.py失败了。虽然importlib是现代的做事方式,但到目前为止我只知道如何使用已弃用的imp模块直接导入特定文件。更新A.py

# B.so
this is a fake binary
Run Code Online (Sandbox Code Playgroud)

使它再次工作。但是,引入C.so再次打破了它,因为在导入机制中没有全局注册.py而不是查找.so

# C.so
this is a fake binary
Run Code Online (Sandbox Code Playgroud)

请注意,在此示例中,我只允许编辑A.py. 我需要 Python 3.8 的解决方案,但我怀疑 3.x 的任何解决方案也适用于 3.8。

jmd*_*_dk 2

我现在有一个可行的解决方案。这有点老套,但我认为它很强大。

事实证明,sys.path_importer_cache存储了各种查找器,而查找器又存储了list顺序采石的装载机import。这些加载程序存储为 2 元组,第一个元素恰好是给定加载程序处理的文件扩展名。

我只是遍历所有list加载器并将那些具有.so扩展名的加载器推到后面list,实现尽可能低的优先级(我可以完全删除它们,但随后我无法导入任何 .so文件)。我会跟踪所做的更改sys.path_importer_cache,并在完成特殊导入后撤消它们。所有这些都巧妙地包含在上下文管理器中:

import collections, contextlib, sys

@contextlib.contextmanager
def disable_loader(ext):
    ext = '.' + ext.lstrip('.')
    # Push any loaders for the ext extension to the back
    edits = collections.defaultdict(list)
    path_importer_cache = list(sys.path_importer_cache.values())
    for i, finder in enumerate(path_importer_cache):
        loaders = getattr(finder, '_loaders', None)
        if loaders is None:
            continue
        for j, loader in enumerate(loaders):
            if j + len(edits[i]) == len(loaders):
                break
            if loader[0] != ext:
                continue
            # Loader for the ext extension found.
            # Push to the back.
            loaders.append(loaders.pop(j))
            edits[i].append(j)
    try:
        # Yield control back to the caller
        yield
    finally:
        # Undo changes to path importer cache
        for i, edit in edits.items():
            loaders = path_importer_cache[i]._loaders
            for j in reversed(edit):
                loaders.insert(j, loaders.pop())

# Demonstrate import failure
try:
    import A
except Exception as e:
    print(e)

# Demonstrate solution
with disable_loader('.so'):
    import A

# Demonstrate (wanted) failure outside with statement
import A2
Run Code Online (Sandbox Code Playgroud)

请注意,为了import A2正确失败,您需要复制测试设置,以便您还拥有A2.pyB2.pyC2.pyB2.soC2.so它们以与原始测试文件相同的方式相互导入。

人们可以通过在进行更改之前edits进行完整备份并在完成后粘贴此备份来摆脱有些复杂的簿记工作。它在上面的有限测试中确实有效,但由于导入机制的各个部分可能保存对不同嵌套对象的引用,我认为仅使用突变更安全。copy.deepcopy(sys.path_importer_cache)sys

  • @datdinhquoc 是的,它们甚至比 `.py` 优先。 (2认同)