最近 rush 代码遇到一些问题,如一种典型的结构
| 1 | |-main/ | 
如上,想在 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 | |-main/ | 
在 test1 目录下的 __init__.py 中写入:
| 1 | print('module1 was called') | 
在 test2 目录下的 __init__.py 中写入:
| 1 | print('module2 was called') | 
在 main 目录下的 __init__.py 中写入:
| 1 | print('parent package was called') | 
实验
通俗来说,__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 | print('parent package was called') | 
通过以上例子,我们可以看出,__init__.py 会起到以下作用:
- 导入模块时初始化一些信息,如 web项目中,启动session等
- 在父目录中,导入多个子模块
进阶
也许你会觉得以上功能比较弱,或者说没啥用。那么来看一些实用的简化工作量的写法 2 。此时的目录结构如下:
| 1 | ├─ main.py | 
在 send.py 中,定义如下函数:
| 1 | def send_msg(msg): | 
如果想在 main.py 中调用这个函数,需要以下写法:
| 1 | from network.msg.info import send | 
但无论那种方法,都要写长长的路径,甚为不便。这个时候,我们可以在 network 文件夹下面创建一个 __init__.py 文件,并在里面填写如下内容:from .msg.info.send import send_msg。而 main.py 文件中的内容可以修改为:
| 1 | from network import send_msg | 
是不是简短了很多。这是因为,当一个文件夹里面有 __init__.py 以后,这个文件夹就会被 python 作为一个包 package 来处理。此时,对于这个包里面层级比较深的函数、常量、类,我们可以先把它们导入到 __init__.py 中。这样以来,包外面再想导入这些内容时,就可以用 from 包名 import 函数名 来导入了。
这样做有很多好处,由于调用包的其他模块所在的绝对路径是千变万化的,当有一些代码会在很多地方被使用时,我们可以把这些代码打包起来,作为一个公共的接口提供给其他模块调用,这会方便很多。
所以在包的内部调用自身其他文件中的函数、常量、类,就不应该使用相对路径,而是绝对路径。这里以添加新功能为例,如下所示,在 parse.py 文件中添加以下内容:
| 1 | # 两种都可以 | 
可以看到,在包里面的一个文件调用这个包里面的另一个文件,只需要知道另一个文件的相对位置就可以了,不用关心这个包被放在哪里。上 面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 | from network import parse_msg | 
此外,当一个文件夹里面包含 __init__.py 时,这个文件夹会被 python 认为是一个包 package,此时,包内部的文件之间互相导入可以使用相对导入,并且通过提前把函数、常量、类导入到 __init__.py 中再在其他文件中导入,可以精简代码。
问题解决
既然了解了 __init__.py 的用法,那么去解决文章最开始提到的问题。目录结构如下:
| 1 | main | 
实现的想法也很简单,m2.py 调用 m1.py 中的函数。
m1.py 定义如下:
| 1 | def send(msg): | 
m2.py 定义如下:
| 1 | from test1 import m1 | 
距离成功只差一步,那就是修改 test1 中的 __init__.py 的内容,把 test1 看成一个 package,暴露其中的 m1 即可。
| 1 | from test1 import m1 | 
这样,在外部的 main 函数中:
| 1 | import test2.m2 as m2 | 
就可以了。
 
        