查看全课大纲(第 35 / 51 节)
  1. 1.Python 是什么?为什么值得学?
  2. 2.如何为 Python 编程准备一个“全家桶”环境?
  3. 3.Python 有哪几种执行方法?
  4. 4.行与缩进到底有什么用?
  5. 5.注释有什么用,怎么写?
  6. 6.变量:如何给数据起个好名字?
  7. 7.运算符的优先级和常见坑点是什么?
  8. 8.Python 中,数字和布尔值究竟有哪几种?
  9. 9.输入函数 `input()` 和文件读取 `open()` 怎么用?
  10. 10.如何将程序的结果展示给用户或保存下来?
  11. 11.CSV 文件怎么读怎么写才方便?
  12. 12.列表是什么,如何创建和访问它?
  13. 13.列表的修改操作,有哪些方法?
  14. 14.列表切片为什么能取到部分元素?排序和查找又该怎么用?
  15. 15.判断语句:如何让程序根据条件做不同的事?
  16. 16.for 循环如何遍历序列中的元素?
  17. 17.while 循环什么时候才会停?
  18. 18.while 循环的结束条件,你写对了吗?
  19. 19.循环中途想提前结束,怎么办?
  20. 20.`break` 和 `continue` 到底有什么区别?
  21. 21.列表推导式如何让代码更简洁高效?
  22. 22.字符串怎么定义、拼接和操作?
  23. 23.元组:为什么定义单个元素时要加逗号?
  24. 24.集合(set)怎么用?如何快速去重和做集合运算?
  25. 25.字典是什么,为什么比列表更方便?
  26. 26.字典的增删改查与遍历,如何避免 KeyError?
  27. 27.函数里的变量,为什么有的能用有的不能用?
  28. 28.在函数内部修改全局变量,为什么有时不生效?
  29. 29.函数里能修改全局变量吗?返回值怎么用?
  30. 30.匿名函数:如何用 lambda 快速定义简单函数?
  31. 31.面向对象编程是什么,为什么需要它?
  32. 32.类中的方法和属性到底怎么定义和使用?
  33. 33.如何控制对对象内部属性和方法的访问?
  34. 34.异常发生时,程序会怎样继续执行?
  35. 35.如何组织代码:包和模块
  36. 36.`if __name__ == '__main__'` 到底有什么用?
  37. 37.如何用 IDE 的 Debug 功能排查程序错误?
  38. 38.Matplotlib 绘图的第一步该做什么?
  39. 39.Figure 是什么,如何设置图形尺寸?
  40. 40.如何自定义 Matplotlib 的坐标轴?
  41. 41.如何为图表添加图例,让不同曲线一目了然?
  42. 42.饼图怎么画才能直观展示数据占比?
  43. 43.如何用 Matplotlib 绘制散点图?
  44. 44.柱状图怎么画?
  45. 45.如何使用 Matplotlib 绘制等高线图?
  46. 46.如何在 Matplotlib 中绘制子图?
  47. 47.zip 函数如何把多个列表“打包”在一起?
  48. 48.如何给可迭代对象加上索引?
  49. 49.random 模块如何生成随机数?
  50. 50.math 模块怎么用?
  51. 51.Counter 如何快速统计元素出现次数?

如何组织代码:包和模块

约 21 分钟

小象实战讲义 · Python 编程从入门到实践

当我们编写的代码越来越多,功能越来越复杂时,把所有代码都放在一个文件里会变得难以管理。Python 用两个层次来组织代码:包(Package)与模块(Module),让代码结构清晰,也方便复用。本节将学习如何创建、导入和使用包与模块,这是编写大型项目的基础。

💡 核心导读

  • 模块就是一个 .py 文件,里面可以定义函数、类等。
  • 包是一个目录,里面可以放多个模块;按传统包规范,目录里还会放一个 __init__.py 文件。
  • 使用 import 语句可以导入模块或包,从而使用其中定义的功能。
  • __init__.py 文件在包被导入时自动执行,常用于包的初始化。
  • 了解 Python 的模块搜索路径,知道它如何找到你要导入的模块。

模块和包是什么

在 Python 中,一个 .py 文件就是一个模块(Module)。模块里可以定义函数、类、变量等。例如,一个名为 calculator.py 的文件就是一个模块,里面可以定义 add、subtract 等函数。

所谓包(Package),就是一个存放多个模块的目录,目录里通常还会有一个特殊文件 __init__.py。包是用来组织相关模块的,就像一个文件夹用来整理文件。

它们的关系可以这样理解:

  • 包 → 目录(文件夹)
  • 模块 → 目录里的 .py 文件
  • 函数/类 → 文件里的具体代码

例如,一个项目可能这样组织:

my_project/
├── main.py
├── utils/               # 一个包
│   ├── __init__.py     # 包的初始化文件
│   ├── file_utils.py   # 模块
│   └── math_utils.py   # 模块
└── models/              # 另一个包
    ├── __init__.py
    └── user.py

导入模块的几种方式

1. 导入整个模块

使用 import 语句后跟模块名(不含 .py 后缀)。例如 business 包下有 module1.py,就写 import business.module1。

2. 一次导入多个模块

可以在一个 import 语句中导入多个模块,用逗号分隔。

下面用内存中的模块对象模拟包 business、tools 及其 module1 模块(真实项目中,它们就是两个目录下的 module1.py 文件),演示上面两种导入方式:

import sys, types

# 在内存中模拟 business/ 和 tools/ 两个包及其 module1 模块
def project_info():
    print("*" * 20)
    print(" " * 4, "Python项目", " " * 4)
    print("*" * 20)

def tool_info():
    print("*" * 20)
    print(" " * 4, "常用工具", " " * 4)
    print("*" * 20)

business = types.ModuleType("business")
business.__path__ = []
business_module1 = types.ModuleType("business.module1")
business_module1.project_info = project_info
business.module1 = business_module1

tools = types.ModuleType("tools")
tools.__path__ = []
tools_module1 = types.ModuleType("tools.module1")
tools_module1.tool_info = tool_info
tools.module1 = tools_module1

sys.modules["business"] = business
sys.modules["business.module1"] = business_module1
sys.modules["tools"] = tools
sys.modules["tools.module1"] = tools_module1

# 方式一:导入整个模块
import business.module1
business.module1.project_info()

# 方式二:一次导入多个模块,用逗号分隔
import business.module1, tools.module1
business.module1.project_info()
tools.module1.tool_info()
********************
     Python项目     
********************
********************
     Python项目     
********************
********************
     常用工具     
********************

3. 给模块起别名

如果模块名很长,可以用 as 给它起一个短别名,之后调用时就不必写全名。

4. 从模块中导入特定函数

如果只需要模块中的某个函数,可以用 from ... import ... 语法,调用时直接写函数名,不需要模块名前缀。

import sys, types

def project_info():
    print("*" * 20)
    print(" " * 4, "Python项目", " " * 4)
    print("*" * 20)

def tool_info():
    print("*" * 20)
    print(" " * 4, "常用工具", " " * 4)
    print("*" * 20)

business = types.ModuleType("business")
business.__path__ = []
business_module1 = types.ModuleType("business.module1")
business_module1.project_info = project_info
business.module1 = business_module1

tools = types.ModuleType("tools")
tools.__path__ = []
tools_module1 = types.ModuleType("tools.module1")
tools_module1.tool_info = tool_info
tools.module1 = tools_module1

sys.modules["business"] = business
sys.modules["business.module1"] = business_module1
sys.modules["tools"] = tools
sys.modules["tools.module1"] = tools_module1

# as 起别名:之后用 bm 代替 business.module1
import business.module1 as bm
bm.project_info()

# from 包.模块 import 函数:调用时不再需要模块名前缀
from tools.module1 import tool_info
tool_info()
********************
     Python项目     
********************
********************
     常用工具     
********************

包的特殊文件:__init__.py

说明:课程录制时(Python 3.6)按传统包规范讲解,每个包目录都会放一个 __init__.py。Python 3.3 及以后支持命名空间包,没有 __init__.py 的目录也能被导入;但常规项目仍建议每个包目录放一个 __init__.py,便于初始化与工具识别。

__init__.py 文件有两个主要作用:

  1. 声明这是一个常规包。传统规范下,没有这个文件的目录不会被当成包;现代 Python 的命名空间包允许省略它,但常规项目仍建议保留,便于初始化与工具识别。
  2. 在包被导入时自动执行,可用于初始化包。

例如,在 business/__init__.py 中写一句打印语句:

# business/__init__.py
print("__init__.py 被自动调用")

之后执行 import business.module1 时,Python 会先执行 business/__init__.py,再加载 module1,所以会先看到初始化语句的输出:

__init__.py 被自动调用
********************
     Python项目     
********************

使用 __all__ 控制 from package import *

在 __init__.py 中定义 __all__ 列表,可以控制当使用 from package import * 时,哪些模块会被导入。下面的例子用内存模块模拟 demo_pkg 包,并把 __all__ 设为只包含 mod_a:

import sys, types

def func_a():
    print("这是模块A的功能")

def func_b():
    print("这是模块B的功能")

demo_pkg = types.ModuleType("demo_pkg")
demo_pkg.__path__ = []
mod_a = types.ModuleType("demo_pkg.mod_a")
mod_a.func_a = func_a
mod_b = types.ModuleType("demo_pkg.mod_b")
mod_b.func_b = func_b
demo_pkg.mod_a = mod_a
demo_pkg.mod_b = mod_b

# 相当于在 demo_pkg/__init__.py 中写 __all__ = ["mod_a"]
demo_pkg.__all__ = ["mod_a"]

sys.modules["demo_pkg"] = demo_pkg
sys.modules["demo_pkg.mod_a"] = mod_a
sys.modules["demo_pkg.mod_b"] = mod_b

from demo_pkg import *   # 只导入 __all__ 中列出的 mod_a
mod_a.func_a()
# mod_b.func_b()  # 取消注释会报错:NameError: name 'mod_b' is not defined
这是模块A的功能

Python 如何找到模块:搜索路径

当你执行 import something 时,Python 会在一系列目录中查找 something 模块。这个查找顺序存储在 sys.path 列表中。

搜索顺序:Python 按 sys.path 列表中的路径顺序逐项查找。列表的第一项通常是当前脚本所在目录;在交互式环境(如 Jupyter)下则为空字符串,表示当前工作目录。因此放在当前目录下的包和模块可以被直接导入。

常见场景:如果你的模块不在 sys.path 包含的路径中,导入会失败。这时可以临时修改 sys.path。下面的演示不打印具体路径(不同电脑的路径各不相同),只验证追加是否生效:

import sys

before = len(sys.path)
sys.path.append("my_modules")
print("追加后条目数增加:", len(sys.path) - before)
print("my_modules 已在搜索路径中:", "my_modules" in sys.path)
追加后条目数增加: 1
my_modules 已在搜索路径中: True

📝 动手练一练

  1. 模块导入练习:创建一个名为 greetings.py 的模块,在里面定义一个函数 say_hello(name),功能是打印 "Hello, [name]!"。然后在另一个脚本中导入这个模块并调用该函数。
👉 点击查看参考答案

思路:先创建模块文件,定义函数;再在主脚本中导入并调用。真实项目中 greetings.py 是磁盘上的文件,这里把它放进内存模块来演示导入过程。

import sys, types

# 相当于 greetings.py 文件中的内容
greetings = types.ModuleType("greetings")
def say_hello(name):
    print("Hello, {}!".format(name))
greetings.say_hello = say_hello

# 让 Python 能找到名为 greetings 的模块
sys.modules["greetings"] = greetings

# 导入并使用
import greetings
greetings.say_hello("小象")
Hello, 小象!
  1. 包结构理解:以下目录结构是一个有效的 Python 包吗?为什么?
    myapp/
    ├── utils/
    │   ├── helper.py
    │   └── validator.py
    └── main.py
👉 点击查看参考答案

在现代 Python(3.3+)中,这个目录可以被导入:utils/ 没有 __init__.py 时会被当作命名空间包,from utils import helper 依然能成功。不过按常规项目规范,仍建议在 utils/ 下放一个空的 __init__.py,便于初始化与工具识别。

常见错误

错误写法现象或报错原因正确写法
包目录下漏写 __init__.py,却依赖包的初始化逻辑现代 Python(3.3+)仍能正常 import,但 __init__.py 里的初始化代码不会执行,包可能没有按预期准备好Python 3.3+ 支持命名空间包,没有 __init__.py 也能导入;但常规项目仍建议放 __init__.py,便于初始化与工具识别在包目录下创建 __init__.py,把初始化代码写进去
from mymodule import * 但期望导入所有函数可能只导入了一部分,或者报未定义错误。如果没有在模块中定义 __all__ 列表,import * 只会导入不以单下划线(_)或双下划线(__)开头的名字,行为不易预期。明确导入需要的函数:from mymodule import func1, func2。或在模块中定义 __all__ = ["func1", "func2"]。
模块文件名与标准库模块重名(如 base64.py)导入的不是标准库的 base64 模块,而是同名的本地文件。纯 Python 实现的标准库模块会被当前目录下的同名文件覆盖(内置模块如 math 不受影响)。避免使用与标准库模块相同的文件名。
修改了模块代码后,重新导入发现没变化代码行为还是旧的。已导入的模块会被运行环境缓存,不会自动重新读取源文件。重启 Python 解释器;在 Jupyter 中执行 Kernel Restart,再重新导入。

小结

  • 模块是 .py 文件,包是用于组织模块的目录(传统项目会在其中放一个 __init__.py)。
  • 使用 import module 导入整个模块,用 module.func() 调用函数。
  • 使用 from module import func 直接导入特定函数,调用时无需模块名前缀。
  • 可以用 as 给模块或函数起别名,简化代码。
  • __init__.py 在包被导入时自动执行,常用于初始化。其中的 __all__ 列表控制 from package import * 的行为。
  • Python 按 sys.path 中的路径顺序搜索模块。自定义模块需要确保其所在路径在 sys.path 中。

📋 行动清单

  • 尝试创建一个自己的包,包含至少两个模块,并在模块中定义不同的函数。
  • 使用 import 和 from ... import ... 两种方式导入你的模块并调用函数,体会区别。
  • 在包的 __init__.py 中添加一句打印语句,然后导入包下的模块,观察打印语句是否执行。

—— 小象教研组

配套资料 · 直接下载

课件、代码、数据集等配套资料全部免费,点击即可直接下载,无需分享或解锁。

  • 第4章代码包:包和模块(含1 个 Notebook、5 个 .py)
    ZIP · 2KB
    下载