Python 包(Package)
引言:用"超市货架"理解 Python 包
想象你开了一家超市,货物越来越多,你该怎么办?
- 单个商品——比如一瓶水、一袋盐——这相当于 Python 里的模块(Module),就是一个
.py文件; - 货架分区——饮料区、零食区、日用品区,每个区域里放着同类商品——这相当于 Python 里的包(Package),就是一个装着多个模块的文件夹;
- 仓库里的子区域——饮料区里又分"碳酸饮料货架""果汁货架"——这相当于子包(Sub-package)。
如果你的代码只有一个 .py 文件,就像杂货铺,东西随手一扔没问题;但当代码涨到几十个、几百个文件时,就必须像超市一样分区管理,否则谁也找不到东西。这就是"包"存在的意义。
这一篇,我们把 Python 包讲透:它是什么、怎么用(标准库/第三方/自定义)、和模块到底有什么区别、开发中有哪些坑,最后附上新手最常见的 ImportError 排查手册。
一、包到底是什么:定义、本质与分类
1.1 官方定义拆解
在 Python 官方文档中,包(Package) 的定义是:
一种用"点号模块名"来组织 Python 模块命名空间的方式。例如模块名
A.B表示包A中名为B的子模块。
通俗地说:
- 模块:一个
.py文件就是一个模块,文件名即模块名; - 包:一个包含
__init__.py文件的文件夹就是一个包,文件夹名即包名; - 包可以嵌套——包里面还可以有子包。
术语解释:__init__.py 是包的"身份证",只要文件夹里有这个文件,Python 就把这个文件夹当作包来处理。文件内容可以为空,也可以写初始化代码。
1.2 包的本质作用
- 命名空间隔离:不同包里的模块可以重名而不冲突(
utils.string和myapp.string井水不犯河水); - 代码组织:把相关功能的模块归类存放,项目结构清晰;
- 可复用分发:包可以打包发布到 PyPI(Python 官方第三方包仓库),供全世界安装使用。
1.3 包的三大分类
| 分类 | 来源 | 举例 | 获取方式 |
|---|---|---|---|
| 标准库包 | Python 自带 | os、json、urllib、re | 安装 Python 就有 |
| 第三方包 | 社区开发者发布到 PyPI | requests、numpy、pandas | pip install |
| 自定义包 | 你自己写的 | 项目里的 utils、services | 手动创建文件夹 |
1.4 最小化的目录结构示例
一个包最基础的组成只需两样东西:一个文件夹 + 一个 __init__.py。
myproject/ # 项目根目录
├── main.py # 入口脚本
└── mypackage/ # 一个自定义包(文件夹)
├── __init__.py # 包标识文件(可以为空)
├── calculator.py # 模块 1
└── greeting.py # 模块 2在 main.py 中就可以这样使用:
# main.py
from mypackage import calculator # 从包中导入 calculator 模块
print(calculator.add(1, 2))二、标准库包的使用
标准库是 Python 官方"出厂自带"的工具箱,无需安装,直接导入即可。导入主要有两种写法。
2.1 import 模块名:整体导入
import json # 导入整个 json 模块
data = {"name": "小明", "age": 18}
text = json.dumps(data) # 使用时必须带"模块名."前缀
print(text) # 输出: {"name": "小明", "age": 18}特点:调用时要写全名 json.dumps(),代码可读性好,一眼看出 dumps 来自哪个模块。
2.2 from ... import ...:精确导入
from json import dumps, loads # 只导入 dumps 和 loads 两个函数
text = dumps({"name": "小明"}) # 直接使用,不用写 json. 前缀
data = loads(text)
print(data["name"]) # 输出: 小明特点:使用更简洁,但多个模块若有同名函数容易混淆来源。
2.3 import ... as ...:起别名
import urllib.request as req # 模块路径太长,起个短别名
response = req.urlopen("https://www.example.com")
print(response.status) # 输出: 2002.4 不推荐:from ... import *
from json import * # 把 json 里所有公开名字一次性倒入当前命名空间为什么不推荐:你无法预知导入了哪些名字,可能覆盖掉你自己定义的同名函数,也会让阅读代码的人一头雾水。Python 之禅说:"明确优于隐式",这条正是反例。
三、第三方包:安装与调用全流程
第三方包是社区开发者写好并发布到 PyPI(Python Package Index,Python 官方包仓库,类比"应用商店")上的包。使用第三方包分两步:安装 → 导入。
3.1 用 pip 安装(最主流)
pip 是 Python 自带的包管理工具,相当于手机上的应用商店 App。
# 安装指定包
pip install requests
# 安装指定版本
pip install requests==2.31.0
# 升级已安装的包
pip install --upgrade requests
# 卸载
pip uninstall requests
# 查看已安装的包
pip list安装步骤示意(以 requests 为例):
# 第一步:安装
pip install requests
# 第二步:在代码中导入并使用import requests
# 发送 GET 请求
resp = requests.get("https://api.github.com")
print(resp.status_code) # 输出: 2003.2 国内镜像加速配置
由于 PyPI 服务器在国外,直接下载经常超时。国内可改用镜像源:
# 临时使用清华镜像安装
pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple
# 永久配置(Windows)
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple常用国内镜像:
| 镜像 | 地址 |
|---|---|
| 清华 | https://pypi.tuna.tsinghua.edu.cn/simple |
| 阿里云 | https://mirrors.aliyun.com/pypi/simple/ |
| 豆瓣 | https://pypi.douban.com/simple/ |
3.3 用 conda 安装(数据科学场景)
conda 是 Anaconda/Miniconda 自带的包管理器,适合科学计算生态(能顺带处理 C 库等二进制依赖):
# conda 安装
conda install numpy
# 指定频道(channel)安装
conda install -c conda-forge pandaspip 与 conda 怎么选:纯 Python 包两者都可以;涉及底层二进制依赖(如某些科学计算库)时 conda 更省心;conda 仓库里没有的包可以用 pip 补充安装。
3.4 用 requirements.txt 锁定依赖
项目协作时,把所有依赖写进 requirements.txt,别人一条命令即可复现环境:
# requirements.txt
requests==2.31.0
numpy>=1.24.0
pandas# 一键安装所有依赖
pip install -r requirements.txt四、自定义包:从零创建到本地导入
4.1 创建步骤
我们来创建一个带子包嵌套的完整示例:
demo_project/ # 项目根目录
├── main.py # 入口脚本
└── shop/ # 包:shop
├── __init__.py # 包标识文件
├── goods.py # 模块:商品
└── drinks/ # 子包:饮品区(嵌套规则与包相同)
├── __init__.py # 子包的标识文件
└── juice.py # 模块:果汁嵌套规则:子包就是一个放在包里面的、带 __init__.py 的文件夹,可以无限层嵌套(但实际项目建议不超过 3 层)。
4.2 __init__.py 的作用与写法
作用一:标识文件夹为包(最基础,文件可以为空)。
作用二:执行包的初始化逻辑,包被首次导入时自动运行:
# shop/__init__.py
print("shop 包被导入了") # 包首次被 import 时执行一次作用三:简化外部调用路径。把深层模块里的常用内容"提前"到包顶层:
# shop/__init__.py
from shop.goods import Goods # 把 Goods 提升到包顶层
# 这样外部就不必写 from shop.goods import Goods
# 直接写 from shop import Goods 即可4.3 各模块代码
# shop/goods.py
class Goods:
"""商品类"""
def __init__(self, name, price):
self.name = name
self.price = price
def info(self):
return f"{self.name} 售价 {self.price} 元"# shop/drinks/juice.py
def make_juice(fruit):
"""榨果汁"""
return f"一杯鲜榨{fruit}汁"# shop/drinks/__init__.py(可以为空,仅作标识)4.4 完整可运行的调用示例
# main.py
from shop import Goods # 通过 __init__.py 的"提升"直接导入
from shop.drinks.juice import make_juice # 导入子包中的函数
apple = Goods("苹果", 5)
print(apple.info()) # 输出: 苹果 售价 5 元
print(make_juice("橙子")) # 输出: 一杯鲜榨橙汁4.5 本地导入的路径问题
Python 找包时只在 sys.path(搜索路径列表)里找,默认包含:当前运行目录、标准库目录、第三方包目录(site-packages)。
所以 main.py 能 import 到 shop,是因为它们在同一目录。如果入口脚本在别处运行导致找不到包,可以查看并临时添加路径:
import sys
import os
print(sys.path) # 查看当前搜索路径
# 把项目根目录加入搜索路径(仅当前进程有效)
sys.path.append(os.path.dirname(os.path.abspath(__file__)))更规范的做法是:在包所在目录的上级目录运行入口脚本,或把包用 pip install -e . 以"可编辑模式"装进环境。
五、包 vs 模块:四维度完整对比
5.1 对比表格
| 维度 | 模块(Module) | 包(Package) |
|---|---|---|
| 定义本质 | Python 代码组织的最小单元 | 组织多个模块的"容器",是更高一层的结构 |
| 文件形态 | 单个 .py 文件 | 一个文件夹,内含 __init__.py 和多个模块/子包 |
| 使用场景 | 功能单一、代码量小的脚本或工具 | 功能成体系、模块数量多的项目或需要发布的库 |
| 导入逻辑 | import module / from module import func | import pkg.module / from pkg import module,可多级点号嵌套 |
5.2 联系:包是由模块组成的
一句话概括两者关系:模块是砖,包是墙。一个包内部可以有多个模块,模块被包含在包之内;从导入的角度看,import a.b.c 中 a、b 是包(或子包),c 是模块。
5.3 什么时候单独用模块
- 写一次性的小工具脚本:一个
backup.py搞定,没必要建包; - 功能边界清晰、未来不会膨胀的辅助代码。
5.4 什么时候用包
- 代码开始按功能分裂成多个文件(如
db.py、api.py、models.py); - 要发布给别人安装使用的库;
- 团队协作、需要明确目录结构的项目。
5.5 配合使用的实际案例
webapp/ # 包:整个 Web 应用
├── __init__.py
├── db.py # 模块:数据库操作
├── api.py # 模块:接口逻辑
└── models/ # 子包:数据模型
├── __init__.py
├── user.py # 模块:用户模型
└── order.py # 模块:订单模型api.py 中配合使用:
# webapp/api.py
from webapp.db import get_connection # 同级包里的模块
from webapp.models.user import User # 子包里的模块
def create_user(name):
conn = get_connection() # 用 db 模块拿连接
user = User(name) # 用 models 子包里的类
# ... 省略保存逻辑
return user六、开发注意事项(避坑清单)
6.1 包命名规范
- 全部小写 + 下划线:如
my_utils、data_tools,不要用大写或连字符(-在 import 语句中是非法字符); - 避免与标准库重名:自定义包叫
json、email、os会遮蔽标准库,导致莫名其妙地报错; - 避免与第三方包重名:起名字前先到 PyPI 搜一下是否已被占用;
- 见名知意:
payment好于pkg1。
6.2 版本管理:semver 语义化版本
发布自己的包时遵循 SemVer(Semantic Versioning,语义化版本),格式 主版本.次版本.修订号:
- 修订号(1.0.0 → 1.0.1):只修 bug,接口不变;
- 次版本(1.0.0 → 1.1.0):新增功能,向后兼容;
- 主版本(1.0.0 → 2.0.0):做了不兼容的破坏性修改。
在 setup.py / pyproject.toml 中声明:
# pyproject.toml
[project]
name = "my-tools"
version = "1.2.0" # 遵循 semver 规范6.3 依赖冲突的预防与解决
问题:包 A 要求 requests>=2.30,包 B 要求 requests<2.30,两者无法共存。
预防:
- 每个项目使用独立虚拟环境(venv 或 conda env),避免全局污染:
python -m venv .venv # 创建虚拟环境
.venv\Scripts\activate # Windows 激活
# source .venv/bin/activate # macOS/Linux 激活- 用
requirements.txt或pip freeze锁定版本。
解决:
- 用
pip install pipdeptree && pipdeptree查看依赖树,定位冲突来源; - 尝试安装兼容版本区间,或寻找不冲突的替代包;
- 极端情况拆分项目或升级其中一方。
6.4 __init__.py 的过度导出问题
把 __init__.py 当成"万能中转站",导入一堆子模块,会导致:
- 导入变慢:哪怕只用一个小功能,整个包的模块都被加载;
- 循环导入风险上升。
建议:__init__.py 只导出最常用、最稳定的少数接口,其余让使用者显式从子模块导入。
6.5 相对导入与绝对导入
mypkg/
├── __init__.py
├── a.py
└── sub/
├── __init__.py
└── b.py# mypkg/sub/b.py
# 绝对导入:从包的顶层写全路径(推荐,清晰明确)
from mypkg.a import helper
# 相对导入:用点号表示"当前包/上级包"
from ..a import helper # .. 表示上级包 mypkg
from . import c # . 表示当前包 sub使用建议:
- 包内部模块互相引用,可以用相对导入(包改名时不用批量改代码);
- 跨包引用、对外暴露的示例代码,用绝对导入;
- 注意:直接运行的脚本中不能用相对导入(会报
attempted relative import with no known parent package),相对导入只在被当作包的一部分导入时有效。
6.6 私有包的发布注意事项
不想发布到公共 PyPI 时:
- 私有 PyPI 服务器:用
devpi、Nexus等搭建公司内部仓库,安装时加-i http://your-server/simple; - 直接从 Git 安装:
pip install git+https://github.com/your-org/your-pkg.git@v1.0.0; - 本地 wheel 分发:
python -m build打包成.whl文件,拷贝给同事后pip install your_pkg-1.0.0-py3-none-any.whl; - 发布前检查敏感信息:不要把密钥、内网地址写进包代码里,打包前确认
MANIFEST.in/pyproject.toml没有误包含机密文件。
6.7 循环导入的规避
问题:a.py 导入 b.py,b.py 又导入 a.py——两个模块互相依赖,谁都无法完成加载。
# a.py
from b import func_b # a 依赖 b
# b.py
from a import func_a # b 又依赖 a → 循环导入,报错!规避方案:
- 重构代码:把公共部分抽到第三个模块
common.py,a 和 b 都只依赖它(最根本的解法); - 延迟导入:把 import 语句移到函数内部,用到时才导入:
# b.py
def func_b():
from a import func_a # 延迟到调用时才导入,打破循环
return func_a()- 用
TYPE_CHECKING处理仅为类型注解的导入:
# b.py
from typing import TYPE_CHECKING
if TYPE_CHECKING: # 仅在类型检查时导入,运行时不执行
from a import A
def func_b(obj: "A"): # 注解用字符串形式
...七、常见问题排查(FAQ)
Q1:ModuleNotFoundError: No module named 'xxx'
常见诱因与解决:
- 包根本没安装 →
pip install xxx; - 装到了别的 Python 环境(比如系统里装了多个 Python)→ 用
python -m pip install xxx确保安装到当前解释器对应的环境,并用where python/which python确认解释器位置; - 自定义包不在搜索路径里 → 确认运行脚本的工作目录,或检查
sys.path; - 包名拼写错误 → 注意
pip安装名和import名可能不同(如pip install pillow对应import PIL)。
Q2:ImportError: cannot import name 'xxx' from 'yyy'
常见诱因:
- 循环导入 → 按 6.7 节的方案重构或延迟导入;
- 本地文件与包重名 → 当前目录下有个
json.py遮蔽了标准库,改名即可; - 包里确实没有这个名字 → 版本不对,老代码引用了新版包中已删除的接口,检查版本并降级/改代码。
Q3:明明改了代码,导入的还是旧的?
Python 会缓存编译结果到 __pycache__ 目录。极少数情况下缓存过期不生效,删除项目里所有 __pycache__ 文件夹后重试;另外确认你是否在 Jupyter 中运行——Jupyter 内核会长期持有已导入的模块,需重启内核或用 %load_ext autoreload。
Q4:相对导入报错 "attempted relative import with no known parent package"
你把包含相对导入的文件当作脚本直接运行了(python b.py)。改为在项目根目录用模块方式运行:
python -m mypkg.sub.bQ5:ImportError: DLL load failed(Windows 特有)
第三方包依赖的底层 C 库缺失或位数不匹配(32 位 vs 64 位)。解决:确认 Python 与包同为 64 位;用 conda 重装该包(conda 会自动处理二进制依赖)。
本章知识要点回顾
| 模块 | 核心内容 |
|---|---|
| 包的概念 | 带 __init__.py 的文件夹,用点号命名空间组织模块 |
| 包的分类 | 标准库包、第三方包、自定义包 |
| 标准库用法 | import、from...import、import...as,避免 import * |
| 第三方包 | pip/conda 安装、国内镜像加速、requirements.txt 锁版本 |
| 自定义包 | __init__.py 三重作用、子包嵌套、sys.path 搜索路径 |
| 包 vs 模块 | 模块是单文件最小单元,包是装模块的文件夹;模块是砖,包是墙 |
| 注意事项 | 小写下划线命名、semver 版本、虚拟环境防冲突、慎用过度导出与相对导入 |
| 高频问题 | ModuleNotFoundError 查环境与路径,ImportError 查循环导入与同名遮蔽 |
全章核心结论:包是 Python 组织代码的"货架系统"——小项目用模块轻装上阵,大项目用包分区分层;用好 __init__.py、虚拟环境和版本锁定这三件武器,就能避开 90% 的导入坑。