背景
钩子函数(hook function),可以理解是一个挂钩,作用是有需要的时候挂一个东西上去。具体的解释是:钩子函数是把我们自己实现的hook函数在某一时刻挂接到目标挂载点上。
在 Python 众多特性中,有一个特性隐秘而强大,它就是 Python 的 import hook 机制。利用它,我们可以接管 Python 的模块导入流程,实现非常强大的自定义功能,下面是一些知名的 Python 库使用 import hook 的应用:
- pytest:大名鼎鼎的 Python 测试框架,可以直接使用 assert 进行断言,并且在断言失败时能够输出更加详细的错误信息。
- Flask:Flask 实现了插件库的统一入口,例如我们安装了 Flask_API 这个库,我们可以直接通过 import flask.ext.api 引用它。
- MacroPy:Python 的宏语法实现,在 Python 里实现了 case class, pattern matching, 尾递归优化, Linq 等等酷炫的特性。
- Py2exe、Pyinstaller:可以直接将你的 Python 程序和 Python 运行环境进行打包,使得 Python 程序可以在没有安装 Python 的环境中运行,也可以作为一个独立文件方便传递和管理。
作为 Python 的一项 “高级” 特性,我们日常的编码一般很少会接触到,大多数时候它都隐藏在 Python 的一些第三方库中。但是当你深入了解它之后,你就会感叹它的强大。
理解 import hook 需要先了解 Python 导入模块的过程。
一、 导入过程
Python 通常使用 import 语句来实现类库的引用,当然内建的
- 查找模块
- 加载模块到当前名字空间
那么,一个模块的导入过程大致可以分为三个步骤:搜索、加载 和 名字绑定。
1.1 搜索
搜索是整个导入过程的核心,也是最为复杂的一步。这个过程主要是完成查找要引入模块的功能,查找的过程如下:
- 1、在缓存 sys.modules 中查找要导入的模块,若找到则直接返回该模块对象
- 2、如果在 sys.modules 中没有找到相应模块的缓存,则顺序搜索
sys.meta_path ,逐个借助其中的finder 来查找模块,若找到则加载后返回相应模块对象。
- 3、如果以上步骤都没找到该模块,则执行默认导入。即如果模块M在一个包P中(如
import P.M ),则以P.__path__ 为搜索路径进行查找;如果模块M不在一个包中(如import M ),则以sys.path为搜索路径进行查找。
- 4、如果都未找到,则抛出
ImportError 异常。
查找过程也会检查?些隐式的
1.2 加载
对于搜索到的模块,如果在缓存
- 设置属性:包括
__name__ 、__file__ 、__package__ 、__loader__ 和__path__ 等 - 编译源码:将模块文件(对于包,则是其对应的
__init__.py 文件)编译为字节码(*.pyc 或者*.pyo ),如果字节码文件已存在且仍然是最新的,则不重新编译 - 执行字节码:执行编译生成的字节码(即模块文件或
__init__.py 文件中的语句)
需要注意的是,加载不只是发生在导入时,还可以发生在 reload 时。
1.3 名字绑定
加载完模块后,作为最后一步,import 语句会为 导入的对象 绑定名字,并把这些名字加入到当前的名字空间中。其中,导入的对象 根据导入语句的不同有所差异:
- 如果导入语句为
import obj ,则对象 obj 可以是包或者模块 - 如果导入语句为
from package import obj ,则对象 obj 可以是 package 的子包、package 的属性或者 package 的子模块 - 如果导入语句为
from module import obj ,则对象 obj 只能是 module 的属性
根据 The import statement 中的描述,以下是导入原理对应的Python伪码:
import sys import os.path def do_import(name): """导入""" parent_pkg_name = name.rpartition('.')[0] if parent_pkg_name: parent_pkg = do_import(parent_pkg_name) else: parent_pkg = None return do_find(name, parent_pkg) def do_find(name, parent_pkg): """搜索""" if not name: return None # step 1 if name in sys.modules: return sys.modules[name] else: # step 2 for finder in sys.meta_path: module = do_load(finder, name, parent_pkg) if module: return module # step 3 src_paths = parent_pkg.__path__ if parent_pkg else sys.path for path in src_paths: if path in sys.path_importer_cache: finder = sys.path_importer_cache[path] if finder: module = do_load(finder, name, parent_pkg) if module: return module else: # todo handled by an implicit, file-based finder pass else: finder = None for callable in sys.path_hooks: try: finder = callable(path) break except ImportError: continue if finder: sys.path_importer_cache[path] = finder module = do_load(finder, name, parent_pkg) if module: return module elif os.path.exists(path): sys.path_importer_cache[path] = None else: sys.path_importer_cache[path] = finder() # a finder which always returns None raise ImportError def do_load(finder, name, parent_pkg): """加载""" path = parent_pkg.__path__ if parent_pkg else None loader = finder.find_module(name, path) if loader: return loader.load_module(name) else: return None
二、模块缓存
进行搜索时,搜索的第一个地方是便是
可以删除 sys.modules 中对应的的键或者将值设置为 None 来使缓存无效。
当启动 Python 解释器时,打印一下 sys.modules 中的 key:
>>> import sys >>> sys.modules.keys() dict_keys(['sys', 'builtins', '_frozen_importlib', '_imp', '_warnings', '_frozen_importlib_external', '_io', 'marshal', 'nt', '_thread', '_weakref', 'winreg', 'time', 'zipimport', '_codecs', 'codecs', 'encodings.aliases', 'encodings', 'encodings.utf_8', '_signal', '__main__', 'encodings.latin_1', '_abc', 'abc', 'io', '_stat', 'stat', '_collections_abc', 'genericpath', 'ntpath', 'os.path', 'os', '_sitebuiltins', '_locale', '_bootlocale', '_codecs_cn', '_multibytecodec', 'encodings.gbk', 'types', 'importlib._bootstrap', 'importlib._bootstrap_external', 'warnings', 'importlib', 'importlib.machinery', 'importlib.abc', '_operator', 'operator', 'keyword', '_heapq', 'heapq', 'itertools', 'reprlib', '_collections', 'collections', '_functools', 'functools', 'contextlib', 'importlib.util', 'google', 'mpl_toolkits', 'sphinxcontrib', 'zope', 'site', '_pydev_bundle', '_pydev_bundle._pydev_getopt', '_pydev_comm', '_socket', 'collections.abc', 'math', 'select', 'selectors', 'enum', 'errno', 'socket', '_weakrefset', 'threading', '_sre', 'sre_constants', 'sre_parse', 'sre_compile', 'copyreg', 're', 'token', 'tokenize', 'linecache', 'traceback', '__future__', '_shaded_ply', 'weakref', 'copy', '_opcode', 'opcode', 'dis', 'inspect', '_shaded_ply.lex', '_struct', 'struct', 'binascii', 'base64', '_shaded_ply.yacc', '_shaded_thriftpy.parser.exc', '_shaded_thriftpy.parser.lexer', 'platform', 'urllib', '_bisect', 'bisect', 'email', '_hashlib', '_blake2', '_sha3', 'hashlib', 'http', 'email.errors', '_string', 'string', 'email.quoprimime', 'email.base64mime', 'quopri', 'email.encoders', 'email.charset', 'email.header', '_sha512', '_random', 'random', '_datetime', 'datetime', 'urllib.parse', 'locale', 'calendar', 'email._parseaddr', 'email.utils', 'email._policybase', 'email.feedparser', 'email.parser', 'uu', 'email._encoded_words', 'email.iterators', 'email.message', '_ssl', 'ssl', 'http.client', 'posixpath', 'fnmatch', 'zlib', '_compression', '_bz2', 'bz2', '_lzma', 'lzma', 'shutil', 'tempfile', 'urllib.response', 'urllib.error', 'nturl2path', 'urllib.request', '_shaded_thriftpy._compat', '_shaded_thriftpy.thrift', '_shaded_thriftpy.parser.parser', '_shaded_thriftpy.parser', '_shaded_thriftpy.hook', '_shaded_thriftpy', 'atexit', 'logging', '_shaded_thriftpy.protocol.base', '_shaded_thriftpy.protocol.exc', '_shaded_thriftpy.protocol.binary', '_json', 'json.scanner', 'json.decoder', 'json.encoder', 'json', '_shaded_thriftpy.protocol.json', 'array', '_shaded_thriftpy.protocol.compact', '_shaded_thriftpy.protocol.multiplex', '_shaded_thriftpy.protocol', '_shaded_thriftpy.transport.base', '_shaded_thriftpy.transport.socket', '_shaded_thriftpy.transport._ssl', '_shaded_thriftpy.transport.sslsocket', '_shaded_thriftpy.transport.buffered', '_shaded_thriftpy.transport.framed', '_shaded_thriftpy.transport.memory', '_shaded_thriftpy.transport', '_shaded_thriftpy.server', '_pydev_comm.pydev_server', '_pydev_comm.pydev_io', '_pydev_comm.pydev_transport', '_pydev_comm.pydev_rpc', '_pydev_imps', '_queue', 'queue', 'xmlrpc', 'numbers', '_decimal', 'decimal', 'xml', 'xml.parsers', 'pyexpat.errors', 'pyexpat.model', 'pyexpat', 'xml.parsers.expat.model', 'xml.parsers.expat.errors', 'xml.parsers.expat', 'gzip', 'xmlrpc.client', 'html.entities', 'html', 'mimetypes', 'socketserver', 'http.server', 'pkgutil', 'sysconfig', 'pydoc', 'xmlrpc.server', '_pydev_imps._pydev_saved_modules', 'codeop', 'code', '_pydevd_bundle', '_pydevd_bundle.pydevd_vm_type', '_pydevd_bundle.pydevd_constants', '_pydev_bundle.pydev_log', '_pydev_bundle._pydev_filesystem_encoding', '_pydevd_bundle.pydevd_comm_constants', '_ctypes', 'ctypes._endian', 'ctypes', 'ctypes.wintypes', 'pydevd_file_utils', '_pydevd_bundle.pydevd_utils', '_pydev_bundle.fix_getpass', 'msvcrt', 'getpass', '_pydev_imps._pydev_execfile', '_pydevd_bundle.pydevd_exec2', '_pydev_bundle.pydev_imports', '_pydev_bundle.pydev_stdin', 'signal', '_pydev_bundle._pydev_tipper_common', '_pydevd_bundle.pydevd_resolver', '_pydev_bundle._pydev_imports_tipper', '_pydev_bundle._pydev_calltip_util', '_compat_pickle', '_pickle', 'pickle', '_pydevd_bundle.pydevd_custom_frames', 'zipfile', 'plistlib', 'textwrap', 'pkg_resources.extern', 'pkg_resources._vendor', 'win32api', '_win32sysloader', 'pywintypes', 'pythoncom', 'win32com.gen_py', 'win32com', 'win32com.shell', 'pkg_resources._vendor.appdirs', 'pkg_resources.extern.appdirs', 'pkg_resources._vendor.packaging.__about__', 'pkg_resources._vendor.packaging', 'pkg_resources.extern.packaging', 'pkg_resources.extern.packaging._structures', 'pkg_resources.extern.packaging._typing', 'pkg_resources.extern.packaging.version', 'pkg_resources.extern.packaging._compat', 'pkg_resources.extern.packaging.utils', 'pkg_resources.extern.packaging.specifiers', 'pprint', 'pkg_resources._vendor.pyparsing', 'pkg_resources.extern.pyparsing', 'pkg_resources.extern.packaging.markers', 'pkg_resources.extern.packaging.requirements', 'encodings.cp437', 'pkg_resources', 'pydevd_plugins', 'pydevd_plugins.extensions', '_pydevd_bundle.pydevd_extension_utils', '_pydevd_bundle.pydevd_extension_api', '_pydevd_bundle.pydevd_xml', '_pydevd_bundle.pydevd_save_locals', '_pydevd_bundle.pydevd_vars', '_pydev_bundle.pydev_code_executor', '_pydev_bundle.pydev_console_types', 'pydev_console', 'console_thrift', 'pydev_console.pydev_protocol', '_pydevd_bundle.pydevd_thrift', '_pydev_bundle.pydev_override', '_pydevd_bundle.pydevd_console_pytest', 'IPython.core', 'IPython.core.getipython', 'IPython.core.release', 'glob', '_ast', 'ast', 'traitlets.utils', 'traitlets.utils.getargspec', 'traitlets.utils.importstring', 'traitlets.utils.sentinel', 'traitlets.utils.bunch', 'traitlets.utils.descriptions', 'traitlets.traitlets', 'traitlets.utils.decorators', 'traitlets._version', 'traitlets', 'gettext', 'argparse', 'ipython_genutils._version', 'ipython_genutils', 'ipython_genutils.encoding', 'ipython_genutils.py3compat', 'ipython_genutils.path', 'traitlets.config.loader', 'ipython_genutils.text', 'traitlets.config.configurable', 'traitlets.config.application', 'traitlets.config', 'bdb', 'IPython.utils', 'IPython.utils.ipstruct', 'IPython.utils.coloransi', 'pygments', 'IPython.utils.colorable', 'IPython.utils.PyColorize', 'IPython.utils.encoding', 'IPython.utils.py3compat', 'IPython.core.excolors', 'IPython.testing', 'IPython.testing.skipdoctest', 'cmd', 'pdb', 'IPython.core.debugger', 'IPython.core.display_trap', '_winapi', 'subprocess', 'shlex', 'IPython.utils._process_common', 'IPython.utils._process_win32', 'IPython.utils.process', 'IPython.utils.decorators', 'IPython.utils.path', 'IPython.utils.data', 'IPython.utils.terminal', 'IPython.core.ultratb', 'IPython.utils._sysinfo', 'IPython.utils.sysinfo', 'IPython.core.crashhandler', 'IPython.utils.importstring', 'IPython.paths', 'IPython.core.profiledir', 'IPython.core.application', 'IPython.terminal', 'IPython.core.compilerop', 'IPython.core.error', 'pathlib', 'IPython.utils.text', 'IPython.core.magic_arguments', 'getopt', 'typing.io', 'typing.re', 'typing', 'IPython.core.display', 'IPython.core.page', 'IPython.lib.security', 'IPython.lib', 'IPython.lib.pretty', 'IPython.utils.openpy', 'IPython.utils.dir2', 'IPython.utils.wildcard', 'pygments.lexers._mapping', 'pygments.modeline', 'pygments.plugin', 'pygments.util', 'pygments.lexers', 'pygments.filter', 'pygments.token', 'pygments.filters', 'pygments.regexopt', 'pygments.lexer', 'pygments.unistring', 'pygments.lexers.python', 'pygments.formatters._mapping', 'pygments.formatters', 'pygments.styles', 'pygments.formatter', 'pygments.formatters.html', 'IPython.core.oinspect', 'IPython.core.inputtransformer2', 'decorator', 'IPython.core.magic', 'runpy', 'pickleshare', 'IPython.core.autocall', 'IPython.core.macro', 'IPython.core.splitinput', 'IPython.core.prefilter', 'IPython.core.alias', 'IPython.core.builtin_trap', 'backcall.backcall', 'backcall', 'IPython.core.events', 'IPython.core.displayhook', 'IPython.core.displaypub', 'IPython.core.extensions', 'IPython.utils.sentinel', 'IPython.core.formatters', '_sqlite3', 'sqlite3.dbapi2', 'sqlite3', 'IPython.core.history', 'IPython.core.logger', 'IPython.core.payload', 'IPython.core.usage', 'IPython.lib.display', 'IPython.display', 'IPython.utils.capture', 'IPython.utils.io', 'IPython.core.hooks', 'IPython.utils.strdispatch', 'IPython.utils.syspathcontext', 'IPython.utils.tempdir', 'IPython.utils.contexts', 'IPython.core.async_helpers', 'IPython.core.interactiveshell', 'concurrent', 'concurrent.futures._base', 'concurrent.futures', 'asyncio.constants', 'asyncio.format_helpers', 'asyncio.base_futures', 'asyncio.log', 'asyncio.coroutines', '_contextvars', 'contextvars', 'asyncio.exceptions', 'asyncio.base_tasks', '_asyncio', 'asyncio.events', 'asyncio.futures', 'asyncio.protocols', 'asyncio.transports', 'asyncio.sslproto', 'asyncio.locks', 'asyncio.tasks', 'asyncio.staggered', 'asyncio.trsock', 'asyncio.base_events', 'asyncio.runners', 'asyncio.queues', 'asyncio.streams', 'asyncio.subprocess', '_overlapped', 'asyncio.base_subprocess', 'asyncio.proactor_events', 'asyncio.selector_events', 'asyncio.windows_utils', 'asyncio.windows_events', 'asyncio', 'prompt_toolkit.application.current', 'prompt_toolkit.eventloop.utils', 'prompt_toolkit.eventloop.async_generator', 'wcwidth.table_wide', 'wcwidth.table_zero', 'wcwidth.unicode_versions', 'wcwidth.wcwidth', 'wcwidth', 'prompt_toolkit.utils', 'prompt_toolkit.eventloop.inputhook', 'prompt_toolkit.eventloop', 'prompt_toolkit.application.run_in_terminal', 'prompt_toolkit.selection', 'prompt_toolkit.clipboard.base', 'prompt_toolkit.clipboard.in_memory', 'prompt_toolkit.clipboard', 'prompt_toolkit.cache', 'prompt_toolkit.enums', 'prompt_toolkit.filters.base', 'prompt_toolkit.filters.app', 'prompt_toolkit.filters.cli', 'prompt_toolkit.filters.utils', 'prompt_toolkit.filters', 'prompt_toolkit.document', 'prompt_toolkit.auto_suggest', 'prompt_toolkit.data_structures', 'prompt_toolkit.styles.base', 'prompt_toolkit.styles.named_colors', 'prompt_toolkit.styles.style', 'prompt_toolkit.styles.defaults', 'prompt_toolkit.styles.pygments', 'colorsys', 'prompt_toolkit.styles.style_transformation', 'prompt_toolkit.styles', 'prompt_toolkit.output.color_depth', 'prompt_toolkit.output.base', 'prompt_toolkit.output.defaults', 'prompt_toolkit.output', 'prompt_toolkit.output.vt100', 'prompt_toolkit.mouse_events', 'prompt_toolkit.formatted_text.base', 'prompt_toolkit.formatted_text.ansi', 'xml.dom.domreg', 'xml.dom', 'xml.dom.minicompat', 'xml.dom.NodeFilter', 'xml.dom.xmlbuilder', 'xml.dom.minidom', 'prompt_toolkit.formatted_text.html', 'prompt_toolkit.formatted_text.pygments', 'prompt_toolkit.formatted_text.utils', 'prompt_toolkit.formatted_text', 'prompt_toolkit.completion.base', 'prompt_toolkit.completion.deduplicate', 'prompt_toolkit.completion.filesystem', 'prompt_toolkit.completion.word_completer', 'prompt_toolkit.completion.fuzzy_completer', 'prompt_toolkit.completion.nested', 'prompt_toolkit.completion', 'prompt_toolkit.history', 'prompt_toolkit.keys', 'prompt_toolkit.key_binding.key_bindings', 'prompt_toolkit.key_binding.key_processor', 'prompt_toolkit.key_binding', 'prompt_toolkit.key_binding.vi_state', 'prompt_toolkit.search', 'prompt_toolkit.validation', 'prompt_toolkit.buffer', 'prompt_toolkit.input.base', 'prompt_toolkit.input.defaults', 'prompt_toolkit.input', 'prompt_toolkit.input.typeahead', 'prompt_toolkit.key_binding.bindings', 'prompt_toolkit.key_binding.bindings.scroll', 'prompt_toolkit.key_binding.bindings.page_navigation', 'prompt_toolkit.lexers.base', 'prompt_toolkit.lexers.pygments', 'prompt_toolkit.lexers', 'prompt_toolkit.layout.utils', 'prompt_toolkit.layout.processors', 'prompt_toolkit.layout.controls', 'prompt_toolkit.layout.dimension', 'prompt_toolkit.layout.margins', 'prompt_toolkit.layout.mouse_handlers', 'prompt_toolkit.layout.screen', 'prompt_toolkit.layout.containers', 'prompt_toolkit.layout.layout', 'prompt_toolkit.layout.menus', 'prompt_toolkit.layout.scrollable_pane', 'prompt_toolkit.layout', 'prompt_toolkit.key_binding.bindings.completion', 'prompt_toolkit.key_binding.bindings.named_commands', 'prompt_toolkit.key_binding.bindings.basic', 'prompt_toolkit.key_binding.bindings.cpr', 'prompt_toolkit.key_binding.bindings.emacs', 'prompt_toolkit.key_binding.bindings.mouse', 'prompt_toolkit.input.ansi_escape_sequences', 'prompt_toolkit.input.vt100_parser', 'prompt_toolkit.key_binding.digraphs', 'prompt_toolkit.key_binding.bindings.vi', 'prompt_toolkit.key_binding.defaults', 'prompt_toolkit.key_binding.emacs_state', 'prompt_toolkit.layout.dummy', 'prompt_toolkit.renderer', 'prompt_toolkit.application.application', 'prompt_toolkit.application.dummy', 'prompt_toolkit.application', 'prompt_toolkit.key_binding.bindings.focus', 'prompt_toolkit.widgets.toolbars', 'prompt_toolkit.widgets.base', 'prompt_toolkit.widgets.dialogs', 'prompt_toolkit.widgets.menus', 'prompt_toolkit.widgets', 'prompt_toolkit.shortcuts.dialogs', 'prompt_toolkit.shortcuts.progress_bar.formatters', 'prompt_toolkit.shortcuts.progress_bar.base', 'prompt_toolkit.shortcuts.progress_bar', 'prompt_toolkit.key_binding.bindings.auto_suggest', 'prompt_toolkit.key_binding.bindings.open_in_editor', 'prompt_toolkit.shortcuts.prompt', 'prompt_toolkit.shortcuts.utils', 'prompt_toolkit.shortcuts', 'prompt_toolkit', 'prompt_toolkit.patch_stdout', 'pygments.style', 'unicodedata', 'IPython.core.latex_symbols', 'IPython.utils.generics', 'parso._compatibility', 'parso.utils', 'parso.tree', 'parso.python', 'parso.python.token', 'parso.python.tokenize', 'parso.pgen2.grammar_parser', 'parso.pgen2.generator', 'parso.pgen2', 'parso.parser', 'difflib', 'parso.python.prefix', 'parso.python.tree', 'parso.python.parser', 'parso.python.diff', 'gc', 'parso.cache', 'parso.normalizer', 'parso.python.errors', 'parso.python.pep8', 'parso.file_io', 'parso.grammar', 'parso', 'jedi.file_io', 'jedi._compatibility', 'jedi.parser_utils', 'jedi.debug', 'jedi.settings', 'jedi.cache', 'jedi.inference.cache', 'jedi.inference.helpers', 'jedi.inference.utils', 'jedi.inference.base_value', 'jedi.common', 'jedi.inference.sys_path', 'jedi.inference.recursion', 'jedi.inference.flow_analysis', 'jedi.inference.lazy_value', 'jedi.inference.docstrings', 'jedi.plugins', 'jedi.inference.names', 'jedi.inference.filters', 'jedi.inference.compiled.getattr_static', 'jedi.inference.compiled.access', 'jedi.inference.signature', 'jedi.inference.context', 'jedi.inference.compiled.value', 'jedi.inference.compiled', 'jedi.inference.analysis', 'jedi.inference.gradual', 'jedi.inference.value.module', 'jedi.inference.value.dynamic_arrays', 'jedi.inference.value.iterable', 'jedi.inference.arguments', 'jedi.inference.parser_cache', 'jedi.inference.gradual.generics', 'jedi.inference.value.function', 'jedi.inference.value.klass', 'jedi.inference.value.instance', 'jedi.inference.value', 'jedi.inference.gradual.base', 'jedi.inference.gradual.type_var', 'jedi.inference.gradual.typing', 'jedi.inference.gradual.stub_value', 'jedi.inference.gradual.typeshed', 'jedi.inference.imports', 'jedi.inference.param', 'jedi.inference.gradual.annotation', 'jedi.inference.value.decorator', 'jedi.inference.syntax_tree', 'jedi.inference', 'jedi.inference.gradual.conversion', 'jedi.inference.compiled.mixed', 'pydoc_data', 'pydoc_data.topics', 'jedi.api.keywords', 'jedi.api.completion_cache', 'jedi.api.helpers', 'jedi.api.classes', 'jedi.api.interpreter', 'jedi.api.strings', 'jedi.api.file_name', 'jedi.api.completion', 'filecmp', 'jedi.inference.compiled.subprocess.functions', 'jedi.api.exceptions', 'jedi.inference.compiled.subprocess', 'jedi.api.environment', 'jedi.inference.references', 'jedi.api.project', 'jedi.api.errors', 'jedi.api.refactoring', 'jedi.api.refactoring.extract', 'jedi.inference.gradual.utils', 'jedi.api', 'jedi.plugins.stdlib', 'jedi.plugins.flask', 'jedi.plugins.pytest', 'jedi.plugins.django', 'jedi.plugins.registry', 'jedi', 'IPython.core.completer', 'IPython.terminal.ptutils', 'IPython.lib.clipboard', 'IPython.terminal.shortcuts', 'IPython.terminal.debugger', 'IPython.terminal.magics', 'IPython.terminal.pt_inputhooks', 'IPython.terminal.prompts', 'IPython.terminal.interactiveshell', 'IPython.core.magics.auto', 'IPython.core.magics.basic', 'IPython.core.magics.code', 'IPython.core.magics.config', 'IPython.core.magics.display', 'timeit', '_lsprof', 'profile', 'cProfile', 'pstats', 'IPython.utils.module_paths', 'IPython.utils.timing', 'IPython.core.magics.execution', 'IPython.core.magics.extension', 'IPython.core.magics.history', 'IPython.core.magics.logging', 'IPython.core.magics.namespace', 'IPython.core.magics.osm', 'IPython.core.magics.packaging', 'IPython.core.pylabtools', 'IPython.core.magics.pylab', 'IPython.lib.backgroundjobs', 'IPython.core.magics.script', 'IPython.core.magics', 'IPython.core.shellapp', 'IPython.extensions', 'IPython.extensions.storemagic', 'IPython.terminal.ipapp', 'IPython.terminal.embed', 'IPython.utils.frame', 'IPython', '_pydev_bundle.pydev_ipython_console_011', '_pydev_bundle.pydev_ipython_code_executor', '_pydevd_bundle.pydevd_console_integration', '_pydev_bundle.pydev_monkey', 'pydevd_tracing', '_pydevd_bundle.pydevd_bytecode_utils', '_pydev_bundle.pydev_is_thread_alive', '_pydev_bundle._pydev_completer', '_pydevd_bundle.pydevd_tables', '_pydevd_bundle.pydevd_io', '_pydevd_bundle.pydevd_console', '_pydevd_bundle.pydevd_dont_trace_files', '_pydevd_bundle.pydevd_comm', '_pydev_bundle.pydev_console_utils', '_pydev_bundle.pydev_umd', '_pydev_bundle.pydev_ipython_console', 'pydevconsole', '_pydev_bundle.pydev_localhost', 'stringprep', 'encodings.idna', 'pydev_ipython', 'pydev_ipython.matplotlibtools', 'IPython.core.completerlib', 'colorama.ansi', 'colorama.win32', 'colorama.winterm', 'colorama.ansitowin32', 'colorama.initialise', 'colorama', 'pydev_ipython.inputhook', '_pydev_bundle.pydev_import_hook.import_hook', '_pydev_bundle.pydev_import_hook', 'django.utils', 'django.utils.functional', 'django.utils.regex_helper', 'django.utils.version', 'django', 'django.utils.datastructures', 'django.core', 'django.utils.itercompat', 'django.utils.hashable', 'django.core.exceptions', 'django.conf.global_settings', 'asgiref', 'concurrent.futures.thread', 'asgiref.current_thread_executor', 'asgiref.local', 'asgiref.sync', 'django.utils.deprecation', 'django.conf', 'django.utils.module_loading', 'django.template.context', 'django.apps.config', 'django.apps.registry', 'django.apps', 'django.utils.inspect', 'django.dispatch.dispatcher', 'django.dispatch', 'django.core.signals', 'django.utils.autoreload', 'django.utils.translation', 'django.utils.dates', 'backports', 'backports.zoneinfo._tzpath', 'backports.zoneinfo._common', 'backports.zoneinfo._version', 'backports.zoneinfo._czoneinfo', 'backports.zoneinfo', 'django.utils.timezone', 'django.utils.dateformat', 'django.utils.safestring', 'django.utils.numberformat', 'django.utils.formats', '_markupbase', 'html.parser', 'django.utils.encoding', 'django.utils.http', 'django.utils.text', 'django.utils.html', 'django.template.exceptions', 'django.template.base', 'django.template.library', 'django.template.engine', 'django.template.utils', 'django.template.backends', 'django.utils._os', 'django.template.backends.base', 'django.template.backends.django', 'django.template.autoreload', 'django.template', 'django.template.loader', 'django.forms.renderers', 'django.forms.utils', 'django.templatetags', 'django.templatetags.static', 'django.utils.topological_sort', 'django.forms.widgets', 'django.forms.boundfield', 'uuid', 'ipaddress', 'django.utils.deconstruct', 'django.utils.ipv6', 'django.core.validators', 'django.utils.dateparse', 'django.utils.duration', 'django.forms.fields', 'django.forms.forms', 'django.forms.formsets', 'django.forms.models', 'django.forms', 'pydevd_plugins.extensions.types', 'pydevd_plugins.extensions.types.pydevd_helpers', 'pydevd_plugins.extensions.types.pydevd_plugin_numpy_types', 'pydevd_plugins.extensions.types.pydevd_plugins_django_form_str', '_pydev_bundle.pydev_console_commands'])
可以看出一些模块已经被解释器导入,但是我们却不能直接使用这些模块。这是因为这些模块还没有被绑定到当前名字空间,仍然需要执行 import 语句才能完成名字绑定。
三、查找器和加载器
在搜索过程中我们提到
>>> import sys >>> sys.meta_path [<class '_frozen_importlib.BuiltinImporter'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib_external.PathFinder'>]
- _frozen_importlib.BuiltinImporter 知道如何导入内置模块,例如 sys 模块
- _ frozen_importlib.FrozenImporter 知道如何导入冻结模块,例如__ hello __模块
- _frozen_importlib_external.PathFinder 知道如何导入存在于 import path(导入路径) 中的模块,例如 socket 模块
在 Python 中,不仅定义了 finder 和 loader 的概念,还定义了
- 查找器(finder): 决定自己是否能够通过运用其所知的任何策略找到相应的模块。在 Python2 中,finder 对象必须实现
find_module() 方法,在 Python3 中必须要实现find_module() 或者find_loader() 方法。如果 finder 可以查找到模块,则会返回一个 loader 对象(在 Python 3.4中,修改为返回一个模块分支module specs ,加载器在导入中仍被使用,但几乎没有责任),没有找到则返回 None。 - 加载器(loader): 负责加载模块,它必须实现一个
load_module() 的方法 - 导入器(importer): 实现了 finder 和 loader 这两个接口的对象称为导入器
我们可以想 sys.meta_path 中添加一些自定义的加载器,来实现在加载模块时对模块进行修改。例如一个简单的例子,在每次加载模块时打印模块信息:
from __future__ import print_function import sys class Watcher(object): @classmethod def find_module(cls, name, path, target=None): print("Importing", name, path, target) return None sys.meta_path.insert(0, Watcher) import subprocess
输出结果:
Importing subprocess None None Importing gc None None Importing time None None Importing select None None Importing fcntl None None Importing pickle None None Importing marshal None None Importing struct None None Importing _struct None None Importing org None None Importing binascii None None Importing cStringIO None None
四、导入钩子程序
Python 的导入机制被设计为可扩展的,其基础的运行机制便是
在其他任何导入程序运行之前,除了 sys.modules 缓存查找,在导入处理开始时调用元钩子程序。这就允许元钩子程序覆盖 sys.path 处理程序,冻结模块,或甚至内建模块。可以通过给 sys.meta_path 添加新的查找器对象来注册元钩子程序。
当相关路径项被冲突时,导入路径钩子程序作为
>>> import sys >>> sys.path_hooks [ zipimport.zipimporter, <function _frozen_importlib_external.FileFinder.path_hook.<locals>.path_hook_for_FileFinder(path)> ]
- zipimport.zipimporter 可以用来处理 zip 包类型的路径
- FileFinder.path_hook..path_hook_for_FileFinder 可以用来处理普通的文件目录
Python import path hooks 可以用来扩展支持从不同的路径类型中导入模块,例如默认的 zipimporter 可以直接从 zip 包中导入模块,我们也可以通过自定义的 path hooks 实现从远程文件服务器、redis 缓存、mysql 数据库等各种路径类型中导入模块。从远程文件服务器导入模块的实现可以参考python3-cookbook中的例子,这里就不再赘述。
通过
from __future__ import print_function import sys import pip from importlib import import_module class AutoInstall(object): _loaded = set() @classmethod def find_module(cls, name, path, target=None): if path is None and name not in cls._loaded: cls._loaded.add(name) print("Installing", name) installed = pip.main(["install", name]) if installed == 0: return import_module(name) else: return None sys.meta_path.append(AutoInstall)
Python 还提供了一些模块和函数,可以用来实现简单的
__import__ : Python 的内置函数;- imputil: Python 的 import 工具库,在 Python2.6 被声明废弃,Python3 中彻底移除;
- imp: Python2 和 Python3 都存在的一个 import 库;
- importlib: Python3 中最新添加,backport 到 Python2.7,但只有很小的子集(只有一个 import_module 函数)。
还有一些更加实用和强大的应用场景,比如:上传 Python 程序的审计信息、Python 程序性能监控、模块的懒加载、将配置文件当做一个 python 的模块导入等等。
文章开头提到的 pytest 的例子中,pytest 使用 import hook 拦截了 Python 标准的模块导入流程,当 pytest 检测到导入的模块是一个测试用例文件时,pytest 首先会使用 Python 内置的 ast 模块将目标文件的源码解析成 AST(语法树),然后在语法树中查找并重写了 assert 语句,使得在 assert 失败时,能够输出更加详细的调试信息。
五、site 模块
- 将
sys.prefix 、sys.exec_prefix 和lib/pythonX.Y/site-packages 合成 module 的search path 。加入sys.path。eg: /home/jay/env/tornado/lib/python2.7/site-packages - 在添加的路径下寻找
pth 文件。 该文件中描述了添加到 sys.path 的子文件夹路径。 import sitecustomize , sitecustomize 内部可以做任意的设置。import usercustomize , usercustomize 一般放在用户的 path 环境下, 如:/home/jay/.local/lib/python2.7/site-packages/usercustomize , 其内部可以做任意的设置。
以 Newrelic 为例,Newrelic 是一个性能监控的工具,支持 Python 语言,当安装好它的 newrelic-python-agent 之后,直接使用
$ newrelic-admin run-program python hello.py
就可以启动目标程序并对目标程序进行性能监控了,它的实现方式就是在 newrelic-admin 程序中将 newrelic-python-agent 自定义的 sitecustomize.py 所在的目录加入 PYTHONPATH 中,当被监控程序 python hello.py 启动时,就会加载 newrelic 中注册的 hook 并将性能监控的逻辑注入的程序中。
应用:
用户特定的路径都基于 USER_BASE 目录,该目录通常位于当前用户拥有(和可写)的文件系统的一部分中。
在 USER_BASE 目录中是一个 site-packages 目录,其路径可以作为 USER_SITE 访问。
>> import site >> site.USER_BASE Out[1]: 'C:\Users\rnanprince\AppData\Roaming\Python' >> site.USER_SITE Out[2]: 'C:\Users\rnanprince\AppData\Roaming\Python\Python38\site-packages' >> userdir = site.getusersitepackages() Out[3]: 'C:\Users\rnanprince\AppData\Roaming\Python\Python38\site-packages' >>>import os >>>os.makedirs(userdir) >>>os.system('explorer.exe %s' % userdir)
在路径下创建usercustomize.py:
打印我的专属信息,将自定义路径加入sys.path
import sys print('__________append_my_path___________') sys.path.append('my_path')
启动python,验证钩子程序:
C:Users nanprince>python __________append_my_path___________ Python 3.8.8 (default, Apr 13 2021, 15:08:03) [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32 Warning: This Python interpreter is in a conda environment, but the environment has not been activated. Libraries may fail to load. To activate this environment please see https://conda.io/activation Type "help", "copyright", "credits" or "license" for more information. >>> import sys >>> 'my_path' in sys.path True >>> exit()
使用 -S 参数显式关闭导入:
C:Users nanprince>python -s Python 3.8.8 (default, Apr 13 2021, 15:08:03) [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32 Warning: This Python interpreter is in a conda environment, but the environment has not been activated. Libraries may fail to load. To activate this environment please see https://conda.io/activation Type "help", "copyright", "credits" or "license" for more information. >>> import sys >>> 'my_path' in sys.path False
六、导入搜索路径
Python 在 import 时会在系统中搜索模块或者包所在的位置,
- 程序主目录(默认定义): 如果是以脚本方式启动的程序,则为启动脚本所在目录;如果在交互式解释器中,则为当前目录;
- PYTHONPATH目录(可选扩展): 以 os.pathsep 分隔的多个目录名,即环境变量
os.environ['PYTHONPATH'] (类似 shell 环境变量 PATH); - 标准库目录(默认定义): Python 标准库所在目录(与安装目录有关);
- .pth文件目录(可选扩展): 以 “.pth” 为后缀的文件,其中列有一些目录名(每行一个目录名)。
因此如果想要添加库的搜索路径,可以有如下方法:
- 直接修改 sys.path 列表
- 使用 PYTHONPATH 扩展
- 使用 .pth 文件扩展
七、重新加载
关于 import,还有一点非常关键:加载只在第一次导入时发生。Python 这样设计的目的是因为加载是个代价高昂的操作。
通常情况下,如果模块没有被修改,这正是我们想要的行为;但如果我们修改了某个模块,重复导入不会重新加载该模块,从而无法起到更新模块的作用。有时候我们希望在 运行时(即不终止程序运行的同时),达到即时更新模块的目的,内建函数 reload() 提供了这种 重新加载 机制(在 Python3 中被挪到了 imp 模块下)。
关于 reload 与 import 的不同:
- import 是语句,而 reload 是函数
- import 使用 模块名,而 reload 使用 模块对象(即已被import语句成功导入的模块)
重新加载
- 会重新编译和执行模块文件中的顶层语句
- 会更新模块的名字空间(字典
M.__dict__ ):覆盖相同的名字(旧的有,新的也有),保留缺失的名字(旧的有,新的没有),添加新增的名字(旧的没有,新的有) - 对于由
import M 语句导入的模块 M:调用 reload(M) 后,M.x 为 新模块 的属性 x(因为更新M后,会影响M.x的求值结果) - 对于由
from M import x 语句导入的属性 x:调用 reload(M) 后,x 仍然是 旧模块 的属性 x(因为更新M后,不会影响x的求值结果) - 如果在调用
reload(M) 后,重新执行 import M(或者from M import x)语句,那么 M.x(或者x)为 新模块 的属性 x
参考
- https://www.cnblogs.com/russellluo/p/3328683.html
- Python import hook
- 6. Simple statements — Python 2.7.18 documentation
- [QTA] Python Import Hooks 的实现和应用 · TesterHome
- https://www.cnblogs.com/fnng/p/16993400.html
- 让我们使用usercustomize.py代替sitecustomize.py | 码农家园
- Python:sitecustomize 和 usercustomize_mob6047570191d0的技术博客_51CTO博客