import是 Python 程序的第一行代码,也是出问题的重灾区。ModuleNotFoundError、循环导入、相对路径报错……本文从 Python 的导入系统原理讲起,帮你彻底解决这些问题。
目录
- import 语句的工作原理
- 模块搜索路径:sys.path
- 绝对导入与相对导入
- 包与 init.py
- 循环导入与解决方案
- 从本地安装到私有 PyPI
- 现代打包与发布(PEP 517 / PEP 660)
- 常见问题排查
1. import 语句的工作原理
import foo.bar
│
├── 1. 检查 sys.modules 中是否已加载
│ 是 → 直接返回缓存的模块对象
│
├── 2. 在 sys.path 中搜索 foo/bar.py
│ → 找到后创建 module 对象
│
├── 3. 执行 bar.py 的顶层代码
│ → 变量 → bar.__dict__
│
└── 4. 将模块存入 sys.modules 缓存
import sys
# 查看已加载的模块
print(len(sys.modules)) # 通常 200+
print('json' in sys.modules) # True(已加载)
# 模块的缓存机制
import json
print(id(json)) # 对象地址
import json as j
print(id(j)) # 同一个对象!
重新加载模块(开发调试)
import importlib
import mymodule
# 修改 mymodule.py 后重新加载
importlib.reload(mymodule)
2. 模块搜索路径:sys.path
import sys
for p in sys.path:
print(p)
# 典型输出:
# '' ← 当前目录
# '/usr/local/lib/python311' ← 标准库
# '/usr/local/lib/python311/site-packages' ← 第三方包
修改搜索路径
# 方式 1:临时添加(当前会话有效)
import sys
sys.path.insert(0, "/path/to/your/modules")
# 方式 2:PYTHONPATH 环境变量
# PYTHONPATH=/path/to/modules python script.py
# 方式 3:.pth 文件(推荐)
# 在 site-packages 目录创建 mypaths.pth
# 内容:/path/to/your/modules
# 方式 4:sitecustomize.py
import site
site.addsitedir("/path/to/modules")
3. 绝对导入与相对导入
绝对导入(推荐)
# 项目结构:
# myproject/
# ├── app/
# │ ├── __init__.py
# │ ├── models.py
# │ └── utils.py
# └── tests/
# app/models.py
from app.utils import helper # 绝对导入
import app.utils # 另一种写法
相对导入
# app/models.py
from .utils import helper # 同级目录
from . import utils # 导入同级包
from ..config import settings # 上级目录
from .auth.password import hash_password # 子目录
# 注意:相对导入只能在包内使用,不能直接运行!
# python app/models.py # ❌ ModuleNotFoundError
# python -m app.models # ✅ 作为模块运行
4. 包与 init.py
4.1 经典包(Python 3.2 及之前需要)
# mypackage/__init__.py
# 控制 from mypackage import * 的行为
__all__ = ['module1', 'module2']
# 包级别的初始化代码
print("mypackage 被加载")
# 简化导入路径
from .module1 import MyClass
from .module2 import helper
# 用户可以直接 from mypackage import MyClass
4.2 Namespace Package(Python 3.3+)
不需要 __init__.py 的包:
# 分布式子包可以放在不同位置
/path1/mynamespace/subpkg1/
/path2/mynamespace/subpkg2/
import mynamespace.subpkg1
import mynamespace.subpkg2
5. 循环导入与解决方案
问题复现
# a.py
from b import func_b
def func_a():
return "A"
# b.py
from a import func_a
def func_b():
return "B"
# 运行:python a.py
# → ImportError: cannot import name 'func_a' from partially initialized module 'a'
解决方案
方案 1:重构代码,提取公共模块
# common.py
def func_a():
return "A"
def func_b():
return "B"
# a.py
from common import func_a, func_b
方案 2:延迟导入(函数内导入)
# a.py
def func_a():
from b import func_b # 函数调用时才导入
return f"A calls {func_b()}"
方案 3:使用 TYPE_CHECKING
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .b import B # 只在类型检查时导入
class A:
def method(self, b: "B"): # 字符串前向引用
pass
6. 从本地安装到私有 PyPI
本地可编辑安装
# 在 pyproject.toml 所在目录
pip install -e . # 可编辑安装(修改代码立即生效)
pip install -e ".[dev]" # 带 dev 依赖
私有 PyPI 仓库
# 使用 devpi 搭建私有 PyPI
pip install devpi-server
devpi-init
devpi-server --start
# 上传包
devpi upload
# 安装时指定索引
pip install --index-url http://your-devpi/simple mypackage
7. 现代打包与发布(PEP 517 / PEP 660)
Python 打包已从 setup.py 演进至声明式的 pyproject.toml。了解构建后端(build backend)的区别,是发布高质量包的基础。
7.1 pyproject.toml 核心配置
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "myproject"
dynamic = ["version"]
description = "A short description"
readme = "README.md"
requires-python = ">=3.9"
license = {text = "MIT"}
authors = [
{name = "Alice", email = "alice@example.com"}
]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
]
dependencies = [
"requests>=2.28.0",
"pydantic>=2.0",
]
[project.optional-dependencies]
dev = ["pytest", "black", "mypy"]
docs = ["mkdocs-material"]
[project.scripts]
mycli = "myproject.cli:main"
| 构建后端 | 特点 | 适用场景 |
|---|---|---|
| setuptools | 生态最成熟,兼容性强 | 传统项目、C/C++ 扩展 |
| hatchling | Hatch 默认后端,版本管理灵活 | 现代纯 Python 项目 |
| flit | 极简,纯 Python 专用 | 小型库、教学项目 |
| pdm-backend | PEP 621 原生,editable 稳定 | 追求规范的团队 |
| maturin | Rust 扩展专用 | PyO3/Rust 绑定 |
| scikit-build | CMake C++ 扩展 | 科学计算、图像处理 |
7.2 PEP 660 Editable Install 原理
pip install -e . 在 PEP 660 之前的旧实现是通过 .pth 文件指向源码目录。PEP 660 则要求构建后端在可编辑模式下安装一个指向源码的轻量包装器,解决旧方式下 __file__ 指向源码而非 site-packages 的问题。
# 可编辑安装(现代方式)
pip install -e .
# 带 dev 依赖的完整开发环境
pip install -e ".[dev,test]"
# 使用 pdm(无需虚拟环境切换)
pdm install --dev
7.3 使用 importlib.metadata 动态发现包信息
from importlib.metadata import version, entry_points, requires
# 获取已安装包的版本
print(version("requests")) # 2.31.0
# 列出项目的依赖
for req in requires("requests"):
print(req)
# 遍历所有入口点(console_scripts)
eps = entry_points()
for ep in eps.select(group="console_scripts"):
print(f"{ep.name} = {ep.value}")
# 获取包的文件列表
from importlib.metadata import files
for file in files("requests"):
print(file)
8. 常见问题排查
ModuleNotFoundError
# 1. 检查包是否安装
pip list | grep mypackage
# 2. 检查 Python 路径
python -c "import sys; print(sys.path)"
# 3. 检查模块名拼写
# 4. 检查是否在虚拟环境中
which python
relative import beyond top-level package
# 错误原因:直接运行了包内的文件
python app/models.py # ❌
# 正确做法
python -m app.models # ✅
# 或者从项目根目录运行
python -m myproject.app.models
延伸阅读
- Python 极简入门教程 —— 函数与模块基础
- pyproject.toml 配置完全手册 —— 打包配置
- Python 现代工具链 —— uv + pip 依赖管理
- Python 最佳实践:代码组织与工程结构 —— 大型项目结构
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。