[python] __init__的使用

文章转自:为什么大厂代码都有__init__.py?这篇文章彻底终结你的困惑!


如果你经常写 Python 代码,应该经常在一些项目里见过 __init__.py 这个文件。尤其是当你接触到大型 Python 项目时,它几乎无处不在。

那么,__init__.py 究竟是做什么的?什么时候需要用它?它是如何工作的?

今天,我们就来彻底搞清楚这个问题,带你从 Python 模块(module)开始,一步步深入到 Python 包(package)的概念,最终理解 __init__.py 的真正作用。

什么是 Python 模块?

在 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 包?

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. 1. 初始化包:当包被导入时,__init__.py 里的代码会自动执行,适用于需要一些启动配置的情况。

  2. 2. 简化导入路径:可以在 __init__.py 里定义包的默认导出内容,让导入更方便。

用例 1:初始化包

假设我们在 __init__.py 里写上一行代码:

print("utils package has been imported!")

当 main.py 运行时,它会自动触发 __init__.py,所以终端会输出:

utils package has been imported!

这意味着:无论我们导入 utils 里的哪个模块,__init__.py 里的代码都会执行一次。

这在很多情况下很有用,比如你想在包初始化时加载某些数据、创建数据库连接等。

用例 2:简化导入路径

现在来看一个更实用的例子。假设我们不想每次都写:

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 来优化导入体验。

用例 3:动态导入模块

有时候,我们的包可能会随着时间增长,新增多个模块。如果我们希望所有的模块都被自动导入,而不需要手动维护 __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,尤其是在 相对导入 和 绝对导入 的问题上。

问题 1:直接运行 __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 这个上下文中运行,而不是独立执行。


问题 2:包的导入方式不同

如果我们在 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. 1. __init__.py 主要用于标记目录为 Python 包,同时可以用来:

    • • 简化导入路径

    • • 执行初始化操作

    • • 动态加载模块

  2. 2. 在使用 __init__.py 时,需要注意:

    • • 避免直接运行

    • • 正确处理相对导入与绝对导入

  3. 3. 在大型项目中,可以利用 __init__.py 设计 更清晰的模块接口,提高可读性。

你可能感兴趣的:(Python,python)