Skip to content

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 包的本质作用

  1. 命名空间隔离:不同包里的模块可以重名而不冲突(utils.stringmyapp.string 井水不犯河水);
  2. 代码组织:把相关功能的模块归类存放,项目结构清晰;
  3. 可复用分发:包可以打包发布到 PyPI(Python 官方第三方包仓库),供全世界安装使用。

1.3 包的三大分类

分类来源举例获取方式
标准库包Python 自带osjsonurllibre安装 Python 就有
第三方包社区开发者发布到 PyPIrequestsnumpypandaspip install
自定义包你自己写的项目里的 utilsservices手动创建文件夹

1.4 最小化的目录结构示例

一个包最基础的组成只需两样东西:一个文件夹 + 一个 __init__.py

text
myproject/               # 项目根目录
├── main.py              # 入口脚本
└── mypackage/           # 一个自定义包(文件夹)
    ├── __init__.py      # 包标识文件(可以为空)
    ├── calculator.py    # 模块 1
    └── greeting.py      # 模块 2

main.py 中就可以这样使用:

python
# main.py
from mypackage import calculator   # 从包中导入 calculator 模块

print(calculator.add(1, 2))

二、标准库包的使用

标准库是 Python 官方"出厂自带"的工具箱,无需安装,直接导入即可。导入主要有两种写法。

2.1 import 模块名:整体导入

python
import json              # 导入整个 json 模块

data = {"name": "小明", "age": 18}
text = json.dumps(data)  # 使用时必须带"模块名."前缀
print(text)              # 输出: {"name": "小明", "age": 18}

特点:调用时要写全名 json.dumps(),代码可读性好,一眼看出 dumps 来自哪个模块。

2.2 from ... import ...:精确导入

python
from json import dumps, loads   # 只导入 dumps 和 loads 两个函数

text = dumps({"name": "小明"})   # 直接使用,不用写 json. 前缀
data = loads(text)
print(data["name"])              # 输出: 小明

特点:使用更简洁,但多个模块若有同名函数容易混淆来源。

2.3 import ... as ...:起别名

python
import urllib.request as req     # 模块路径太长,起个短别名

response = req.urlopen("https://www.example.com")
print(response.status)           # 输出: 200

2.4 不推荐:from ... import *

python
from json import *   # 把 json 里所有公开名字一次性倒入当前命名空间

为什么不推荐:你无法预知导入了哪些名字,可能覆盖掉你自己定义的同名函数,也会让阅读代码的人一头雾水。Python 之禅说:"明确优于隐式",这条正是反例。


三、第三方包:安装与调用全流程

第三方包是社区开发者写好并发布到 PyPI(Python Package Index,Python 官方包仓库,类比"应用商店")上的包。使用第三方包分两步:安装 → 导入

3.1 用 pip 安装(最主流)

pip 是 Python 自带的包管理工具,相当于手机上的应用商店 App。

bash
# 安装指定包
pip install requests

# 安装指定版本
pip install requests==2.31.0

# 升级已安装的包
pip install --upgrade requests

# 卸载
pip uninstall requests

# 查看已安装的包
pip list

安装步骤示意(以 requests 为例)

bash
# 第一步:安装
pip install requests

# 第二步:在代码中导入并使用
python
import requests

# 发送 GET 请求
resp = requests.get("https://api.github.com")
print(resp.status_code)   # 输出: 200

3.2 国内镜像加速配置

由于 PyPI 服务器在国外,直接下载经常超时。国内可改用镜像源:

bash
# 临时使用清华镜像安装
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 库等二进制依赖):

bash
# conda 安装
conda install numpy

# 指定频道(channel)安装
conda install -c conda-forge pandas

pip 与 conda 怎么选:纯 Python 包两者都可以;涉及底层二进制依赖(如某些科学计算库)时 conda 更省心;conda 仓库里没有的包可以用 pip 补充安装。

3.4 用 requirements.txt 锁定依赖

项目协作时,把所有依赖写进 requirements.txt,别人一条命令即可复现环境:

text
# requirements.txt
requests==2.31.0
numpy>=1.24.0
pandas
bash
# 一键安装所有依赖
pip install -r requirements.txt

四、自定义包:从零创建到本地导入

4.1 创建步骤

我们来创建一个带子包嵌套的完整示例:

text
demo_project/              # 项目根目录
├── main.py                # 入口脚本
└── shop/                  # 包:shop
    ├── __init__.py        # 包标识文件
    ├── goods.py           # 模块:商品
    └── drinks/            # 子包:饮品区(嵌套规则与包相同)
        ├── __init__.py    # 子包的标识文件
        └── juice.py       # 模块:果汁

嵌套规则:子包就是一个放在包里面的、带 __init__.py 的文件夹,可以无限层嵌套(但实际项目建议不超过 3 层)。

4.2 __init__.py 的作用与写法

作用一:标识文件夹为包(最基础,文件可以为空)。

作用二:执行包的初始化逻辑,包被首次导入时自动运行:

python
# shop/__init__.py
print("shop 包被导入了")   # 包首次被 import 时执行一次

作用三:简化外部调用路径。把深层模块里的常用内容"提前"到包顶层:

python
# shop/__init__.py
from shop.goods import Goods      # 把 Goods 提升到包顶层

# 这样外部就不必写 from shop.goods import Goods
# 直接写 from shop import Goods 即可

4.3 各模块代码

python
# 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} 元"
python
# shop/drinks/juice.py
def make_juice(fruit):
    """榨果汁"""
    return f"一杯鲜榨{fruit}汁"
python
# shop/drinks/__init__.py(可以为空,仅作标识)

4.4 完整可运行的调用示例

python
# 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,是因为它们在同一目录。如果入口脚本在别处运行导致找不到包,可以查看并临时添加路径:

python
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 funcimport pkg.module / from pkg import module,可多级点号嵌套

5.2 联系:包是由模块组成的

一句话概括两者关系:模块是砖,包是墙。一个包内部可以有多个模块,模块被包含在包之内;从导入的角度看,import a.b.cab 是包(或子包),c 是模块。

5.3 什么时候单独用模块

  • 写一次性的小工具脚本:一个 backup.py 搞定,没必要建包;
  • 功能边界清晰、未来不会膨胀的辅助代码。

5.4 什么时候用包

  • 代码开始按功能分裂成多个文件(如 db.pyapi.pymodels.py);
  • 要发布给别人安装使用的库;
  • 团队协作、需要明确目录结构的项目。

5.5 配合使用的实际案例

text
webapp/                  # 包:整个 Web 应用
├── __init__.py
├── db.py                # 模块:数据库操作
├── api.py               # 模块:接口逻辑
└── models/              # 子包:数据模型
    ├── __init__.py
    ├── user.py          # 模块:用户模型
    └── order.py         # 模块:订单模型

api.py 中配合使用:

python
# 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_utilsdata_tools,不要用大写或连字符(- 在 import 语句中是非法字符);
  • 避免与标准库重名:自定义包叫 jsonemailos 会遮蔽标准库,导致莫名其妙地报错;
  • 避免与第三方包重名:起名字前先到 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 中声明:

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),避免全局污染:
bash
python -m venv .venv          # 创建虚拟环境
.venv\Scripts\activate        # Windows 激活
# source .venv/bin/activate   # macOS/Linux 激活
  • requirements.txtpip freeze 锁定版本。

解决

  • pip install pipdeptree && pipdeptree 查看依赖树,定位冲突来源;
  • 尝试安装兼容版本区间,或寻找不冲突的替代包;
  • 极端情况拆分项目或升级其中一方。

6.4 __init__.py 的过度导出问题

__init__.py 当成"万能中转站",导入一堆子模块,会导致:

  • 导入变慢:哪怕只用一个小功能,整个包的模块都被加载;
  • 循环导入风险上升。

建议__init__.py 只导出最常用、最稳定的少数接口,其余让使用者显式从子模块导入。

6.5 相对导入与绝对导入

text
mypkg/
├── __init__.py
├── a.py
└── sub/
    ├── __init__.py
    └── b.py
python
# 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 服务器:用 devpiNexus 等搭建公司内部仓库,安装时加 -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.pyb.py 又导入 a.py——两个模块互相依赖,谁都无法完成加载。

python
# a.py
from b import func_b    # a 依赖 b

# b.py
from a import func_a    # b 又依赖 a → 循环导入,报错!

规避方案

  1. 重构代码:把公共部分抽到第三个模块 common.py,a 和 b 都只依赖它(最根本的解法);
  2. 延迟导入:把 import 语句移到函数内部,用到时才导入:
python
# b.py
def func_b():
    from a import func_a   # 延迟到调用时才导入,打破循环
    return func_a()
  1. TYPE_CHECKING 处理仅为类型注解的导入
python
# 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'

常见诱因与解决

  1. 包根本没安装pip install xxx
  2. 装到了别的 Python 环境(比如系统里装了多个 Python)→ 用 python -m pip install xxx 确保安装到当前解释器对应的环境,并用 where python / which python 确认解释器位置;
  3. 自定义包不在搜索路径里 → 确认运行脚本的工作目录,或检查 sys.path
  4. 包名拼写错误 → 注意 pip 安装名和 import 名可能不同(如 pip install pillow 对应 import PIL)。

Q2:ImportError: cannot import name 'xxx' from 'yyy'

常见诱因

  1. 循环导入 → 按 6.7 节的方案重构或延迟导入;
  2. 本地文件与包重名 → 当前目录下有个 json.py 遮蔽了标准库,改名即可;
  3. 包里确实没有这个名字 → 版本不对,老代码引用了新版包中已删除的接口,检查版本并降级/改代码。

Q3:明明改了代码,导入的还是旧的?

Python 会缓存编译结果到 __pycache__ 目录。极少数情况下缓存过期不生效,删除项目里所有 __pycache__ 文件夹后重试;另外确认你是否在 Jupyter 中运行——Jupyter 内核会长期持有已导入的模块,需重启内核或用 %load_ext autoreload

Q4:相对导入报错 "attempted relative import with no known parent package"

你把包含相对导入的文件当作脚本直接运行了python b.py)。改为在项目根目录用模块方式运行:

bash
python -m mypkg.sub.b

Q5:ImportError: DLL load failed(Windows 特有)

第三方包依赖的底层 C 库缺失或位数不匹配(32 位 vs 64 位)。解决:确认 Python 与包同为 64 位;用 conda 重装该包(conda 会自动处理二进制依赖)。


本章知识要点回顾

模块核心内容
包的概念__init__.py 的文件夹,用点号命名空间组织模块
包的分类标准库包、第三方包、自定义包
标准库用法importfrom...importimport...as,避免 import *
第三方包pip/conda 安装、国内镜像加速、requirements.txt 锁版本
自定义包__init__.py 三重作用、子包嵌套、sys.path 搜索路径
包 vs 模块模块是单文件最小单元,包是装模块的文件夹;模块是砖,包是墙
注意事项小写下划线命名、semver 版本、虚拟环境防冲突、慎用过度导出与相对导入
高频问题ModuleNotFoundError 查环境与路径,ImportError 查循环导入与同名遮蔽

全章核心结论:包是 Python 组织代码的"货架系统"——小项目用模块轻装上阵,大项目用包分区分层;用好 __init__.py、虚拟环境和版本锁定这三件武器,就能避开 90% 的导入坑。