Python包模块与模块导入查找顺序详解
作者:trayvontang
简介
无论是为了看懂别人的代码,还是为了更好的组织我们的个人的工程代码,了解一下Python的模块和包都非常有必要。
另外,知道搜索模块的顺序,也能帮助我们更好的理解一些常见的错误,方便我们快速定位问题。
模块导入查找顺序
首先,我们先看一下Python的模块查找顺序:
- sys.modules查找(导入模块缓存)
- 执行脚本所在的目录:__name__==‘__main__’
- 环境变量PYTHONPATH指定的目录
- 标准库目录(Python安装跟目录下的Lib目录)
- 配置文件(Python安装跟目录下.pth文件中指定的目录)
- 三方库(Python安装跟目录下site-packages中的目录)
搜索路径是由sys.path决定,我们可以打印看一下:
import sys print(sys.path)
输出:
['E:\\app\\python\\fin\\md', 'E:\\app\\python\\fin', 'E:\\app\\python\\fin\\.venv\\Scripts\\python313.zip', 'D:\\Env\\python\\py3130i\\DLLs', 'D:\\Env\\python\\py3130i\\Lib', 'D:\\Env\\python\\py3130i', 'E:\\app\\python\\fin\\.venv', 'E:\\app\\python\\fin\\.venv\\Lib\\site-packages']
我们可以手动添加搜索目录和顺序:
import sys # 添加在末尾,最后查找 sys.path.append('dir') # 添加在开头,最先查找 sys.path.insert(0, 'dir2') # 支持zip sys.path.append('module.zip') # 支持zip中指定目录 sys.path.append('module.zip/lib/python')
入口文件所在目录
环境变量PYTHONPATH
- Linux系统:
echo 'export PYTHONPATH="/home/tim/pylib/"' >> ~/.bashrc source ~/.bashrc
- Windows:
Lib目录(标准库)
.pth配置文件
注意._pth和.pth的区别
._pth:安装包中没有这个文件要自己创建,内嵌包中有
内容如下:
python311.zip . D:\\Env\\python\\py3114\\Lib\\site-packages\\ # Uncomment to run site.main() automatically #import site
._pth会覆盖sys.path
而.pth是添加sys.path
.pth的处理逻辑在标准库site中:
新版本的.pth文件有很多限制,可以参考:site
注意:
这个文件名字必须要对,不知道,可以直接复制安装目录上的PythonXXX.dll这个文件的名字。
另外,一般不需要修改,因为这个文件会覆盖sys.path,让其他的都失效。
三方库
标准库Lib下的site-packages目录
模块
Python中的模块就是指py文件,一个文件代表一个模块。
包
很多时候,一个工程不止一个模块,我们可以用包来组织不同模块。
包中包含一个__init__.py文件,用来控制包的导入行为。
在2.x中__init__.py文件是必修有,3.x可选。
import
import 模块名
Python解释器第1次导入模块就加载到内存,多次import不会重新执行模块内的语句
我们看一个示例:
我们有一个base_module.py文件
print('这是一个自定义基本模块') num = 1000 def fun_a(): print('fun_a', num) def fun_b(): print('fun_b') if __name__=='__main__': print('执行了该文件')
我们在module_import_test.py文件中导入base_module模块(文件)
import sys print(sys.modules) import base_module # 多了base_module模块信息 print(sys.modules) base_module.fun_a() import base_module print(sys.modules) base_module.fun_a()
注意:文件名字不要用中划线,否则import模块会出错。
我们可以看到没有打印:执行了该文件,这是因为我们导入的是模块,没有直接执行这个文件。
现在是不是就更清楚为啥要加上:if name==‘main’:了
if __name__=='__main__': print('执行了该文件')
from mmm import fffvvv
从包或者模块中导入函数、变量等。
和直接import模块不同,from方式相当于将模块指定的元素导入到当前命名空间。
因此,我们可以直接调用方法,不用通过模块名.xxx的方式调用了。
from base_module import fun_a,fun_b fun_a() fun_b()
from mmm import fff as new_name
还可以用as重命名:
from base_module import fun_a as xxx xxx()
form mmm import *
*表示将模块中除了下划线(_)开头的元素全部元素导入
from base_module import * fun_a() fun_b() print(num)
__all__控制import *的行为
我们在模块中添加__all__,注意是一个字符串列表,下面是fun_a
print('这是一个自定义基本模块') num = 1000 def fun_a(): print('fun_a', num) def fun_b(): print('fun_b') __all__ = ['fun_a'] if __name__=='__main__': print('执行了该文件')
我们再调用fun_b(),就会出现NameError:
相对导入与绝对导入
- pkg_b.py
num_b = "num_b" def fun_b(): print("pkg_b fun_b")
- pkg_c.py
from .pkg_b import fun_b from . import pkg_b num_c = "num_c" def fun_c(): print("pkg_c fun_c") fun_b() print(f"pkg_c中导入pkg_b:{pkg_b.num_b}")
- base_import.py
import pkg_test.sub_pkg.pkg_b from pkg_test.sub_pkg import pkg_b from pkg_test.sub_pkg import pkg_c pkg_test.sub_pkg.pkg_b.fun_b() pkg_b.fun_b() pkg_c.fun_c() pkg_c.pkg_b.fun_b()
相对导入的模块不能作为主程序运行,因为.会被替换为__main__,然后就会出现:
ImportError: attempted relative import with no known parent package
小结
导入包和导入模块基本一致,如何判断是包还是模块呢?
包通常是有结构,所以,看到import xxx.yyy这种包含.的import,就是导入的包。
import 包名.模块名 as 别名 from . import 模块名 from 包名 import 模块名 as 别名 from 包名.模块名 import 成员名 as 别名
更多内容可以参考:
命名空间
Python的命名空间有点类似于变量表。
- local namespace,就是函数的命名空间,相当于局部变量表,包含函数的变量
- global namespace,就是模块的命名空间,包含functions、classes、modules、变量、常量等
- build-in namespace,包含build-in function、exceptions,所有模块均可访问
Python对于变量的查找顺序为:
- local namespace,当前函数或类方法,若找到,则停止搜索
- global namespace,当前模块,若找到,则停止搜索
- build-in namespace,最后查找build-in命名空间
- 若变量不是build-in的内置函数或变量,Python将报错NameError
闭包比较特殊:在local namespace找不到该变量,会查找目标是父函数的local namespace
看一个简单的示例:
print('这是一个自定义基本模块') num = 1000 def fun_a(): print('fun_a', num) def fun_b(): print('fun_b') if __name__=='__main__': fun_a() print('执行了该文件')
from base_module import fun_a,num print(num) num = 5000 print(num) fun_a()
我们可以看到,改变了num值,调用模块中的方法的时候,还是方法的模块直接的命名空间的num。
各类文件与目录
- .pyo:优化编译后的文件
- .pyc:Python编译之后的文件
- .pyd:包含Python代码的库文件(Windows)
- .pyi:静态类型信息文件
- .so:Linux的动态链接库
- .dll:Windows的动态链接库
- __pycache__:用于存储编译Python源文件的字节码文件目录(sys.dont_write_bytecode开关控制)
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。