Python import hook

转自http://blog.konghy.cn/2016/10/25/python-import-hook/,这里有好多好文章

import hook 通常被译为 探针。我们可以认为每当导入模块的时候,所触发的操作就是 import hook。使用 import 的 hook 机制可以让我们做很多事情,比如加载网络上的模块,在导入模块时对模块进行修改,自动安装缺失模块,上传审计信息,延迟加载等等。

理解 import hook 需要先了解 Python 导入模块的过程。

一、 导入过程

Python 通常使用 import 语句来实现类库的引用,当然内建的 __import__() 函数等都能实现。 import 语句负责做两件事:

  • 查找模块
  • 加载模块到当前名字空间

那么,一个模块的导入过程大致可以分为三个步骤:搜索加载名字绑定

1.1 搜索

搜索是整个导入过程的核心,也是最为复杂的一步。这个过程主要是完成查找要引入模块的功能,查找的过程如下:

  • 1、在缓存 sys.modules 中查找要导入的模块,若找到则直接返回该模块对象
  • 2、如果在 sys.modules 中没有找到相应模块的缓存,则顺序搜索 sys.meta_path,逐个借助其中的 finder 来查找模块,若找到则加载后返回相应模块对象。
  • 3、如果以上步骤都没找到该模块,则执行默认导入。即如果模块在一个包中(如import a.b),则以 a.__path__ 为搜索路径进行查找;如果模块不在一个包中(如import a),则以 sys.path 为搜索路径进行查找。
  • 4、如果都未找到,则抛出 ImportError 异常。

查找过程也会检查?些隐式的 finder 对象,不同的 Python 实现有不同的隐式finder,但是都会有 sys.path_hooks, sys.path_importer_cache 以及sys.path

1.2 加载

对于搜索到的模块,如果在缓存 sys.modules 中则直接返回模块对象,否则就需要加载模块以创建一个模块对象。加载是对模块的初始化处理,包括以下步骤:

  • 设置属性:包括 __name____file____package____loader____path__
  • 编译源码:将模块文件(对于包,则是其对应的 __init__.py 文件)编译为字节码(*.pyc 或者 *.pyo),如果字节码文件已存在且仍然是最新的,则不重新编译
  • 执行字节码:执行编译生成的字节码(即模块文件或 __init__.py 文件中的语句)

需要注意的是,加载不只是发生在导入时,还可以发生在 reload 时。

1.3 名字绑定

加载完模块后,作为最后一步,import 语句会为 导入的对象 绑定名字,并把这些名字加入到当前的名字空间中。其中,导入的对象 根据导入语句的不同有所差异:

  • 如果导入语句为 import obj,则对象 obj 可以是包或者模块
  • 如果导入语句为 from package import obj,则对象 obj 可以是 package 的子包、package 的属性或者 package 的子模块
  • 如果导入语句为 from module import obj,则对象 obj 只能是 module 的属性

二、模块缓存

进行搜索时,搜索的第一个地方是便是 sys.modulessys.modules 是一个字典,键字为模块名,键值为模块对象。它包含了从 Python 开始运行起,被导入的所有模块的一个缓存,包括中间路径。所以,假如 foo.bar.baz 前期已被导入,那么,sys.modules 将包含进入 foo,foo.bar 和 foo.bar.baz的入口。每个键都有自己的数值,都有对应的模块对象。也就是说,如果导入 foo.bar.baz 则整个层次结构下的模块都被加载到了内存。

可以删除 sys.modules 中对应的的键或者将值设置为 None 来使缓存无效。

当启动 Python 解释器时,打印一下 sys.modules 中的 key:

>>> import sys
>>> sys.modules.keys()
[‘copy_reg‘, ‘sre_compile‘, ‘_sre‘, ‘encodings‘, ‘site‘, ‘__builtin__‘, ‘sysconfig‘, ‘__main__‘, ‘encodings.encodings‘, ‘abc‘, ‘posixpath‘, ‘_weakrefset‘, ‘errno‘, ‘encodings.codecs‘, ‘sre_constants‘, ‘re‘, ‘_abcoll‘, ‘types‘, ‘_codecs‘, ‘encodings.__builtin__‘, ‘_warnings‘, ‘genericpath‘, ‘stat‘, ‘zipimport‘, ‘_sysconfigdata‘, ‘warnings‘, ‘UserDict‘, ‘encodings.utf_8‘, ‘sys‘, ‘codecs‘, ‘readline‘, ‘_sysconfigdata_nd‘, ‘os.path‘, ‘sitecustomize‘, ‘signal‘, ‘traceback‘, ‘linecache‘, ‘posix‘, ‘encodings.aliases‘, ‘exceptions‘, ‘sre_parse‘, ‘keyrings‘, ‘os‘, ‘_weakref‘]

可以看出一些模块已经被解释器导入,但是我们却不能直接使用这些模块。这是因为这些模块还没有被绑定到当前名字空间,仍然需要执行 import 语句才能完成名字绑定。

三、查找器和加载器

在搜索过程中我们提到 sys.meta_path 中保存了一些 finder 对象。在 Python 查找的时候,如果在 sys.modules 中没有查找到,就会依次调用 sys.meta_path 中的 finder 对象,即调用导入协议来查找和加载模块。导入协议包含两个概念性的对象,查找器(loader) 和 加载器(loader)。sys.meta_path 在任何默认查找程序或 sys.path 之前搜索。默认的情况下,在 Python2 中 sys.meta_path 是一个空列表,并没有任何 finder 对象;而在 Python3 中则在 Python 中则默认包含三个查找器:第一个知道如何定位内置模块,第二个知道如何定位冻结模块,第三个搜索模块的导入路径:

[<class ‘_frozen_importlib.BuiltinImporter‘>, <class ‘_frozen_importlib.FrozenImporter‘>, <class ‘_frozen_importlib.PathFinder‘>]

在 Python 中,不仅定义了 finder 和 loader 的概念,还定义了 importor 的概念:

  • 查找器(finder): 决定自己是否能够通过运用其所知的任何策略找到相应的模块。在 Python2 中,finder 对象必须实现 find_module() 方法,在 Python3 中必须要实现 find_module() 或者 find_loader() 方法。如果 finder 可以查找到模块,则会返回一个 loader 对象(在 Python 3.4中,修改为返回一个模块分支module specs,加载器在导入中仍被使用,但几乎没有责任),没有找到则返回 None。
  • 加载器(loader): 负责加载模块,它必须实现一个 load_module() 的方法
  • 导入器(importer): 实现了 finder 和 loader 这两个接口的对象称为导入器

我们可以想 sys.meta_path 中添加一些自定义的加载器,来实现在加载模块时对模块进行修改。例如一个简单的例子,在每次加载模块时打印模块信息:

from __future__ import print_function
import sys

class Watcher(object):

    @classmethod
    def find_module(cls, name, path, target=None):
        print("Importing", name, path, target)
        return None

sys.meta_path.insert(0, Watcher)

import subprocess

输出结果:

Importing subprocess None None
Importing gc None None
Importing time None None
Importing select None None
Importing fcntl None None
Importing pickle None None
Importing marshal None None
Importing struct None None
Importing _struct None None
Importing org None None
Importing binascii None None
Importing cStringIO None None

四、导入钩子程序

Python 的导入机制被设计为可扩展的,其基础的运行机制便是 import hook(导入钩子程序)。Python 存在两种导入钩子程序的形态:一类是上文提到的 meta hook(元钩子程序), 另一类是 path hook(导入路径钩子程序)

在其他任何导入程序运行之前,除了 sys.modules 缓存查找,在导入处理开始时调用元钩子程序。这就允许元钩子程序覆盖 sys.path 处理程序,冻结模块,或甚至内建模块。可以通过给 sys.meta_path 添加新的查找器对象来注册元钩子程序。

当相关路径项被冲突时,导入路径钩子程序作为 sys.path (或者 package.__path__) 处理程序的一部分被调用。可以通过给 sys.path_hooks 添加新的调用来注册导入路径钩子程序。

sys.path_hooks 是由可被调用的对象组成,它会顺序的检查以决定他们是否可以处理给定的 sys.path 的一项。每个对象会使用 sys.path 项的路径来作为参数被调用。如果它不能处理该路径,就必须抛出 ImportError 异常,如果可以,则会返回一个 importer 对象。之后,不会再尝试其它的 sys.path_hooks 对象,即使前一个 importer 出错了。

通过 import hook 我们可以根据需求来扩展 Python 的 import 机制。一个简单的使用导入钩子的实例,在 import 时判断库是否被安装,否则就自动安装:

from __future__ import print_function
import sys
import pip
from importlib import import_module

class AutoInstall(object):

    _loaded = set()

    @classmethod
    def find_module(cls, name, path, target=None):
        if path is None and name not in cls._loaded:
            cls._loaded.add(name)
            print("Installing", name)
            installed = pip.main(["install", name])
            if installed == 0:
                return import_module(name)
            else:
                return None

sys.meta_path.append(AutoInstall)

Python 还提供了一些模块和函数,可以用来实现简单的 import hook,主要有一下几种:

  • __import__: Python 的内置函数;
  • imputil: Python 的 import 工具库,在 Python2.6 被声明废弃,Python3 中彻底移除;
  • imp: Python2 和 Python3 都存在的一个 import 库;
  • importlib: Python3 中最新添加,backport 到 Python2.7,但只有很小的子集(只有一个 import_module 函数)。

五、site 模块

site 模块用于 python 程序启动的时候,做一些自定义的处理。在 Python 程序运行前,site 模块会自动导入,并按照如下顺序完成初始化工作:

  • sys.prefixsys.exec_prefixlib/pythonX.Y/site-packages 合成 module 的 search path。加入sys.path。eg: /home/jay/env/tornado/lib/python2.7/site-packages
  • 在添加的路径下寻找 pth 文件。 该文件中描述了添加到 sys.path 的子文件夹路径。
  • import sitecustomize, sitecustomize 内部可以做任意的设置。
  • import usercustomize, usercustomize 一般放在用户的 path 环境下, 如: /home/jay/.local/lib/python2.7/site-packages/usercustomize, 其内部可以做任意的设置。

site 模块的本质可以说是补充 sys.path 路径,协助解释器预配置第三方模块目录。所以可以设置特殊的 sitecustomize.py 或者 usercustomize.py 文件, 在 python 代码执行之前,添加 import hook

六、导入搜索路径

Python 在 import 时会在系统中搜索模块或者包所在的位置,sys.path 变量中保存了所有可搜索的库路径,它是一个路径名的列表,其中的路径主要分为以下几部分:

  • 程序主目录(默认定义): 如果是以脚本方式启动的程序,则为启动脚本所在目录;如果在交互式解释器中,则为当前目录;
  • PYTHONPATH目录(可选扩展): 以 os.pathsep 分隔的多个目录名,即环境变量 os.environ[‘PYTHONPATH‘](类似 shell 环境变量 PATH);
  • 标准库目录(默认定义): Python 标准库所在目录(与安装目录有关);
  • .pth文件目录(可选扩展): 以 “.pth” 为后缀的文件,其中列有一些目录名(每行一个目录名)。

因此如果想要添加库的搜索路径,可以有如下方法:

  • 直接修改 sys.path 列表
  • 使用 PYTHONPATH 扩展
  • 使用 .pth 文件扩展

七、重新加载

关于 import,还有一点非常关键:加载只在第一次导入时发生。Python 这样设计的目的是因为加载是个代价高昂的操作。

通常情况下,如果模块没有被修改,这正是我们想要的行为;但如果我们修改了某个模块,重复导入不会重新加载该模块,从而无法起到更新模块的作用。有时候我们希望在 运行时(即不终止程序运行的同时),达到即时更新模块的目的,内建函数 reload() 提供了这种 重新加载 机制(在 Python3 中被挪到了 imp 模块下)。

关于 reload 与 import 的不同:

  • import 是语句,而 reload 是函数
  • import 使用 模块名,而 reload 使用 模块对象(即已被import语句成功导入的模块)

重新加载 reload(module) 有以下几个特点:

  • 会重新编译和执行模块文件中的顶层语句
  • 会更新模块的名字空间(字典 M.__dict__):覆盖相同的名字(旧的有,新的也有),保留缺失的名字(旧的有,新的没有),添加新增的名字(旧的没有,新的有)
  • 对于由 import M 语句导入的模块 M:调用 reload(M) 后,M.x 为 新模块 的属性 x(因为更新M后,会影响M.x的求值结果)
  • 对于由 from M import x 语句导入的属性 x:调用 reload(M) 后,x 仍然是 旧模块 的属性 x(因为更新M后,不会影响x的求值结果)
  • 如果在调用 reload(M) 后,重新执行 import M(或者from M import x)语句,那么 M.x(或者x)为 新模块 的属性 x

八、参考资料

原文地址:https://www.cnblogs.com/wuyongqiang/p/9810173.html

时间: 2024-11-09 08:34:19

Python import hook的相关文章

Python import 【总结】

Python import总结 1     前言 可能网上很多文章或博客都没解释清楚,作者自己也苦心于Python的import.至此,把自己的总结的分享给大家,本文不做基础讲解,仅说明疑惑的地方. 新版本的Pycharm 2017.1.1,对自己定义的模块都有提示,写代码的提示(如方法,变量). 2     目录 3     本质 import的本质,针对包和模块来说,对于版本Python2和Python3来说,意义一样的,仅拿Python2做实验说明,Python3不做赘述.Python2版

python import 搜索路径 路径设置 pythonpath 库

python import 导入概述 在一个导入语句中的模块名起到两个作用:识别加载的外部文档,也会变成赋值给被载入模块的变量,模块定义的对象也会在执行时创建,就是在Import执行时,import会一次运行在目标文档中的语句从而建立其中的内容 程序第一次导入指定文件文件时执行的步骤: 找到模块文件 编译成位码(如果需要) 执行模块的代码来创建其所定义的对象 这三个步骤只在程序执行时, 模块第一次导入时才会进行,在这之后导入相同模块时,会跳过这三个步骤,而只是提取内存中已加载的模块对象 impo

python import 与 from .... import ...区别

在python用import或者from...import来导入相应的模块. 模块其实就一些函数和类的集合文件,它能实现一些相应的功能,当我们需要使用这些功能的时候, 直接把相应的模块导入到我们的程序中,我们就可以使用了. pycharm 工具 #!/bin/python # filename:test.py import time; print time.ctime() time.sleep(5) print time.ctime() 打印: C:\Python27\python.exe C:

python import this

>>> import thisThe Zen of Python, by Tim Peters Beautiful is better than ugly.Explicit is better than implicit.Simple is better than complex.Complex is better than complicated.Flat is better than nested.Sparse is better than dense.Readability cou

python import 自己写的模块

在使用python 的 import 时,可能会发生 "TypeError: 'module' object is not callable" 这个信息是说你试图把这个模块作为一个函数来调用,但它却无法调用. 一个模块里有a,b两个函数, 要调用函数a,必须先调用整个模块,再调用函数a 举个例子:

python import 自己的包

在写python时,有时候写的一个python文件可能需要被其他python文件所用,那么可以用导入包 import 的 方式: 1.自己写的包放到哪里? >>> import sys >>> sys.path ['', '/usr/lib64/python34.zip', '/usr/lib64/python3.4', '/usr/lib64/python3.4/plat-linux', '/usr/lib64/python3.4/lib-dynload', '/us

Python import 自定义模块

1.如果导入的模块和主程序在同个目录下,直接import就行了 2.如果导入的模块是在主程序所在目录的子目录下,可以在子目录中增加一个空白的__init__.py文件,该文件使得python解释器能将子目录其他.py文件当成一个模块,然后直接通过"import 子目录.模块"导入即可. 比如: import sub.test                    #sub为子目录,test为模块名 sub.test.function()                #调用模块函数 3

Python import 模块导入问题

最近在用Python做决策树(Decision tree)时, 遇见了一个以前没有遇到的问题,就是用'import sklearn.tree'时一切正常,但是'import sklearn..... sklearn.tree'时却报错说: AttributeError: 'module' object has no attribute 'tree'. python的导入机制是这样的,在用 'import sklearn' 时,它只会导入 \${sklearn_dir}/__init__.py 里

python import 和 from.........import 模块加载和作用域

import导入:如import moduleName 变量名moduleNmae有两个目的:识别要被载入的外部文件同时生成脚本中的变量,在文件加载后,用来引用模块对象:因为import使一个变量名引用整个模块对象,我们必须通过模块名称来得到该模块的属性 from语句: from会把模块内的变量名赋值到另一个作用域(把模块中的变量名并且在from字句中选择的复制到了进行导入的作用域之内),所以它就可以让我们直接在脚本中使用复制后的变量而不用通过模块 默认情况下,Python只对每个文件的每个进程