到现在为止我们所有代码都写在一个 .py 文件里。真实项目怎么可能这么单调?一个中等规模项目动辄几十上百个 .py 文件,怎么组织、怎么导入、怎么发布?答案就是模块(module) 和包(package)。这一篇要把 import 的所有姿势、__init__.py、__name__ 是什么、相对导入 vs 绝对导入、循环导入等问题一次讲清楚。
一、什么是模块
一个 .py 文件就是一个模块。模块是 Python 组织代码的最小单位。
1 2 3 4 5 6 7 8
| PI = 3.14159
def add(a, b): return a + b
def multiply(a, b): return a * b
|
在另一个文件里用它:
1 2 3 4 5
| import math_utils
print(math_utils.PI) print(math_utils.add(3, 4))
|
只要在同一个目录(或 PYTHONPATH 里),import 就找得到。
二、import 的四种姿势
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import math math.sqrt(16)
import numpy as np np.array([1, 2])
from math import sqrt, pi sqrt(16) pi
from math import sqrt as square_root square_root(16)
from math import * sqrt(16)
|
推荐用 import 模块 或 from 模块 import 具体名字。避免 from x import *——它会把 x 里所有公开名字塞进当前命名空间,容易冲突且不好追踪。
三、模块的搜索路径
import mymodule 时 Python 按顺序找:
- 当前脚本所在目录;
- PYTHONPATH 环境变量里的目录;
- 标准库目录;
- site-packages(pip 安装的第三方包)。
查看当前搜索路径:
1 2
| import sys print(sys.path)
|
想加自定义路径:
1
| sys.path.append("/path/to/dir")
|
不过更好的做法是把代码组织成”包”或者做 pip install -e .(下面讲)。
四、什么是包
一个包含 __init__.py 的文件夹就是一个包。包可以嵌套模块和子包:
1 2 3 4 5 6 7 8 9
| myproject/ ├── main.py └── mypkg/ ├── __init__.py ├── utils.py ├── models.py └── sub/ ├── __init__.py └── helper.py
|
在 main.py 里:
1 2 3
| import mypkg.utils from mypkg.models import User from mypkg.sub.helper import format_
|
__init__.py 的作用:
- 告诉 Python “这是一个包”(Python 3.3+ 不严格要求,但强烈建议保留);
- 可以在里面写”包被导入时执行的代码”;
- 可以定义
__all__ 控制 from pkg import * 的行为;
- 可以在里面”提升”子模块的名字,方便使用者:
1 2 3 4 5 6
| from .utils import add from .models import User
|
五、绝对导入 vs 相对导入
在包内部导入其他模块,两种写法:
1 2 3 4 5 6 7 8
|
from mypkg.utils import add
from ..utils import add from . import buddy
|
.:当前包;
..:上一级包;
...:上上级包(一般用不到)。
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 2 3 4 5 6 7 8 9
| def add(a, b): return a + b
def _demo(): print(add(3, 5))
if __name__ == "__main__": _demo()
|
python math_utils.py → 运行 _demo();
import math_utils → 只导入函数,不运行 _demo。
这是 Python 最重要的惯用法之一,每个脚本都建议加这行。
七、导入时”执行”vs “定义”
一个模块被 import 时,Python 会从头到尾执行一遍:
1 2 3 4 5
| print("模块被加载!") x = 42 def f(): print("f 被调用")
|
1 2
| import mymod import mymod
|
- 第一次 import 会执行整个模块;
- 之后的 import 直接用缓存(存在
sys.modules);
- 想强制重新加载:
importlib.reload(mymod)(调试用,生产别用)。
推论:模块顶层不要写”重活”(比如连数据库、下载文件)——那会拖慢所有 import 它的地方。把重活放在函数里,被调用时才做。
八、__all__:控制 from x import *
在模块顶层写 __all__ = ["公开名字1", "公开名字2"]:
1 2 3 4 5 6
| __all__ = ["add", "multiply"]
def add(a, b): return a + b def multiply(a, b): return a * b def _internal(): pass
|
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 2 3 4 5 6 7
| import b def foo(): b.bar()
import a def bar(): a.foo()
|
Python 加载时会陷入循环,可能报 ImportError。
解决办法:
- 重构代码——把共享的东西抽到第 3 个模块
common.py,让 a 和 b 都 import common;
- 函数内部导入(延迟到调用时才 import):
1 2 3
| def bar(): from a import foo foo()
|
- 只 import 模块,不 from … import 具体名字——通常能规避。
十一、包的组织结构
一个规范的项目结构大致:
1 2 3 4 5 6 7 8 9 10 11 12 13
| myproject/ ├── pyproject.toml # 项目配置 ├── README.md ├── src/ # 源代码 │ └── mypkg/ │ ├── __init__.py │ ├── models.py │ ├── utils.py │ └── cli.py # 命令行入口 ├── tests/ # 测试 │ ├── test_models.py │ └── test_utils.py └── requirements.txt # 依赖
|
十二、pip install -e .:开发模式安装
写好一个包后,想在别的项目里 import 它,可以:
- 复制目录——粗暴不推荐;
pip install . —— 装到 site-packages,修改源码后要重装;
pip install -e . —— 开发模式,改源码立即生效,最常用。
需要在项目根目录准备一个 pyproject.toml:
1 2 3 4 5 6 7
| [project] name = "mypkg" version = "0.1.0"
[build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta"
|
然后:
十三、常见 import 陷阱
陷阱 1:文件名与标准库冲突
1 2 3
| my_project/ ├── math.py # ❌ 遮蔽了标准库的 math └── main.py
|
import math 在 main.py 里可能会拿到你的 math.py。别用标准库同名的文件名(math、json、time、random、os 等)。
陷阱 2:直接跑相对导入的文件
1 2 3
| mypkg/ ├── __init__.py └── sub.py # 里面有 from . import xxx
|
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 2
| from .heavy_module import ...
|
包被 import 时,__init__.py 会立刻执行。控制它的开销,不要 import 昂贵的东西。
陷阱 5:绝对/相对导入混用
同一文件里既有 from mypkg.x import ... 又有 from .x import ...,容易搞混。建议一个包内统一使用一种风格。
十四、一个完整例子:小型 CLI 包
1 2 3 4 5 6 7 8 9
| todocli/ ├── pyproject.toml ├── src/ │ └── todocli/ │ ├── __init__.py │ ├── storage.py │ └── cli.py └── tests/ └── test_storage.py
|
storage.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| """持久化 TODO 列表到 JSON 文件。""" import json from pathlib import Path
DEFAULT_FILE = Path.home() / ".todocli.json"
def load(path: Path = DEFAULT_FILE) -> list[str]: if not path.exists(): return [] return json.loads(path.read_text(encoding="utf-8"))
def save(items: list[str], path: Path = DEFAULT_FILE) -> None: path.write_text(json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8")
|
cli.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| """命令行入口。""" import sys from . import storage
def main(): items = storage.load() if len(sys.argv) < 2: for i, t in enumerate(items, 1): print(f"{i}. {t}") return
cmd = sys.argv[1] if cmd == "add": items.append(" ".join(sys.argv[2:])) storage.save(items) elif cmd == "done": idx = int(sys.argv[2]) - 1 items.pop(idx) storage.save(items)
if __name__ == "__main__": main()
|
init.py:
1 2 3 4 5
| from .storage import load, save from .cli import main
__all__ = ["load", "save", "main"]
|
安装:pip install -e .,运行:python -m todocli.cli add 学 Python。
十五、小结与延伸阅读
- 一个
.py 是模块,含 __init__.py 的文件夹是包;
- import 有绝对、相对两种,包内优先绝对;
if __name__ == "__main__": 让文件既是模块又是脚本;
- 模块顶层代码只在第一次 import 时执行;
- 循环导入靠”重构 + 延迟导入”解;
- 用
pip install -e . 做本地开发;
- 避免文件名与标准库冲突。
延伸阅读:
下一篇 pip 与虚拟环境 venv 我们讲怎么装第三方库和隔离项目依赖。