模块和包
约 2739 字大约 9 分钟
2025-04-04
在学习编程的过程中,我们经常会遇到一些有趣的问题。今天,让我们一起来探讨关于模块和包的三个核心问题:
- 认识模块和包
- 模块
- 包
- 相对路径导入
- 1在 b.py 里加一行 print(__name__),分别 import b 和直接 python b.py,看到模块名 vs __main__ 两种结果。
- 2每个新模块的“可执行入口”都放在 if __name__ == '__main__': 下,import 时不会被误触发。
- 3用 from x import * 后跑代码,看名字污染;改回 import x 或 from x import only_what_you_need。
- 4对同一个模块连续 import 两次,确认初始化代码只跑一次(模块缓存在 sys.modules 里)。
- 5建一个含 __init__.py 的子包,跑 from .util import foo 失败、用 python -m pkg.main 又能跑 —— 体会“以模块运行”和“以脚本运行”的区别。
认识模块和包
在学习编程的过程中,我们经常会遇到一些有趣的问题。今天,让我们一起来探讨关于模块和包的三个核心问题:
- 在介绍高阶函数时,我们提到了
reduce函数。然而,在 Python3 中,默认情况下这个函数并不直接可用。我们需要在文件开头通过from functools import reduce才能使用它。这是如何实现的呢? - 如果只是编写一个几百行甚至上千行的小型脚本或项目,通常一个
.py文件就足够了。但是,当项目变得更大、更复杂时,把所有代码都放在一个文件中会显得难以管理。这时,我们需要将代码拆分成多个.py文件,并将相同类别或功能的文件归类到同一个文件夹中。那么,如何在一个.py文件中使用其他文件中的代码内容呢? - 你可能听过“不要重复造轮子”这句话。Python 的一大优势在于其活跃的社区,这意味着许多功能已经有了现成且经过验证的实现。那么,我们该如何利用别人已经写好的功能呢?
这些问题的答案,其实都与模块和包息息相关
模块
在 Python 中,一个模块就是一个 .py 文件。模块是代码组织的基本单位,能够帮助我们将代码分解为更小、更易维护的部分
假设我们在同一个文件夹中有两个文件: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__变量会被赋值为模块的文件名(无扩展名)。而在主程序中,__name__变量的值会被设置为字符串'__main__'- 避免使用
from 模块名 import *。这种方式虽然可以一次性导入模块中的所有内容,但会降低代码的可读性,还可能导致变量名冲突等问题,因此不推荐使用
我们首先验证一下模块的加载行为,修改 b.py 的代码如下:
print(f'b.py 中的代码被执行了,__name__ = {__name__}')
def add(x, y):
print('add 函数中的代码')
return x + y然后在 a.py 中执行两次导入操作:
import b
import b运行 a.py 后,输出结果如下:
b.py 中的代码被执行了,__name__ = b可以看到,尽管我们导入了两次 b.py,但它的代码只执行了一次,这验证了模块的加载机制。同时,__name__ 变量确实被赋值为了模块名 b
如果我们直接运行 b.py,输出结果则会变成:
b.py 中的代码被执行了,__name__ = __main__这表明,当模块作为主程序运行时,__name__ 的值会被设置为 '__main__'
关于 if __name__ == '__main__'
在实际开发中,我们常常需要为模块编写测试代码。例如,在 b.py 中定义了一个函数 add,我们希望在开发过程中快速测试它。但如果这些测试代码被其他模块导入时也执行,就会显得不够优雅
为了解决这个问题,我们可以将测试代码放在 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 标准库命名
- 双下划线开头的模块和包是私有的,只能在当前目录下使用,跨目录可能无法使用
接下来,我们在如下目录结构中展开说明和测试:
在 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
- 一个 .py 就是一个模块;模块第一次 import 会从头执行一遍,之后从 sys.modules 缓存中取。
- __name__ 在导入时是模块名、直接运行时是 '__main__' —— 用 if __name__ == '__main__': 隔离“脚本入口”和“被导入”两种用法。
- 包是带 __init__.py 的目录(3.3+ 命名空间包也行,但项目里建议显式建空文件)。
- 导入避免 from x import *:污染命名空间、IDE 也猜不到来源。
- 相对导入 from .util import foo 只在“被作为包导入”时有效;直接 python sub/foo.py 跑会报错,用 python -m pkg.foo。
版权所有
版权归属:Shuo Liu
