python的import问题踩坑
在Serverless平台的研发过程中,意外在python的import问题上踩坑了
大概还是对python的包管理基本原理不够了解,首先对python的包管理机制做一个总结,然后分析这一次踩坑的问题
python的包管理
Import基本语法
Python 中 import 有四种常见语法形式:
- 语法1:导入整个模块
1 | import random |
- 语法2:从模块中导入特定函数或类
1 | from random import randint |
- 语法3:为导入模块取别名
1 | import random as rd |
- 语法4:从模块中导入所有(不推荐,可能造成名称冲突,代码难以维护)
1 | from random import * |
绝对导入与相对导入
Absolute Import(绝对导入):从项目的根目录开始指定模块路径,例如
1 | from package.subpackage.module import my_func |
Relative Import(相对导入):使用基于当前文件所在位置的相对路径进行导入,以.
代表当前目录,..
代表上一层,如:
1 | from .module import my_func |
注意: 相对导入必须用from语法,不能写为import .module
这种方式。
两种导入方法的区别与各自适用场合说明:
- absolute import 明确具体,维护容易,但结构变化时若路径需修改则较为麻烦。
- relative import 路径随结构改变自动适应,但相对路径可能导致不同位置执行时出错。
模块与包
模块 (module):
Python模块是以
.py
结尾的单个文件,包含函数、类或变量的定义。使用:通过
import 模块名
引入。示例:
1
2import math
result = math.sqrt(9)常见内置模块如:
math
,os
,sys
,random
等。
包 (package):
包含
__init__.py
文件的文件夹,用于整合多个相关模块。使用:
import 包名.模块名
或者from 包名 import 模块名
。示例目录结构:
1
2
3
4mypackage/
├── __init__.py
├── module1.py
└── module2.py示例:
1
2from mypackage import module1
module1.myfunction()
init文件
上述示例本质上还是模块的用法,包下的__init__.py
,在包被import的时候会自动加载,来完成包的初始化,从而更方便的使用包
将包内一些模块的内容导入到包的命名空间供外部使用。
1 | # __init__.py内容 |
这样用户就可以很方便地使用:
1 | from package import function1, ClassA, function2 |
即达到包的统一导出、自动加载功能,不必关心具体模块内部结构。
否则用户需明确导入具体模块位置:
1 | from package.module1 import function1, ClassA |
踩坑实践
在https://github.com/tedcy/python_test这个项目中,我复现了serverless平台的场景:
同时加载了两个包Script0和Script1,包内的每一级目录的__init__.py
都使用from .模块名 import *
的方式,允许对外导出包中的全部符号
当执行python3 test_import.py
时,首先在test_import.py导入了Script0和Script1
1 | import Script0 |
包内的每一级目录的__init__.py
都使用from .模块名 import *
的方式,允许对外导出包中的全部符号
先导入的Script0,因此执行了Script0/__init__.py
1 | from .test0 import * |
从而执行Script0/test0.py中的全局代码:
1 | sys.path.insert(0, os.path.abspath("Script0")) |
导入成功,然而在Script1/test1.py中一模一样的代码抛出了异常
1 | try: |
完整的执行日志如下
1 | 导入新增模块: |
可以发现,在Script0/test0.py执行结束以后,就导入了模块HUYA
到了sys.modules的缓存中
因此继续使用from HUYA
的导出方式,只会使用缓存的模块文件/root/python_test/test_import/Script0/HUYA/__init__.py
即使当前的sys.path已经把当前目录插入到了最高优先级,依然失败了
这和C++中按顺序查找include的方式是不一样的!
C++:
每个文件各自独立,每次编译到
#include
都得重新根据路径和顺序去查找文件,每次都从头来,没有共享的缓存。(编译器处理方式)Python:
第一次导入模块(
import
)的时候,会根据sys.path
路径像查地图一样去找,这个时候找到模块并加载到内存里一份。后续如果再次导入同个模块,Python就直接从内存里这一份(缓存)拿去用,不会再去磁盘重复找一次。(解释器缓存模块方式)
在Script1/test1.py后续代码中,换了两种方式去导入
1 | #相对路径导入 |
输出日志
1 | Script1使用from .HUYA: I am Req1 from Script1 |
这里能导入成功,是因为不管是相对路径还是绝对路径,导入的模块名都是绝对路径Script1.HUYA
,从而避免了干扰的问题
在更深目录的Script1/sub_test/sub_test.py如果想引用同包下的HUYA.ProcessJobSendMessageReq1
,也必须使用绝对路径
1 | #不带路径导入 |
输出日志
1 | Script1.sub_test使用from HUYA:导入 ProcessJobSendMessageReq1 失败: cannot import name 'ProcessJobSendMessageReq1' from 'HUYA' (/root/python_test/test_import/Script0/HUYA/__init__.py) |
总结
不能先入为主,Python的导入方式和C++不同,虽然都可以指定查找路径的优先级,但是Python存在模块缓存,可以打印sys.modules迅速定位失败原因
尽量不用不带路径的导入方式,很容易造成导入失败
绝对路径和相对路径又有他们推荐的用法:
包里的python文件调自己这个下属包里的其他python文件里的方法或者类的时候,可用相对路径或者绝对路径
其中相对路径的方式更清晰,推荐使用
包里的下属包A调另一个下属包B的python文件,要用绝对路径
参考资料
https://docs.python.org/zh-cn/3/reference/import.html