python执行import语句时,只有两个步骤,第一步是搜索模块,第二步是将搜索结果绑定到局部命名空间。
搜索时,分为两步:
导入一个模块时,会将这个导入的模块以及这个模块里调用的其他模块信息以字典的形式保存到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的错误。
导入模块时,首先会去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类
/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
如果模块存在于这些路径中,那么不管它身在何处,都可以使用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解释器却可以找得到,这也是一种动态加载模块的技术。
我们可以通过一些技术手段来扩展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模块加载指定模块。
QQ交流群: 211426309