python模块import原理

1. import导入过程

python执行import语句时,只有两个步骤,第一步是搜索模块,第二步是将搜索结果绑定到局部命名空间。

搜索时,分为两步:

  1. 搜索sys.modules
  2. 搜索sys.meta_path

导入一个模块时,会将这个导入的模块以及这个模块里调用的其他模块信息以字典的形式保存到sys.modules中,如果再次导入词模块,则优先从sys.modules查找模块,你可以在脚本里执行print(sys.modules)查看已经加载的模块,我们甚至可以直接修改sys.modules里的内容

import os
import sys

sys.modules['fos'] = os
import fos
print(fos.getpid())

执行import fos时,会先到sys.modules里查找是否有该模块,'fos'做key,找到的value是os模块,因此可以调用getpid方法。

如果在sys.modules模块中找不到目标模块,则从sys.meta_path中继续寻找。sys.meta_path是一个list,里面的对象是importer对象,importer对象是指实现了finders 和 loaders 接口的对象,输出sys.meta_path里的内容可以查看有什么

<class '_frozen_importlib.BuiltinImporter'>
<class '_frozen_importlib.FrozenImporter'>
<class '_frozen_importlib_external.PathFinder'>

这三个importer对象分别查找及导入build-in模块,frozen模块(即已编译为Unix可执行文件的模块),import path中的模块,如果都找不到,就会报ModuleNotFoundError的错误。

2. sys.path

导入模块时,首先会去sys.modules里查看,如果查不到会使用sys.meta_path里的importer继续查找,这些importer首先会查找内置模块,然后查找frozen模块,最后会根据sys.path里的路径进行查找。在脚本里执行print(sys.path),在我的电脑上输出结果为

/Users/kwsy/kwsy/coolpython
/Library/Frameworks/Python.framework/Versions/3.6/lib/python36.zip
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/proxypool-2.0.0-py3.6.egg
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/bs4-0.0.1-py3.6.egg
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/Flask_Mail-0.9.1-py3.6.egg
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/blinker-1.4-py3.6.egg

sys.path里路径的顺序决定了搜索的顺序,这里的路径分为3类

  1. sys.path[0] 是当前路径,也是最先被搜索的
  2. 第二类是安装python时内置进去的,比如 /Library/Frameworks/Python.framework/Versions/3.6/lib/python36.zip
    /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6
    /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload
    /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages
  3. 最后一类就是安装的第三方模块

如果模块存在于这些路径中,那么不管它身在何处,都可以使用import直接导入,下面是一个python脚本的地址

/Users/kwsy/PycharmProjects/pythonclass/mytest/import_demo/conf/offline.py

脚本内容为

host='192.168.0.2'

在其他的项目里,只要将offline.py的地址加入到sys.path中,就可以在脚本里直接引入offline

import sys
import importlib

sys.path.append('/Users/kwsy/PycharmProjects/pythonclass/mytest/import_demo/conf')
import offline
print(offline.host)

module = __import__('offline')
print(module.host)

module = importlib.import_module('offline')
print(module.host)

实践中你的编辑器甚至会在import offline这一行显示红色的波浪线,那是在警告你找不到模块,这个警告是编辑器发出的,因为offline.py的目录是在程序执行期间加入到sys.path中的,编辑器在你编写代码阶段还检查不到sys.path中有这个目录,因此是编辑器找到不到这个模块,程序执行时,python解释器却可以找得到,这也是一种动态加载模块的技术。

3. import hooks

我们可以通过一些技术手段来扩展import的行为,为了让你有一个直观的理解,推荐一个第三方库pypi,此模块实现了一个神奇的功能,你可以在代码里导入根本不存在的模块,遗憾的是还不能使用pip来安装这个模块,你可以直接将pypi.py文件放在site-packages文件下或者直接放在项目里,该模块的git地址是 https://github.com/miedzinski/import-pypi

现在,来做一个实现

import pypi
import requests

print(requests)

我在执行这段代码时,已经将requests模块卸载,但是执行这段代码时却不会报错误,在等待一段时间后,程序正常执行print语句。

pypi.py的源码并不复杂

import importlib.abc
import importlib.machinery
import subprocess
import sys


def install(pkgname, version=None):
    cmd = [sys.executable, '-m', 'pip', 'install']
    if version:
        cmd.append('{}=={}'.format(pkgname, version))
    else:
        cmd.append(pkgname)
    subprocess.check_call(
        cmd,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )


class PipFinder(importlib.abc.MetaPathFinder):
    def find_spec(self, fullname, path, target=None):
        try:
            install(fullname)
        except subprocess.CalledProcessError:
            return None
        else:
            return importlib.machinery.PathFinder().find_spec(
                fullname,
                path,
                target,
            )


sys.meta_path.append(PipFinder())

sys.meta_path里存储了importer对象,如果在sys.modules里找不到目标模块,就会利用这里的对象继续寻找,在pypi中,作者实现了一个名为PipFinder的importer并将其放入到sys.meta_path,find_spec专门用来寻找指定的模块,进入函数后,首先调用install函数对模块进行安装,假设模块已经安装那么什么都不会发生,假设模块之前没有被安装则直接进行安装。安装结束后,调用importlib模块加载指定模块。

4. 参考资料

  1. https://sikx.io/2017/10/09/Python_import/

  2. https://mozillazg.com/2016/04/apm-python-agent-principle.html

  3. https://github.com/miedzinski/import-pypi/blob/master/pypi.py

扫描关注, 与我技术互动

QQ交流群: 211426309

加入知识星球, 每天收获更多精彩内容

分享日常研究的python技术和遇到的问题及解决方案