文章转自:为什么大厂代码都有__init__.py?这篇文章彻底终结你的困惑!
如果你经常写 Python 代码,应该经常在一些项目里见过 __init__.py
这个文件。尤其是当你接触到大型 Python 项目时,它几乎无处不在。
那么,__init__.py
究竟是做什么的?什么时候需要用它?它是如何工作的?
今天,我们就来彻底搞清楚这个问题,带你从 Python 模块(module)开始,一步步深入到 Python 包(package)的概念,最终理解 __init__.py
的真正作用。
在 Python 中,模块(module)就是一个 Python 文件,里面包含了 Python 代码,通常用于被其他 Python 文件导入和使用。
换句话说,每个 .py
文件都可以算是一个模块。比如,假设我们有两个模块:
• string_util.py
(处理字符串的工具)
• math_util.py
(处理数学计算的工具)
如果我们的项目里还有一个 main.py
文件,我们可以在 main.py
里导入这两个模块,使用其中的功能:
from string_util import capitalize
from math_util import add
这样,我们就可以在 main.py
里直接调用 capitalize()
和 add()
方法,而不用把所有代码都写在 main.py
里。这样做的好处是:
• 代码更清晰:不同功能的代码分开存放,逻辑更清楚。
• 更易维护:如果想修改字符串处理的逻辑,只需要修改 string_util.py
,不会影响 math_util.py
。
• 更易复用:这些模块可以在不同的项目中重复使用,而不用复制粘贴代码。
到这里,我们理解了 Python 模块的作用。但当项目越来越大,模块越来越多时,我们就需要一种更好的组织方式——这时 Python 包(package) 就派上用场了。
Python 包(package)就是一个包含多个 Python 模块的文件夹,用于管理相关的 Python 代码。
比如,我们可以创建一个 utils
目录,把 string_util.py
和 math_util.py
都放进去,这样它们就组成了一个 Python 包:
my_project/
│── main.py
│── utils/
│ │── string_util.py
│ │── math_util.py
在较早的 Python 版本中,如果要让 utils/
目录被识别为一个包,必须在目录里创建一个 __init__.py
文件:
my_project/
│── main.py
│── utils/
│ │── __init__.py
│ │── string_util.py
│ │── math_util.py
但在现代 Python 版本中,即使没有 __init__.py
,Python 也会自动把 utils/
识别为一个包。
不过,即使 __init__.py
不是必须的,我们仍然可以用它来做一些有趣的事情,稍后会详细讲解。
现在,如果我们想在 main.py
里使用 utils
包中的模块,导入方式就要稍微调整一下:
from utils.string_util import capitalize
from utils.math_util import add
这样,我们就可以从 utils
包中导入 string_util
和 math_util
里的方法了。
但是你可能会注意到,当我们这么做的时候,Python 目录里可能会出现一个 __pycache__
文件夹:
my_project/
│── main.py
│── utils/
│ │── __pycache__/
│ │── string_util.py
│ │── math_util.py
这个 __pycache__
目录是 Python 的缓存机制,每当你导入一个 Python 模块时,Python 都会把它的 编译结果 存在 __pycache__
里,以加快下次运行的速度。这个目录不需要手动管理,Python 会自动处理。
__init__.py
的作用现在,我们终于来到了 __init__.py
这个文件。虽然它不是必须的,但它依然有一些非常重要的用途,比如:
1. 初始化包:当包被导入时,__init__.py
里的代码会自动执行,适用于需要一些启动配置的情况。
2. 简化导入路径:可以在 __init__.py
里定义包的默认导出内容,让导入更方便。
假设我们在 __init__.py
里写上一行代码:
print("utils package has been imported!")
当 main.py
运行时,它会自动触发 __init__.py
,所以终端会输出:
utils package has been imported!
这意味着:无论我们导入 utils
里的哪个模块,__init__.py
里的代码都会执行一次。
这在很多情况下很有用,比如你想在包初始化时加载某些数据、创建数据库连接等。
现在来看一个更实用的例子。假设我们不想每次都写:
from utils.string_util import capitalize
from utils.math_util import add
而是希望能直接写成:
from utils import capitalize, add
这时,我们可以在 __init__.py
里做一些小调整:
from .math_util import add
from .string_util import capitalize
这样,在 main.py
里,我们就可以直接这样导入:
from utils import capitalize, add
这个技巧在你想提供一个更简洁的 API 时非常有用,比如 pandas
这样的大型库,就大量使用了 __init__.py
来优化导入体验。
有时候,我们的包可能会随着时间增长,新增多个模块。如果我们希望所有的模块都被自动导入,而不需要手动维护 __init__.py
,我们可以使用 动态导入 技巧。
import os
import importlib
package_dir = os.path.dirname(__file__) # 获取当前包的路径
for module in os.listdir(package_dir):
if module.endswith(".py") and module != "__init__.py":
module_name = module[:-3] # 去掉 ".py"
# 动态导入模块
mod = importlib.import_module(f".{module_name}", package=__name__)
# 将模块中的所有公共名称(不以下划线开头)添加到包命名空间
for name in dir(mod):
if not name.startswith("_"):
globals()[name] = getattr(mod, name)
__all__.append(name)
这样,每当我们在 utils
目录下新增一个 Python 文件时,它都会被自动导入,无需手动修改 __init__.py
。
到这里,我们已经理解了 Python 模块、包和 __init__.py
的基本作用。
但在实际开发中,我们还会遇到 相对导入 和 绝对导入 的问题,尤其是在处理 多层嵌套包 时,经常会出现 ModuleNotFoundError
这样的错误。
__init__.py
引发的 ImportError在 Python 中,__init__.py
可能会引发 ImportError,尤其是在 相对导入 和 绝对导入 的问题上。
__init__.py
如果我们直接运行 python utils/__init__.py
,会报错:
ImportError: attempted relative import with no known parent package
原因:相对导入(from .math_util import add
)必须在包的上下文中运行,而直接运行 __init__.py
时,它不属于任何包。
解决方案:始终确保 __init__.py
只是在 import utils
这个上下文中运行,而不是独立执行。
如果我们在 main.py
里运行:
from utils import add
但在 utils/__init__.py
里写的是:
from .math_util import add
那么,在 不同的运行方式下,可能会遇到 ModuleNotFoundError
:
ModuleNotFoundError: No module named 'utils.math_util'
解决方案:
• 如果是 顶层脚本,建议使用 绝对导入:from utils.math_util import add
• 如果是 模块内部导入,建议使用 相对导入:from .math_util import add
__init__.py
在大型项目中,我们可能会有多层级的包,比如:
my_project/
│── main.py
│── utils/
│ │── __init__.py
│ │── math_util.py
│ │── text/
│ │ │── __init__.py
│ │ │── string_util.py
此时,text/__init__.py
也可以用于控制 子包的导入:
from .string_util import capitalize
这样,在 main.py
里,我们可以直接:
from utils.text import capitalize
避免了 from utils.text.string_util import capitalize
这种冗长的写法。
1. __init__.py
主要用于标记目录为 Python 包,同时可以用来:
• 简化导入路径
• 执行初始化操作
• 动态加载模块
2. 在使用 __init__.py
时,需要注意:
• 避免直接运行
• 正确处理相对导入与绝对导入
3. 在大型项目中,可以利用 __init__.py
设计 更清晰的模块接口,提高可读性。