Python中的包特定导入钩子
我正在创建一个Python模块,目的是把其他语言或框架提供的API映射到Python中。理想情况下,我希望这个模块能作为一个根包,提供一些辅助方法,并把那个框架中的所有命名空间映射到Python的包或模块中。为了方便,我们以CLR为例:
import clr.System.Data
import clr.System.Windows.Forms
这里的clr
是一个神奇的顶层包,它展示了CLR命名空间中的System.Data
和System.Windows.Forms
子包或子模块(据我所知,一个包就是有子模块或子包的模块;其中也可以有其他类型的成员)。
我读过PEP-302,并写了一个简单的原型程序,使用自定义的meta_path
钩子实现类似的效果。clr
模块本身是一个合格的Python模块,当它被导入时,会设置__path__ = []
(这使它成为一个包,以便import
可以尝试查找子模块),并注册这个钩子。这个钩子会拦截任何包的加载,只要包的全名以"clr."
开头,它会动态创建一个新模块,使用imp.new_module()
,并把它注册到sys.modules
中,然后用一些神奇的方式把原API中的类和方法填充进去。以下是代码:
clr.py
import sys
import imp
class MyLoader:
def load_module(self, fullname):
try:
return sys.modules[fullname]
except KeyError:
pass
print("--- load ---")
print(fullname)
m = imp.new_module(fullname)
m.__file__ = "clr:" + fullname
m.__path__ = []
m.__loader__ = self
m.speak = lambda: print("I'm " + fullname)
sys.modules.setdefault(fullname, m)
return m
class MyFinder:
def find_module(self, fullname, path = None):
print("--- find ---")
print(fullname)
print(path)
if fullname.startswith("clr."):
return MyLoader()
return None
print("--- init ---")
__path__ = []
sys.meta_path.append(MyFinder())
test.py
import clr.Foo.Bar.Baz
clr.Foo.speak()
clr.Foo.Bar.speak()
clr.Foo.Bar.Baz.speak()
总的来说,这似乎运行得很好。Python保证模块是从左到右导入的,所以clr
总是最先被导入,它设置了钩子,使得后面的模块可以被导入。
不过,我在想我这样做是不是有点过头了。毕竟,我安装了一个全局钩子,它会在任何模块导入时被调用,尽管我过滤掉了那些我不关心的模块。有没有可能安装一个只在我特定包的导入时被调用的钩子,而不是其他的?还是说上面的方法就是在Python中做这类事情的正确方式?
1 个回答
总的来说,我觉得你的做法看起来不错。我不太担心它是“全局”的,因为这样做的主要目的是指定哪些路径应该由你来处理。如果把这个测试放到导入逻辑里面,只会让事情变得复杂,所以这部分的决定留给钩子的实现者去做。
不过有一个小小的担心,你可以考虑使用 sys.path_hooks
吗?它似乎没有 sys.meta_path
那么“强大”。
sys.path_hooks
是一个可调用对象的列表,这些对象会按顺序检查,以确定它们是否能够处理给定的路径项。这个可调用对象会接收一个参数,也就是路径项。如果它无法处理这个路径项,就必须抛出ImportError
错误;如果可以处理,就返回一个导入器对象。