python-cookbook
  • Introduction
  • 第 1 章 数据结构和算法
    • 1.1 解压序列赋值给多个变量
    • 1.2 解压可迭代对象赋值给多个变量
    • 1.3 保留最后N个元素
    • 1.4 查找最大或最小的N个元素
    • 1.5 实现一个优先级队列
    • 1.6 字典中的键映射多个值
    • 1.7 字典排序
    • 1.8 字典的运算
    • 1.9 查找两字典的相同点
    • 1.10 删除序列相同元素并保持顺序
    • 1.11 命名切片
    • 1.12 序列中出现次数最多的元素
    • 1.13 通过某个关键字排序一个字典列表
    • 1.14 排序不支持原生比较的对象
    • 1.15 通过某个字段将记录分组
    • 1.16 过滤序列元素
    • 1.17 从字典中提取子集
    • 1.18 映射名称到序列元素
    • 1.19 转换并同时计算数据
    • 1.20 合并多个字典或映射
  • 第 2 章 字符串和文本
    • 2.1 使用多个界定符分割字符串
    • 2.2 字符串开头或结尾匹配
    • 2.3 用Shell通配符匹配字符串
    • 2.4 字符串匹配和搜索
    • 2.5 字符串搜索和替换
    • 2.6 字符串忽略大小写的搜索替换
    • 2.7 最短匹配模式
    • 2.8 多行匹配模式
    • 2.9 将Unicode文本标准化
    • 2.10 在正则式中使用Unicode
    • 2.11 删除字符串中不需要的字符
    • 2.12 审查清理文本字符串
    • 2.13 字符串对齐
    • 2.14 合并拼接字符串
    • 2.15 字符串中插入变量
    • 2.16 以指定列宽格式化字符串
    • 2.17 在字符串中处理html和xml
    • 2.18 字符串令牌解析
    • 2.19 实现一个简单的递归下降分析器
    • 2.20 字节字符串上的字符串操作
  • 第 3 章 数字日期和时间
    • 3.1 数字的四舍五入
    • 3.2 执行精确的浮点数运算
    • 3.3 数字的格式化输出
    • 3.4 二八十六进制整数
    • 3.5 字节到大整数的打包与解包
    • 3.6 复数的数学运算
    • 3.7 无穷大与NaN
    • 3.8 分数运算
    • 3.9 大型数组运算
    • 3.10 矩阵与线性代数运算
    • 3.11 随机选择
    • 3.12 基本的日期与时间转换
    • 3.13 计算最后一个周五的日期
    • 3.14 计算当前月份的日期范围
    • 3.15 字符串转换为日期
    • 3.16 结合时区的日期操作
  • 第 4 章 迭代器与生成器
    • 4.1 手动遍历迭代器
    • 4.2 代理迭代
    • 4.3 使用生成器创建新的迭代模式
    • 4.4 实现迭代器协议
    • 4.5 反向迭代
    • 4.6 带有外部状态的生成器函数
    • 4.7 迭代器切片
    • 4.8 跳过可迭代对象的开始部分
    • 4.9 排列组合的迭代
    • 4.10 序列上索引值迭代
    • 4.11 同时迭代多个序列
    • 4.12 不同集合上元素的迭代
    • 4.13 创建数据处理管道
    • 4.14 展开嵌套的序列
    • 4.15 顺序迭代合并后的排序迭代对象
    • 4.16 迭代器代替while无限循环
  • 第 5 章 文件与 IO
    • 5.1 读写文本数据
    • 5.2 打印输出至文件中
    • 5.3 使用其他分隔符或行终止符打印
    • 5.4 读写字节数据
    • 5.5 文件不存在才能写入
    • 5.6 字符串的I-O操作
    • 5.7 读写压缩文件
    • 5.8 固定大小记录的文件迭代
    • 5.9 读取二进制数据到可变缓冲区中
    • 5.10 内存映射的二进制文件
    • 5.11 文件路径名的操作
    • 5.12 测试文件是否存在
    • 5.13 获取文件夹中的文件列表
    • 5.14 忽略文件名编码
    • 5.15 打印不合法的文件名
    • 5.16 增加或改变已打开文件的编码
    • 5.17 将字节写入文本文件
    • 5.18 将文件描述符包装成文件对象
    • 5.19 创建临时文件和文件夹
    • 5.20 与串行端口的数据通信
    • 5.21 序列化Python对象
  • 第 6 章 数据编码和处理
    • 6.1 读写CSV数据
    • 6.2 读写JSON数据
    • 6.3 解析简单的XML数据
    • 6.4 增量式解析大型XML文件
    • 6.5 将字典转换为XML
    • 6.6 解析和修改XML
    • 6.7 利用命名空间解析XML文档
    • 6.8 与关系型数据库的交互
    • 6.9 编码和解码十六进制数
    • 6.10 编码解码Base64数据
    • 6.11 读写二进制数组数据
    • 6.12 读取嵌套和可变长二进制数据
    • 6.13 数据的累加与统计操作
  • 第 7 章 函数
    • 7.1 可接受任意数量参数的函数
    • 7.2 只接受关键字参数的函数
    • 7.3 给函数参数增加元信息
    • 7.4 返回多个值的函数
    • 7.5 定义有默认参数的函数
    • 7.6 定义匿名或内联函数
    • 7.7 匿名函数捕获变量值
    • 7.8 减少可调用对象的参数个数
    • 7.9 将单方法的类转换为函数
    • 7.10 带额外状态信息的回调函数
    • 7.11 内联回调函数
    • 7.12 访问闭包中定义的变量
  • 第 8 章 类与对象
    • 8.1 改变对象的字符串显示
    • 8.2 自定义字符串的格式化
    • 8.3 让对象支持上下文管理协议
    • 8.4 创建大量对象时节省内存方法
    • 8.5 在类中封装属性名
    • 8.6 创建可管理的属性
    • 8.7 调用父类方法
    • 8.8 子类中扩展property
    • 8.9 创建新的类或实例属性
    • 8.10 使用延迟计算属性
    • 8.11 简化数据结构的初始化
    • 8.12 定义接口或者抽象基类
    • 8.13 实现数据模型的类型约束
    • 8.14 实现自定义容器
    • 8.15 属性的代理访问
    • 8.16 在类中定义多个构造器
    • 8.17 创建不调用init方法的实例
    • 8.18 利用Mixins扩展类功能
    • 8.19 实现状态对象或者状态机
    • 8.20 通过字符串调用对象方法
    • 8.21 实现访问者模式
    • 8.22 不用递归实现访问者模式
    • 8.23 循环引用数据结构的内存管理
    • 8.24 让类支持比较操作
    • 8.25 创建缓存实例
  • 第 9 章 元编程
    • 9.1 在函数上添加包装器
    • 9.2 创建装饰器时保留函数元信息
    • 9.3 解除一个装饰器
    • 9.4 定义一个带参数的装饰器
    • 9.5 可自定义属性的装饰器
    • 9.6 带可选参数的装饰器
    • 9.7 利用装饰器强制函数上的类型检查
    • 9.8 将装饰器定义为类的一部分
    • 9.9 将装饰器定义为类
    • 9.10 为类和静态方法提供装饰器
    • 9.11 装饰器为被包装函数增加参数
    • 9.12 使用装饰器扩充类的功能
    • 9.13 使用元类控制实例的创建
    • 9.14 捕获类的属性定义顺序
    • 9.15 定义有可选参数的元类
    • 9.16 args和*kwargs的强制参数签名
    • 9.17 在类上强制使用编程规约
    • 9.18 以编程方式定义类
    • 9.19 在定义的时候初始化类的成员
    • 9.20 利用函数注解实现方法重载
    • 9.21 避免重复的属性方法
    • 9.22 定义上下文管理器的简单方法
    • 9.23 在局部变量域中执行代码
    • 9.24 解析与分析Python源码
    • 9.25 拆解Python字节码
  • 第 10 章 模块与包
    • 10.1 构建一个模块的层级包
    • 10.2 控制模块被全部导入的内容
    • 10.3 使用相对路径名导入包中子模块
    • 10.4 将模块分割成多个文件
    • 10.5 利用命名空间导入目录分散的代码
    • 10.6 重新加载模块
    • 10.7 运行目录或压缩文件
    • 10.8 读取位于包中的数据文件
    • 10.9 将文件夹加入到sys.path
    • 10.10 通过字符串名导入模块
    • 10.11 通过钩子远程加载模块
    • 10.12 导入模块的同时修改模块
    • 10.13 安装私有的包
    • 10.14 创建新的Python环境
    • 10.15 分发包
  • 第 11 章 网络与 Web 编程
    • 11.1 作为客户端与HTTP服务交互
    • 11.2 创建TCP服务器
    • 11.3 创建UDP服务器
    • 11.4 通过CIDR地址生成对应的IP地址集
    • 11.5 创建一个简单的REST接口
    • 11.6 通过XML-RPC实现简单的远程调用
    • 11.7 在不同的Python解释器之间交互
    • 11.8 实现远程方法调用
    • 11.9 简单的客户端认证
    • 11.10 在网络服务中加入SSL
    • 11.11 进程间传递Socket文件描述符
    • 11.12 理解事件驱动的IO
    • 11.13 发送与接收大型数组
  • 第 12 章 并发编程
    • 12.1 启动与停止线程
    • 12.2 判断线程是否已经启动
    • 12.3 线程间通信
    • 12.4 给关键部分加锁
    • 12.5 防止死锁的加锁机制
    • 12.6 保存线程的状态信息
    • 12.7 创建一个线程池
    • 12.8 简单的并行编程
    • 12.9 Python的全局锁问题
    • 12.10 定义一个Actor任务
    • 12.11 实现消息发布-订阅模型
    • 12.12 使用生成器代替线程
    • 12.13 多个线程队列轮询
    • 12.14 在Unix系统上面启动守护进程
  • 第 13 章 脚本编程与系统管理
    • 13.1 通过重定向-管道-文件接受输入
    • 13.2 终止程序并给出错误信息
    • 13.3 解析命令行选项
    • 13.4 运行时弹出密码输入提示
    • 13.5 获取终端的大小
    • 13.6 执行外部命令并获取它的输出
    • 13.7 复制或者移动文件和目录
    • 13.8 创建和解压归档文件
    • 13.9 通过文件名查找文件
    • 13.10 读取配置文件
    • 13.11 给简单脚本增加日志功能
    • 13.12 给函数库增加日志功能
    • 13.13 实现一个计时器
    • 13.14 限制内存和CPU的使用量
    • 13.15 启动一个WEB浏览器
  • 第 14 章 测试、调试和异常
    • 14.1 测试stdout输出
    • 14.2 在单元测试中给对象打补丁
    • 14.3 在单元测试中测试异常情况
    • 14.4 将测试输出用日志记录到文件中
    • 14.5 忽略或期望测试失败
    • 14.6 处理多个异常
    • 14.7 捕获所有异常
    • 14.8 创建自定义异常
    • 14.9 捕获异常后抛出另外的异常
    • 14.10 重新抛出被捕获的异常
    • 14.11 输出警告信息
    • 14.12 调试基本的程序崩溃错误
    • 14.13 给你的程序做性能测试
    • 14.14 加速程序运行
  • 第 15 章 C 语言扩展
    • 15.1 使用ctypes访问C代码
    • 15.2 简单的C扩展模块
    • 15.3 编写扩展函数操作数组
    • 15.4 在C扩展模块中操作隐形指针
    • 15.5 从扩张模块中定义和导出C的API
    • 15.6 从C语言中调用Python代码
    • 15.7 从C扩展中释放全局锁
    • 15.8 C和Python中的线程混用
    • 15.9 用WSIG包装C代码
    • 15.10 用Cython包装C代码
    • 15.11 用Cython写高性能的数组操作
    • 15.12 将函数指针转换为可调用对象
    • 15.13 传递NULL结尾的字符串给C函数库
    • 15.14 传递Unicode字符串给C函数库
    • 15.15 C字符串转换为Python字符串
    • 15.16 不确定编码格式的C字符串
    • 15.17 传递文件名给C扩展
    • 15.18 传递已打开的文件给C扩展
    • 15.19 从C语言中读取类文件对象
    • 15.20 处理C语言中的可迭代对象
    • 15.21 诊断分段错误
Powered by GitBook
On this page
  • 问题
  • 解决方案
  • 讨论

Was this helpful?

  1. 第 10 章 模块与包

10.11 通过钩子远程加载模块

问题

你想自定义 Python 的 import 语句,使得它能从远程机器上面透明的加载模块。

解决方案

首先要提出来的是安全问题。

本节核心是设计导入语句的扩展功能。有很多种方法可以做这个, 不过为了演示的方便,我们开始先构造下面这个 Python 代码结构:

testcode/
    spam.py
    fib.py
    grok/
        __init__.py
        blah.py

这些文件的内容并不重要,不过我们在每个文件中放入了少量的简单语句和函数, 这样你可以测试它们并查看当它们被导入时的输出。例如:

# spam.py
print("I'm spam")

def hello(name):
    print('Hello %s' % name)

# fib.py
print("I'm fib")

def fib(n):
    if n < 2:
        return 1
    else:
        return fib(n-1) + fib(n-2)

# grok/__init__.py
print("I'm grok.__init__")

# grok/blah.py
print("I'm grok.blah")

这里的目的是允许这些文件作为模块被远程访问。 也许最简单的方式就是将它们发布到一个 web 服务器上面。在testcode 目录中像下面这样运行 Python:

bash % cd testcode
bash % python3 -m http.server 15000
Serving HTTP on 0.0.0.0 port 15000 ...

服务器运行起来后再启动一个单独的 Python 解释器。 确保你可以使用 urllib 访问到远程文件。例如:

>>> from urllib.request import urlopen
>>> u = urlopen('http://localhost:15000/fib.py')
>>> data = u.read().decode('utf-8')
>>> print(data)
# fib.py

为了替代手动的通过 urlopen() 来收集源文件, 我们通过自定义 import 语句来在后台自动帮我们做到。

加载远程模块的第一种方法是创建一个显示的加载函数来完成它。例如:

import imp
import urllib.request
import sys

def load_module(url):
    u = urllib.request.urlopen(url)
    source = u.read().decode('utf-8')
    mod = sys.modules.setdefault(url, imp.new_module(url))
    code = compile(source, url, 'exec')
    mod.__file__ = url
    mod.__package__ = ''
    exec(code, mod.__dict__)
    return mod

这个函数会下载源代码,并使用 compile() 将其编译到一个代码对象中, 然后在一个新创建的模块对象的字典中来执行它。下面是使用这个函数的方式:

>>> fib = load_module('http://localhost:15000/fib.py')
I'm fib
>>> fib.fib(10)
89
>>> fib
<module 'http://localhost:15000/fib.py' from 'http://localhost:15000/fib.py'>

对于简单的模块这个是行得通的。 不过它并没有嵌入到通常的 import 语句中,如果要支持更高级的结构比如包就需要更多的工作了。

一个更酷的做法是创建一个自定义导入器。第一种方法是创建一个元路径导入器。如下:

# urlimport.py
import sys
import importlib.abc
import imp
from urllib.request import urlopen
from urllib.error import HTTPError, URLError
from html.parser import HTMLParser

# Debugging
import logging
log = logging.getLogger(__name__)

# Get links from a given URL
def _get_links(url):
    class LinkParser(HTMLParser):
        def handle_starttag(self, tag, attrs):
            if tag == 'a':
                attrs = dict(attrs)
                links.add(attrs.get('href').rstrip('/'))
    links = set()
    try:
        log.debug('Getting links from %s' % url)
        u = urlopen(url)
        parser = LinkParser()
        parser.feed(u.read().decode('utf-8'))
    except Exception as e:
        log.debug('Could not get links. %s', e)
    log.debug('links: %r', links)
    return links

class UrlMetaFinder(importlib.abc.MetaPathFinder):
    def __init__(self, baseurl):
        self._baseurl = baseurl
        self._links = { }
        self._loaders = { baseurl : UrlModuleLoader(baseurl) }

    def find_module(self, fullname, path=None):
        log.debug('find_module: fullname=%r, path=%r', fullname, path)
        if path is None:
            baseurl = self._baseurl
        else:
            if not path[0].startswith(self._baseurl):
                return None
            baseurl = path[0]
        parts = fullname.split('.')
        basename = parts[-1]
        log.debug('find_module: baseurl=%r, basename=%r', baseurl, basename)

        # Check link cache
        if basename not in self._links:
            self._links[baseurl] = _get_links(baseurl)

        # Check if it's a package
        if basename in self._links[baseurl]:
            log.debug('find_module: trying package %r', fullname)
            fullurl = self._baseurl + '/' + basename
            # Attempt to load the package (which accesses __init__.py)
            loader = UrlPackageLoader(fullurl)
            try:
                loader.load_module(fullname)
                self._links[fullurl] = _get_links(fullurl)
                self._loaders[fullurl] = UrlModuleLoader(fullurl)
                log.debug('find_module: package %r loaded', fullname)
            except ImportError as e:
                log.debug('find_module: package failed. %s', e)
                loader = None
            return loader
        # A normal module
        filename = basename + '.py'
        if filename in self._links[baseurl]:
            log.debug('find_module: module %r found', fullname)
            return self._loaders[baseurl]
        else:
            log.debug('find_module: module %r not found', fullname)
            return None

    def invalidate_caches(self):
        log.debug('invalidating link cache')
        self._links.clear()

# Module Loader for a URL
class UrlModuleLoader(importlib.abc.SourceLoader):
    def __init__(self, baseurl):
        self._baseurl = baseurl
        self._source_cache = {}

    def module_repr(self, module):
        return '<urlmodule %r from %r>' % (module.__name__, module.__file__)

    # Required method
    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.rpartition('.')[0]
        exec(code, mod.__dict__)
        return mod

    # Optional extensions
    def get_code(self, fullname):
        src = self.get_source(fullname)
        return compile(src, self.get_filename(fullname), 'exec')

    def get_data(self, path):
        pass

    def get_filename(self, fullname):
        return self._baseurl + '/' + fullname.split('.')[-1] + '.py'

    def get_source(self, fullname):
        filename = self.get_filename(fullname)
        log.debug('loader: reading %r', filename)
        if filename in self._source_cache:
            log.debug('loader: cached %r', filename)
            return self._source_cache[filename]
        try:
            u = urlopen(filename)
            source = u.read().decode('utf-8')
            log.debug('loader: %r loaded', filename)
            self._source_cache[filename] = source
            return source
        except (HTTPError, URLError) as e:
            log.debug('loader: %r failed. %s', filename, e)
            raise ImportError("Can't load %s" % filename)

    def is_package(self, fullname):
        return False

# Package loader for a URL
class UrlPackageLoader(UrlModuleLoader):
    def load_module(self, fullname):
        mod = super().load_module(fullname)
        mod.__path__ = [ self._baseurl ]
        mod.__package__ = fullname

    def get_filename(self, fullname):
        return self._baseurl + '/' + '__init__.py'

    def is_package(self, fullname):
        return True

# Utility functions for installing/uninstalling the loader
_installed_meta_cache = { }
def install_meta(address):
    if address not in _installed_meta_cache:
        finder = UrlMetaFinder(address)
        _installed_meta_cache[address] = finder
        sys.meta_path.append(finder)
        log.debug('%r installed on sys.meta_path', finder)

def remove_meta(address):
    if address in _installed_meta_cache:
        finder = _installed_meta_cache.pop(address)
        sys.meta_path.remove(finder)
        log.debug('%r removed from sys.meta_path', finder)

下面是一个交互会话,演示了如何使用前面的代码:

>>> # importing currently fails
>>> import fib
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named 'fib'
>>> # Load the importer and retry (it works)
>>> import urlimport
>>> urlimport.install_meta('http://localhost:15000')
>>> import fib
I'm fib
>>> import spam
I'm spam
>>> import grok.blah
I'm grok.__init__
I'm grok.blah
>>> grok.blah.__file__
'http://localhost:15000/grok/blah.py'

这个特殊的方案会安装一个特别的查找器 UrlMetaFinder 实例, 作为 sys.meta_path 中最后的实体。 当模块被导入时,会依据 sys.meta_path 中的查找器定位模块。 在这个例子中,UrlMetaFinder 实例是最后一个查找器方案, 当模块在任何一个普通地方都找不到的时候就触发它。

作为常见的实现方案,UrlMetaFinder 类包装在一个用户指定的 URL 上。 在内部,查找器通过抓取指定URL的内容构建合法的链接集合。 导入的时候,模块名会跟已有的链接作对比。如果找到了一个匹配的, 一个单独的 UrlModuleLoader 类被用来从远程机器上加载源代码并创建最终的模块对象。 这里缓存链接的一个原因是避免不必要的HTTP请求重复导入。

自定义导入的第二种方法是编写一个钩子直接嵌入到 sys.path 变量中去, 识别某些目录命名模式。 在 urlimport.py 中添加如下的类和支持函数:

# urlimport.py
# ... include previous code above ...
# Path finder class for a URL
class UrlPathFinder(importlib.abc.PathEntryFinder):
    def __init__(self, baseurl):
        self._links = None
        self._loader = UrlModuleLoader(baseurl)
        self._baseurl = baseurl

    def find_loader(self, fullname):
        log.debug('find_loader: %r', fullname)
        parts = fullname.split('.')
        basename = parts[-1]
        # Check link cache
        if self._links is None:
            self._links = [] # See discussion
            self._links = _get_links(self._baseurl)

        # Check if it's a package
        if basename in self._links:
            log.debug('find_loader: trying package %r', fullname)
            fullurl = self._baseurl + '/' + basename
            # Attempt to load the package (which accesses __init__.py)
            loader = UrlPackageLoader(fullurl)
            try:
                loader.load_module(fullname)
                log.debug('find_loader: package %r loaded', fullname)
            except ImportError as e:
                log.debug('find_loader: %r is a namespace package', fullname)
                loader = None
            return (loader, [fullurl])

        # A normal module
        filename = basename + '.py'
        if filename in self._links:
            log.debug('find_loader: module %r found', fullname)
            return (self._loader, [])
        else:
            log.debug('find_loader: module %r not found', fullname)
            return (None, [])

    def invalidate_caches(self):
        log.debug('invalidating link cache')
        self._links = None

# Check path to see if it looks like a URL
_url_path_cache = {}
def handle_url(path):
    if path.startswith(('http://', 'https://')):
        log.debug('Handle path? %s. [Yes]', path)
        if path in _url_path_cache:
            finder = _url_path_cache[path]
        else:
            finder = UrlPathFinder(path)
            _url_path_cache[path] = finder
        return finder
    else:
        log.debug('Handle path? %s. [No]', path)

def install_path_hook():
    sys.path_hooks.append(handle_url)
    sys.path_importer_cache.clear()
    log.debug('Installing handle_url')

def remove_path_hook():
    sys.path_hooks.remove(handle_url)
    sys.path_importer_cache.clear()
    log.debug('Removing handle_url')

要使用这个路径查找器,你只需要在 sys.path 中加入 URL 链接。例如:

>>> # Initial import fails
>>> import fib
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
ImportError: No module named 'fib'

>>> # Install the path hook
>>> import urlimport
>>> urlimport.install_path_hook()

>>> # Imports still fail (not on path)
>>> import fib
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
ImportError: No module named 'fib'

>>> # Add an entry to sys.path and watch it work
>>> import sys
>>> sys.path.append('http://localhost:15000')
>>> import fib
I'm fib
>>> import grok.blah
I'm grok.__init__
I'm grok.blah
>>> grok.blah.__file__
'http://localhost:15000/grok/blah.py'

关键点就是 handle_url() 函数,它被添加到了 sys.path_hooks 变量中。 当 sys.path的实体被处理时,会调用 sys.path_hooks 中的函数。 如果任何一个函数返回了一个查找器对象,那么这个对象就被用来为 sys.path 实体加载模块。

远程模块加载跟其他的加载使用方法几乎是一样的。例如:

>>> fib
<urlmodule 'fib' from 'http://localhost:15000/fib.py'>
>>> fib.__name__
'fib'
>>> fib.__file__
'http://localhost:15000/fib.py'
>>> import inspect
>>> print(inspect.getsource(fib))

讨论

首先,如果你想创建一个新的模块对象,使用 imp.new_module() 函数:

>>> import imp
>>> m = imp.new_module('spam')
>>> m
<module 'spam'>
>>> m.__name__
'spam'

模块对象通常有一些期望属性,包括 __file__ (运行模块加载语句的文件名) 和 __package__ (包名)。

其次,模块会被解释器缓存起来。模块缓存可以在字典 sys.modules 中被找到。 因为有了这个缓存机制,通常可以将缓存和模块的创建通过一个步骤完成:

>>> import sys
>>> import imp
>>> m = sys.modules.setdefault('spam', imp.new_module('spam'))
>>> m
<module 'spam'>

如果给定模块已经存在那么就会直接获得已经被创建过的模块,例如:

>>> import math
>>> m = sys.modules.setdefault('math', imp.new_module('math'))
>>> m
<module 'math' from '/usr/local/lib/python3.3/lib-dynload/math.so'>
>>> m.sin(2)
0.9092974268256817
>>> m.cos(2)
-0.4161468365471424

由于创建模块很简单,很容易编写简单函数比如第一部分的 load_module() 函数。 这个方案的一个缺点是很难处理复杂情况比如包的导入。 为了处理一个包,你要重新实现普通 import 语句的底层逻辑(比如检查目录,查找 __init__.py 文件, 执行那些文件,设置路径等)。这个复杂性就是为什么最好直接扩展 import 语句而不是自定义函数的一个原因。

扩展 import 语句很简单,但是会有很多移动操作。 最高层上,导入操作被一个位于 sys.meta_path 列表中的“元路径”查找器处理。 如果你输出它的值,会看到下面这样:

>>> from pprint import pprint
>>> pprint(sys.meta_path)
[<class '_frozen_importlib.BuiltinImporter'>,
<class '_frozen_importlib.FrozenImporter'>,
<class '_frozen_importlib.PathFinder'>]

当执行一个语句比如 import fib 时,解释器会遍历 sys.mata_path 中的查找器对象, 调用它们的 find_module() 方法定位正确的模块加载器。 可以通过实验来看看:

>>> class Finder:
...     def find_module(self, fullname, path):
...         print('Looking for', fullname, path)
...         return None
...
>>> import sys
>>> sys.meta_path.insert(0, Finder()) # Insert as first entry
>>> import math
Looking for math None
>>> import types
Looking for types None
>>> import threading
Looking for threading None
Looking for time None
Looking for traceback None
Looking for linecache None
Looking for tokenize None
Looking for token None

注意看 find_module() 方法是怎样在每一个导入就被触发的。 这个方法中的 path 参数的作用是处理包。 多个包被导入,就是一个可在包的 __path__ 属性中找到的路径列表。 要找到包的子组件就要检查这些路径。 比如注意对于 xml.etree 和 xml.etree.ElementTree 的路径配置:

>>> import xml.etree.ElementTree
Looking for xml None
Looking for xml.etree ['/usr/local/lib/python3.3/xml']
Looking for xml.etree.ElementTree ['/usr/local/lib/python3.3/xml/etree']
Looking for warnings None
Looking for contextlib None
Looking for xml.etree.ElementPath ['/usr/local/lib/python3.3/xml/etree']
Looking for _elementtree None
Looking for copy None
Looking for org None
Looking for pyexpat None
Looking for ElementC14N None

在 sys.meta_path 上查找器的位置很重要,将它从队头移到队尾,然后再试试导入看:

>>> del sys.meta_path[0]
>>> sys.meta_path.append(Finder())
>>> import urllib.request
>>> import datetime

现在你看不到任何输出了,因为导入被 sys.meta_path 中的其他实体处理。 这时候,你只有在导入不存在模块的时候才能看到它被触发:

>>> import fib
Looking for fib None
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
ImportError: No module named 'fib'
>>> import xml.superfast
Looking for xml.superfast ['/usr/local/lib/python3.3/xml']
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
ImportError: No module named 'xml.superfast'

你之前安装过一个捕获未知模块的查找器,这个是 UrlMetaFinder 类的关键。 一个 UrlMetaFinder 实例被添加到 sys.meta_path 的末尾,作为最后一个查找器方案。 如果被请求的模块名不能定位,就会被这个查找器处理掉。 处理包的时候需要注意,在 path 参数中指定的值需要被检查,看它是否以查找器中注册的 URL 开头。 如果不是,该子模块必须归属于其他查找器并被忽略掉。

对于包的其他处理可在 UrlPackageLoader 类中被找到。 这个类不会导入包名,而是去加载对应的 __init__.py 文件。 它也会设置模块的 __path__ 属性,这一步很重要, 因为在加载包的子模块时这个值会被传给后面的 find_module() 调用。 基于路径的导入钩子是这些思想的一个扩展,但是采用了另外的方法。 我们都知道,sys.path 是一个 Python 查找模块的目录列表,例如:

>>> from pprint import pprint
>>> import sys
>>> pprint(sys.path)
['',
'/usr/local/lib/python33.zip',
'/usr/local/lib/python3.3',
'/usr/local/lib/python3.3/plat-darwin',
'/usr/local/lib/python3.3/lib-dynload',
'/usr/local/lib/...3.3/site-packages']

在 sys.path 中的每一个实体都会被额外的绑定到一个查找器对象上。 你可以通过查看 sys.path_importer_cache 去看下这些查找器:

>>> pprint(sys.path_importer_cache)
{'.': FileFinder('.'),
'/usr/local/lib/python3.3': FileFinder('/usr/local/lib/python3.3'),
'/usr/local/lib/python3.3/': FileFinder('/usr/local/lib/python3.3/'),
'/usr/local/lib/python3.3/collections': FileFinder('...python3.3/collections'),
'/usr/local/lib/python3.3/encodings': FileFinder('...python3.3/encodings'),
'/usr/local/lib/python3.3/lib-dynload': FileFinder('...python3.3/lib-dynload'),
'/usr/local/lib/python3.3/plat-darwin': FileFinder('...python3.3/plat-darwin'),
'/usr/local/lib/python3.3/site-packages': FileFinder('...python3.3/site-packages'),
'/usr/local/lib/python33.zip': None}

sys.path_importer_cache 比 sys.path 会更大点, 因为它会为所有被加载代码的目录记录它们的查找器。 这包括包的子目录,这些通常在 sys.path 中是不存在的。

要执行 import fib ,会顺序检查 sys.path 中的目录。 对于每个目录,名称“fib”会被传给相应的 sys.path_importer_cache 中的查找器。 这个可以让你创建自己的查找器并在缓存中放入一个实体。试试这个:

>>> class Finder:
... def find_loader(self, name):
...     print('Looking for', name)
...     return (None, [])
...
>>> import sys
>>> # Add a "debug" entry to the importer cache
>>> sys.path_importer_cache['debug'] = Finder()
>>> # Add a "debug" directory to sys.path
>>> sys.path.insert(0, 'debug')
>>> import threading
Looking for threading
Looking for time
Looking for traceback
Looking for linecache
Looking for tokenize
Looking for token

在这里,你可以为名字“debug”创建一个新的缓存实体并将它设置成 sys.path 上的第一个。 在所有接下来的导入中,你会看到你的查找器被触发了。 不过,由于它返回 (None, []),那么处理进程会继续处理下一个实体。

sys.path_importer_cache 的使用被一个存储在 sys.path_hooks 中的函数列表控制。 试试下面的例子,它会清除缓存并给 sys.path_hooks 添加一个新的路径检查函数:

>>> sys.path_importer_cache.clear()
>>> def check_path(path):
...     print('Checking', path)
...     raise ImportError()
...
>>> sys.path_hooks.insert(0, check_path)
>>> import fib
Checked debug
Checking .
Checking /usr/local/lib/python33.zip
Checking /usr/local/lib/python3.3
Checking /usr/local/lib/python3.3/plat-darwin
Checking /usr/local/lib/python3.3/lib-dynload
Checking /Users/beazley/.local/lib/python3.3/site-packages
Checking /usr/local/lib/python3.3/site-packages
Looking for fib
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
ImportError: No module named 'fib'

正如你所见,check_path() 函数被每个 sys.path 中的实体调用。 不顾,由于抛出了 ImportError 异常, 啥都不会发生了(仅仅将检查转移到 sys.path_hooks 的下一个函数)。

知道了怎样 sys.path 是怎样被处理的,你就能构建一个自定义路径检查函数来查找文件名,比如 URL。例如:

>>> def check_url(path):
...     if path.startswith('http://'):
...         return Finder()
...     else:
...         raise ImportError()
...
>>> sys.path.append('http://localhost:15000')
>>> sys.path_hooks[0] = check_url
>>> import fib
Looking for fib # Finder output!
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
ImportError: No module named 'fib'

>>> # Notice installation of Finder in sys.path_importer_cache
>>> sys.path_importer_cache['http://localhost:15000']
<__main__.Finder object at 0x10064c850>

事实上,一个用来在 sys.path 中查找 URL 的自定义路径检查函数已经构建完毕。 当它们被碰到的时候,一个新的 UrlPathFinder 实例被创建并被放入 sys.path_importer_cache. 之后,所有需要检查 sys.path 的导入语句都会使用你的自定义查找器。

基于路径导入的包处理稍微有点复杂,并且跟 find_loader() 方法返回值有关。 对于简单模块,find_loader() 返回一个元组 (loader, None), 其中的 loader 是一个用于导入模块的加载器实例。

对于一个普通的包,find_loader() 返回一个元组 (loader, path), 其中的 loader 是一个用于导入包(并执行 __init__.py)的加载器实例, path 是一个会初始化包的 __path__ 属性的目录列表。 例如,如果基础 URL是 http://localhost:15000) 并且一个用户执行 import grok , 那么 find_loader() 返回的 path 就会是 [ ‘http://localhost:15000/grok‘ ]

find_loader() 还要能处理一个命名空间包。 一个命名空间包中有一个合法的包目录名,但是不存在 __init__.py 文件。 这样的话,find_loader() 必须返回一个元组 (None, path), path 是一个目录列表,由它来构建包的定义有 __init__.py 文件的 __path__ 属性。 对于这种情况,导入机制会继续前行去检查sys.path 中的目录。 如果找到了命名空间包,所有的结果路径被加到一起来构建最终的命名空间包。 关于命名空间包的更多信息请参考 10.5 小节。

所有的包都包含了一个内部路径设置,可以在 __path__ 属性中看到,例如:

>>> import xml.etree.ElementTree
>>> xml.__path__
['/usr/local/lib/python3.3/xml']
>>> xml.etree.__path__
['/usr/local/lib/python3.3/xml/etree']
>>>

之前提到,__path__ 的设置是通过 find_loader() 方法返回值控制的。 不过,__path__ 接下来也被sys.path_hooks 中的函数处理。 因此,但包的子组件被加载后,位于 __path__ 中的实体会被 handle_url() 函数检查。 这会导致新的 UrlPathFinder 实例被创建并且被加入到 sys.path_importer_cache 中。

还有个难点就是 handle_url() 函数以及它跟内部使用的 _get_links() 函数之间的交互。 如果你的查找器实现需要使用到其他模块(比如 urllib.request ), 有可能这些模块会在查找器操作期间进行更多的导入。 它可以导致 handle_url() 和其他查找器部分陷入一种递归循环状态。 为了解释这种可能性,实现中有一个被创建的查找器缓存(每一个 URL 一个)。 它可以避免创建重复查找器的问题。 另外,下面的代码片段可以确保查找器不会在初始化链接集合的时候响应任何导入请求:

# Check link cache
if self._links is None:
    self._links = [] # See discussion
    self._links = _get_links(self._baseurl)

最后,查找器的 invalidate_caches() 方法是一个工具方法,用来清理内部缓存。 这个方法再用户调用 importlib.invalidate_caches() 的时候被触发。 如果你想让 URL 导入者重新读取链接列表的话可以使用它。

对比下两种方案(修改 sys.meta_path 或使用一个路径钩子)。 使用 sys.meta_path 的导入者可以按照自己的需要自由处理模块。 例如,它们可以从数据库中导入或以不同于一般模块/包处理方式导入。 这种自由同样意味着导入者需要自己进行内部的一些管理。 另外,基于路径的钩子只是适用于对 sys.path 的处理。 通过这种扩展加载的模块跟普通方式加载的特性是一样的。

如果到现在为止你还是不是很明白,那么可以通过增加一些日志打印来测试下本节。像下面这样:

>>> import logging
>>> logging.basicConfig(level=logging.DEBUG)
>>> import urlimport
>>> urlimport.install_path_hook()
DEBUG:urlimport:Installing handle_url
>>> import fib
DEBUG:urlimport:Handle path? /usr/local/lib/python33.zip. [No]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named 'fib'
>>> import sys
>>> sys.path.append('http://localhost:15000')
>>> import fib
DEBUG:urlimport:Handle path? http://localhost:15000. [Yes]
DEBUG:urlimport:Getting links from http://localhost:15000
DEBUG:urlimport:links: {'spam.py', 'fib.py', 'grok'}
DEBUG:urlimport:find_loader: 'fib'
DEBUG:urlimport:find_loader: module 'fib' found
DEBUG:urlimport:loader: reading 'http://localhost:15000/fib.py'
DEBUG:urlimport:loader: 'http://localhost:15000/fib.py' loaded
I'm fib
Previous10.10 通过字符串名导入模块Next10.12 导入模块的同时修改模块

Last updated 5 years ago

Was this helpful?