【Python】`__init__.py` 文件详解
本文针对Python开发者, 详细描述了
__init__.py
文件在参与包、模块、命名空间导入/重命名/初始化时的使用方法,以及其中代码的执行机制。
本文使用边执行案例,边分析,边给结论的方法,描述了__init__.py
文件的四大作用。
- 模块搜索标记
- 初始化命名空间, 空间名称即为目录名
- 设置__all__ 通配符导入目标
- 为同一目录的其他模块定义命名空间。
__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__.py
、 tdouya2.py
都是空文件。 入口文件test01.py
的代码为
import tdouya
import tdouya2
if __name__ == "__main__":
print("tdouya.{}".format(tdouya.__path__))
print("tdouya2.{}".format(tdouya2.__path__))
执行结果:
D:Python-grpminiconda3_dataenvpy3.10python.exe E:developpythontest01test01.py
tdouya.['E:\develop\python\test01\tdouya']
Traceback (most recent call last):
File "E:developpythontest01test01.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.path
os.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 module02
if __name__ == "__main__":
module02.hello()
执行结果(3.10):
D:Python-grpminiconda3_dataenvpy3.10python.exe E:developpythontest02test02.py
hello, it's module02.py
进程已结束,退出代码0
执行结果(2.7)
D:Python-grpminiconda3_dataenvpy2.7python.exe E:developpythontest02test02.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.module03
if __name__ == "__main__":
tdouya.module03.hello()
此时执行结果: python=3.10
D:Python-grpminiconda3_dataenvpy3.10python.exe E:developpythontest03test03.py
hello world, this is module03.py
进程已结束,退出代码0
而在python=2.7下面, 执行结果是:
D:Python-grpminiconda3_dataenvpy2.7python.exe E:developpythontest03test03.py
Traceback (most recent call last):
File "E:developpythontest03test03.py", line 16, in <module>
import tdouya.module03
ImportError: No module named tdouya.module03
进程已结束,退出代码1
这里,项目test03
在python3.10下正常工作,而在2.7下报错。 这时就需要__init__.py
文件大显身手了。
我们在tdouya
目录下,建立__init__.py
空文件。 那么此时在2.7下执行项目test03
结果为:
D:Python-grpminiconda3_dataenvpy2.7python.exe E:developpythontest03test03.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
文件的作用就凸显出来了。
我们直接在项目test03
的tdouya
/__init__.py
文件中,增加代码代码如下:
def hello():
print('hello world from tdouya')
入口文件test03.py
中加入代码
import tdouya.module03
if __name__ == "__main__":
tdouya.module03.hello()
tdouya.hello() # 新增代码
执行结果:(python=2.7 和 python=3.10 直接结果相同)
D:Python-grpminiconda3_dataenvpy2.7python.exe E:developpythontest03test03.py
hello world, this is module03.py
hello world from tdouya
进程已结束,退出代码0
可以看出,tdouya
包下面直接的hello()
方法可以被直接调用并正确显示。
田老师并没有找到关于最初设立__init__.py
文件的时候,开发者的意图。 但是,我总觉得,这至少是最开始设立这个文件之一。
3. __init__.py
文件的作用
综合以上的讨论,以及一些编程实践的案例。 __init__.py
文件应该有以下四个作用
- 模块搜索标记
- 初始化命名空间, 空间名称即为目录名
- 设置
__all__
通配符导入目标 - 为同一目录的其他模块定义命名空间。
下面,我们逐一说明:
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-grpminiconda3_dataenvpy3.10python.exe E:developpythontest04test04.py
Traceback (most recent call last):
File "E:developpythontest04test04.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-grpminiconda3_dataenvpy3.10python.exe E:developpythontest04test04.py
hello, this is tdouya.art
hello, this is tdouya.develop
hello, this is tdouya.manage
Traceback (most recent call last):
File "E:developpythontest04test04.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-grpminiconda3_dataenvpy3.10python.exe E:developpythontest04test04.py
hello, this is tdouya.art
hello, this is tdouya.develop
hello, this is tdouya.manage
hello, this is tdouya.training
进程已结束,退出代码0
这样就实现了,在使用的时候,我们可以修改模块的名字。 但是功能上不会发生变化, 比如上面的运行结果。 我们知道,模块实际的名字仍然是training
, 所以最终输出的文本中,也是training
,而不是 train
4. 总结
- 相对复杂的,命名空间分层次的模块,使用
__init__.py
文件是必要的。 - 包里面的模块使用前, 需要进行一些初始化操作的模块,使用
__init__.py
文件是必要的。 -
__init__.py
文件里面的程序, 只在模块被识别/找到模块名的时候被执行一次。 即使你多次导入这些模块,也只会执行一次。 除非你同时使用importlib
和import.reload()
的时候,才会被显式的被执行。