欢迎,来自IP地址为:216.73.216.52 的朋友
Python 有一个特殊的 __init__.py 文件,它可以将一个目录标记为一个常规的 Python 包,并允许导入其模块。该文件在第一次导入其所在包时自动运行,可以使用它来初始化包级变量、定义函数或类,并为用户清晰地构建包的命名空间。
了解如何有效地使用 __init__.py,可以以清晰、可维护的方式构建 Python 包,从而提高可用性和命名空间管理效率。
我们经常会在大型 Python 项目中见过名为 __init__.py 的文件,并好奇它们到底有什么用处。或者我们自己可能也使用过 __init__.py 文件,但并不是很清楚它们为何重要,也不知道如何利用它们的功能。或许我们还注意到,即使忘记将 __init__.py 添加到包中,Python 代码有时也能正常工作。
简而言之:__init__.py 可以将文件夹声明为常规 Python 包
特殊文件 __init__.py 用作标记,表明其所在目录是一个常规包,包名就是目录名。当人们在 Python 中谈论包时,通常指的是常规包。__init__.py 文件是一个 Python 源文件,这意味着它也是一个模块。
下表是 Python 中模块和包这两个术语以及它们在 Python 中的用法区别:
模块 | 包 | |
---|---|---|
定义 | 包含代码的单个 Python 文件 | 包含一个或多个 Python 模块的目录 |
命名 | 不带 .py 扩展名的文件名 | 目录名 |
内容 | 函数、类和变量 | 模块以及可选的子包 |
用途 | 组织小型项目,简单代码复用 | 组织大型项目,代码复用 |
导入 | 直接导入(import module) | 导入包或其模块(import package.module) |
由于 __init__.py 的存在使得 Python 加载器将其包含的目录识别为常规包。这意味着我们可以使用包名称导入整个目录以及其中的特定模块,甚至导入这些模块中的单个函数、变量和类。
我们还可以将这些代码对象导入其他模块,从而以多种灵活的方式复用代码。
如上所述,__init__.py 可能是一个空文件,在这种情况下,它仅用于标识包。
如果向 __init__.py 中添加代码时会发生什么?
如果 __init__.py 中包含了代码,那么这些代码将在首次导入包时执行。
__init__.py 中的代码可以执行各种有用的任务。例如,它可以导入其他模块或包,并定义自己的函数或数据。导入包后,导入模块可以访问其自身的所有代码以及它自身导入的所有内容。
以下是一个基于单个包的简单示例开始。结构设置为:
project/ │ └── tools/ └── __init__.py
示例项目名称为”project”,它包含一个名为”tools”的包,”tools/__init__.py”文件本来是空的即可指示 tools 包,这里给它添加一些内容代码:
__version__ = "1.0.0" magic_number = 42
有了这些文件后,在 project 目录中启动 REPL 会话,并观察导入和使用工具包时发生的情况:
当在第 1 行执行 import tools 时,tools 目录中的 __init__.py 文件会被隐式执行。这使得 tools/__init__.py 中的名称可以通过 tools 前缀访问。通常,可能会使用 __init__.py 来声明与导入的包相关的变量,例如包版本或包范围的常量。
需要注意的是:即使多次导入该包,__init__.py 中的代码也只会执行一次。
如果不添加 __init__.py 文件会发生什么?
到目前为止,我们接触的一直都是常规包,但 Python 还支持另一种类型的包,称为命名空间包。命名空间包没有 __init__.py 文件。但我们仍然可以导入它,前提是 Python 可以通过 sys.path 找到它。如果我们没有将 __init__.py 文件添加到目录中,那么它将被视为命名空间包。
常规包和命名空间包都定义了命名空间。区别在于,命名空间包不限于单个目录——标识包内容的是命名空间前缀,而不是物理目录。
注意:命名空间包已经存在很多年了,但它们以前需要一些额外的设置。自从 Python 3.3 采用 PEP 420 以来,该语言就拥有了隐式命名空间包,仅通过缺少 __init__.py 来标识。这使得它们更容易被有意创建,但也更容易被无意创建!
回想一下,命名空间是一种用于区分不同代码对象的约定,这些代码对象可能恰好具有相同的基本名称,无论该名称是变量、函数、类还是其他任何名称。使用不同的前缀可以避免混淆。
例如,在大量代码中,很可能有两个名为 utils 的不同模块。假设其中一个包含财务实用程序,而另一个包含工资单实用程序。通过将它们放在不同的命名空间(分别称为 finance 和 payroll)中,即使它们共享相同的基本名称,Python 也可以区分它们。这是因为 Python 将 finance.utils 和 payroll.utils 视为完全独立的代码对象。
我们可以轻松地将命名空间包添加到之前的演示项目中。只需在 project 下添加一个新的空目录即可:
project/ │ ├── tools/ │ └── __init__.py │ └── tools_ns/
由于模块和包都必须是有效的 Python 标识符。因此,新的目录名称应该包含下划线 (_),而不是连字符 (-)。
现在,在 project 目录中,使用 REPL 验证是否也可以导入此包。每个 Python 包都有一个 .__path__ 属性。请注意,tools_ns.__path__ 与 tools.__path__ 略有不同:
Python 在当前目录中找到了 tools_ns,发现其中没有 __init__.py 文件,于是将其作为命名空间包导入。
虽然 tools_ns 已成功导入,但其 .__path__ 属性表明它是一个命名空间包,而不是常规包。
Python 的运行时维护一个名为 sys.path 的目录列表,它最初用于加载其自身的标准库,之后用于定位代码中声明的任何导入。该列表包含已安装的所有库的路径,以及正在运行的脚本的当前目录。
我们可以通过将更多目录添加到 PYTHONPATH 环境变量中,或在运行时操作 sys.path 来向 sys.path 添加更多目录。不过,这通常是不必要的,因为本地开发的代码将在脚本自身的目录及其子目录中找到。在本例中,Python 就是这样查找 tools 和 tools_ns 的。
命名空间包可用于将多个代码库合并为统一的命名结构。例如,一家拥有多个开发站点的公司可能希望将其各种贡献合并到一个命名空间下。
举例来说,一家名为 MegaCorp Inc. 的公司可能使用命名空间 com.megacorp。与其强迫用户将其命名空间内的所有代码都下载到一个包中,不如发布一些较小的、独立的库,这些库都共享相同的前缀,例如 com.megacorp.finance 和 com.megacorp.sales。
如果 MegaCorp 将这些库发布为单独的包,则可以单独维护、分发和使用它们。这种方法可以应用于任何以逻辑分离和模块化为关键的大型软件生态系统或产品。
命名空间包当然有其用途,但也有一些注意事项。其中之一是,在导入命名空间包时,解释器必须搜索整个 sys.path 以确保找到所有对命名空间有贡献的文件。这会导致导入速度变慢。此外,由于完整的命名空间只能在运行时组装,因此任何名称冲突在此之前都无法检测到。
除非确定要构建命名空间包,否则建议选择常规包,即在项目中的每个包中包含一个 __init__.py 文件。这将使导入更加高效且可预测。
在本教程中,我们了解了 Python 的 __init__.py 文件在定义常规包以及将其与命名空间包区分开来方面发挥的特殊作用。
有效地使用 __init__.py 可以帮助我们创建组织良好、易于维护且直观的包。通过合理地构建包,可以提高代码可读性、促进可重用性并简化包级初始化。