在 Python 3 中创建"virtual"模块的最佳方式



问题描述

我有一个包含操作系统相关模块的软件包。这些模块都有相同的API。

在客户端代码中,我可以做一些类似的事情:

import platform
if platform.system() == "Windows":
from my_package import os_win32 as my_package_platform
elif platform.system() == "Linux":
from my_package import os_linux as my_package_platform
# use my_package_platform in client code

然而,我想做一些类似的事情

from my_package import my_package_platform

并让它自动导入正确的模块。

可能的解决方案1:真实模块从特定模块导入所有内容

实际上有一个模块my_package_platform.py:

import platform
import importlib as imp
import importlib.util as impu
import sys
exports = ["symbol1", "symbol2"]
def update_globals(mod, var_list):
for var in var_list:
globals()[var] = mod.__dict__.get(var)

if platform.system() == "Windows":
mod = imp.import_module(".os_win32", __package__)
elif platform.system() == "Linux":
mod = imp.import_module(".os_linux", __package__)
else:
raise NotImplementedError("not implemented for platform %s".format(platform.system()))
update_globals(mod, exports)

这似乎奏效了,但似乎有点古怪。此外,还必须保留exports变量。

可能的解决方案2:使用sys.meta_path操作创建"虚拟模块">

在包的__init__.py中添加以下内容:

import platform
import importlib.abc
import importlib.util as impu
import sys

class _VirtualModuleFinder(importlib.abc.MetaPathFinder):
def find_spec(self, fullname: str, path, target=None):
if fullname == __package__ + ".my_package_platform":
if platform.system() == "win32":
return impu.find_spec(__package__ + ".os_win32")
elif platform.system() == "Linux":
return impu.find_spec(__package__ + ".os_linux")
else:
return None

sys.meta_path.append(_VirtualModuleFinder())

这相当优雅,但似乎有点过头了。另一方面,只有当通过其他方式找不到模块时,才调用额外的Finder

需要注意的是,模块规范将包含原始模块名称。(这可能是一个bug或功能——我目前还不完全理解其含义(。

问题

  • 你认为每种方法都有哪些缺点
  • 有更好的方法或"最佳实践"吗

我想我找到了一个较少被黑客攻击的解决方案,并且似乎对角落的情况更有效:

import platform
import importlib as imp
import importlib.util as impu
import sys
mpp_name = impu.resolve_name(".my_package_platform", __package__)
if mpp_name not in sys.modules:
if platform.system() == "win32":
my_package_platform = imp.import_module(".os_win32", __package__)
elif platform.system() == "Linux":
my_package_platform = imp.import_module(".os_linux", __package__)
else:
raise ValueError("my_package_platform undefined for this platform")
sys.modules[mpp_name] = my_package_platform

即使使用Mock.patch也可以。只有IDE与PyCharm的集成是缺乏的——不知道为什么——我想他们有自己的模块搜索算法。

最新更新