Python:强制导入更喜欢 .py 而不是 .so
Python: Force import to prefer .py over .so
我遇到的情况是,相同的 Python 模块存在于同一目录中的两个不同版本; mymodule.py
和 mymodule.so
(我通过 Cython 从第一个获得后者,但这与我的问题无关)。当从 Python 我做
import mymodule
它总是选择mymodule.so
。有时我真的很想导入 mymodule.py
。我可以暂时将 mymodule.so
移动到另一个位置,但是如果我同时有另一个需要导入 mymodule.so
.
的 Python 实例 运行,那效果就不好了
问题是如何让 import
更喜欢 .py
文件而不是 .so
,而不是相反?
以下是我对解决方案的看法:
我想象使用 importlib
执行一些魔术并可能编辑 sys.meta_path
。具体来说,我看到 sys.meta_path[2]
包含用于导入外部模块的 _frozen_importlib_external.PathFinder
,即用于 mymodule.py
和 mymodule.so
。如果我可以用类似的 PathFinder
替换它,它使用文件类型的反向排序,我会有一个解决方案。
我正在使用 Python 3.7,如果这会影响解决方案。
编辑
请注意,仅阅读 mymodule.py
和 exec
的源代码行是行不通的,因为 mymodule.py
本身可能会导入其他模块,这些模块又存在于.py
和 .so
版本(我也想导入这些版本的 .py
)。
使用 these notes 我想到了这个。不是很漂亮,但似乎有用。
import glob, importlib, sys
def hook(name):
if name != '.':
raise ImportError()
modnames = set(f.rstrip('.py') for f in glob.glob('*.py'))
return Finder(modnames)
sys.path_hooks.insert(1, hook)
sys.path.insert(0, '.')
class Finder(object):
def __init__(self, modnames):
self.modnames = modnames
def find_spec(self, modname, target=None):
if modname in self.modnames:
origin = './' + modname + '.py'
loader = Loader()
return importlib.util.spec_from_loader(modname, loader, origin=origin)
else:
return None
class Loader(object):
def create_module(self, target):
return None
def exec_module(self, module):
with open(module.__spec__.origin, 'r', encoding='utf-8') as f:
code = f.read()
compile(code, module.__spec__.origin, 'exec')
exec(code, module.__dict__)
这是另一个解决方案,只需调整运行时默认生成的查找器即可。这使用了隐藏的实现细节 (FileFinder._loaders
),但我已经在 CPython 3.7、3.8 和 3.9 上进行了测试。
from contextlib import contextmanager
from dataclasses import dataclass
from importlib.machinery import FileFinder
from importlib.abc import Finder
import sys
from typing import Callable
@dataclass
class PreferPureLoaderHook:
orig_hook: Callable[[str], Finder]
def __call__(self, path: str) -> Finder:
finder = self.orig_hook(path)
if isinstance(finder, FileFinder):
# Move pure python file loaders to the front
finder._loaders.sort(key=lambda pair: 0 if pair[0] in (".py", ".pyc") else 1) # type: ignore
return finder
@contextmanager
def prefer_pure_python_imports():
sys.path_hooks = [PreferPureLoaderHook(h) for h in sys.path_hooks]
sys.path_importer_cache.clear()
yield
assert all(isinstance(h, PreferPureLoaderHook) for h in sys.path_hooks)
sys.path_hooks = [h.orig_hook for h in sys.path_hooks]
sys.path_importer_cache.clear()
with prefer_pure_python_imports():
...
我遇到的情况是,相同的 Python 模块存在于同一目录中的两个不同版本; mymodule.py
和 mymodule.so
(我通过 Cython 从第一个获得后者,但这与我的问题无关)。当从 Python 我做
import mymodule
它总是选择mymodule.so
。有时我真的很想导入 mymodule.py
。我可以暂时将 mymodule.so
移动到另一个位置,但是如果我同时有另一个需要导入 mymodule.so
.
问题是如何让 import
更喜欢 .py
文件而不是 .so
,而不是相反?
以下是我对解决方案的看法:
我想象使用 importlib
执行一些魔术并可能编辑 sys.meta_path
。具体来说,我看到 sys.meta_path[2]
包含用于导入外部模块的 _frozen_importlib_external.PathFinder
,即用于 mymodule.py
和 mymodule.so
。如果我可以用类似的 PathFinder
替换它,它使用文件类型的反向排序,我会有一个解决方案。
我正在使用 Python 3.7,如果这会影响解决方案。
编辑
请注意,仅阅读 mymodule.py
和 exec
的源代码行是行不通的,因为 mymodule.py
本身可能会导入其他模块,这些模块又存在于.py
和 .so
版本(我也想导入这些版本的 .py
)。
使用 these notes 我想到了这个。不是很漂亮,但似乎有用。
import glob, importlib, sys
def hook(name):
if name != '.':
raise ImportError()
modnames = set(f.rstrip('.py') for f in glob.glob('*.py'))
return Finder(modnames)
sys.path_hooks.insert(1, hook)
sys.path.insert(0, '.')
class Finder(object):
def __init__(self, modnames):
self.modnames = modnames
def find_spec(self, modname, target=None):
if modname in self.modnames:
origin = './' + modname + '.py'
loader = Loader()
return importlib.util.spec_from_loader(modname, loader, origin=origin)
else:
return None
class Loader(object):
def create_module(self, target):
return None
def exec_module(self, module):
with open(module.__spec__.origin, 'r', encoding='utf-8') as f:
code = f.read()
compile(code, module.__spec__.origin, 'exec')
exec(code, module.__dict__)
这是另一个解决方案,只需调整运行时默认生成的查找器即可。这使用了隐藏的实现细节 (FileFinder._loaders
),但我已经在 CPython 3.7、3.8 和 3.9 上进行了测试。
from contextlib import contextmanager
from dataclasses import dataclass
from importlib.machinery import FileFinder
from importlib.abc import Finder
import sys
from typing import Callable
@dataclass
class PreferPureLoaderHook:
orig_hook: Callable[[str], Finder]
def __call__(self, path: str) -> Finder:
finder = self.orig_hook(path)
if isinstance(finder, FileFinder):
# Move pure python file loaders to the front
finder._loaders.sort(key=lambda pair: 0 if pair[0] in (".py", ".pyc") else 1) # type: ignore
return finder
@contextmanager
def prefer_pure_python_imports():
sys.path_hooks = [PreferPureLoaderHook(h) for h in sys.path_hooks]
sys.path_importer_cache.clear()
yield
assert all(isinstance(h, PreferPureLoaderHook) for h in sys.path_hooks)
sys.path_hooks = [h.orig_hook for h in sys.path_hooks]
sys.path_importer_cache.clear()
with prefer_pure_python_imports():
...