所谓的模块导入( import
),是指在一个模块中使用另外一个模块的代码的操做,它有利于代码的复用。html
在 Python 中使用 import 关键字来实现这个操做,但不是惟一的方法,还有 importlib.import_module()
和 __import__()
等。python
也许你看到这个标题,会说我怎么会发这么基础的文章?shell
与此相反。偏偏我以为这篇文章的内容能够算是 Python 的进阶技能,会深刻地探讨并以真实案例讲解 Python import Hook 的知识点。json
固然为了使文章更系统、全面,前面会有小篇幅讲解基础知识点,但请你有耐心的日后读下去,由于后面才是本篇文章的精华所在,但愿你不要错过。flask
导入单元有多种,能够是模块、包及变量等。缓存
对于这些基础的概念,对于新手仍是有必要介绍一下它们的区别。服务器
模块:相似 *.py,*.pyc, *.pyd ,*.so,*.dll 这样的文件,是 Python 代码载体的最小单元。网络
包 还能够细分为两种:app
__init__.py
文件的文件夹,此文件夹下可包含其余子包,或者模块关于 Namespace packages,有的人会比较陌生,我这里摘抄官方文档的一段说明来解释一下。框架
Namespace packages 是由多个 部分 构成的,每一个部分为父包增长一个子包。 各个部分可能处于文件系统的不一样位置。 部分也可能处于 zip 文件中、网络上,或者 Python 在导入期间能够搜索的其余地方。 命名空间包并不必定会直接对应到文件系统中的对象;它们有多是无实体表示的虚拟模块。
命名空间包的 __path__
属性不使用普通的列表。 而是使用定制的可迭代类型,若是其父包的路径 (或者最高层级包的 sys.path) 发生改变,这种对象会在该包内的下一次导入尝试时自动执行新的对包部分的搜索。
命名空间包没有 parent/__init__.py
文件。 实际上,在导入搜索期间可能找到多个 parent 目录,每一个都由不一样的部分所提供。 所以 parent/one 的物理位置不必定与 parent/two 相邻。 在这种状况下,Python 将为顶级的 parent 包建立一个命名空间包,不管是它自己仍是它的某个子包被导入。
当咱们 import 导入模块或包时,Python 提供两种导入方式:
你能够根据实际须要进行选择,但有必要说明的是,在早期的版本( Python2.6 以前),Python 默认使用的相对导入。然后来的版本中( Python2.6 以后),都以绝对导入为默认使用的导入方式。
使用绝对路径和相对路径各有利弊:
在 PEP8 中对模块的导入提出了要求,遵照 PEP8规范能让你的代码更具备可读性,我这边也列一下:
# bad import os,sys # good import os import sys
# bad from ..bar import Bar # good from foo.bar import test
import语句应当放在文件头部,置于模块说明及docstring以后,全局变量以前
# 内置模块 import os import sys # 第三方模块 import flask # 本地模块 from foo import bar
sys.path
能够列出 Python 模块查找的目录列表
>>> import sys >>> from pprint import pprint >>> pprint(sys.path) ['', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python36.zip', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload', '/Users/MING/Library/Python/3.6/lib/python/site-packages', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages'] >>>
sys.meta_path
存放的是全部的查找器。
>>> import sys >>> from pprint import pprint >>> pprint(sys.meta_path) [<class '_frozen_importlib.BuiltinImporter'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib_external.PathFinder'>]
sys.path_importer_cache
比 sys.path
会更大点, 由于它会为全部被加载代码的目录记录它们的查找器。 这包括包的子目录,这些一般在 sys.path
中是不存在的。
>>> import sys >>> from pprint import pprint >>> pprint(sys.path_importer_cache) {'/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6': FileFinder('/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6'), '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/collections': FileFinder('/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/collections'), '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/encodings': FileFinder('/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/encodings'), '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload': FileFinder('/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload'), '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages': FileFinder('/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages'), '/Library/Frameworks/Python.framework/Versions/3.6/lib/python36.zip': None, '/Users/MING': FileFinder('/Users/MING'), '/Users/MING/Library/Python/3.6/lib/python/site-packages': FileFinder('/Users/MING/Library/Python/3.6/lib/python/site-packages')}
import 关键字的使用,能够说是基础中的基础。
但这不是模块惟一的方法,还有 importlib.import_module()
和 __import__()
等。
和 import 不一样的是,__import__
是一个函数,也正是由于这个缘由,使得 __import__
的使用会更加灵活,经常用于框架中,对于插件的动态加载。
实际上,当咱们调用 import 导入模块时,其内部也是调用了 __import__
,请看以下两种导入方法,他们是等价的。
# 使用 import import os # 使用 __import__ os = __import__('os')
经过触类旁通,下面两种方法一样也是等价的。
# 使用 import .. as .. import pandas as pd # 使用 __import__ pd = __import__('pandas')
上面我说 __import__
经常用于插件的动态,事实上也只有它能作到(相对于 import 来讲)。
插件
一般会位于某一特定的文件夹下,在使用过程当中,可能你并不会用到所有的插件,也可能你会新增插件。
若是使用 import 关键字这种硬编码的方式,显然太不优雅了,当你要新增/修改插件的时候,都须要你修改代码。更合适的作法是,将这些插件以配置的方式,写在配置文件中,而后由代码去读取你的配置,动态导入你要使用的插件,即灵活又方便,也不容易出错。
假如个人一个项目中,有 plugin01
、plugin02
、plugin03
、plugin04
四个插件,这些插件下都会实现一个核心方法 run()
。但有时候我不想使用所有的插件,只想使用 plugin02
、plugin04
,那我就在配置文件中写我要使用的两个插件。
# my.conf custom_plugins=['plugin02', 'plugin04']
那我如何使用动态加载,并运行他们呢?
# main.py for plugin in conf.custom_plugins: __import__(plugin) sys.modules[plugin].run()
在一个模块内部重复引用另外一个相同模块,实际并不会导入两次,缘由是在使用关键字 import
导入模块时,它会先检索 sys.modules
里是否已经载入这个模块了,若是已经载入,则不会再次导入,若是不存在,才会去检索导入这个模块。
来实验一下,在 my_mod02
这个模块里,我 import 两次 my_mod01
这个模块,按逻辑每一次 import 会一次 my_mod01
里的代码(即打印 in mod01
),可是验证结果是,只打印了一次。
$ cat my_mod01.py print('in mod01') $ cat my_mod02.py import my_mod01 import my_mod01 $ python my_mod02.py in mod01
该现象的解释是:由于有 sys.modules
的存在。
sys.modules
是一个字典(key:模块名,value:模块对象),它存放着在当前 namespace 全部已经导入的模块对象。
# test_module.py import sys print(sys.modules.get('json', 'NotFound')) import json print(sys.modules.get('json', 'NotFound'))
运行结果以下,可见在 导入后 json 模块后,sys.modules
才有了 json 模块的对象。
$ python test_module.py NotFound <module 'json' from 'C:\Python27\lib\json\__init__.pyc'>
因为有缓存的存在,使得咱们没法从新载入一个模块。
但若你想反其道行之,能够借助 importlib 这个神奇的库来实现。事实也确实有此场景,好比在代码调试中,在发现代码有异常并修改后,咱们一般要重启服务再次载入程序。这时候,如有了模块重载,就无比方便了,修改完代码后也无需服务的重启,就能继续调试。
仍是以上面的例子来理解,my_mod02.py
改写成以下
# my_mod02.py import importlib import my_mod01 importlib.reload(my_mod01)
使用 python3 来执行这个模块,与上面不一样的是,这边执行了两次 my_mod01.py
$ python3 my_mod02.py in mod01 in mod01
若是指定名称的模块在 sys.modules
找不到,则将发起调用 Python 的导入协议以查找和加载该模块。
此协议由两个概念性模块构成,即 查找器
和 加载器
。
一个 Python 的模块的导入,其实能够再细分为两个过程:
查找器(finder),简单点说,查找器定义了一个模块查找机制,让程序知道该如何找到对应的模块。
其实 Python 内置了多个默认查找器,其存在于 sys.meta_path 中。
但这些查找器对应使用者来讲,并非那么重要,所以在 Python 3.3 以前, Python 解释将其隐藏了,咱们称之为隐式查找器。
# Python 2.7 >>> import sys >>> sys.meta_path [] >>>
因为这点不利于开发者深刻理解 import 机制,在 Python 3.3 后,全部的模块导入机制都会经过 sys.meta_path 暴露,不会在有任何隐式导入机制。
# Python 3.6 >>> import sys >>> from pprint import pprint >>> pprint(sys.meta_path) [<class '_frozen_importlib.BuiltinImporter'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib_external.PathFinder'>]
观察一下 Python 默认的这几种查找器 (finder),能够分为三种:
那咱们能不能自已定义一个查找器呢?固然能够,你只要
import sys class MyFinder(object): @classmethod def find_module(cls, name, path, target=None): print("Importing", name, path, target) # 将在后面定义 return MyLoader() # 因为 finder 是按顺序读取的,因此必须插入在首位 sys.meta_path.insert(0, MyFinder)
查找器能够分为两种:
object +-- Finder (deprecated) +-- MetaPathFinder +-- PathEntryFinder
这里须要注意的是,在 3.4 版前,查找器会直接返回 加载器(Loader)对象,而在 3.4 版后,查找器则会返回模块规格说明(ModuleSpec),其中 包含加载器。
而关于什么是 加载器 和 模块规格说明, 请继续日后看。
查找器只负责查找定位找模,而真正负责加载模块的,是加载器(loader)。
通常的 loader 必须定义名为 load_module()
的方法。
为何这里说通常,由于 loader 还分多种:
object +-- Finder (deprecated) | +-- MetaPathFinder | +-- PathEntryFinder +-- Loader +-- ResourceLoader --------+ +-- InspectLoader | +-- ExecutionLoader --+ +-- FileLoader +-- SourceLoader
经过查看源码可知,不一样的加载器的抽象方法各有不一样。
加载器一般由一个 finder 返回。详情参见 PEP 302,对于 abstract base class 可参见 importlib.abc.Loader。
那如何自定义咱们本身的加载器呢?
你只要
若你想看具体的例子,能够接着日后看。
导入机制在导入期间会使用有关每一个模块的多种信息,特别是加载以前。 大多数信息都是全部模块通用的。 模块规格说明的目的是基于每一个模块来封装这些导入相关信息。
模块的规格说明会做为模块对象的 __spec__
属性对外公开。 有关模块规格的详细内容请参阅 ModuleSpec
。
在 Python 3.4 后,查找器再也不返回加载器,而是返回 ModuleSpec 对象,它储存着更多的信息
那如何查看一个模块的 ModuleSpec ?
这边举个例子
$ cat my_mod02.py import my_mod01 print(my_mod01.__spec__) $ python3 my_mod02.py in mod01 ModuleSpec(name='my_mod01', loader=<_frozen_importlib_external.SourceFileLoader object at 0x000000000392DBE0>, origin='/home/MING/my_mod01.py')
从 ModuleSpec 中能够看到,加载器是包含在内的,那咱们若是要从新加载一个模块,是否是又有了另外一种思路了?
来一块儿验证一下。
如今有两个文件:
一个是 my_info.py
# my_info.py name='wangbm'
另外一个是:main.py
# main.py import my_info print(my_info.name) # 加一个断点 import pdb;pdb.set_trace() # 再加载一次 my_info.__spec__.loader.load_module() print(my_info.name)
在 main.py
处,我加了一个断点,目的是当运行到断点处时,我修改 my_info.py 里的 name 为 ming
,以便验证重载是否有效?
$ python3 main.py wangbm > /home/MING/main.py(9)<module>() -> my_info.__spec__.loader.load_module() (Pdb) c ming
从结果来看,重载是有效的。
导入器(importer),也许你在其余文章里会见到它,但其实它并非个新鲜的东西。
它只是同时实现了查找器和加载器两种接口的对象,因此你能够说导入器(importer)是查找器(finder),也能够说它是加载器(loader)。
因为 Python 默认的 查找器和加载器 仅支持本地的模块的导入,并不支持实现远程模块的导入。
为了让你更好的理解 Python Import Hook 机制,我下面会经过实例演示,如何本身实现远程导入模块的导入器。
当导入一个包的时候,Python 解释器首先会从 sys.meta_path 中拿到查找器列表。
默认顺序是:内建模块查找器 -> 冻结模块查找器 -> 第三方模块路径(本地的 sys.path)查找器
若通过这三个查找器,仍然没法查找到所需的模块,则会抛出ImportError异常。
所以要实现远程导入模块,有两种思路。
我这里选择第一种方法来作为示例。
实现导入器,咱们须要分别查找器和加载器。
首先是查找器
由源码得知,路径查找器分为两种
这里使用 MetaPathFinder 来进行查找器的编写。
在 Python 3.4 版本以前,查找器必须实现 find_module()
方法,而 Python 3.4+ 版,则推荐使用 find_spec()
方法,但这并不意味着你不能使用 find_module()
,可是在没有 find_spec()
方法时,导入协议仍是会尝试 find_module()
方法。
我先举例下使用 find_module()
该如何写。
from importlib import abc class UrlMetaFinder(abc.MetaPathFinder): def __init__(self, baseurl): self._baseurl = baseurl def find_module(self, fullname, path=None): if path is None: baseurl = self._baseurl else: # 不是原定义的url就直接返回不存在 if not path.startswith(self._baseurl): return None baseurl = path try: loader = UrlMetaLoader(baseurl) loader.load_module(fullname) return loader except Exception: return None
若使用 find_spec()
,要注意此方法的调用须要带有两到三个参数。
第一个是被导入模块的完整限定名称,例如 foo.bar.baz
。 第二个参数是供模块搜索使用的路径条目。 对于最高层级模块,第二个参数为 None
,但对于子模块或子包,第二个参数为父包 __path__
属性的值。 若是相应的 __path__
属性没法访问,将引起 ModuleNotFoundError
。 第三个参数是一个将被做为稍后加载目标的现有模块对象。 导入系统仅会在重加载期间传入一个目标模块。
from importlib import abc from importlib.machinery import ModuleSpec class UrlMetaFinder(abc.MetaPathFinder): def __init__(self, baseurl): self._baseurl = baseurl def find_spec(self, fullname, path=None, target=None): if path is None: baseurl = self._baseurl else: # 不是原定义的url就直接返回不存在 if not path.startswith(self._baseurl): return None baseurl = path try: loader = UrlMetaLoader(baseurl) return ModuleSpec(fullname, loader, is_package=loader.is_package(fullname)) except Exception: return None
接下来是加载器
由源码得知,路径查找器分为三种
按理说,两种加载器均可以实现咱们想要的功能,我这里选用 SourceLoader 来示范。
在 SourceLoader 这个抽象类里,有几个很重要的方法,在你写实现加载器的时候须要注意
module.__dict__
在一些老的博客文章中,你会常常看到 加载器 要实现 load_module()
,而这个方法早已在 Python 3.4 的时候就被废弃了,固然为了兼容考虑,你若使用 load_module()
也是能够的。
from importlib import abc class UrlMetaLoader(abc.SourceLoader): def __init__(self, baseurl): self.baseurl = baseurl def get_code(self, fullname): f = urllib2.urlopen(self.get_filename(fullname)) return f.read() def load_module(self, fullname): code = self.get_code(fullname) mod = sys.modules.setdefault(fullname, imp.new_module(fullname)) mod.__file__ = self.get_filename(fullname) mod.__loader__ = self mod.__package__ = fullname exec(code, mod.__dict__) return None def get_data(self): pass def execute_module(self, module): pass def get_filename(self, fullname): return self.baseurl + fullname + '.py'
当你使用这种旧模式实现本身的加载时,你须要注意两点,很重要:
作为替换,你应该使用 execute_module()
和 create_module()
。因为基类里已经实现了 execute_module
和 create_module()
,而且知足咱们的使用场景。我这边能够不用重复实现。和旧模式相比,这里也不须要在设查找器里手动执行 execute_module()
。
import urllib.request as urllib2 class UrlMetaLoader(importlib.abc.SourceLoader): def __init__(self, baseurl): self.baseurl = baseurl def get_code(self, fullname): f = urllib2.urlopen(self.get_filename(fullname)) return f.read() def get_data(self): pass def get_filename(self, fullname): return self.baseurl + fullname + '.py'
查找器和加载器都有了,别忘了往sys.meta_path 注册咱们自定义的查找器(UrlMetaFinder)。
def install_meta(address): finder = UrlMetaFinder(address) sys.meta_path.append(finder)
全部的代码都解析完毕后,咱们将其整理在一个模块(my_importer.py)中
# my_importer.py import sys import importlib import urllib.request as urllib2 class UrlMetaFinder(importlib.abc.MetaPathFinder): def __init__(self, baseurl): self._baseurl = baseurl def find_module(self, fullname, path=None): if path is None: baseurl = self._baseurl else: # 不是原定义的url就直接返回不存在 if not path.startswith(self._baseurl): return None baseurl = path try: loader = UrlMetaLoader(baseurl) return loader except Exception: return None class UrlMetaLoader(importlib.abc.SourceLoader): def __init__(self, baseurl): self.baseurl = baseurl def get_code(self, fullname): f = urllib2.urlopen(self.get_filename(fullname)) return f.read() def get_data(self): pass def get_filename(self, fullname): return self.baseurl + fullname + '.py' def install_meta(address): finder = UrlMetaFinder(address) sys.meta_path.append(finder)
最开始我说了,要实现一个远程导入模块的方法。
我还缺一个在远端的服务器,来存放个人模块,为了方便,我使用python自带的 http.server
模块用一条命令便可实现。
$ mkdir httpserver && cd httpserver $ cat>my_info.py<EOF name='wangbm' print('ok') EOF $ cat my_info.py name='wangbm' print('ok') $ $ python3 -m http.server 12800 Serving HTTP on 0.0.0.0 port 12800 (http://0.0.0.0:12800/) ... ...
一切准备好,咱们就能够验证了。
>>> from my_importer import install_meta >>> install_meta('http://localhost:12800/') # 往 sys.meta_path 注册 finder >>> import my_info # 打印ok,说明导入成功 ok >>> my_info.name # 验证能够取获得变量 'wangbm'
至此,我实现了一个简易的能够导入远程服务器上的模块的导入器。