第3章 3.4 模块与包
上一章我们聊完了作用域和闭包,你已经知道了 Python 变量查找的"寻亲地图"——局部作用域找不着就去外围找,外围找不着就去全局找,最后才求助内置作用域。这套规则保证了你写的函数不会乱认别人家的变量。
但你有没有想过这个问题:如果项目大了,有几百个函数,代码全堆在一个文件里会发生什么?
光找代码就要找半天,改了一个地方可能影响另一个地方,团队合作更是噩梦。程序员对付这种乱糟糟的场面,有一套成熟的"整理房间"方法——模块与包。
这一章我们就来聊聊,怎么把代码从"大杂烩"变成"有序集合",让你下次找代码像图书馆找书一样轻松。
学完这章,你能解决这些问题:
- 代码越写越长,改一处坏三处?
- 想用别人写的工具,不知道怎么导入?
- 自己的工具想给别人用,不知道怎么分享?
🧱 基础 25 分钟:核心概念
什么是模块?——把你的代码变成"工具箱"
生活类比:你家的工具箱里,有螺丝刀、有锤子、有钳子。每种工具各司其职,你不需要把所有工具混在一起拎着走。模块就是这个道理——把相关的函数、变量打包成一个 .py 文件,你要用的时候直接"从工具箱里拿"。
为什么用:想象你要写一个计算器程序,有计算工资的、算利息的、算税率的函数。如果全塞在 main.py 里,3000 行代码,你自己都看晕了。但如果你把工资相关的放 salary.py,利息相关的放 interest.py,就像给工具贴了标签,用哪个拿哪个。
怎么用:
# 先创建一个文件叫 greeting.py,里面写:
def say_hello(name):
return f"你好,{name}!"
def say_bye(name):
return f"再见,{name},下次见!"
# 在另一个文件里导入使用:
import greeting
print(greeting.say_hello("小明"))
print(greeting.say_bye("小明"))
输出:
你好,小明!
再见,小明,下次见!
import greeting 的意思就是"把 greeting.py 这个工具箱拿过来",然后用 greeting.say_hello() 调用里面的函数。模块名就是文件名(不带 .py)。
怎么导入?——四种方式选哪种
四种导入方法对比:
# 方法1:导入整个模块(最常用)
import math
print(math.sqrt(16)) # 4.0,需要加模块名前缀
# 方法2:给模块起个小名(省打字)
import math as m
print(m.sqrt(16)) # 4.0,用小名调用
# 方法3:只导入需要的函数(用起来方便)
from math import sqrt
print(sqrt(16)) # 4.0,直接用函数名,不用加前缀
# 方法4:导入所有(方便但污染命名空间,不推荐)
from math import *
print(sqrt(16)) # 4.0
什么时候用哪种:
- import xxx:不确定用多少,全部导入,代码清晰
- import xxx as y:模块名太长,起个小名
- from xxx import y:只用到一两个函数,不想每次都打模块名
- from xxx import *:别用,因为你不知道会覆盖什么同名变量
类比:就像你去超市买菜,import 买菜 是推一整车回来慢慢挑;from 买菜 import 白菜 是你只跟店员说"来颗白菜"。前者东西全但推车沉,后者轻便但可能漏买。
什么是包?——把工具箱再装进抽屉柜
生活类比:工具箱里的螺丝刀、锤子、钳子都是同一类工具。但如果你家大了,有电动工具、手动工具、测量工具……你可能会再分几个抽屉,把工具箱们放进去。包就是那个"抽屉柜"——它是一个文件夹,里面可以放多个模块(.py 文件)。
为什么用:包让你能把模块也组织起来。比如你写了个"飞书机器人"工具,可能有:
feishu/ # 包文件夹(也叫命名空间)
├── __init__.py # 包的"说明书"
├── message.py # 发消息模块
├── file.py # 上传文件模块
└── user.py # 用户管理模块
这样别人用的时候就可以 from feishu import message,层次清晰,谁开发的、做什么用,一目了然。
怎么用:
首先创建目录结构(自己动手试试):
myproject/
├── __init__.py
├── utils.py
└── main.py
# utils.py - 工具模块
def count_words(text):
"""统计文本中的单词数"""
return len(text.split())
# __init__.py - 告诉 Python 这是一个包(可以先空着)
# 这个文件必须存在,即使什么都不写
# main.py - 主程序
from utils import count_words
sentence = "Python 入门到精通其实不难"
print(f"这句话有 {count_words(sentence)} 个词")
输出:
这句话有 7 个词

注意! __init__.py 这个文件是 Python 2/3 兼容用的老家伙。虽然 Python 3.3+ 的隐式命名空间包让它不再是必须的,但强烈建议加上——它能帮你在导入时自动加载一些东西,也让代码更明确"这是一个包"。
__init__.py 里面可以写什么?
这个文件有3个妙用:
# 1. 批量导出(简化导入路径)
# 假设 utils.py 里有很多函数,你可以在 __init__.py 里统一导出:
from .utils import count_words, count_chars # . 表示同目录
这样别人就可以 from myproject import count_words,不用记函数在哪个文件里。
# 2. 包初始化代码(自动运行)
# 比如每次导入这个包时,自动检查某个依赖是否安装:
import sys
print("欢迎使用 myproject 包!")
# 3. 设置 __all__(控制 from myproject import * 时导出什么)
__all__ = ['count_words', 'count_chars']
if __name__ == "__main__" 是什么?
这是 Python 里最容易让人懵,但超重要的写法。先看例子:
# greet.py
def hello():
print("你好!")
# 这行下面的代码,只在直接运行这个文件时执行
# 如果是被其他文件 import 进来,不会执行
if __name__ == "__main__":
hello()
print("greet.py 被直接运行了")
运行 python greet.py(直接运行):
你好!
greet.py 被直接运行了
在其他文件里 import greet(作为模块导入):
欢迎使用 myproject 包! # __init__.py 输出的
# 不会输出 "greet.py 被直接运行了"
为什么用:
| 场景 | 直接运行 python xxx.py |
被 import |
|---|---|---|
__name__ 的值 |
"__main__" |
"模块名" |
if __name__ == "__main__" 下的代码 |
会执行 | 不会执行 |
生活类比:就像家电的"测试按钮"——厂家检测时会按(直接运行),但你家里用的时候(被导入)不会自动触发。
实际用途:写一个工具函数,平时可以单独测试,但被别人 import 时不会乱输出东西。

🔥 实战 35 分钟:三个递进小项目
项目 1:温度转换工具箱(5 分钟)
场景:你要写一个气象数据处理脚本,需要经常在华氏度和摄氏度之间转换。
目标:学会把函数封装成模块,然后在主程序里调用。
# temperature.py
"""温度转换工具模块"""
def celsius_to_fahrenheit(c):
"""摄氏度转华氏度"""
return c * 9/5 + 32
def fahrenheit_to_celsius(f):
"""华氏度转摄氏度"""
return (f - 32) * 5/9
def kelvin_to_celsius(k):
"""开尔文转摄氏度"""
return k - 273.15
def celsius_to_kelvin(c):
"""摄氏度转开尔文"""
return c + 273.15
# main.py
import temperature as temp
# 今天北京气温 28°C
beijing_c = 28
beijing_f = temp.celsius_to_fahrenheit(beijing_c)
print(f"北京 {beijing_c}°C = {beijing_f:.1f}°F")
# 纽约 86°F
ny_f = 86
ny_c = temp.fahrenheit_to_celsius(ny_f)
print(f"纽约 {ny_f}°F = {ny_c:.1f}°C")
# 绝对零度
absolute_zero_k = 0
absolute_zero_c = temp.kelvin_to_celsius(absolute_zero_k)
print(f"绝对零度 {absolute_zero_k}K = {absolute_zero_c:.2f}°C")
预期输出:
北京 28°C = 82.4°F
纽约 86°F = 30.0°C
绝对零度 0K = -273.15°C
一句话解释:我们把温度转换函数打包到 temperature.py,主程序只需要知道"这个模块能帮我转换",不用关心具体怎么算。
项目 2:天气数据 CSV 处理器(15 分钟)
场景:你从气象局拿到一份 CSV 文件,里面是过去一周的每日最高/最低气温(华氏度)。你需要转换成摄氏度,然后算出平均温度。
准备:先手动创建一个 weather_data.csv 文件:
date,high_f,low_f
2024-01-15,42,28
2024-01-16,45,32
2024-01-17,38,25
2024-01-18,40,29
2024-01-19,43,30
2024-01-20,41,27
2024-01-21,39,26
# weather_processor.py
"""天气数据处理包"""
from .temperature import fahrenheit_to_celsius
def read_csv(filepath):
"""读取 CSV 文件,返回字典列表"""
data = []
with open(filepath, 'r', encoding='utf-8') as f:
lines = f.readlines()
# 解析表头
headers = lines[0].strip().split(',')
# 解析数据行
for line in lines[1:]:
if not line.strip():
continue
values = line.strip().split(',')
row = dict(zip(headers, values))
data.append(row)
return data
def convert_temperatures(data):
"""把华氏度转成摄氏度"""
for day in data:
day['high_c'] = round(fahrenheit_to_celsius(float(day['high_f'])), 1)
day['low_c'] = round(fahrenheit_to_celsius(float(day['low_f'])), 1)
return data
def calculate_average(data):
"""计算平均温度"""
highs = [float(day['high_c']) for day in data]
lows = [float(day['low_c']) for day in data]
return sum(highs) / len(highs), sum(lows) / len(lows)
def print_report(data, avg_high, avg_low):
"""打印天气报告"""
print("=" * 50)
print("📅 一周天气汇总(摄氏度)")
print("=" * 50)
for day in data:
print(f"{day['date']} | 最高 {day['high_c']:>5}°C | 最低 {day['low_c']:>5}°C")
print("-" * 50)
print(f"📊 本周平均:最高 {avg_high:.1f}°C | 最低 {avg_low:.1f}°C")
print("=" * 50)
# __init__.py
"""天气数据处理包 - 初始化"""
from .weather_processor import read_csv, convert_temperatures, calculate_average, print_report
__all__ = ['read_csv', 'convert_temperatures', 'calculate_average', 'print_report']
# main.py
"""主程序入口"""
from weather_processor import read_csv, convert_temperatures, calculate_average, print_report
# 读取数据
data = read_csv('weather_data.csv')
# 转换温度
data = convert_temperatures(data)
# 计算平均值
avg_high, avg_low = calculate_average(data)
# 打印报告
print_report(data, avg_high, avg_low)
预期输出:
==================================================
📅 一周天气汇总(摄氏度)
==================================================
2024-01-15 | 最高 5.6°C | 最低 -2.2°C
2024-01-16 | 最高 7.2°C | 最低 0.0°C
2024-01-17 | 最高 3.3°C | 最低 -3.9°C
2024-01-18 | 最高 4.4°C | 最低 -1.7°C
2024-01-19 | 最高 6.1°C | 最低 -1.1°C
2024-01-20 | 最高 5.0°C | 最低 -2.8°C
2024-01-21 | 最高 3.9°C | 最低 -3.3°C
--------------------------------------------------
📊 本周平均:最高 5.1°C | 最低 -2.1°C
==================================================
一句话解释:我们把数据读取、温度转换、报表生成拆成了三个独立的函数,修改其中一个不影响其他——这就是模块化的好处。
项目 3:待办事项 CLI 小工具(15 分钟)
场景:做一个命令行待办清单工具,可以添加任务、查看任务、标记完成。数据存成 JSON 文件,持久化保存。
项目结构:
todo_app/
├── __init__.py
├── storage.py # 负责读写 JSON 文件
├── todo.py # 待办事项的核心逻辑
└── cli.py # 命令行界面(主入口)
# storage.py
"""数据持久化模块"""
import json
import os
DATA_FILE = 'todos.json'
def load_todos():
"""从文件加载待办事项"""
if not os.path.exists(DATA_FILE):
return []
with open(DATA_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
def save_todos(todos):
"""保存待办事项到文件"""
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(todos, f, ensure_ascii=False, indent=2)
# todo.py
"""待办事项核心逻辑"""
from .storage import load_todos, save_todos
def add_todo(task):
"""添加新任务"""
todos = load_todos()
todo_item = {
'id': len(todos) + 1,
'task': task,
'done': False
}
todos.append(todo_item)
save_todos(todos)
return f"✅ 已添加:「{task}」"
def list_todos():
"""列出所有任务"""
todos = load_todos()
if not todos:
return "📝 还没有任何任务,添加一个吧!"
result = ["📋 待办清单:", "-" * 30]
for item in todos:
status = "☑️" if item['done'] else "⬜"
done_mark = " [已完成]" if item['done'] else ""
result.append(f"{status} {item['id']}. {item['task']}{done_mark}")
result.append("-" * 30)
return "\n".join(result)
def complete_todo(task_id):
"""标记任务为完成"""
todos = load_todos()
for item in todos:
if item['id'] == task_id:
item['done'] = True
save_todos(todos)
return f"🎉 任务「{item['task']}」已完成!"
return f"❌ 没找到 ID 为 {task_id} 的任务"
def delete_todo(task_id):
"""删除任务"""
todos = load_todos()
for i, item in enumerate(todos):
if item['id'] == task_id:
removed = todos.pop(i)
# 重新编号
for j, t in enumerate(todos):
t['id'] = j + 1
save_todos(todos)
return f"🗑️ 已删除:「{removed['task']}」"
return f"❌ 没找到 ID 为 {task_id} 的任务"
# __init__.py
from .todo import add_todo, list_todos, complete_todo, delete_todo
# cli.py
"""命令行入口 - 可以直接运行"""
from todo import add_todo, list_todos, complete_todo, delete_todo
def main():
print("📌 待办事项工具 v1.0")
print("命令:add <任务> / list / done <ID> / del <ID> / quit")
print("-" * 40)
while True:
user_input = input("\n> ").strip()
if not user_input:
continue
parts = user_input.split(maxsplit=1)
command = parts[0].lower()
if command == 'add' and len(parts) > 1:
print(add_todo(parts[1]))
elif command == 'list':
print(list_todos())
elif command == 'done' and len(parts) > 1:
print(complete_todo(int(parts[1])))
elif command == 'del' and len(parts) > 1:
print(delete_todo(int(parts[1])))
elif command == 'quit':
print("👋 再见!")
break
else:
print("⚠️ 不认识这个命令,请重试")
if __name__ == "__main__":
main()
运行效果(交互示例):
📌 待办事项工具 v1.0
命令:add <任务> / list / done <ID> / del <ID> / quit
----------------------------------------
> add 买早餐
✅ 已添加:「买早餐」
> add 写周报
✅ 已添加:「写周报」
> add 健身
✅ 已添加:「健身」
> list
📋 待办清单:
------------------------------
⬜ 1. 买早餐
⬜ 2. 写周报
⬜ 3. 健身
------------------------------
> done 1
🎉 任务「买早餐」已完成!
> del 2
🗑️ 已删除:「写周报」
> list
📋 待办清单:
------------------------------
⬜ 1. 买早餐 [已完成]
⬜ 2. 健身
------------------------------
> quit
👋 再见!
一句话解释:我们把"怎么存数据"、"怎么处理任务"、"怎么跟用户交互"拆成了三个文件。下次想加个"截止日期"功能,直接改 todo.py 就好,不用动存储逻辑。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:循环导入(Circular Import)
错误示例:
# a.py
import b
def hello():
return "Hello from A"
b.greet()
# b.py
import a
def greet():
print(a.hello()) # 可能出问题!
运行 python a.py 会报错:ImportError: cannot import name 'b' from partially initialized module 'b'
原因:A 导入 B 时,B 又想导入 A,但 A 还没初始化完。
✅ 正确做法:把 import 移到函数内部,或者重构代码结构。
# b.py
def greet():
import a # 延迟导入,用到时再加载
print(a.hello())
坑 2:相对导入和绝对导入混用
错误示例:
# 在 mypackage/utils.py 里
from . import module_x # 相对导入(Python 3)
import module_x # 以为是相对,实际是绝对导入
✅ 正确做法:Python 3 建议明确用 . 表示相对导入,或者用完整路径。
from mypackage import module_x # 绝对导入,清楚明了
坑 3:__all__ 和不 __all__ 的区别
混淆示例:
# utils.py
def public_func():
pass
def _private_func():
pass
# 没定义 __all__,默认导出所有不以 _ 开头的函数
✅ 正确做法:明确声明公开接口。
# utils.py
__all__ = ['public_func'] # 只导出 public_func
def public_func():
pass
def _private_func():
pass
坑 4:把主程序当模块导入时也想运行
错误示例:
# main.py
print("主程序开始执行")
import my_module # 这时也会打印上面的句子!
✅ 正确做法:所有要执行的代码都包在 if __name__ == "__main__" 里。
# main.py
def main():
print("主程序开始执行")
# 其他逻辑...
if __name__ == "__main__":
main()
坑 5:找不到模块(ModuleNotFoundError)
错误:ModuleNotFoundError: No module named 'xxx'
原因:Python 搜索路径里没有这个模块。
✅ 排查三步:
import sys
print(sys.path) # 1. 看看搜索路径有哪些
# 2. 确认文件确实存在且文件名正确
# 3. 如果是自定义模块,确保在 sys.path 的目录里
性能小贴士:避免重复导入
Python 第一次导入模块后会缓存起来,同一个进程内再导入不会重新执行。如果你在开发时改了模块代码,需要重启 Python 进程才能看到效果(Jupyter Notebook 用 importlib.reload)。
调试技巧:用 __name__ 打印日志
# 在关键模块里加调试日志
def process_data(data):
print(f"[DEBUG] 开始处理数据,{len(data)} 条记录") # 开发时打开
result = [x for x in data if x > 0]
print(f"[DEBUG] 处理完成,保留 {len(result)} 条")
return result
# 正式发布时,用 logging 替代 print
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def process_data(data):
logger.info(f"开始处理数据,{len(data)} 条记录")
✏️ 练习题 + 作业题
练习题(10 分钟)
练习 1(2 分钟):换个单位
- 输入:把项目 1 里的华氏度转摄氏度,改成把 100°F 转成摄氏度
- 预期输出:100°F = 37.8°C
- 提示:fahrenheit_to_celsius(100) 保留一位小数
练习 2(2 分钟):加个判断
- 输入:在温度转换模块里,加一个函数判断温度是否在冰点以下(0°C)
- 预期输出:-5 返回 True,10 返回 False
- 提示:新建一个 is_below_freezing(c) 函数
练习 3(3 分钟):处理新数据
- 输入:创建一个新 CSV scores.csv,内容是学生姓名和分数(自己编3行),用项目 2 的方法读取并打印
- 预期输出:能正确读取并打印每行数据
- 提示:修改 read_csv() 的表头解析逻辑
练习 4(3 分钟):串两个项目
- 输入:把项目 2 的天气数据处理结果,自动添加到项目 3 的待办清单里
- 预期输出:运行后待办清单里多了"处理天气数据"这条
- 提示:调用 add_todo("处理天气数据")
练习 5(挑战题,5 分钟):读懂报错
- 输入:下面这段代码运行时报什么错?为什么?
# main.py
from mypackage.utils import helper
if __name__ == "__main__":
helper()
# mypackage/__init__.py
# 空文件
# mypackage/utils.py
def helper():
print("我是 helper")
# 没有 __all__ = ['helper']
- 预期输出:说出错误类型、错误信息、原因、修复方法
- 提示:
__init__.py导出了什么?
作业题(30 分钟 - 2 小时)
作业:做一个「个人理财小助手」
-
需求描述:管理你的日常收支记录,数据存 JSON 文件,可以记账、查账、统计
-
功能点:
1. 记账:add 支出 早餐 15或add 收入 工资 5000
2. 查账:list显示所有记录
3. 统计:stats显示本月总支出、总收入、结余 -
加分项(选做):
1. 支持按月份筛选记录
2. 导出 CSV 格式报表 -
验收标准:
- 能运行
python cli.py启动 add能保存数据到 JSON,重启后list还能看到-
stats能正确计算收支合计 -
提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
一句话总结本文学到的 3 个核心点:
- 模块(.py 文件)让你的代码可以复用,import 拿来就能用
- 包(文件夹 + __init__.py)把相关模块组织在一起,层次清晰
- if __name__ == "__main__" 让你的模块既能当工具被导入,也能单独运行测试
延伸学习资源:
- Python 官方文档:Modules —— 最权威的参考手册
- 《Python 编程:从入门到实践》 第 10 章 —— 模块与包实战
- Real Python: Python Modules and Packages —— 进阶阅读
互动钩子:
你在工作或学习中有用过别人写的 Python 包吗?是怎么安装和导入的?遇到过什么坑?评论区聊聊,老粉优先回复!
下一章我们要做一个综合实战项目:把这一章学的模块化思维用起来,从零搭一个自己的「工具函数库」。想象一下,你写的 temperature.py、todo.py 以后可以直接被其他项目引用——这就是程序员的"代码资产"。准备好了吗?

评论(0)