# 8.11 【进阶】实现远程导入模块
由于 Python 默认的 查找器和加载器 仅支持本地的模块的导入,并不支持实现远程模块的导入。
为了让你更好的理解 Python Import Hook 机制,我下面会通过实例演示,如何自己实现远程导入模块的导入器。
## 1. 动手实现导入器
当导入一个包的时候,Python 解释器首先会从 sys.meta_path 中拿到查找器列表。
默认顺序是:内建模块查找器 -> 冻结模块查找器 -> 第三方模块路径(本地的 sys.path)查找器
若经过这三个查找器,仍然无法查找到所需的模块,则会抛出ImportError异常。
因此要实现远程导入模块,有两种思路。
- 一种是实现自己的元路径导入器;
- 另一种是编写一个钩子,添加到sys.path_hooks里,识别特定的目录命名模式。
我这里选择第一种方法来做为示例。
实现导入器,我们需要分别查找器和加载器。
**首先是查找器**
由源码得知,路径查找器分为两种
- MetaPathFinder
- PathEntryFinder
这里使用 MetaPathFinder 来进行查找器的编写。
在 Python 3.4 版本之前,查找器必须实现 `find_module()` 方法,而 Python 3.4+ 版,则推荐使用 `find_spec()` 方法,但这并不意味着你不能使用 `find_module()`,但是在没有 `find_spec()` 方法时,导入协议还是会尝试 `find_module()` 方法。
我先举例下使用 `find_module()` 该如何写。
```python
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`](https://docs.python.org/zh-cn/3/library/exceptions.html#ModuleNotFoundError)。 第三个参数是一个将被作为稍后加载目标的现有模块对象。 导入系统仅会在重加载期间传入一个目标模块。
```python
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
```
**接下来是加载器**
由源码得知,路径查找器分为三种
- FileLoader
- SourceLoader
按理说,两种加载器都可以实现我们想要的功能,我这里选用 SourceLoader 来示范。
在 SourceLoader 这个抽象类里,有几个很重要的方法,在你写实现加载器的时候需要注意
- get_code:获取源代码,可以根据自己场景实现实现。
- exec_module:执行源代码,并将变量赋值给 `module.__dict__`
- get_data:抽象方法,必须实现,返回指定路径的字节码。
- get_filename:抽象方法,必须实现,返回文件名
在一些老的博客文章中,你会经常看到 加载器 要实现 `load_module()` ,而这个方法早已在 Python 3.4 的时候就被废弃了,当然为了兼容考虑,你若使用 `load_module()` 也是可以的。
```python
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 必须重载,而且不应该有任何逻辑,即使它并不是抽象方法。
- load_module,需要你在查找器里手动执行,才能实现模块的加载。。
做为替换,你应该使用 `execute_module()` 和 `create_module()` 。由于基类里已经实现了 `execute_module` 和 `create_module()`,并且满足我们的使用场景。我这边可以不用重复实现。和旧模式相比,这里也不需要在设查找器里手动执行 `execute_module()`。
```python
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)。
```python
def install_meta(address):
finder = UrlMetaFinder(address)
sys.meta_path.append(finder)
```
所有的代码都解析完毕后,我们将其整理在一个模块(my_importer.py)中
```python
# 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)
```
## 2. 搭建远程服务端
最开始我说了,要实现一个远程导入模块的方法。
我还缺一个在远端的服务器,来存放我的模块,为了方便,我使用python自带的 `http.server` 模块用一条命令即可实现。
```shell
$ mkdir httpserver && cd httpserver
$ cat>my_info.py<EOF
name='wangbm'
print('ok')
EOF
5sM!ebM5sM!ebMt0fNkt0fNk
$ 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/) ...
...
```
一切准备好,我们就可以验证了。
```python
>>> from my_importer import install_meta
>>> install_meta('http://localhost:12800/') # 往 sys.meta_path 注册 finder
>>> import my_info # 打印ok,说明导入成功
ok
>>> my_info.name # 验证可以取得到变量
'wangbm'
```
至此,我实现了一个简易的可以导入远程服务器上的模块的导入器。
- 第一章:安装运行
- 1.1 【环境】快速安装 Python 解释器
- 1.2 【环境】Python 开发环境的搭建
- 1.3 【基础】两种运行 Python 程序方法
- 第二章:数据类型
- 2.1 【基础】常量与变量
- 2.2 【基础】字符串类型
- 2.3 【基础】整数与浮点数
- 2.4 【基础】布尔值:真与假
- 2.5 【基础】学会输入与输出
- 2.6 【基础】字符串格式化
- 2.6 【基础】运算符(超全整理)
- 第三章:数据结构
- 3.1 【基础】列表
- 3.2 【基础】元组
- 3.3 【基础】字典
- 3.4 【基础】集合
- 3.5 【基础】迭代器
- 3.6 【基础】生成器
- 第四章:控制流程
- 4.1 【基础】条件语句:if
- 4.2 【基础】循环语句:for
- 4.3 【基础】循环语句:while
- 4.4 【进阶】五种推导式
- 第五章:学习函数
- 5.1 【基础】普通函数
- 5.2 【基础】匿名函数
- 5.3 【基础】高阶函数
- 5.4 【基础】反射函数
- 5.5 【基础】偏函数
- 5.6 【进阶】泛型函数
- 5.7 【基础】变量的作用域
- 5.8 【进阶】上下文管理器
- 5.9 【进阶】装饰器的六种写法
- 第六章:错误异常
- 6.1 【基础】什么是异常?
- 6.2 【基础】如何抛出和捕获异常?
- 6.3 【基础】如何自定义异常?
- 6.4 【进阶】如何关闭异常自动关联上下文?
- 6.5 【进阶】异常处理的三个好习惯
- 第七章:类与对象
- 7.1 【基础】类的理解与使用
- 7.2 【基础】静态方法与类方法
- 7.3 【基础】私有变量与私有方法
- 7.4 【基础】类的封装(Encapsulation)
- 7.5 【基础】类的继承(Inheritance)
- 7.6 【基础】类的多态(Polymorphism)
- 7.7 【基础】类的 property 属性
- 7.8 【进阶】类的 Mixin 设计模式
- 7.9 【进阶】类的魔术方法(超全整理)
- 7.10 【进阶】神奇的元类编程(metaclass)
- 7.11 【进阶】深藏不露的描述符(Descriptor)
- 第八章:包与模块
- 8.1 【基础】什么是包、模块和库?
- 8.2 【基础】安装第三方包的八种方法
- 8.3 【基础】导入单元的构成
- 8.4 【基础】导入包的标准写法
- 8.5 【进阶】常规包与空间命名包
- 8.6 【进阶】花式导包的八种方法
- 8.7 【进阶】包导入的三个冷门知识点
- 8.8 【基础】pip 的超全使用指南
- 8.9 【进阶】理解模块的缓存
- 8.10 【进阶】理解查找器与加载器
- 8.11 【进阶】实现远程导入模块
- 8.12 【基础】分发工具:distutils和setuptools
- 8.13 【基础】源码包与二进制包有什么区别?
- 8.14 【基础】eggs与wheels 有什么区别?
- 8.15 【进阶】超详细讲解 setup.py 的编写
- 8.16 【进阶】打包辅助神器 PBR 是什么?
- 8.17 【进阶】开源自己的包到 PYPI 上
- 第九章:调试技巧
- 9.1 【调试技巧】超详细图文教你调试代码
- 9.2 【调试技巧】PyCharm 中指定参数调试程序
- 9.3 【调试技巧】PyCharm跑完后立即进入调试模式
- 9.4 【调试技巧】脚本报错后立即进入调试模式
- 9.5 【调试技巧】使用 PDB 进行无界面调试
- 9.6 【调试技巧】如何调试已经运行的程序?
- 9.7 【调试技巧】使用 PySnopper 调试疑难杂症
- 9.8 【调试技巧】使用 PyCharm 进行远程调试
- 第十章:并发编程
- 10.1 【并发编程】从性能角度初探并发编程
- 10.2 【并发编程】创建多线程的几种方法
- 10.3 【并发编程】谈谈线程中的“锁机制”
- 10.4 【并发编程】线程消息通信机制
- 10.5 【并发编程】线程中的信息隔离
- 10.6 【并发编程】线程池创建的几种方法
- 10.7 【并发编程】从 yield 开始入门协程
- 10.8 【并发编程】深入理解yield from语法
- 10.9 【并发编程】初识异步IO框架:asyncio 上篇
- 10.10 【并发编程】深入异步IO框架:asyncio 中篇
- 10.11 【并发编程】实战异步IO框架:asyncio 下篇
- 10.12 【并发编程】生成器与协程,你分清了吗?
- 10.14 【并发编程】浅谈线程安全那些事儿
- 第十二章:虚拟环境
- 12.1 【虚拟环境】为什么要有虚拟环境?
- 12.2 【虚拟环境】方案一:使用 virtualenv
- 12.3 【虚拟环境】方案二:使用 pipenv
- 12.4 【虚拟环境】方案三:使用 pipx
- 12.5 【虚拟环境】方案四:使用 poetry
- 第十三章:绝佳工具
- 13.1 【静态检查】mypy 的使用
- 13.2 【代码测试】pytest 的使用
- 13.3 【代码提交】pre-commit hook
- 13.4 【项目生成】cookiecutter 的使用
- 第十四章:数据可视化
- 14.1 【可视化之matplotlib】一图带你入门matplotlib
- 14.2 【可视化之matplotlib】详解六种可视化图表
- 14.3 【可视化之matplotlib】 绘制正余弦函数图象
- 14.4 【可视化之matplotlib】难点:子图与子区
- 14.5 【可视化之matplotlib】绘制酷炫的gif动态图
- 14.6 【可视化之matplotlib】自动生成图像视频
- 14.7 【可视化神器】最高级的可视化神器: plotly_express