模块和包
约 2413 字大约 8 分钟
2025-04-04
认识模块和包
思考下这三个问题:
- 我们在介绍高阶函数时,介绍了
reduce
函数,当时我们说 Python3 版本中默认是没有这个函数的,我们需要在文件的开头执行from functools import reduce
才能使用,这是怎么做到的呢? - 如果我们只是写一个几百上甚至上千行的小脚本小项目,可能只需要一个
.py
文件就可以实现。可是如果我们的项目变大变复杂之后,把所有代码都放在一个.py
文件中就变得很难管理,这时候我们就可能需要把代码分成多个.py
文件,还要把同一类别或者同一功能的.py
文件放到同一个文件夹中。那我们该如何在某个.py
文件中使用其他文件中的代码内容呢? - 相信你应该听过这样的话,不要重复造轮子。Python 的一大优势就是有十分活跃的社区,这就意味着很多功能其实已经有了现成的已经得到验证的实现,我们该如何使用别人已经实现好的功能呢?
我们今天要聊的模块和包就是为了解决这三个问题的
模块
在 Python 中,模块就是一个 .py
文件。
现在假如我们在同一个文件夹中有两个文件,a.py
和 b.py
,结构如下:
- module_folder
- a.py
- b.py
b.py
中有如下代码:
def add(x, y):
return x + y
如果我们现在想要在 a.py
中使用 add
方法就可以这样操作:
import b
print(b.add(1, 2)) # 3
当然也可以直接导入 add
函数,像这样:
from b import add
print(add(1, 2))
我们还可以给导入的方法取别名:
from b import add as add_int
print(add_int(1, 2))
基本的导入和导入之后如何使用其实就是这么多,但是有这么几点需要注意:
- 模块被导入之后,模块中的代码会从前往后执行一遍,且即便多次导入也只会执行一遍
- 执行过程中,每个模块内部的
__name__
变量会被复制为模块的文件名,而调用者的__name__
变量会被赋值为'__main__'
- 可以通过
from 模块名 import *
将模块内的全部内容导入进来。但是强烈不建议这么做,因为这样会导致可读性变差,也可能会导致模块中的变量名和我们自己的代码有冲突等等很多问题
举个例子说明下前两点,把 b.py
的代码改成这个样子:
print(f'b.py 中的代码被执行了,__name__ = {__name__}')
def add(x, y):
print('add 函数中的代码')
return x + y
然后我们在 a.py
中执行两次导入 b.py
的操作:
import b
import b
执行 a.py,只打印出下面一行内容:
b.py 中的代码被执行了,__name__ = b
很显然我上面说的两条都得到了验证:从 a
中导入 b
,即便没有手动执行 b
中的代码,但是 b
中的代码也会执行。可是即便我导入两次 b
,但 b
中的代码也只执行了一次;__name__
变量被赋值成了 b
模块的名字 b
当然我们也可以验证一下直接运行 b.py
,看结果如何:
b.py 中的代码被执行了,__name__ = __main__
这时 __name__
变量就被赋值为了 '__main__'
关于 if __name__ == '__main__'
想一下这样的场景,我们的项目有很多模块(也就是很多 .py
文件),我们在 b.py
中开发某一个功能函数的时候,需要写一点测试代码。为了方便,我们把这些代码直接写在了 b.py
的最下面。这段代码我们只想测试的时候执行,后续其他模块,比如 a.py
调用的时候不想让它执行,该怎么办呢?每次测试完就删除掉或者注释掉,日后再需要测试的时候再加回来?这未免太麻烦,也不优雅。为了应付这种我们只想某个模块自己运行的时候会调用,但是别的模块导入它的时候不会被调用的情况,我们可以把这些代码放到 if __name__ == '__main__'
中
比如我们把 b.py
改成这样:
def add(x, y):
return x + y
if __name__ == '__main__':
print(f'这段代码用来测试 1 + 2 = {add(1, 2)}')
这时直接执行 b.py
,测试代码是可以被打印出来的:
这段代码用来测试 1 + 2 = 3
而如果我们在 a.py
中尝试调用 b.py
的函数:
import b
print(f'a 调用 add 的结果:{b.add(1, 2)}')
执行 a.py
后,只有 a.py
的内容打印出来,b.py
中的测试代码并不会被执行:
a 调用 add 的结果:3
这是怎么实现的呢?我们已经知道,如果代码模块自己调用的,那么__name__
变量的值为 '__main__'
,否则它将被赋值文模块名。于是当模块是自己运行的时候,__name__ == '__main__'
条件为 True
,后续代码会被执行,否则不会被执行
包
当我们项目变得越来越大越来越复杂的时候,将文件拆分成很多不同的模块,也就是很多 .py
文件确实在一定程度上缓解了我们的问题。但是日常开发中,我们常常需要使用文件夹来管理我们的代码文件,可能需要把相同功能或者相同业务块的代码放在同一个目录中。用来管理我们项目中 .py
文件的目录就是包。Python 的包中都会有一个__init__.py
文件,这样 Python 才能正确识别它是一个有效的包
包和模块有这么几点需要注意:
- Python 包的所有父目录一直到项目根目录都需要有
__init__.py
文件。现阶段这个文件什么都不需要写,也不需要知道它的作用,只需要知道在 Python3.2 和之前的版本中,使用import
导入一个包,如果包中没有__init__.py
文件会报错;在 Python3.3 版本开始,如果包中没有__init__.py
文件不会报错(这是因为从 3.3 版本引入了 命名空间包 的概念),但是偶尔会有莫名其妙的问题,这不是当前阶段需要考虑的。现阶段认为 Python 包中都需要有一个空的__init__.py
文件即可 - 请按照变量命名的规范命名包和模块,只能使用数字、字母、下划线,且不能以数字开头,不能使用 Python 关键字命名,也不要使用 Python 标准库命名
- 双下划线开头的模块和包是私有的,只能在当前目录下使用,跨目录可能无法使用
接下来,我们在如下目录结构中展开说明和测试:
- module
- parent
- child
- __init__.py
- child_package.py
- child_main.py
- __init__.py
- parent_package.py
- child
- __init__.py
- parent
- __init__.py
- main.py
在 child_package.py
中定义如下函数:
def child():
print('I am child')
在 parent_package.py 中定义如下函数:
def parent():
print('I am parent')
在 Python 中,导入包的方式最基本的结构为(注意不能直接 import
包,也不能 import 模块.函数/变量
):
import 包.包.模块
现在如果我们想要在 main.py
中调用 child
函数,那么可以这么做:
import module.parent.parent_module
module.parent.parent_module.parent()
因为调用写得太长,我们可以取个别名方便调用:
import module.parent.parent_module as pm
pm.parent()
我们也可以通过 from...import...
结构来导入包中指定的函数或变量,而不是将包整体导入。其基本结构为(现阶段 from...import...
结构不能直接导入包):
from 包.包 import 模块
from 包.包.模块 import 变量/函数/类/* # * 表示导入模块全部内容,但是实际编程过程中不建议这么做,可读性低且有可能出现意想不到的问题
例如上面导入 parent
函数就可以用这样两种方式实现:
# 方法一,导入模块
from module.parent import parent_module
parent_module.parent()
# 方法二,导入函数
from module.parent.parent_module import parent
parent()
当然 from...import...
结构也可以使用别名:
from module.parent import parent_module as pm
pm.parent()
相对路径导入
之前所述的导包方式使用的都是绝对路径,即以项目根目录开始依次写直到模块和变量或方法结束。但其实,Python 还可以使用相对路径导入模块,开头的 .
表示当前目录,..
表示上一级目录
例如,我要想在 child_main.py
中调用 child
和 parent
函数,就可以这么写:
from .child_module import child
from ..parent_module import parent
def child_main():
child()
parent()
但是如果此时直接在 child_main.py
中调用 child_main
方法会报错:
Traceback (most recent call last):
File "/module/parent/child/child_main.py", line 1, in <module>
from .child_module import child
ImportError: attempted relative import with no known parent package
导致这个错误的原因是 Python 找相对路径的时候是通过 __name__
来寻找的。我们前面讨论过,如果直接运行某个 .py
文件,__name__
将会被赋值为 '__name__'
,也就无法解析其上级目录的信息。换句话说,如果我们从其他模块是可以使用 child_main
方法的,例如我们在 main.py
中写这个代码就可以正常运行了:
from module.parent.child.child_main import child_main
child_main()
关于相对路径导入更多讨论,参见 https://stackoverflow.com/questions/16981921/relative-imports-in-python-3
版权所有
版权归属:Shuo Liu