文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

【Python】`__init__.py` 文件详解

2023-09-03 07:18

关注

本文针对Python开发者, 详细描述了__init__.py文件在参与包、模块、命名空间导入/重命名/初始化时的使用方法,以及其中代码的执行机制。
本文使用边执行案例,边分析,边给结论的方法,描述了__init__.py文件的四大作用。

  1. 模块搜索标记
  2. 初始化命名空间, 空间名称即为目录名
  3. 设置__all__ 通配符导入目标
  4. 为同一目录的其他模块定义命名空间。

__init__.py 文件是每一个Python开发者都非常熟悉的文件。

这个文件究竟是干什么用的? 除了空文件以外,它里面可以有什么样的代码?我查阅了很多关于Python的文档,始终没有找到非常令人信服的详细文档资料。 即使是在Python的官方文档中,有的时候我也不知道究竟怎么解释才是正确的。

所以,以下是我对于__init__.py文件的一个总结,里面包含了一些案例,以及相应的步骤。 当然,本文的缺点是:有点长……:(

1. 模块、包和命名空间

module, package and namespace

要想搞清楚__init__.py文件,就必须要搞清楚模块、包和命名空间之间的关系

1.1. 模块

module

从一个宽泛的意义上来说,任何一个python文件都是一个模块。 可以被单独执行,也可以通过import语句被其他的Python程序所调用。 在模块内,python代码可以去调用其他的模块。

我们都知道python是可以在控制台中去一行一行去执行的。 那么,可以说,模块是将这些命令组织成为”程序“的基本组织单位了。

官方参考资料:3.11.1 Documentation » Python 教程 » 6. 模块

1.2. 包

package

包由多个文件和一个目录结构组成。 一个包可以包含模块、子包等内容。 在官方文档上有这样一句话:

要注意的一个重点概念是所有包都是模块,但并非所有模块都是包。 或者换句话说,包只是一种特殊的模块。 特别地,任何具有 __path__ 属性的模块都会被当作是包。

当然,这句话对于初学者有点拗口。 我们来搞一个实际的例子试试:

在PyCharm创建项目test01,目录如下

./├─ tdouya2.py├─ test01.py└─ tdouya    └─ __init__.py

test01.py是唯一的入口文件。 目录tdouya是一个包,而tdouya2.py是一个模块。

__init__.pytdouya2.py 都是空文件。 入口文件test01.py的代码为

import tdouyaimport tdouya2if __name__ == "__main__":    print("tdouya.{}".format(tdouya.__path__))    print("tdouya2.{}".format(tdouya2.__path__))

执行结果:

D:\Python-grp\miniconda3_data\env\py3.10\python.exe E:\develop\python\test01\test01.py tdouya.['E:\\develop\\python\\test01\\tdouya']Traceback (most recent call last):  File "E:\develop\python\test01\test01.py", line 7, in <module>    print("tdouya2.{}".format(tdouya2.__path__))AttributeError: module 'tdouya2' has no attribute '__path__'. Did you mean: '__name__'?进程已结束,退出代码1

我们发现,果然tdouya被识别为一个包。 而tdouya2并不认为一个包,只是一个文件/模块。

1.3. 命名空间

namespace

命名空间实际上是对名称空间的一个划分,防止模块和包中各种各样的名称发生冲突。 比如builtins.open()sys.open(),通过命名空间,无论是人还是机器都能够清晰的知道究竟是哪个模块来实现。

示例

import os.pathos.path.join('/home',user)

2. 模块和层次结构

2.1. 单文件的模块

国外的资料中常称之为simple project

在PyCharm中创建项目test02,入口文件test02.py,模块module02.py,同级目录

./├─ module02.py└─ test02.py

module02.py文件内容

def hello():    print('hello, it\'s module02.py')

test02.py文件内容

import module02if __name__ == "__main__":    module02.hello()

执行结果(3.10):

D:\Python-grp\miniconda3_data\env\py3.10\python.exe E:\develop\python\test02\test02.py hello, it's module02.py进程已结束,退出代码0

执行结果(2.7)

D:\Python-grp\miniconda3_data\env\py2.7\python.exe E:\develop\python\test02\test02.py hello, it's module02.py进程已结束,退出代码0

在Python2.7和Python3.10 的版本下执行,结果是一样的。

此时,没有必要创建__init__.py文件。module02.py 文件名去掉 .py 即为模块名。

2.2. 目录层次结构和命名空间

如果项目变得更加的复杂。 模块被按照目录管理了。 那么此时,我们来看下面的工程。

在PyCharm中创建项目test03,目录结构为:

./├─ test03.py└─ tdouya/    └─ module03.py

module03.py 文件内容为:

def hello():    print("hello world, this is module03.py")

入口文件test03.py内容为:

import tdouya.module03if __name__ == "__main__":    tdouya.module03.hello()

此时执行结果: python=3.10

D:\Python-grp\miniconda3_data\env\py3.10\python.exe E:\develop\python\test03\test03.py hello world, this is module03.py进程已结束,退出代码0

而在python=2.7下面, 执行结果是:

D:\Python-grp\miniconda3_data\env\py2.7\python.exe E:\develop\python\test03\test03.py Traceback (most recent call last):  File "E:\develop\python\test03\test03.py", line 16, in <module>    import tdouya.module03ImportError: No module named tdouya.module03进程已结束,退出代码1

这里,项目test03 在python3.10下正常工作,而在2.7下报错。 这时就需要__init__.py文件大显身手了。

我们在tdouya目录下,建立__init__.py空文件。 那么此时在2.7下执行项目test03结果为:

D:\Python-grp\miniconda3_data\env\py2.7\python.exe E:\develop\python\test03\test03.py hello world, this is module03.py进程已结束,退出代码0

这里的原因是从python=3.3 版本开始, 即使没有__init__.py文件, 目录也可以当作模块调用,即所谓的“Namespace Packages”。 但在“普通包”中, __init__.py文件应该是有的。

这里可以参考官方文档:

这里刚才提到了“普通包”的概念, 你可以理解为用__init__.py文件定义的包。 Python中还有一类包,称之为“命名空间包”, 这种包不在本文的讨论范围中。有兴趣的可以看刚才的参考文件:PEP 420 – Implicit Namespace Packages

2.3. 目录和命名空间映射

在刚才的示例中(项目test03),我们看到有一个叫做tdouya(田豆芽)的包。然后里面有一个模块module03.py

但是,tdouya 这个包里直接下面就没有代码了么? tdouya(田豆芽)的包,就不配有个hello()方法么? 这时候,__init__.py文件的作用就凸显出来了。

我们直接在项目test03tdouya/__init__.py文件中,增加代码代码如下:

def hello():    print('hello world from tdouya')

入口文件test03.py中加入代码

import tdouya.module03if __name__ == "__main__":    tdouya.module03.hello()    tdouya.hello()  # 新增代码

执行结果:(python=2.7 和 python=3.10 直接结果相同)

D:\Python-grp\miniconda3_data\env\py2.7\python.exe E:\develop\python\test03\test03.py hello world, this is module03.pyhello world from tdouya进程已结束,退出代码0

可以看出,tdouya包下面直接的hello()方法可以被直接调用并正确显示。

田老师并没有找到关于最初设立__init__.py文件的时候,开发者的意图。 但是,我总觉得,这至少是最开始设立这个文件之一。

3. __init__.py文件的作用

综合以上的讨论,以及一些编程实践的案例。 __init__.py文件应该有以下四个作用

  1. 模块搜索标记
  2. 初始化命名空间, 空间名称即为目录名
  3. 设置__all__ 通配符导入目标
  4. 为同一目录的其他模块定义命名空间。

下面,我们逐一说明:

3.1. 模块搜索标记

__init__.py用作在层次结构中定位模块的标记。必须存在才能加载目录分层模块。(注:‘命名空间包’ 不在本文的讨论范畴内)

3.2. 命名空间初始化

import一个包的时候, 首先执行__init__.py的代码, 在导入某些子模块之前,如果需要进行某些初始化操作,那么写入__init__.py是一个好方法。

3.3. 设置__all__ 通配符导入目标

我们在导入模块的时候,经常有这样的写法:from tdouya import * 意思是导入tdouya 这个包中的所有模块。

如果我们什么都不做,那么依赖的是python自己的找包机制,我们来试一下。

用PyCharm创建项目test04,目录结构如下:

./├─ test04.py└─ tdouya    └─ __init__.py    └─ art.py    └─ develop.py    └─ manage.py    └─ training.py

__init__.py是空文件,tdouya包下面的所有其他四个文件的代码是相同的,如下。 这样设定,每个文件在输出的时候,都会加上自己的模块名。

def hello():    print("hello, this is {}".format(__name__))

入口文件 test04.py 内容如下:

from tdouya import *if __name__ == "__main__":    art.hello()    develop.hello()    manage.hello()    training.hello()

我想,这里的意图很清楚,通过from tdouya import * 自动导入包的四个模块,然后执行他们的hello()方法。但是执行结果是:

D:\Python-grp\miniconda3_data\env\py3.10\python.exe E:\develop\python\test04\test04.py Traceback (most recent call last):  File "E:\develop\python\test04\test04.py", line 20, in <module>    art.hello()NameError: name 'art' is not defined进程已结束,退出代码1

也就是说,并不能找到要导入的模块。

此时,我们需要指定__all__指定import *到底处理哪些模块。

我们修改__init__.py文件(我们故意少放一个training模块):

__all__ = ['art','develop','manage']

执行结果:

D:\Python-grp\miniconda3_data\env\py3.10\python.exe E:\develop\python\test04\test04.py hello, this is tdouya.arthello, this is tdouya.develophello, this is tdouya.manageTraceback (most recent call last):  File "E:\develop\python\test04\test04.py", line 23, in <module>    training.hello()NameError: name 'training' is not defined. Did you mean: 'Warning'?进程已结束,退出代码1

果然,如我们所料, 前面三个import *成功, 最后一个没有导入。

3.4. 为其他模块定义命名空间

在复杂项目中, 如果需要调用很多模块,甚至模块名都有重名的情况。 我们可以通过__init__.py重新为模块指定新的名字。

还是在刚才的项目test04中, 我们将刚才未加入__all__ 的training模块换个名字导入。

修改__init__.py 文件

import tdouya.training as train# 在这里,我们将training(名词)改为了它的动词形式train__all__ = ['art','develop','manage','train']# 然后,我们将修改后的模块名,放入__all__

修改入口文件 test04.py

from tdouya import *if __name__ == "__main__":    art.hello()    develop.hello()    manage.hello()    # training.hello() # 本句注释掉    train.hello() # 用新的名字调用tdouya.training模块的hello()方法。

运行结果:python=3.10 和 python=2.7 运行结果一致。

D:\Python-grp\miniconda3_data\env\py3.10\python.exe E:\develop\python\test04\test04.py hello, this is tdouya.arthello, this is tdouya.develophello, this is tdouya.managehello, this is tdouya.training进程已结束,退出代码0

这样就实现了,在使用的时候,我们可以修改模块的名字。 但是功能上不会发生变化, 比如上面的运行结果。 我们知道,模块实际的名字仍然是training, 所以最终输出的文本中,也是training,而不是 train

4. 总结

  1. 相对复杂的,命名空间分层次的模块,使用__init__.py文件是必要的。
  2. 包里面的模块使用前, 需要进行一些初始化操作的模块,使用__init__.py文件是必要的。
  3. __init__.py文件里面的程序, 只在模块被识别/找到模块名的时候被执行一次。 即使你多次导入这些模块,也只会执行一次。 除非你同时使用importlibimport.reload()的时候,才会被显式的被执行。

来源地址:https://blog.csdn.net/u013589130/article/details/128743332

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯