10.11 通过钩子远程加载模块

问题

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

解决方案

首先要提出来的是安全问题。本节讨论的思想如果没有一些额外的安全和认知机制的话会很糟糕。也就是说,我们的主要目的是深入分析Python的import语句机制。如果你理解了本节内部原理,你就能够为其他任何目的而自定义import。有了这些,让我们继续向前走。

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

  1. testcode/
  2. spam.py
  3. fib.py
  4. grok/
  5. __init__.py
  6. blah.py

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

  1. # spam.py
  2. print("I'm spam")
  3.  
  4. def hello(name):
  5. print('Hello %s' % name)
  6.  
  7. # fib.py
  8. print("I'm fib")
  9.  
  10. def fib(n):
  11. if n < 2:
  12. return 1
  13. else:
  14. return fib(n-1) + fib(n-2)
  15.  
  16. # grok/__init__.py
  17. print("I'm grok.__init__")
  18.  
  19. # grok/blah.py
  20. print("I'm grok.blah")

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

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

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

  1. >>> from urllib.request import urlopen
  2. >>> u = urlopen('http://localhost:15000/fib.py')
  3. >>> data = u.read().decode('utf-8')
  4. >>> print(data)
  5. # fib.py
  6. print("I'm fib")
  7.  
  8. def fib(n):
  9. if n < 2:
  10. return 1
  11. else:
  12. return fib(n-1) + fib(n-2)
  13. >>>

从这个服务器加载源代码是接下来本节的基础。为了替代手动的通过 urlopen() 来收集源文件,我们通过自定义import语句来在后台自动帮我们做到。

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

  1. import imp
  2. import urllib.request
  3. import sys
  4.  
  5. def load_module(url):
  6. u = urllib.request.urlopen(url)
  7. source = u.read().decode('utf-8')
  8. mod = sys.modules.setdefault(url, imp.new_module(url))
  9. code = compile(source, url, 'exec')
  10. mod.__file__ = url
  11. mod.__package__ = ''
  12. exec(code, mod.__dict__)
  13. return mod

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

  1. >>> fib = load_module('http://localhost:15000/fib.py')
  2. I'm fib
  3. >>> fib.fib(10)
  4. 89
  5. >>> spam = load_module('http://localhost:15000/spam.py')
  6. I'm spam
  7. >>> spam.hello('Guido')
  8. Hello Guido
  9. >>> fib
  10. <module 'http://localhost:15000/fib.py' from 'http://localhost:15000/fib.py'>
  11. >>> spam
  12. <module 'http://localhost:15000/spam.py' from 'http://localhost:15000/spam.py'>
  13. >>>

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

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

  1. # urlimport.py
  2. import sys
  3. import importlib.abc
  4. import imp
  5. from urllib.request import urlopen
  6. from urllib.error import HTTPError, URLError
  7. from html.parser import HTMLParser
  8.  
  9. # Debugging
  10. import logging
  11. log = logging.getLogger(__name__)
  12.  
  13. # Get links from a given URL
  14. def _get_links(url):
  15. class LinkParser(HTMLParser):
  16. def handle_starttag(self, tag, attrs):
  17. if tag == 'a':
  18. attrs = dict(attrs)
  19. links.add(attrs.get('href').rstrip('/'))
  20. links = set()
  21. try:
  22. log.debug('Getting links from %s' % url)
  23. u = urlopen(url)
  24. parser = LinkParser()
  25. parser.feed(u.read().decode('utf-8'))
  26. except Exception as e:
  27. log.debug('Could not get links. %s', e)
  28. log.debug('links: %r', links)
  29. return links
  30.  
  31. class UrlMetaFinder(importlib.abc.MetaPathFinder):
  32. def __init__(self, baseurl):
  33. self._baseurl = baseurl
  34. self._links = { }
  35. self._loaders = { baseurl : UrlModuleLoader(baseurl) }
  36.  
  37. def find_module(self, fullname, path=None):
  38. log.debug('find_module: fullname=%r, path=%r', fullname, path)
  39. if path is None:
  40. baseurl = self._baseurl
  41. else:
  42. if not path[0].startswith(self._baseurl):
  43. return None
  44. baseurl = path[0]
  45. parts = fullname.split('.')
  46. basename = parts[-1]
  47. log.debug('find_module: baseurl=%r, basename=%r', baseurl, basename)
  48.  
  49. # Check link cache
  50. if basename not in self._links:
  51. self._links[baseurl] = _get_links(baseurl)
  52.  
  53. # Check if it's a package
  54. if basename in self._links[baseurl]:
  55. log.debug('find_module: trying package %r', fullname)
  56. fullurl = self._baseurl + '/' + basename
  57. # Attempt to load the package (which accesses __init__.py)
  58. loader = UrlPackageLoader(fullurl)
  59. try:
  60. loader.load_module(fullname)
  61. self._links[fullurl] = _get_links(fullurl)
  62. self._loaders[fullurl] = UrlModuleLoader(fullurl)
  63. log.debug('find_module: package %r loaded', fullname)
  64. except ImportError as e:
  65. log.debug('find_module: package failed. %s', e)
  66. loader = None
  67. return loader
  68. # A normal module
  69. filename = basename + '.py'
  70. if filename in self._links[baseurl]:
  71. log.debug('find_module: module %r found', fullname)
  72. return self._loaders[baseurl]
  73. else:
  74. log.debug('find_module: module %r not found', fullname)
  75. return None
  76.  
  77. def invalidate_caches(self):
  78. log.debug('invalidating link cache')
  79. self._links.clear()
  80.  
  81. # Module Loader for a URL
  82. class UrlModuleLoader(importlib.abc.SourceLoader):
  83. def __init__(self, baseurl):
  84. self._baseurl = baseurl
  85. self._source_cache = {}
  86.  
  87. def module_repr(self, module):
  88. return '<urlmodule %r from %r>' % (module.__name__, module.__file__)
  89.  
  90. # Required method
  91. def load_module(self, fullname):
  92. code = self.get_code(fullname)
  93. mod = sys.modules.setdefault(fullname, imp.new_module(fullname))
  94. mod.__file__ = self.get_filename(fullname)
  95. mod.__loader__ = self
  96. mod.__package__ = fullname.rpartition('.')[0]
  97. exec(code, mod.__dict__)
  98. return mod
  99.  
  100. # Optional extensions
  101. def get_code(self, fullname):
  102. src = self.get_source(fullname)
  103. return compile(src, self.get_filename(fullname), 'exec')
  104.  
  105. def get_data(self, path):
  106. pass
  107.  
  108. def get_filename(self, fullname):
  109. return self._baseurl + '/' + fullname.split('.')[-1] + '.py'
  110.  
  111. def get_source(self, fullname):
  112. filename = self.get_filename(fullname)
  113. log.debug('loader: reading %r', filename)
  114. if filename in self._source_cache:
  115. log.debug('loader: cached %r', filename)
  116. return self._source_cache[filename]
  117. try:
  118. u = urlopen(filename)
  119. source = u.read().decode('utf-8')
  120. log.debug('loader: %r loaded', filename)
  121. self._source_cache[filename] = source
  122. return source
  123. except (HTTPError, URLError) as e:
  124. log.debug('loader: %r failed. %s', filename, e)
  125. raise ImportError("Can't load %s" % filename)
  126.  
  127. def is_package(self, fullname):
  128. return False
  129.  
  130. # Package loader for a URL
  131. class UrlPackageLoader(UrlModuleLoader):
  132. def load_module(self, fullname):
  133. mod = super().load_module(fullname)
  134. mod.__path__ = [ self._baseurl ]
  135. mod.__package__ = fullname
  136.  
  137. def get_filename(self, fullname):
  138. return self._baseurl + '/' + '__init__.py'
  139.  
  140. def is_package(self, fullname):
  141. return True
  142.  
  143. # Utility functions for installing/uninstalling the loader
  144. _installed_meta_cache = { }
  145. def install_meta(address):
  146. if address not in _installed_meta_cache:
  147. finder = UrlMetaFinder(address)
  148. _installed_meta_cache[address] = finder
  149. sys.meta_path.append(finder)
  150. log.debug('%r installed on sys.meta_path', finder)
  151.  
  152. def remove_meta(address):
  153. if address in _installed_meta_cache:
  154. finder = _installed_meta_cache.pop(address)
  155. sys.meta_path.remove(finder)
  156. log.debug('%r removed from sys.meta_path', finder)

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

  1. >>> # importing currently fails
  2. >>> import fib
  3. Traceback (most recent call last):
  4. File "<stdin>", line 1, in <module>
  5. ImportError: No module named 'fib'
  6. >>> # Load the importer and retry (it works)
  7. >>> import urlimport
  8. >>> urlimport.install_meta('http://localhost:15000')
  9. >>> import fib
  10. I'm fib
  11. >>> import spam
  12. I'm spam
  13. >>> import grok.blah
  14. I'm grok.__init__
  15. I'm grok.blah
  16. >>> grok.blah.__file__
  17. 'http://localhost:15000/grok/blah.py'
  18. >>>

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

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

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

  1. # urlimport.py
  2. # ... include previous code above ...
  3. # Path finder class for a URL
  4. class UrlPathFinder(importlib.abc.PathEntryFinder):
  5. def __init__(self, baseurl):
  6. self._links = None
  7. self._loader = UrlModuleLoader(baseurl)
  8. self._baseurl = baseurl
  9.  
  10. def find_loader(self, fullname):
  11. log.debug('find_loader: %r', fullname)
  12. parts = fullname.split('.')
  13. basename = parts[-1]
  14. # Check link cache
  15. if self._links is None:
  16. self._links = [] # See discussion
  17. self._links = _get_links(self._baseurl)
  18.  
  19. # Check if it's a package
  20. if basename in self._links:
  21. log.debug('find_loader: trying package %r', fullname)
  22. fullurl = self._baseurl + '/' + basename
  23. # Attempt to load the package (which accesses __init__.py)
  24. loader = UrlPackageLoader(fullurl)
  25. try:
  26. loader.load_module(fullname)
  27. log.debug('find_loader: package %r loaded', fullname)
  28. except ImportError as e:
  29. log.debug('find_loader: %r is a namespace package', fullname)
  30. loader = None
  31. return (loader, [fullurl])
  32.  
  33. # A normal module
  34. filename = basename + '.py'
  35. if filename in self._links:
  36. log.debug('find_loader: module %r found', fullname)
  37. return (self._loader, [])
  38. else:
  39. log.debug('find_loader: module %r not found', fullname)
  40. return (None, [])
  41.  
  42. def invalidate_caches(self):
  43. log.debug('invalidating link cache')
  44. self._links = None
  45.  
  46. # Check path to see if it looks like a URL
  47. _url_path_cache = {}
  48. def handle_url(path):
  49. if path.startswith(('http://', 'https://')):
  50. log.debug('Handle path? %s. [Yes]', path)
  51. if path in _url_path_cache:
  52. finder = _url_path_cache[path]
  53. else:
  54. finder = UrlPathFinder(path)
  55. _url_path_cache[path] = finder
  56. return finder
  57. else:
  58. log.debug('Handle path? %s. [No]', path)
  59.  
  60. def install_path_hook():
  61. sys.path_hooks.append(handle_url)
  62. sys.path_importer_cache.clear()
  63. log.debug('Installing handle_url')
  64.  
  65. def remove_path_hook():
  66. sys.path_hooks.remove(handle_url)
  67. sys.path_importer_cache.clear()
  68. log.debug('Removing handle_url')

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

  1. >>> # Initial import fails
  2. >>> import fib
  3. Traceback (most recent call last):
  4. File "<stdin>", line 1, in <module>
  5. ImportError: No module named 'fib'
  6.  
  7. >>> # Install the path hook
  8. >>> import urlimport
  9. >>> urlimport.install_path_hook()
  10.  
  11. >>> # Imports still fail (not on path)
  12. >>> import fib
  13. Traceback (most recent call last):
  14. File "<stdin>", line 1, in <module>
  15. ImportError: No module named 'fib'
  16.  
  17. >>> # Add an entry to sys.path and watch it work
  18. >>> import sys
  19. >>> sys.path.append('http://localhost:15000')
  20. >>> import fib
  21. I'm fib
  22. >>> import grok.blah
  23. I'm grok.__init__
  24. I'm grok.blah
  25. >>> grok.blah.__file__
  26. 'http://localhost:15000/grok/blah.py'
  27. >>>

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

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

  1. >>> fib
  2. <urlmodule 'fib' from 'http://localhost:15000/fib.py'>
  3. >>> fib.__name__
  4. 'fib'
  5. >>> fib.__file__
  6. 'http://localhost:15000/fib.py'
  7. >>> import inspect
  8. >>> print(inspect.getsource(fib))
  9. # fib.py
  10. print("I'm fib")
  11.  
  12. def fib(n):
  13. if n < 2:
  14. return 1
  15. else:
  16. return fib(n-1) + fib(n-2)
  17. >>>

讨论

在详细讨论之前,有点要强调的是,Python的模块、包和导入机制是整个语言中最复杂的部分,即使经验丰富的Python程序员也很少能精通它们。我在这里推荐一些值的去读的文档和书籍,包括importlib modulePEP 302.文档内容在这里不会被重复提到,不过我在这里会讨论一些最重要的部分。

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

  1. >>> import imp
  2. >>> m = imp.new_module('spam')
  3. >>> m
  4. <module 'spam'>
  5. >>> m.__name__
  6. 'spam'
  7. >>>

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

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

  1. >>> import sys
  2. >>> import imp
  3. >>> m = sys.modules.setdefault('spam', imp.new_module('spam'))
  4. >>> m
  5. <module 'spam'>
  6. >>>

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

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

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

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

  1. >>> from pprint import pprint
  2. >>> pprint(sys.meta_path)
  3. [<class '_frozen_importlib.BuiltinImporter'>,
  4. <class '_frozen_importlib.FrozenImporter'>,
  5. <class '_frozen_importlib.PathFinder'>]
  6. >>>

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

  1. >>> class Finder:
  2. ... def find_module(self, fullname, path):
  3. ... print('Looking for', fullname, path)
  4. ... return None
  5. ...
  6. >>> import sys
  7. >>> sys.meta_path.insert(0, Finder()) # Insert as first entry
  8. >>> import math
  9. Looking for math None
  10. >>> import types
  11. Looking for types None
  12. >>> import threading
  13. Looking for threading None
  14. Looking for time None
  15. Looking for traceback None
  16. Looking for linecache None
  17. Looking for tokenize None
  18. Looking for token None
  19. >>>

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

  1. >>> import xml.etree.ElementTree
  2. Looking for xml None
  3. Looking for xml.etree ['/usr/local/lib/python3.3/xml']
  4. Looking for xml.etree.ElementTree ['/usr/local/lib/python3.3/xml/etree']
  5. Looking for warnings None
  6. Looking for contextlib None
  7. Looking for xml.etree.ElementPath ['/usr/local/lib/python3.3/xml/etree']
  8. Looking for _elementtree None
  9. Looking for copy None
  10. Looking for org None
  11. Looking for pyexpat None
  12. Looking for ElementC14N None
  13. >>>

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

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

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

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

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

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

  1. >>> from pprint import pprint
  2. >>> import sys
  3. >>> pprint(sys.path)
  4. ['',
  5. '/usr/local/lib/python33.zip',
  6. '/usr/local/lib/python3.3',
  7. '/usr/local/lib/python3.3/plat-darwin',
  8. '/usr/local/lib/python3.3/lib-dynload',
  9. '/usr/local/lib/...3.3/site-packages']
  10. >>>

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

  1. >>> pprint(sys.path_importer_cache)
  2. {'.': FileFinder('.'),
  3. '/usr/local/lib/python3.3': FileFinder('/usr/local/lib/python3.3'),
  4. '/usr/local/lib/python3.3/': FileFinder('/usr/local/lib/python3.3/'),
  5. '/usr/local/lib/python3.3/collections': FileFinder('...python3.3/collections'),
  6. '/usr/local/lib/python3.3/encodings': FileFinder('...python3.3/encodings'),
  7. '/usr/local/lib/python3.3/lib-dynload': FileFinder('...python3.3/lib-dynload'),
  8. '/usr/local/lib/python3.3/plat-darwin': FileFinder('...python3.3/plat-darwin'),
  9. '/usr/local/lib/python3.3/site-packages': FileFinder('...python3.3/site-packages'),
  10. '/usr/local/lib/python33.zip': None}
  11. >>>

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

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

  1. >>> class Finder:
  2. ... def find_loader(self, name):
  3. ... print('Looking for', name)
  4. ... return (None, [])
  5. ...
  6. >>> import sys
  7. >>> # Add a "debug" entry to the importer cache
  8. >>> sys.path_importer_cache['debug'] = Finder()
  9. >>> # Add a "debug" directory to sys.path
  10. >>> sys.path.insert(0, 'debug')
  11. >>> import threading
  12. Looking for threading
  13. Looking for time
  14. Looking for traceback
  15. Looking for linecache
  16. Looking for tokenize
  17. Looking for token
  18. >>>

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

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

  1. >>> sys.path_importer_cache.clear()
  2. >>> def check_path(path):
  3. ... print('Checking', path)
  4. ... raise ImportError()
  5. ...
  6. >>> sys.path_hooks.insert(0, check_path)
  7. >>> import fib
  8. Checked debug
  9. Checking .
  10. Checking /usr/local/lib/python33.zip
  11. Checking /usr/local/lib/python3.3
  12. Checking /usr/local/lib/python3.3/plat-darwin
  13. Checking /usr/local/lib/python3.3/lib-dynload
  14. Checking /Users/beazley/.local/lib/python3.3/site-packages
  15. Checking /usr/local/lib/python3.3/site-packages
  16. Looking for fib
  17. Traceback (most recent call last):
  18. File "<stdin>", line 1, in <module>
  19. ImportError: No module named 'fib'
  20. >>>

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

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

  1. >>> def check_url(path):
  2. ... if path.startswith('http://'):
  3. ... return Finder()
  4. ... else:
  5. ... raise ImportError()
  6. ...
  7. >>> sys.path.append('http://localhost:15000')
  8. >>> sys.path_hooks[0] = check_url
  9. >>> import fib
  10. Looking for fib # Finder output!
  11. Traceback (most recent call last):
  12. File "<stdin>", line 1, in <module>
  13. ImportError: No module named 'fib'
  14.  
  15. >>> # Notice installation of Finder in sys.path_importer_cache
  16. >>> sys.path_importer_cache['http://localhost:15000']
  17. <__main__.Finder object at 0x10064c850>
  18. >>>

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

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

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

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

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

  1. >>> import xml.etree.ElementTree
  2. >>> xml.__path__
  3. ['/usr/local/lib/python3.3/xml']
  4. >>> xml.etree.__path__
  5. ['/usr/local/lib/python3.3/xml/etree']
  6. >>>

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

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

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

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

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

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

  1. >>> import logging
  2. >>> logging.basicConfig(level=logging.DEBUG)
  3. >>> import urlimport
  4. >>> urlimport.install_path_hook()
  5. DEBUG:urlimport:Installing handle_url
  6. >>> import fib
  7. DEBUG:urlimport:Handle path? /usr/local/lib/python33.zip. [No]
  8. Traceback (most recent call last):
  9. File "<stdin>", line 1, in <module>
  10. ImportError: No module named 'fib'
  11. >>> import sys
  12. >>> sys.path.append('http://localhost:15000')
  13. >>> import fib
  14. DEBUG:urlimport:Handle path? http://localhost:15000. [Yes]
  15. DEBUG:urlimport:Getting links from http://localhost:15000
  16. DEBUG:urlimport:links: {'spam.py', 'fib.py', 'grok'}
  17. DEBUG:urlimport:find_loader: 'fib'
  18. DEBUG:urlimport:find_loader: module 'fib' found
  19. DEBUG:urlimport:loader: reading 'http://localhost:15000/fib.py'
  20. DEBUG:urlimport:loader: 'http://localhost:15000/fib.py' loaded
  21. I'm fib
  22. >>>

最后,建议你花点时间看看 PEP 302以及importlib的文档。

原文:

http://python3-cookbook.readthedocs.io/zh_CN/latest/c10/p11_load_modules_from_remote_machine_by_hooks.html