昨天晚上十一点多,下楼拿外卖,手机一震,是我们组那个小李发消息:“哥,问个灵魂问题,python 里那个 __init__.py 到底干嘛的?删了也能跑一会儿,不删又感觉它挺重要的。”
我一看这问题,困意直接少一半,干脆边走边给他语音讲了一通,说白了就是三个事:是不是包、怎么导、要不要顺手干点初始化。我给你复盘一下,大概意思就是这些。
先想象一个很常见的项目结构,差不多这样:
myproject/ __init__.py math_utils.py io_utils.py你在别的地方写:
import myprojectfrom myproject import math_utils为啥 myproject 这个文件夹能被当成一个“模块”来导? 关键就在那个看起来很废的 __init__.py。
在早一点的 Python 版本里,没有 __init__.py 的目录,就只是个普通文件夹,不是包,导入直接报错:ModuleNotFoundError: No module named 'myproject'。 所以很多人说:__init__.py 的第一个作用,就是告诉解释器:喂,这个目录是个包,你可以 import 它。
现在 Python3.3 以后有“命名空间包”,有些场景没 __init__.py 也能导,但是工作里大部分项目还都是老老实实放一个,图省心,工具也好识别。
第二个用处,很多人是踩坑之后才懂:决定你从包里到底能导出啥。
比如你想让别人这样用:
from myproject import add, load_config那你可以在 __init__.py 里写:
# __init__.pyfrom .math_utils import addfrom .io_utils import load_config__all__ = ["add", "load_config"]这样外面的人看到的就像是:
from myproject import addadd(1, 2)以为 add 就在 myproject 下面,其实你在 __init__.py 帮它“转了一手”。__all__ 还有个作用是控制:
from myproject import *这个星号到底会把哪些名字“倒腾”出去。一般库都会在 __init__.py 里把公共 API 收拾一下,不然别人用起来全是 from myproject.xxx.yyy import zzz,又长又丑。
第三个用处就更生活化一点:包级初始化。
比如你有个小工具包,每次被导入时,你想:
那就很自然写在 __init__.py 里:
# __init__.pyimport loggingfrom .config import load_default_configlogger = logging.getLogger(__name__)logger.addHandler(logging.NullHandler())config = load_default_config()__version__ = "1.0.0"外面就可以:
import myprojectprint(myproject.__version__)print(myproject.config["db_url"])这时候 __init__.py 就像这个包的“门面”和“开机脚本”,别人只要 import myproject,你该准备的东西都准备好了。
但这里有个很容易翻车的点:别在 __init__.py 里写太重的逻辑。 比如一导入就连数据库、连 Redis、还跑一堆 IO,这样后面任何地方只要 import 一下你的包,启动时间直接肉眼可见变慢,还容易循环依赖。 一个比较稳妥的习惯:在 __init__.py 里做“轻初始化”,重活放函数里,等真的要用的时候再调。
说到循环依赖,就顺手讲个常见坑。 有一天我们组重构,把某个工具函数从 utils.py 挪到了 helpers.py,然后在 __init__.py 里写:
from .utils import foo结果后来 utils.py 又去 from . import foo,一圈绕回来,运行时直接报 ImportError。 好多这种“鬼打墙”式的问题,其实就是 __init__.py 到处乱导子模块,子模块又反过来导包本身。
简单的经验:
__init__.py 只做“向外暴露”,少去依赖子模块里太具体的实现# in math_utils.pyfrom .io_utils import load_config这样层级关系就比较清楚,不那么容易绕成圈。
还有一个你可能偶尔会碰到的场景:一个项目里多个目录,想合成一个包名。
有些高级玩法会用“命名空间包”,就是好几个物理目录共享同一个顶层包名。这种时候某些目录里可能就没有 __init__.py,但能 import。 如果你哪天看到这种结构,别慌,不是项目忘了,是利用了 Python 的新特性。 不过对于日常开发来说,大可以记住一句话:看到一个目录里有 __init__.py,就当它是一个可以被 import 的小世界,你可以在里面:
最后给你一个最小可跑的例子,你在本地随手建个目录玩一下就懂了。
目录:
demo_pkg/ __init__.py math_utils.pymath_utils.py:
defadd(a, b):return a + b__init__.py:
from .math_utils import add__all__ = ["add"]同级新建个 test.py:
from demo_pkg import addprint(add(1, 2))跑一下 python test.py,屏幕上打印个 3,这一套就是 __init__.py 在背后帮你干的事。
所以回到一开始那个问题:__init__.py 到底有啥用?一句人话版概括就是:
它让“一个文件夹”变成“一个可导入、可打理、可包装的模块世界”。
至于要不要在里面写东西,就看你是想当个“安静的目录标记”,还是想顺便帮自己整理一下这个包的门面了。