5.2 包与项目结构
前置:5.1 · 约 30 分钟 · 官方对应:包、打包 Python 项目
目标
- 理解包(package)是包含模块的目录,
__init__.py在导入包时执行。 - 读懂绝对导入与相对导入(
from . import x、from ..utils import y)。 - 理解 SDK 常见的“在
__init__.py中重新导出”模式,例如anthropic.Anthropic。 - 知道
python -m 包.模块的作用,识别循环导入。
核心概念
包的结构
agentkit/ ← 包(目录)
├── __init__.py ← 导入 agentkit 时执行;定义包的公开接口
├── tools.py ← 子模块 agentkit.tools
└── loop.py ← 子模块 agentkit.loop下面的示例在临时目录中创建这个包并使用它:
import importlib
from pathlib import Path
pkg = Path("agentkit")
pkg.mkdir()
(pkg / "tools.py").write_text('''
def get_weather(city):
return f"{city}:晴,22°C"
TOOL_FUNCTIONS = {"get_weather": get_weather}
''', encoding="utf-8")
(pkg / "loop.py").write_text('''
from .tools import TOOL_FUNCTIONS # 相对导入:同一包内的 tools 模块
def run_tool(name, tool_input):
return TOOL_FUNCTIONS[name](**tool_input)
''', encoding="utf-8")
(pkg / "__init__.py").write_text('''
"""agentkit:演示用的小包。"""
from .loop import run_tool # 重新导出:用户可直接 from agentkit import run_tool
from .tools import get_weather
__all__ = ["run_tool", "get_weather"]
__version__ = "0.1.0"
''', encoding="utf-8")
importlib.invalidate_caches()
import agentkit
from agentkit.tools import TOOL_FUNCTIONS # 绝对导入子模块
print(agentkit.run_tool("get_weather", {"city": "上海"}))
print(agentkit.__version__, list(TOOL_FUNCTIONS))
print(agentkit.loop.run_tool is agentkit.run_tool) # 同一个函数对象,只是多了一个入口绝对导入与相对导入
| 写法 | 含义 | 使用场景 |
|---|---|---|
from agentkit.tools import x | 绝对导入:从 sys.path 根开始 | 应用代码、测试代码 |
from .tools import x | 相对导入:当前包内的 tools | 包内部互相引用(SDK 源码大量使用) |
from ..utils import y | 上一级包中的 utils | 深层子包 |
相对导入只能在包内的模块中使用;直接 python loop.py 运行包内文件会报 ImportError: attempted relative import with no known parent package。这时应在项目根目录用 python -m agentkit.loop。
__init__.py 与重新导出
SDK 通常把实现放在深层私有模块里,再在顶层 __init__.py 中导出常用名字。以 anthropic 为例(结构示意):
# anthropic/__init__.py(示意)
from ._client import Anthropic, AsyncAnthropic
from ._exceptions import APIError, RateLimitError, AuthenticationError
from . import types因此用户写 anthropic.Anthropic(...) 即可,不必知道它定义在 anthropic/_client.py。读 SDK 源码时,从 __init__.py 的导入语句找到名字真正定义的文件。_ 开头的模块是内部实现,不承诺稳定。
python -m
python -m 模块名 以模块方式运行:会把当前目录加入 sys.path,并支持包内相对导入。标准库中很多工具都这样用:
import importlib
import runpy
from pathlib import Path
Path("demo_pkg").mkdir()
Path("demo_pkg/__init__.py").write_text("", encoding="utf-8")
Path("demo_pkg/helpers.py").write_text("def hello():\n return 'hello from helpers'\n", encoding="utf-8")
Path("demo_pkg/cli.py").write_text(
"from .helpers import hello\n"
"if __name__ == '__main__':\n"
" print('cli 以 __main__ 运行:', hello())\n",
encoding="utf-8",
)
importlib.invalidate_caches()
runpy.run_module("demo_pkg.cli", run_name="__main__") # 等价于命令行:python -m demo_pkg.cli
# 常见用法(命令行):uv run python -m json.tool data.json / python -m http.server 8080 / python -m pytest项目布局
| 布局 | 结构 | 说明 |
|---|---|---|
| 脚本式 | examples/agent/minimal_agent.py | 本项目当前形式:独立脚本 + uv run |
| flat | mypkg/ 与 pyproject.toml 同级 | 简单,但可能误导入未安装的源码 |
| src | src/mypkg/ | uv init --package 默认采用;packaging.python.org 教程使用此布局;测试时必须先安装 |
易错点
- 循环导入:
a.py导入b.py,b.py又在顶层导入a.py中的名字,第二个模块执行时第一个模块还没定义完,导致ImportError: cannot import name … (most likely due to a circular import)。解决:抽出公共模块,或把导入移到函数内部。 - 包内文件直接运行导致相对导入失败:用
python -m。 - 忘记
__init__.py:没有它的目录是“命名空间包”,可以导入但行为不同;常规项目应保留__init__.py。
import importlib
from pathlib import Path
Path("a.py").write_text("from b import B\nA = 'a'\n", encoding="utf-8")
Path("b.py").write_text("from a import A\nB = 'b'\n", encoding="utf-8")
importlib.invalidate_caches()
import a对照
| 概念 | Python | JS/TS | Java |
|---|---|---|---|
| 包 | 含 __init__.py 的目录 | 含 package.json 的目录 / 文件夹 | 目录 + package 声明 |
| 包入口 | __init__.py | index.js / exports 字段 | 无 |
| 相对导入 | from .x import y | import { y } from "./x" | 无(同包直接可见) |
| 公开接口 | __init__.py 重新导出 + __all__ | index.ts 重新导出 | public |
| 内部模块 | _private.py(约定) | 不导出 | 包私有 |
读代码
追踪 anthropic.Anthropic 的定义位置(在本地项目中执行):
import anthropic
import inspect
print(anthropic.__file__) # .venv/.../site-packages/anthropic/__init__.py
print(anthropic.Anthropic.__module__) # 真正定义所在的模块
print(inspect.getsourcefile(anthropic.Anthropic))命令行:uv run python -c "import anthropic, inspect; print(inspect.getsourcefile(anthropic.Anthropic))"。打开输出的文件,就能看到 class Anthropic 的实现。10.4 会完整走一遍这个过程。
练习
练习 1:创建并使用包
补全 make_package():在当前目录创建包 mathkit,包含 ops.py(定义 add(a, b) 返回和)和 __init__.py(用相对导入从 .ops 重新导出 add)。检查会执行 from mathkit import add。
from pathlib import Path
def make_package():
pkg = Path("mathkit")
pkg.mkdir(exist_ok=True)
(pkg / "ops.py").write_text("def add(a, b):\n return a + b\n", encoding="utf-8")
(pkg / "__init__.py").write_text("", encoding="utf-8")
练习 2:定位定义模块
实现 defined_in(obj):返回对象真正定义所在的模块名(提示:函数和类都有 __module__ 属性)。
def defined_in(obj):
return obj.__name__
小结
- 包是带
__init__.py的目录;__init__.py常用于重新导出公开接口。 - 包内用相对导入,应用代码用绝对导入;包内文件用
python -m运行。 - 读 SDK:顺着
__init__.py的导入找到真正的定义文件;__module__、inspect.getsourcefile帮你定位。 - 循环导入的根源是模块顶层互相依赖。