11 - 模块与包
写到一定程度的代码,你会发现把所有东西塞在一个文件里太乱了。模块和包就是帮你组织代码的。
模块是什么
简单说,一个 .py 文件就是一个模块。你之前写的 hello.py、calculator.py 都是模块。
模块的好处是可以互相引用。比如你写了一个工具函数放在 utils.py 里,其他文件可以直接拿来用。
创建和使用模块
先创建一个 utils.py:
python
# utils.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b
PI = 3.14159
然后在另一个文件里使用它:
python
# main.py
import utils
print(utils.add(3, 5)) # 8
print(utils.PI) # 3.14159
import utils 就是把 utils.py 这个模块导进来,然后用 模块名.函数名 的方式调用。
import 的几种写法
python
# 方式一:导入整个模块
import utils
utils.add(3, 5)
# 方式二:导入特定的东西
from utils import add, PI
add(3, 5) # 直接用,不用加模块名
# 方式三:导入全部(不推荐)
from utils import *
add(3, 5)
# 方式四:起别名
import utils as u
u.add(3, 5)
from utils import add as plus
plus(3, 5)
方式三为什么不好?因为你不知道导进来了什么名字,可能跟你自己定义的名字冲突。在大型项目里这种 bug 很难查。
不过有一个例外,在 __init__.py 里用 from xxx import * 是常见的做法,后面讲包的时候会说到。
__name__ 变量
这个在很多 Python 文件底部都能看到:
python
def main():
print("这是主逻辑")
if __name__ == "__main__":
main()
这是什么意思呢?
每个 Python 文件有一个内置变量 __name__:
- 如果这个文件是直接运行 的,
__name__的值是"__main__" - 如果这个文件是被 import 的,
__name__的值是模块名
举个例子。假设 utils.py 里有:
python
# utils.py
def add(a, b):
return a + b
print("utils 被加载了")
if __name__ == "__main__":
print("utils 是直接运行的")
print(add(1, 2))
直接运行 uv run utils.py:
utils 被加载了
utils 是直接运行的
3
在别的文件里 import utils:
utils 被加载了
"utils 是直接运行的"那段代码就不会执行。
所以 if __name__ == "__main__" 的意思就是"如果这个文件是被直接运行的(而不是被导入的),就执行下面的代码"。这样你的模块既可以被导入使用,也可以直接运行做测试。
标准库模块
Python 自带了很多实用的模块,不需要安装,直接 import 就能用。
python
# os --- 操作系统相关
import os
print(os.getcwd()) # 当前目录
print(os.listdir(".")) # 列出目录下的文件
# sys --- 系统相关
import sys
print(sys.version) # Python 版本
print(sys.argv) # 命令行参数
# math --- 数学函数
import math
print(math.sqrt(16)) # 4.0
print(math.ceil(3.2)) # 4(向上取整)
print(math.floor(3.8)) # 3(向下取整)
# random --- 随机数
import random
print(random.randint(1, 100)) # 1-100 之间的随机整数
print(random.choice(["a", "b", "c"])) # 随机选一个
random.shuffle(my_list) # 打乱列表
# datetime --- 日期时间
from datetime import datetime, timedelta
now = datetime.now()
print(now.strftime("%Y-%m-%d %H:%M:%S"))
tomorrow = now + timedelta(days=1)
# json --- JSON 处理
import json
data = {"name": "小明", "age": 25}
json_str = json.dumps(data, ensure_ascii=False, indent=2)
print(json_str)
parsed = json.loads(json_str)
# re --- 正则表达式(第 19 章详细讲)
import re
result = re.findall(r"\d+", "我有 3 个苹果和 5 个橘子")
print(result) # ['3', '5']
这些标准库是 Python 的"电池"------Python 号称"自带电池"的语言,就是因为它附带了这些好用的模块。
第三方包
标准库不够用的时候,就需要安装第三方包了。还记得第一章说的 uv 吗?就是干这个的。
安装包
bash
# 安装 requests(HTTP 请求库)
uv add requests
# 安装指定版本
uv add "requests>=2.28,<3.0"
# 安装多个
uv add requests beautifulsoup4 pandas
使用包
python
import requests
# 发送一个 GET 请求
response = requests.get("https://httpbin.org/get")
print(response.status_code) # 200
print(response.json()) # 解析 JSON 响应
卸载包
bash
uv remove requests
查看已安装的包
bash
uv pip list
pyproject.toml
uv init 创建项目时会自动生成 pyproject.toml,它记录了项目的依赖:
toml
[project]
name = "my-project"
version = "0.1.0"
requires-python = ">=3.12"
[tool.uv]
dev-dependencies = []
你用 uv add 安装的包会自动添加到这个文件里。别人拿到你的项目后,只需要 uv sync 就能安装所有依赖。
包(Package)
包就是"一堆模块的集合",本质上是一个包含 __init__.py 文件的目录。
my_package/
├── __init__.py
├── module_a.py
├── module_b.py
└── sub_package/
├── __init__.py
└── module_c.py
使用方式:
python
from my_package import module_a
from my_package.sub_package import module_c
__init__.py 可以是空文件,它的存在只是告诉 Python "这个目录是个包"。你也可以在里面写代码,通常用来控制包的导入行为:
python
# my_package/__init__.py
from .module_a import func_a
from .module_b import func_b
# 这样用户就可以直接写:
# from my_package import func_a
# 而不用写:
# from my_package.module_a import func_a
虚拟环境(再讲一次)
第一章简单提了一下,这里再深入一点。
为什么需要虚拟环境?假设你有两个项目:
- 项目 A 需要
requests 2.28 - 项目 B 需要
requests 2.31
如果都装在系统全局,就冲突了。虚拟环境让每个项目有自己独立的依赖空间。
用 uv 的话,你在项目目录里执行 uv init 的时候它就自动创建了。所有通过 uv add 安装的包都只在这个项目里有效,不影响其他项目。
bash
# 项目 A
cd project_a
uv add "requests==2.28"
# 项目 B
cd project_b
uv add "requests==2.31"
# 互不影响
一个实际项目的结构
当你写一个稍微大点的项目时,结构大概是这样的:
my_project/
├── pyproject.toml
├── README.md
├── src/
│ └── my_project/
│ ├── __init__.py
│ ├── main.py
│ ├── utils.py
│ └── config.py
├── tests/
│ ├── __init__.py
│ └── test_main.py
└── scripts/
└── setup.sh
不需要一开始就这么复杂。小项目一个 .py 文件就够了,等项目变大了再拆分。
本章小结
- 一个
.py文件就是一个模块,用import导入 from xxx import yyy可以导入特定的函数或变量if __name__ == "__main__"判断文件是被直接运行还是被导入的- Python 标准库自带很多实用模块(os、sys、json、datetime 等)
- 用
uv add安装第三方包,uv remove卸载 - 包是包含
__init__.py的目录,用来组织多个模块 - 虚拟环境让不同项目的依赖互不影响
面试题
Q1:import module 和 from module import func 有什么区别?
点击查看答案
import module:导入整个模块,使用时需要加前缀module.func()from module import func:只导入特定对象,直接调用func()
区别:
- 命名空间 :
import module不会污染当前命名空间,from module import func直接把 func 放进来了 - 可读性 :
module.func()一眼就知道 func 来自哪个模块 - 冲突风险 :
from module import *最容易冲突,不推荐
推荐:大多数情况用 import module 或 from module import specific_name。
Q2:if __name__ == "__main__" 的作用是什么?
点击查看答案
判断当前模块是被直接运行还是被导入的。
- 直接运行脚本时,
__name__等于"__main__" - 被其他文件 import 时,
__name__等于模块名
典型用途:
- 在模块底部放测试代码,导入时不会执行
- 提供命令行入口,
python script.py时执行主逻辑 - 模块既可以当库用,也可以当脚本用
Q3:Python 的模块搜索路径是怎样的?
点击查看答案
当 import xxx 时,Python 按以下顺序搜索:
- 内置模块(如 sys、os)
- sys.path 中的目录 ,包括:
- 当前脚本所在目录
- PYTHONPATH 环境变量中的目录
- Python 安装目录下的标准库
- 第三方包安装目录(site-packages)
可以通过 sys.path 查看完整搜索路径。不建议手动修改 sys.path,用虚拟环境管理依赖更规范。
Q4:什么是循环导入?怎么解决?
点击查看答案
循环导入是指模块 A 导入模块 B,模块 B 又导入模块 A,形成循环依赖。
python
# a.py
from b import func_b
# b.py
from a import func_a
这会导致 ImportError 或部分导入(某些名字找不到)。
解决方法:
- 重构代码:把公共部分提取到第三个模块,打破循环
- 延迟导入:把 import 语句放到函数内部
- 导入模块而非名字 :用
import b代替from b import func_b
根本原因是模块设计耦合度太高,最好的方式是重构。