这两天周末在家学习Python,我发现我们平常接触最多的也就是import这条语句,这两天在编写一些程序的时候恰恰需要import hook去完成一些操作,借着这个周末在家闲着没事儿通过import hook这个命令,把Python的import机制了解了一下。
0x00 Import机制概述
从名字上可以推断出,import hook这个命令是和Python的导入机制有所关联。再具体一点的话,import hook的作用是把我们自己写的脚本直接注入到Python导入的例行操作里去。如果还要继续往下说的话,那我们首先应该来了解一下import默认的时候是如何处理的。
对于我们来说的话,其实这个过程比较简单:当Python的解释器遇到import语句的时候,它回去查阅sys.path里面所有已经储存的目录。这个列表初始化的时候,通常包含一些来自外部的库(external libraries)或者是来自操作系统的一些库,当然也会有一些类似于dist-package的标准库在里面。这些目录通常是被按照顺序或者是直接去搜索想要的:如果说他们当中的一个包含有期望的package或者是module,这个package或者是module将会在整个过程结束的时候被直接提取出来。
我们可以写一段代码来演示一下ImportError,运行下面的代码的时候,我们会catch一个exception,在程序结束之前,它可能会尝试多个imports。
- #!/usr/bin/env python
- #coding=utf8
- try:
- # Python 2.7-3.x
- import json
- except ImportError:
- try:
- # Python 2.6
- import simplejson as json
- except ImportError:
- try:
- from django.utils import simplejson as json
- except ImportError:
- raise Exception("Requires a JSON package!")
虽然说这段sample写的很不beautiful,但是他可以在一定程度上增加我们写的程序或者package的可以执行。庆幸的是我们仅仅需要用这种方式去处理极少数有价值的库,比如说代码中的Json库。
0x01 关于__path__的更多细节
上文中提到的Python的Import流在大多数情况下是想描述一样有用的,但是事实上远不止这些。他省略了一些我们可以根据需要调节的地方。
首先,__path__这个属性是我们可以在__init__.py里面去定义的。你可以认为他像一个sys.path的本地扩展并且只服务于我们导入的package的子模块。换句话说,它包含目录时应该寻找一个package的子模块被导入。默认的情况下只有__init__.py的目录,但是他可以扩展到包含任何其他任何的路径。
举一个典型的例子就是把一些逻辑上的package分割成多个实际上的package,其实就是分割成多个distribution,一般情况下是不同的pypi包。举个例子,让我们假设构造一个test.package,里面包含有test.client和test.server,他们在pypi注册的时候是按照两个不同的distribution去注册的,这样的话用户可以选择其中的一个或多个distribution去安装。我们需要设置test.__path__让他们去指向test.server和test.client的目录(如果你只安装了一个distribution的话只需要设置一个)。听上去好像有点复杂,实际上Python有一个模块叫做pkgutil,这个模块的作用就是让我们很轻松的去实现上述的功能,你只需要在test/__init__.py下面添加一下两行就可以了。
- import pkgutil
- __path__ = pkgutil.extend_path(__path__, __name__)
其实还有比这个还简单的方法,这里推荐一个文章给大家:http://doughellmann.com/PyMOTW/
0x02 真·钩子:sys.meta_path和sys.path_hooks
让我们继续,接着我们就会去分析import的过程,其实这部分正是这篇文章的重点。截下来说的比如说从zip文件或者是repo里面字节获取模块,或者是动态的去用各种方法建立它们,比如说是web服务、dll或者是RESTful API等等几乎你可以想到的任何的方法。我也会提到一些各个独立模块之间拿坑爹的交互性,比如说一个package检测到自己被导入的时候,它能够适应和扩展自己的接口。接着我们将会讨论一下Python的安全增强沙箱,这个沙箱的作用是用来拒绝访问某些模块或者是改变其某些功能。
这些功能其实都可以通过import hooks来实现。有两种不同的hook,一种叫做meta hook(sys.meta_path),另一种叫做path hook(sys.path_hooks)。尽管他们在两个差不多的导入流的阶段被调用,但是他们被创建的时候还是会取决于两个东西,一个叫做模块查找器(Module Finder),一个叫做模块加载器(Module Loader)。
模块查找器其实是一种简单的用来查找模块的对象,他(find_module)的使用方法如下面所示:
- finder.find_module(fullname, path=None)
他需要把一个完整的模块的名字当做参数传进去,path则为这个模块的路径。这个对象的可以完成以下三件事中的任意一件:
- 抛出一个异常,然后完全取消所有的导入流程
- 返回一个None,意思是被导入的这个模块不能够被这个查找器所找到。但是他仍然可以被导入流的下一个阶段所找到,比如说一些自定义的查找器或者是Python的标准导入机制。
- 返回一个加载器对象用来加载实际的模块。
下一个就是模块加载器,模块加载器其实就是一个用来加载制定模块的对象,它(load_module)的使用方法如下面的代码所示:
- loader.load_module(fullname)
这里需要在强调一次,fullname参数需要传进去一个我们想要加载的模块的全名。返回值应当是一个模块的对象,***的结果当然就是完成导入对象的操作。需要注意的是,这些模块可能已经被导入了,或者是复制这些模块的功能用来返回这些已经存在的模块。下面是这个函数的原型:
- def load_module(self, fullname):
- if fullname in sys.modules: return sys.modules[fullname]
如果在这一阶段出现了任何错误,模块加载器应该抛出一个ImportError的异常
0x03 自己构造一个加载器:
上面这些仅仅是一些理论,其实吧PEP302标准里面都描述了这些。在实际当中,其实模块加载器和模块查找器可以是同一个对象,也就是说find_module可以去return self。举个例子,其实这个简单的hook可以去阻止任何特定的模块被导入:
- #!/usr/bin/env python
- #coding=utf8
- import sys
- class ImportBlocker(object):
- def __init__(self, *args):
- self.module_names = args
- def find_module(self, fullname, path=None):
- if fullname in self.module_names:
- return self
- return None
- def load_module(self, name):
- raise ImportError("%s is blocked and cannot be imported" % name)
- sys.meta_path = [ImportBlocker('httplib')]
一旦我们在sys.meta_path中加载了这个hook,他就会去阻止任何导入的新模块并且检查他是否存在于我们的列表里。如果我们去使用Request库的时候,这个hook也会同样起作用。
Import Request
执行这条语句会失败,因为request是在urllib3内部使用的,进而去限制httplib的使用。但是一个hook要是没事儿干总去拦截调用别的模块似乎没啥太大的意思,咱们换个别的玩法。如果说总是拒绝调用特定的模块,我们为啥不用一个warning去代替呢?这样的话,这个hook就可以帮我们检测被导入到项目当中又被弃用的模块。代码如下:
- # !/usr/bin/env python
- # coding=utf-8
- import logging
- import imp
- import sys
- class WarnOnImport(object):
- def __init__(self, *args):
- self.module_names = args
- def find_module(self, fullname, path=None):
- if fullname in self.module_names:
- self.path = path
- return self
- return None
- def load_module(self, name):
- if name in sys.modules:
- return sys.modules[name]
- module_info = imp.find_module(name, self.path)
- module = imp.load_module(name, *module_info)
- sys.modules[name] = module
- logging.warning("Imported deprecated module %s", name)
- return module
- sys.meta_path = [WarnOnImport('getopt', 'optparse')]
为了去访问一个正常的导入机制,我们可以尝试使用imp。它的find_module和load_module函数和我们要导入的hook具有相同的名字。但是imp提供的功能更强大,比如说还包括了load_source和load_compile这些功能甚至可以从头来初始化一个模块(new_module)。