到现在为止我们所有代码都写在一个 .py 文件里。真实项目怎么可能这么单调?一个中等规模项目动辄几十上百个 .py 文件,怎么组织、怎么导入、怎么发布?答案就是模块(module) 和包(package)。这一篇要把 import 的所有姿势、__init__.py、__name__ 是什么、相对导入 vs 绝对导入、循环导入等问题一次讲清楚。
一、什么是模块
一个 .py 文件就是一个模块。模块是 Python 组织代码的最小单位。
1 | # math_utils.py |
在另一个文件里用它:
1 | # main.py |
只要在同一个目录(或 PYTHONPATH 里),import 就找得到。
二、import 的四种姿势
1 | # 1. 导入整个模块 |
推荐用 import 模块 或 from 模块 import 具体名字。避免 from x import *——它会把 x 里所有公开名字塞进当前命名空间,容易冲突且不好追踪。
三、模块的搜索路径
import mymodule 时 Python 按顺序找:
- 当前脚本所在目录;
- PYTHONPATH 环境变量里的目录;
- 标准库目录;
- site-packages(pip 安装的第三方包)。
查看当前搜索路径:
1 | import sys |
想加自定义路径:
1 | sys.path.append("/path/to/dir") # 临时加 |
不过更好的做法是把代码组织成”包”或者做 pip install -e .(下面讲)。
四、什么是包
一个包含 __init__.py 的文件夹就是一个包。包可以嵌套模块和子包:
1 | myproject/ |
在 main.py 里:
1 | import mypkg.utils |
__init__.py 的作用:
- 告诉 Python “这是一个包”(Python 3.3+ 不严格要求,但强烈建议保留);
- 可以在里面写”包被导入时执行的代码”;
- 可以定义
__all__控制from pkg import *的行为; - 可以在里面”提升”子模块的名字,方便使用者:
1 | # mypkg/__init__.py |
五、绝对导入 vs 相对导入
在包内部导入其他模块,两种写法:
1 | # mypkg/sub/helper.py |
.:当前包;..:上一级包;...:上上级包(一般用不到)。
PEP 8 建议:绝对导入优先。相对导入只在包内部用,且路径较深时可以简化。
注意:相对导入只能在包内部使用,直接 python helper.py 会报错。得用 python -m mypkg.sub.helper。
六、__name__ 与 if __name__ == "__main__":
每个模块都有一个 __name__ 属性:
- 当模块被直接执行(
python xxx.py)时,__name__ == "__main__"; - 当模块被 import 时,
__name__是模块名(如"mypkg.utils")。
利用这个特性可以让一个文件”既是模块又是脚本”:
1 | # math_utils.py |
python math_utils.py→ 运行_demo();import math_utils→ 只导入函数,不运行_demo。
这是 Python 最重要的惯用法之一,每个脚本都建议加这行。
七、导入时”执行”vs “定义”
一个模块被 import 时,Python 会从头到尾执行一遍:
1 | # mymod.py |
1 | import mymod # 打印 "模块被加载!" |
- 第一次 import 会执行整个模块;
- 之后的 import 直接用缓存(存在
sys.modules); - 想强制重新加载:
importlib.reload(mymod)(调试用,生产别用)。
推论:模块顶层不要写”重活”(比如连数据库、下载文件)——那会拖慢所有 import 它的地方。把重活放在函数里,被调用时才做。
八、__all__:控制 from x import *
在模块顶层写 __all__ = ["公开名字1", "公开名字2"]:
1 | # utils.py |
from utils import * 只会导入 add 和 multiply,_internal 不会。
惯例:下划线开头的名字(_helper)表示”私有”,就算没写 __all__,from x import * 也不会导入它们。
九、常见标准库模块
Python 标准库巨大,日常写代码经常用到的:
| 模块 | 用途 |
|---|---|
os, sys, pathlib |
系统、路径、文件 |
re |
正则表达式 |
datetime, time |
日期时间 |
json, csv |
数据格式 |
collections |
高级数据结构(deque, Counter 等) |
itertools, functools |
迭代与函数工具 |
math, random, statistics |
数学与随机 |
logging |
日志 |
subprocess |
调用外部命令 |
threading, multiprocessing, asyncio |
并发 |
urllib, http |
网络(更常用的是第三方 requests) |
argparse |
命令行参数 |
unittest |
测试(更常用 pytest) |
十、循环导入
循环导入是新手项目结构乱的时候常见的错误:
1 | # a.py |
Python 加载时会陷入循环,可能报 ImportError。
解决办法:
- 重构代码——把共享的东西抽到第 3 个模块
common.py,让 a 和 b 都import common; - 函数内部导入(延迟到调用时才 import):
1 | def bar(): |
- 只 import 模块,不 from … import 具体名字——通常能规避。
十一、包的组织结构
一个规范的项目结构大致:
1 | myproject/ |
十二、pip install -e .:开发模式安装
写好一个包后,想在别的项目里 import 它,可以:
- 复制目录——粗暴不推荐;
pip install .—— 装到 site-packages,修改源码后要重装;pip install -e .—— 开发模式,改源码立即生效,最常用。
需要在项目根目录准备一个 pyproject.toml:
1 | [project] |
然后:
1 | pip install -e . |
十三、常见 import 陷阱
陷阱 1:文件名与标准库冲突
1 | my_project/ |
import math 在 main.py 里可能会拿到你的 math.py。别用标准库同名的文件名(math、json、time、random、os 等)。
陷阱 2:直接跑相对导入的文件
1 | mypkg/ |
python mypkg/sub.py 会报 ImportError: attempted relative import。用 python -m mypkg.sub 才对。
陷阱 3:sys.path 里没有项目根
在 IDE 里没问题,命令行下报 ModuleNotFoundError。解决:
- 在项目根目录
python -m myproject.main; - 或者用
pip install -e .; - 别硬改
sys.path。
陷阱 4:__init__.py 里 import 太多
1 | # __init__.py |
包被 import 时,__init__.py 会立刻执行。控制它的开销,不要 import 昂贵的东西。
陷阱 5:绝对/相对导入混用
同一文件里既有 from mypkg.x import ... 又有 from .x import ...,容易搞混。建议一个包内统一使用一种风格。
十四、一个完整例子:小型 CLI 包
1 | todocli/ |
storage.py:
1 | # src/todocli/storage.py |
cli.py:
1 | # src/todocli/cli.py |
init.py:
1 | # src/todocli/__init__.py |
安装:pip install -e .,运行:python -m todocli.cli add 学 Python。
十五、小结与延伸阅读
- 一个
.py是模块,含__init__.py的文件夹是包; - import 有绝对、相对两种,包内优先绝对;
if __name__ == "__main__":让文件既是模块又是脚本;- 模块顶层代码只在第一次 import 时执行;
- 循环导入靠”重构 + 延迟导入”解;
- 用
pip install -e .做本地开发; - 避免文件名与标准库冲突。
延伸阅读:
- 官方模块指南:https://docs.python.org/zh-cn/3/tutorial/modules.html
- Python 打包用户指南:https://packaging.python.org/
下一篇 pip 与虚拟环境 venv 我们讲怎么装第三方库和隔离项目依赖。