python数据结构算法题一百八十八:通过字符串名导入模块

问题
你想导入一个模块,但是模块的名字在字符串里。你想对字符串调用导入命令。
解决方案
使用 importlib.import_module() 函数来手动导入名字为字符串给出的一个模块或者包的一部分。举个例子:

>>> math = importlib.import_module('math')
>>> math.sin(2)
0.9092974268256817
>>> mod = importlib.import_module('urllib.request')
>>> u = mod.urlopen('http://www.python.org')
>>>

import_module 只是简单地执行和 import 相同的步骤,但是返回生成的模块对象。
你只需要将其存储在一个变量,然后像正常的模块一样使用。
如果你正在使用的包,import_module() 也可用于相对导入。但是,你需要给它一个额外的参数。例如:

# Same as 'from . import b'
b = importlib.import_module('.b', __package__)

讨论
使用 import_module() 手动导入模块的问题通常出现在以某种方式编写修改或覆盖模块的代码时候。例如,也许你正在执行某种自定义导入机制,需要通过名称来加载一个模块,通过补丁加载代码。
在旧的代码,有时你会看到用于导入的内建函数 import()。尽管它能工作,但是 importlib.import_module() 通常更容易使用。自定义导入过程的高级实例见 10.11 小节
通过钩子远程加载模块
问题
你想自定义 Python 的 import 语句,使得它能从远程机器上面透明的加载模块。
解决方案
首先要提出来的是安全问题。本节讨论的思想如果没有一些额外的安全和认知机制的话会很糟糕。也就是说,我们的主要目的是深入分析 Python 的 import 语句机制。
如果你理解了本节内部原理,你就能够为其他任何目的而自定义 import。有了这些,让我们继续向前走。
本节核心是设计导入语句的扩展功能。有很多种方法可以做这个,不过为了演示的方便,我们开始先构造下面这个 Python 代码结构:

spam.py
fib.py
grok/
__init__.py
blah.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 % python3 -m http.server 15000
Serving HTTP on 0.0.0.0 port 15000 ...

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

>>> u = urlopen('http://localhost:15000/fib.py')
>>> data = u.read().decode('utf-8')
>>> print(data)
# fib.py
print("I'm fib")
def fib(n):
if n < 2:
return 1
else:
return fib(n-1) + fib(n-2)
>>>

从这个服务器加载源代码是接下来本节的基础。为了替代手动的通过 urlopen()来收集源文件,我们通过自定义 import 语句来在后台自动帮我们做到。
加载远程模块的第一种方法是创建一个显示的加载函数来完成它。例如:

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() 将其编译到一个代码对象中,然后在一个新创建的模块对象的字典中来执行它。下面是使用这个函数的方式:

I'm fib
>>> fib.fib(10)
89
>>> spam = load_module('http://localhost:15000/spam.py')
I'm spam
>>> spam.hello('Guido')
Hello Guido
>>> fib
<module 'http://localhost:15000/fib.py' from 'http://localhost:15000/fib.py'>
>>> spam
<module 'http://localhost:15000/spam.py' from 'http://localhost:15000/spam.py
,
→'>
>>>

正如你所见,对于简单的模块这个是行得通的。不过它并没有嵌入到通常的 import语句中,如果要支持更高级的结构比如包就需要更多的工作了。
一个更酷的做法是创建一个自定义导入器。第一种方法是创建一个元路径导入器。如下:

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)

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

>>> 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 中添加如下的类和支持函数:

# ... 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 链接。例如:

>>> 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 实体加载模块。
远程模块加载跟其他的加载使用方法几乎是一样的。例如:

<urlmodule 'fib' from 'http://localhost:15000/fib.py'>
>>> fib.__name__
'fib'
>>> fib.__file__
'http://localhost:15000/fib.py'
>>> import inspect
>>> print(inspect.getsource(fib))
# fib.py
print("I'm fib")
def fib(n):
if n < 2:
return 1
else:
return fib(n-1) + fib(n-2)
>>>
  • 7
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值