这两天周末在家学习Python,发现我们平时接触最多的语句就是import语句。这两天在写一些程序的时候,只是需要importhooks来完成一些操作,这个周末在家闲着的时候通过importhook命令学习了一下Python的import机制。0x00导入机制概述从名字可以推断importhook命令与Python的导入机制有关。更具体的说,importhook的作用是直接将我们编写的脚本注入到Pythonimport的常规操作中。如果我们想继续,我们应该首先了解默认情况下如何处理导入。对我们来说,这个过程其实比较简单:当Python解释器遇到import语句,就回过头去检查sys.path中所有存储的目录。这个列表在初始化的时候,通常会包含一些外部库或者一些来自操作系统的库,当然也会有一些类似dist-package的标准库在里面。这些目录通常是按顺序查找或直接查找:如果其中一个目录包含需要的包或模块,则在整个过程结束时直接提取该包或模块。我们可以写一段代码来演示ImportError。当运行下面的代码时,我们会捕获到一个异常。在程序结束之前,它可能会尝试多次导入。#!/usr/bin/envpython#coding=utf8try:#Python2.7-3.ximportjsonexceptImportError:try:#Python2.6importsimplejsonasjsonexceptImportError:try:fromdjango.utilsimportsimplejsonasjsonexceptImportError:raiseException("RequiresaJSONpackage!")虽然在这个示例中写的是不漂亮,但是可以在一定程度上增加我们写的程序或者包的执行力。幸运的是,我们只需要用这种方法来处理极少数有价值的库,比如代码中的Json库。0x01Moredetailsabout__path__上面提到的Python导入流程在大多数情况下和描述的一样有用,但实际上远不止于此。他省略了一些地方,我们可以根据自己的需要进行调整。首先,__path__属性是我们可以在__init__.py中定义的东西。您可以将其视为sys.path的本地扩展,并且仅服务于我们导入的包的子模块。换句话说,它应该在导入包含目录时查找包的子模块。默认情况下只有__init__.py目录,但可以扩展它以包括任何其他路径。一个典型的例子就是把一些逻辑包分成多个实际的包,实际上是分成多个发行版,通常是不同的pypi包。例如,让我们假设构建了一个test.package,其中包含test.client和test.server。当它们在pypi上注册时,它们是根据两个不同的发行版注册的。这样,用户可以选择安装一个或多个发行版。我们需要设置test.__path__以便它们指向test.server和test.client目录(如果你只安装了一个发行版,你只需要设置一个)。听起来有点复杂。事实上,Python有一个名为pkgutil的模块。这个模块的作用就是让我们可以很方便的实现上面的功能。你只需要在test/__init__.py下添加两行。importpkgutil__path__=pkgutil.extend_path(__path__,__name__)其实还有比这个更简单的方法,这里给大家推荐一篇文章:http://doughellmann.com/PyMOTW/0x02TrueHook:sys.meta_pathandsys.path_hooks让我们继续,然后我们将分析导入过程。其实这部分才是本文的重点。例如,从zip文件或repo中的bytes中获取模块,或者以各种方式动态构建它们,例如web服务、dll或RESTfulAPI等,几乎任何你能想到的方法。我还会提到一些独立模块之间的作弊交互。例如,当包检测到它已被导入时,它可以适配和扩展自己的接口。然后我们将讨论Python的安全增强沙箱,它用于拒绝访问某些模块或更改它们的某些功能。这些功能其实都可以通过importhooks来实现。有两种不同的钩子,一种称为元钩子(sys.meta_path),另一种称为路径钩子(sys.path_hooks)。虽然它们在导入流程的两个相似阶段被调用,但它们在创建时仍然依赖于两个东西,一个叫做模块查找器(ModuleFinder),另一个叫做模块加载器(ModuleLoader)。模块查找器实际上是一个用于查找模块的简单对象。使用方法(find_module)如下:finder.find_module(fullname,path=None)需要传入一个完整的模块名作为参数回车,path是这个模块的路径。该对象可以做以下三件事中的任何一件:抛出异常,然后完全取消所有导入过程返回一个None,表示导入的模块无法被该查找器找到。但它仍然可以被导入流程的下一阶段找到,例如一些自定义查找器或Python的标准导入机制。返回用于加载实际模块的加载程序对象。下一个是模块加载器。模块加载器实际上是一个用于加载指定模块的对象。它的(load_module)使用方法如下代码所示:loader.load_module(fullname)这里需要强调一下,fullname参数需要传入我们要加载的一个模块的全名。返回值应该是一个模块对象,最后的结果当然是完成导入对象的操作。需要注意的是,这些模块可能已经导入,或者使用复制这些模块的功能返回这些已有的模块。下面是这个函数的原型:defload_module(self,fullname):iffullnameinsys.modules:returnsys.modules[fullname]如果这个阶段出现任何错误,模块加载器应该抛出ImportError异常0x03自己构造一个加载器:上面只是一些理论,实际上,这些在PEP302标准中都有描述。实际上,模块加载器和模块查找器可以是同一个对象,也就是说,find_module可以返回self。例如,这个简单的钩子可以防止导入任何特定模块:#!/usr/bin/envpython#coding=utf8importsysclassImportBlocker(object):def__init__(self,*args):self.module_names=argsdeffind_module(self,fullname,path=None):iffullnameinself.module_names:returnsselfreturnNonedefload_module(self,name):raiseImportError("%sisblockedandcannotbeimported"%name)sys.meta_path=[ImportBlocker('httplib')]一旦我们在sys中加载它。meta_pathhook,它将阻止导入任何新模块并检查它是否存在于我们的列表中。如果我们去使用Request库,这个钩子也会起作用。importRequest会执行这条语句失败,因为request是urllib3内部使用的,从而限制了httplib的使用。但是如果一个hook没事做,拦截调用其他模块似乎意义不大。让我们改变游戏玩法。如果我们总是拒绝调用特定模块,为什么不使用警告呢?这样,这个钩子就可以帮助我们检测引入到项目中并被弃用的模块。代码如下:#!/usr/bin/envpython#coding=utf-8importloggingimportimpimportsysclassWarnOnImport(object):def__init__(self,*args):self.module_names=argsdeffind_module(self,fullname,path=None):iffullnameinself.module_names:自己。path=pathreturnselfreturnNonedefload_module(self,name):ifnameinsys.modules:returnsys.modules[name]module_info=imp.find_module(name,self.path)module=imp.load_module(name,*module_info)sys系统.modules[name]=modulelogging.warning("Importeddeprecatedmodule%s",name)returnmodulesys.meta_path=[WarnOnImport('getopt','optparse')]为了访问正常的导入机制,我们可以尝试使用imp。它的find_module和load_module函数与我们导入的钩子同名。但是imp提供的功能更强大。例如,它还包括load_source和load_compile等函数,甚至可以从头开始初始化一个模块(new_module)。
