0%

python __init__.py 文件的用法

最近 rush 代码遇到一些问题,如一种典型的结构

1
2
3
4
5
|-main/
|----test1/
|--------module1.py
|----test2/
|--------module2.py

如上,想在 module2.py 中调用 module1.py 中的某个类,如果在 module2.py 中写:from ..test1 import module1,在 test2 文件夹下执行 python module2.py 会提示:

1
ImportError: attempted relative import with no known parent package

会遇到这样的错误。那么,如何解决呢?如果你只想看如何解决问题,直接翻到文末即可;网上大概搜了一下,需要 __init__.py 来解决下这个问题,但是网上搜了一圈,没啥写的特别好的教程,实在是烂的可以,特此来填坑。

__init__.py 是什么 1

假设此时的路径结构为:

1
2
3
4
5
6
7
8
|-main/
|----__init__.py
|----test1/
|--------__init__.py
|--------module1.py
|----test2/
|--------__init__.py
|--------module2.py

test1 目录下的 __init__.py 中写入:

1
print('module1 was called')

test2 目录下的 __init__.py 中写入:

1
print('module2 was called')

main 目录下的 __init__.py 中写入:

1
2
3
print('parent package was called')
# 导入 [] 里面定义的模块
__all__ = ['test1', 'test2']

实验

通俗来说,__init__.py 可以将文件封装成包,将多个文件合并到一个逻辑命名空间。但是这么说太突兀了,由浅入深,一点一点来。先来看看文件夹中添加 __init__.py 会发生什么。假设此时的路径为 main 文件夹下,尝试导入模块,会发现上述信息被打印:

同理,在 main 文件夹的 上一级路径 下执行导入,也会有同样的效果,但是不会导入子模块。

如果想导入单个子模块,可以 import main.test1,此时会打印 module1 was called;如果再次调用 import main.test1,也就是在模块已经导入的情况下再次导入,则不会打印任何信息。

如果导入全部子模块,也是可以的。因为声明了 __all__,所以子模块被导入。

但是你也许会有疑问,我经常写 import math,而 math.sin 等函数是导入的,且可以使用,为什么这里就不行了呢?如果想行,也是可以的,只需要在 main 目录下的 __init__.py 中写入以下信息就可以了,也就是 import main; main.test1 可用。

1
2
3
4
print('parent package was called')
# 删除 __all__
from . import test1
from . import test2

通过以上例子,我们可以看出,__init__.py 会起到以下作用:

  • 导入模块时初始化一些信息,如 web 项目中,启动 session
  • 在父目录中,导入多个子模块

进阶

也许你会觉得以上功能比较弱,或者说没啥用。那么来看一些实用的简化工作量的写法 2 。此时的目录结构如下:

1
2
3
4
5
6
7
├─ main.py
└─ network
├─ __init__.py
├─ msg
│ └─ info
│ └─ send.py
└─ parse.py

send.py 中,定义如下函数:

1
2
def send_msg(msg):
print('send:', msg)

如果想在 main.py 中调用这个函数,需要以下写法:

1
2
3
4
5
from network.msg.info import send
send.send_msg('hello')
# 或者
# from network.msg.info.send import send_msg
# send_msg('hello')

但无论那种方法,都要写长长的路径,甚为不便。这个时候,我们可以在 network 文件夹下面创建一个 __init__.py 文件,并在里面填写如下内容:from .msg.info.send import send_msg。而 main.py 文件中的内容可以修改为:

1
2
from network import send_msg
send_msg('hello')

是不是简短了很多。这是因为,当一个文件夹里面有 __init__.py 以后,这个文件夹就会被 python 作为一个包 package 来处理。此时,对于这个包里面层级比较深的函数、常量、类,我们可以先把它们导入到 __init__.py 中。这样以来,包外面再想导入这些内容时,就可以用 from 包名 import 函数名 来导入了。

这样做有很多好处,由于调用包的其他模块所在的绝对路径是千变万化的,当有一些代码会在很多地方被使用时,我们可以把这些代码打包起来,作为一个公共的接口提供给其他模块调用,这会方便很多。

所以在包的内部调用自身其他文件中的函数、常量、类,就不应该使用相对路径,而是绝对路径。这里以添加新功能为例,如下所示,在 parse.py 文件中添加以下内容:

1
2
3
4
5
6
# 两种都可以
# from .msg.info.send import send_msg
from . import send_msg
def parse_msg(msg):
print('parse:', msg)
send_msg(msg)

可以看到,在包里面的一个文件调用这个包里面的另一个文件,只需要知道另一个文件的相对位置就可以了,不用关心这个包被放在哪里。上 面parse.py 中导入 send_msg 函数的代码还可以进一步简化,由于 send_msg 函数已经被导入到了 __init__.py 中,所以我们可以直接从 . 里面导入 send_msg 函数。

之后在 __init__.py 中追加:

1
from .parse import parse_msg

此时,main.py 的写法可以如下,可以看到,即使追加了新的模块,main.py 调用起来也会很方便,并不需要知道 parse_msg 这个方法的任何位置信息。

1
2
from network import parse_msg
parse_msg('hhh')

此外,当一个文件夹里面包含 __init__.py 时,这个文件夹会被 python 认为是一个包 package,此时,包内部的文件之间互相导入可以使用相对导入,并且通过提前把函数、常量、类导入到 __init__.py 中再在其他文件中导入,可以精简代码。

问题解决

既然了解了 __init__.py 的用法,那么去解决文章最开始提到的问题。目录结构如下:

1
2
3
4
5
6
7
main
├─ main.py
├─ test1
│ ├─ __init__.py
│ └─ m1.py
└─ test2
└─ m2.py

实现的想法也很简单,m2.py 调用 m1.py 中的函数。

m1.py 定义如下:

1
2
def send(msg):
print(msg)

m2.py 定义如下:

1
2
3
from test1 import m1
def run():
m1.send('hello')

距离成功只差一步,那就是修改 test1 中的 __init__.py 的内容,把 test1 看成一个 package,暴露其中的 m1 即可。

1
2
3
from test1 import m1
# 或者
# from . import m1

这样,在外部的 main 函数中:

1
2
3
4
5
import test2.m2 as m2
m2.run()
# 或者
# from test2 import m2
# m2.run()

就可以了。

references


  1. 1.https://zhuanlan.zhihu.com/p/130927618
  2. 2.https://www.kingname.info/2020/03/23/init-in-python/
感谢上学期间打赏我的朋友们。赛博乞讨:我,秦始皇,打钱。

欢迎订阅我的文章