最近 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 |
就可以了。