第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 个词

配图1 - 配图1

注意! __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 时不会乱输出东西。

配图2 - 配图2


🔥 实战 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 返回 True10 返回 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 支出 早餐 15add 收入 工资 5000
    2. 查账list 显示所有记录
    3. 统计stats 显示本月总支出、总收入、结余

  • 加分项(选做):
    1. 支持按月份筛选记录
    2. 导出 CSV 格式报表

  • 验收标准

  • 能运行 python cli.py 启动
  • add 能保存数据到 JSON,重启后 list 还能看到
  • stats 能正确计算收支合计

  • 提交方式:评论区贴代码或 GitHub 链接


📚 总结 + 资源

一句话总结本文学到的 3 个核心点
- 模块(.py 文件)让你的代码可以复用,import 拿来就能用
- 包(文件夹 + __init__.py)把相关模块组织在一起,层次清晰
- if __name__ == "__main__" 让你的模块既能当工具被导入,也能单独运行测试

延伸学习资源

  1. Python 官方文档:Modules —— 最权威的参考手册
  2. 《Python 编程:从入门到实践》 第 10 章 —— 模块与包实战
  3. Real Python: Python Modules and Packages —— 进阶阅读

互动钩子

你在工作或学习中有用过别人写的 Python 包吗?是怎么安装和导入的?遇到过什么坑?评论区聊聊,老粉优先回复!


下一章我们要做一个综合实战项目:把这一章学的模块化思维用起来,从零搭一个自己的「工具函数库」。想象一下,你写的 temperature.pytodo.py 以后可以直接被其他项目引用——这就是程序员的"代码资产"。准备好了吗?

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。