04-导入机制
Python 3.11+
本章用一个完整的 myshop 项目贯穿所有示例,让每种导入方式都有具体的落地场景。
概念铺垫
为什么需要了解导入机制?
问题场景
你建了一个简单的商店项目,写下这样的导入:
# services/order_service.py
# 写法 A
from myshop.core.config import Config
# 写法 B
from ..core.config import Config
# 写法 C
import config三种写法都想导入同一个 Config 类,但结果却完全不同:
- 写法 A:在任何地方运行都正常,推荐
- 写法 B:直接运行
python order_service.py会报ImportError - 写法 C:可能导入了项目根目录的另一个
config.py,而不是你想要的那个
不理解导入机制,就会出现"明明写了导入,却报找不到模块"的困惑。 本章解决这个问题。
章节导航
| 部分 | 内容 |
|---|---|
| 第一部分 | 绝对导入 — 完整路径,从项目根开始 |
| 第二部分 | 相对导入 — 点号语法,在包内跳转 |
| 第三部分 | 常见陷阱 — 三类高频错误及解法 |
| L2 实践层 | 推荐做法、反模式、常见陷阱、适用场景 |
本章项目:myshop
整个章节使用同一个项目结构,先建立清晰的全局视图:
myshop/ ← 顶层包(有 __init__.py)
├── __init__.py
├── core/ ← 核心配置子包
│ ├── __init__.py
│ └── config.py ← Config 类
├── models/ ← 数据模型子包
│ ├── __init__.py ← 统一导出 User、Product
│ ├── user.py ← User 类
│ └── product.py ← Product 类
└── services/ ← 业务逻辑子包
├── __init__.py
└── order_service.py ← OrderService,依赖 Config、User、Product
main.py ← 项目入口,位于 myshop/ 同级┌──────────────────────────────────────────────────────────────┐
│ myshop 模块依赖关系 │
├──────────────────────────────────────────────────────────────┤
│ │
│ main.py │
│ └── myshop.services.order_service │
│ ├── myshop.core.config (Config) │
│ ├── myshop.models.user (User) │
│ └── myshop.models.product (Product) │
│ │
│ myshop.models.__init__ │
│ ├── .user (User) │
│ └── .product (Product) │
│ │
└──────────────────────────────────────────────────────────────┘L1 理解层:会用
第一部分:绝对导入
1.1 概念动机
绝对导入从项目顶层包开始,写出完整的点分路径。就像写快递地址时写"北京市朝阳区XX街XX号",无论你站在哪里,地址指向的地方是唯一的。
1.2 最简示例
# main.py(位于 myshop/ 同级目录)
from myshop.core.config import Config
print(Config.APP_NAME) # MyShop运行方式(在 myshop/ 的父目录执行):
python main.py1.3 详细讲解
绝对导入路径的构成规则:
┌──────────────────────────────────────────────────────────────┐
│ 绝对导入路径拆解 │
├──────────────────────────────────────────────────────────────┤
│ │
│ from myshop . core . config import Config │
│ ──┬── ──┬── ──┬── ──┬── │
│ │ │ │ └─ 导入的名称 │
│ │ │ └──────────────── 模块文件名(.py) │
│ │ └──────────────────────── 子包目录名 │
│ └──────────────────────────────── 顶层包目录名 │
│ │
│ 对应文件:myshop/core/config.py │
│ │
└──────────────────────────────────────────────────────────────┘三种绝对导入写法的区别:
# 写法 1:导入具体名称(推荐)
from myshop.core.config import Config
config = Config() # 直接使用
# 写法 2:导入模块
from myshop.core import config
config = config.Config() # 需要加模块前缀
# 写法 3:导入顶层包(不推荐,路径太长)
import myshop.core.config
config = myshop.core.config.Config()1.4 渐进复杂化
myshop/core/config.py 的完整内容:
# myshop/core/config.py
from pathlib import Path
from typing import Final
class Config:
"""应用全局配置"""
APP_NAME: Final[str] = "MyShop"
DEBUG: Final[bool] = True
BASE_DIR: Final[Path] = Path(__file__).parent.parent # myshop/
@classmethod
def get_data_dir(cls) -> Path:
"""返回数据目录路径"""
return cls.BASE_DIR / "data"myshop/services/order_service.py 用绝对导入引用所有依赖:
# myshop/services/order_service.py
from myshop.core.config import Config
from myshop.models.user import User
from myshop.models.product import Product
class OrderService:
"""订单服务"""
def create_order(self, user: User, products: list[Product]) -> dict:
total = sum(p.price for p in products)
return {
"app": Config.APP_NAME,
"user": user.name,
"items": [p.name for p in products],
"total": total,
}1.5 实际应用
main.py 作为入口,使用绝对导入调用整个项目:
# main.py
from myshop.models.user import User
from myshop.models.product import Product
from myshop.services.order_service import OrderService
def main() -> None:
user = User(user_id=1, name="张三")
products = [
Product(product_id=101, name="笔记本", price=29.9),
Product(product_id=102, name="钢笔", price=9.9),
]
service = OrderService()
order = service.create_order(user, products)
print(order)
# {'app': 'MyShop', 'user': '张三', 'items': ['笔记本', '钢笔'], 'total': 39.8}
if __name__ == "__main__":
main()关键代码说明:
| 代码 | 含义 | 为什么这样写 |
|---|---|---|
from myshop.core.config import Config | 从顶层包 myshop 开始写完整路径 | 无论从哪个目录运行,路径含义不变 |
Path(__file__).parent.parent | 获取 config.py 向上两级的目录,即项目根 | __file__ 是当前文件路径,.parent 取父目录 |
if __name__ == "__main__" | 只在直接运行时执行 main() | 被其他模块导入时不会自动执行入口代码 |
第二部分:相对导入
2.1 概念动机
相对导入用点号(.)表示"从当前包出发"的相对位置,不用写包名。好处是:当你把整个 myshop/ 目录改名为 mystore/ 时,包内所有相对导入语句一行都不用改。
2.2 最简示例
# myshop/models/__init__.py
# 从当前包(models/)导入 user 和 product 模块
from .user import User
from .product import Product这里的 .user 表示"当前目录(models/)下的 user 模块"。
2.3 详细讲解
┌──────────────────────────────────────────────────────────────┐
│ 相对导入点号含义 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 当前文件:myshop/services/order_service.py │
│ │
│ . = myshop/services/(当前包) │
│ .. = myshop/(上级包) │
│ ..core = myshop/core/(上级包下的 core 子包) │
│ │
│ from . import helpers # myshop/services/helpers │
│ from .. import core # myshop/core │
│ from ..core.config import Config # myshop/core/config.py │
│ from ..models import User # myshop/models/User │
│ │
└──────────────────────────────────────────────────────────────┘| 符号 | 含义 | 在 services/order_service.py 中指向 |
|---|---|---|
. | 当前包 | myshop/services/ |
.. | 上级包 | myshop/ |
... | 上两级包 | myshop/ 的父目录(通常不用) |
2.4 渐进复杂化
用相对导入重写 order_service.py:
# myshop/services/order_service.py(相对导入版本)
from ..core.config import Config # .. → myshop/,再进 core/config
from ..models.user import User # .. → myshop/,再进 models/user
from ..models.product import Product # .. → myshop/,再进 models/product
class OrderService:
def create_order(self, user: User, products: list[Product]) -> dict:
total = sum(p.price for p in products)
return {
"app": Config.APP_NAME,
"user": user.name,
"items": [p.name for p in products],
"total": total,
}models/__init__.py 用相对导入聚合子模块,让外部只需 from myshop.models import User:
# myshop/models/__init__.py
from .user import User
from .product import Product
__all__ = ["User", "Product"]2.5 实际应用
相对导入和绝对导入的完整对照(同一个项目,两种写法等价):
# ─── 绝对导入(推荐用于 main.py 和跨包调用)──────────────────
from myshop.core.config import Config
from myshop.models.user import User
from myshop.models.product import Product
# ─── 相对导入(推荐用于包内部文件)──────────────────────────
# 以下代码位于 myshop/services/order_service.py
from ..core.config import Config
from ..models.user import User
from ..models.product import Product
# ─── models/__init__.py 内部聚合──────────────────────────────
# 以下代码位于 myshop/models/__init__.py
from .user import User # . 表示 models/ 自身
from .product import Product关键代码说明:
| 代码 | 含义 | 为什么这样写 |
|---|---|---|
from .user import User | 从当前包(models/)的 user 模块导入 | 包内聚合用相对导入,改包名时无需修改 |
from ..core.config import Config | 先上一级到 myshop/,再进 core/config | 跨子包访问需要先回到共同父包 |
__all__ = ["User", "Product"] | 声明 from myshop.models import * 时导出哪些名称 | 控制公开接口,隐藏内部实现细节 |
第三部分:常见导入陷阱
3.1 陷阱一:相对导入不能直接运行
现象: 直接用 python 运行包含相对导入的文件时报错。
# ❌ 直接运行包内模块
python myshop/services/order_service.py
# ImportError: attempted relative import with no known parent package原因: 直接运行时,Python 把 order_service.py 当作顶层脚本,它不属于任何包,.. 无处可跳。
┌──────────────────────────────────────────────────────────────┐
│ 直接运行 vs -m 运行 的区别 │
├──────────────────────────────────────────────────────────────┤
│ │
│ python myshop/services/order_service.py │
│ → __name__ = "__main__" │
│ → __package__ = None ← 没有包信息,相对导入失败 │
│ │
│ python -m myshop.services.order_service │
│ → __name__ = "__main__" │
│ → __package__ = "myshop.services" ← 有包信息,相对导入正常│
│ │
└──────────────────────────────────────────────────────────────┘解决方案:
# ✅ 用 -m 运行(在 myshop/ 的父目录执行)
python -m myshop.services.order_service
# ✅ 或把入口逻辑放在 main.py,用绝对导入
python main.py结论:包内模块用相对导入,但只通过 main.py 或 -m 执行,绝不直接运行包内文件。
3.2 陷阱二:循环导入
现象: user.py 和 product.py 互相引用,导入时报 ImportError 或获取到未初始化的空模块。
# ❌ 循环导入示例
# myshop/models/user.py
from myshop.models.product import Product # user 导入 product
class User:
def get_cart(self) -> list[Product]:
return []
# myshop/models/product.py
from myshop.models.user import User # product 又导入 user
class Product:
def get_owner(self) -> User:
return User(1, "")
# Python 执行顺序:
# 1. 开始导入 user.py
# 2. user.py 触发导入 product.py
# 3. product.py 触发导入 user.py —— 但 user.py 还没执行完!
# 4. User 类还不存在 → ImportError 或 AttributeError解决方案:只在方法内部做延迟导入,配合 TYPE_CHECKING 保留类型提示。
# ✅ myshop/models/user.py(修复循环导入)
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
# 这段代码只在静态类型检查时执行,运行时跳过
from myshop.models.product import Product
class User:
def __init__(self, user_id: int, name: str) -> None:
self.user_id = user_id
self.name = name
def get_cart(self) -> list[Product]:
# 延迟导入:方法被调用时才导入,此时两个模块都已加载完毕
from myshop.models.product import Product
return []# ✅ myshop/models/product.py(同样处理)
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from myshop.models.user import User
class Product:
def __init__(self, product_id: int, name: str, price: float) -> None:
self.product_id = product_id
self.name = name
self.price = price
def get_owner(self) -> User:
from myshop.models.user import User
return User(0, "unknown")关键代码说明:
| 代码 | 含义 | 为什么这样写 |
|---|---|---|
from __future__ import annotations | 让类型注解变成字符串,延迟求值 | 注解里写 Product 时,Product 类可以还未加载 |
if TYPE_CHECKING: | 此块只在 mypy/pyright 检查时执行 | 运行时跳过,避免循环导入;检查时能看到类型信息 |
方法内 from myshop.models.product import Product | 延迟到方法调用时才导入 | 此时两个模块都已完整加载,循环不存在了 |
3.3 陷阱三:模块名遮蔽标准库
现象: 自定义文件名和标准库重名,导入到错误的模块。
# ❌ 在项目根目录创建了 email.py
# email.py(自定义)
def send(to: str, msg: str) -> None:
print(f"发送给 {to}: {msg}")
# main.py
import email # 导入的是 Python 标准库的 email 模块,不是自定义的!
email.send("a@b.com", "hello") # AttributeError: module 'email' has no attribute 'send'常见容易冲突的文件名:
| 危险文件名 | 被遮蔽的标准库 |
|---|---|
email.py | email(邮件解析) |
os.py | os(操作系统接口) |
re.py | re(正则表达式) |
random.py | random(随机数) |
test.py | test(Python 内部测试包) |
types.py | types(动态类型创建) |
解决方案:给自定义模块起不与标准库冲突的名字。
# ✅ 重命名为 email_sender.py
# myshop/services/email_sender.py
def send(to: str, msg: str) -> None:
print(f"发送给 {to}: {msg}")
# main.py
from myshop.services.email_sender import send
send("a@b.com", "hello") # 正确导入L2 实践层:用好
┌──────────────────────────────────────────────────────────────┐
│ 导入方式选择指南 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 绝对导入: │
│ ✓ main.py 等顶层入口文件 │
│ ✓ 跨包调用(services → models) │
│ ✓ 不确定时默认选绝对导入 │
│ │
│ 相对导入: │
│ ✓ 包内部文件之间(同一子包内) │
│ ✓ __init__.py 聚合子模块时 │
│ │
│ 绝不直接运行包内文件,始终用 main.py 或 python -m │
│ │
└──────────────────────────────────────────────────────────────┘推荐做法
| 做法 | 原因 | myshop 中的示例 |
|---|---|---|
| 入口文件用绝对导入 | 清晰,不依赖运行位置 | main.py 中 from myshop.models.user import User |
包内 __init__.py 用相对导入聚合 | 改包名无需修改 | from .user import User |
| 跨子包访问用绝对导入 | 路径明确,IDE 跳转友好 | order_service.py 中 from myshop.core.config import Config |
循环依赖用 TYPE_CHECKING + 延迟导入 | 运行时无循环,类型检查有提示 | user.py 和 product.py 互相引用时 |
| 模块名不与标准库冲突 | 避免遮蔽内置模块 | 用 email_sender.py 而非 email.py |
__all__ 声明公开接口 | 明确哪些是公开 API | models/__init__.py 中 __all__ = ["User", "Product"] |
导入顺序规范(每组之间空一行):
# myshop/services/order_service.py
# 第一组:标准库
from __future__ import annotations
from typing import TYPE_CHECKING
# 第二组:第三方库(本项目暂无)
# import requests
# 第三组:本地包(绝对导入)
from myshop.core.config import Config
from myshop.models.user import User
from myshop.models.product import Product
# 第四组:仅类型检查时的导入
if TYPE_CHECKING:
pass # 此处放会产生循环导入的类型反模式:不要这样做
# ❌ 在顶层脚本使用相对导入
# main.py
from .models.user import User # SyntaxError 或 ImportError
# ✅ 顶层脚本永远用绝对导入
from myshop.models.user import User
# ---
# ❌ 直接运行包内模块
# python myshop/services/order_service.py → ImportError
# ✅ 用 -m 运行或通过 main.py 入口
# python -m myshop.services.order_service
# python main.py
# ---
# ❌ 用 import * 导入所有名称
from myshop.models import * # 不清楚导入了什么,污染命名空间
# ✅ 明确导入需要的名称
from myshop.models import User, Product
# ---
# ❌ 把自定义文件命名为标准库同名
# os.py、re.py、email.py ... → 遮蔽标准库
# ✅ 使用不会被冲突的名字
# file_utils.py、email_sender.py
# ---
# ❌ 在模块顶层互相导入(循环)
# user.py: from myshop.models.product import Product
# product.py: from myshop.models.user import User
# ✅ TYPE_CHECKING + 方法内延迟导入
# user.py:
# from __future__ import annotations
# from typing import TYPE_CHECKING
# if TYPE_CHECKING:
# from myshop.models.product import Product常见陷阱
| 陷阱 | 错误信息 | 解决方案 |
|---|---|---|
| 直接运行含相对导入的文件 | ImportError: attempted relative import with no known parent package | 用 python -m 包名.模块名 或通过 main.py 入口运行 |
| 循环导入 | ImportError 或 AttributeError: partially initialized module | TYPE_CHECKING + 方法内延迟导入 |
| 模块名遮蔽标准库 | AttributeError: module 'xxx' has no attribute 'yyy' | 给自定义模块起不冲突的名字 |
在 __init__.py 导入顺序错误 | ImportError: cannot import name 'X' | 把被依赖的模块放在依赖它的模块前面导入 |
| 运行目录不在 Python 路径中 | ModuleNotFoundError: No module named 'myshop' | 确保在项目根目录(myshop/ 的父目录)运行 |
适用场景
| 场景 | 推荐方式 | 原因 |
|---|---|---|
main.py 调用各子包 | ✅ 绝对导入 | 位置独立,语义清晰 |
models/__init__.py 聚合 User、Product | ✅ 相对导入(.user、.product) | 包改名无需修改 |
services/ 访问 core/ 的配置 | ✅ 绝对导入 | 跨子包,路径更清晰 |
user.py 需要引用 Product 做类型注解 | ✅ TYPE_CHECKING + from __future__ import annotations | 避免循环导入 |
| 包改名/迁移 | ✅ 包内用相对导入 | 只改顶层包名 |
| 不确定选择哪种 | ✅ 绝对导入 | 最安全,不会出错 |
L3 专家层:深入
Python 如何实现:importlib 内部架构
┌──────────────────────────────────────────────────────────────┐
│ importlib 导入系统核心架构 │
├──────────────────────────────────────────────────────────────┤
│ │
│ importlib.__import__(name) │
│ │ │
│ ├── 1. 检查 sys.modules[name](缓存) │
│ │ └── 命中 → 直接返回 │
│ │ │
│ ├── 2. 遍历 sys.meta_path(Finder 链) │
│ │ │ │
│ │ ├── BuiltinImporter │
│ │ │ └── 处理 sys.builtin_module_names 中的模块 │
│ │ │ 例如: sys, builtins, errno │
│ │ │ │
│ │ ├── FrozenImporter │
│ │ │ └── 处理冻结模块(_frozen_importlib 自身) │
│ │ │ │
│ │ └── PathFinder │
│ │ │ │
│ │ ├── 遍历 sys.path_hooks │
│ │ │ ├── zipimport.zipimporter │
│ │ │ │ └── 处理 .zip 文件中的模块 │
│ │ │ └── FileFinder │
│ │ │ └── 处理文件系统中的模块 │
│ │ │ │
│ │ └── 对 sys.path 每个目录: │
│ │ ├── 查找 <dir>/<name>.py │
│ │ ├── 查找 <dir>/<name>/__init__.py (包) │
│ │ └── 查找 <dir>/<name>.pyc │
│ │ │
│ ├── 3. Finder.find_spec(name) → ModuleSpec │
│ │ ModuleSpec 包含: │
│ │ - name: 模块名 │
│ │ - loader: Loader 对象 │
│ │ - origin: 源码路径 │
│ │ - submodule_search_locations: 子模块搜索路径 │
│ │ │
│ ├── 4. Loader.create_module(spec) → 创建空模块对象 │
│ │ │
│ ├── 5. Loader.exec_module(module) │
│ │ ├── 编译源码为 CodeObject │
│ │ └── exec(code, module.__dict__) │
│ │ │
│ └── 6. sys.modules[name] = module(写入缓存) │
│ │
└──────────────────────────────────────────────────────────────┘验证 Finder / Loader 链:
import sys
import importlib
import importlib.abc
# 1. 查看 sys.meta_path 上的所有 Finder
print("=== sys.meta_path ===")
for i, finder in enumerate(sys.meta_path):
print(f"[{i}] {type(finder).__module__}.{type(finder).__qualname__}")
# 输出示例:
# [0] _frozen_importlib.BuiltinImporter
# [1] _frozen_importlib.FrozenImporter
# [2] _frozen_importlib_external.PathFinder
# 2. 查看 PathFinder 内部的 path_hooks
print("\n=== sys.path_hooks ===")
for i, hook in enumerate(sys.path_hooks):
print(f"[{i}] {hook}")
# 输出示例:
# [0] <class 'zipimport.zipimporter'>
# [1] <function FileFinder.path_hook.<locals>.path_hook_for_FileFinder at ...>
# 3. 查找指定模块的 spec
for name in ["sys", "json", "math"]:
spec = importlib.util.find_spec(name)
if spec:
print(f"\n=== {name} Spec ===")
print(f" loader: {type(spec.loader).__qualname__}")
print(f" origin: {spec.origin}")
print(f" submodule_search_locations: {spec.submodule_search_locations}")
print(f" has_location: {spec.has_location}")自定义导入钩子
可以利用 sys.meta_path 和 sys.path_hooks 添加自定义导入逻辑:
import sys
import importlib.abc
import importlib.machinery
class DebugFinder(importlib.abc.MetaPathFinder):
"""在控制台打印每个 import 请求的调试 Finder"""
def find_spec(self, fullname, path, target=None):
print(f"[DebugFinder] 查找模块: {fullname!r}, path={path}")
return None # 返回 None 表示不处理,交由下一个 Finder
# 注册自定义 Finder
sys.meta_path.insert(0, DebugFinder())
# 测试:每次 import 都会触发打印
import math # 输出:[DebugFinder] 查找模块: 'math', path=None
import json # 输出:[DebugFinder] 查找模块: 'json', path=None
# 查看导入后的 sys.modules
print(f"\nmath 在 sys.modules 中: {'math' in sys.modules}")
print(f"sys.modules 总数: {len(sys.modules)}")自定义 URL 导入器示例
import sys
import importlib.abc
import importlib.machinery
import types
class StringLoader(importlib.abc.Loader):
"""从字符串加载模块的 Loader"""
def __init__(self, code: str):
self.code = code
def exec_module(self, module: types.ModuleType) -> None:
exec(self.code, module.__dict__)
class StringFinder(importlib.abc.MetaPathFinder):
"""查找以 'virtual_' 开头的模块,从预定义字符串提供"""
_modules = {
"virtual_config": """
SECRET_KEY = "abc123"
DEBUG = True
""",
}
def find_spec(self, fullname, path, target=None):
if fullname in self._modules:
loader = StringLoader(self._modules[fullname])
return importlib.machinery.ModuleSpec(
fullname, loader, origin=f"<string:{fullname}>"
)
return None
# 注册并测试
sys.meta_path.insert(0, StringFinder())
import virtual_config
print(virtual_config.SECRET_KEY) # abc123
print(virtual_config.DEBUG) # True性能考量
| 操作 | 复杂度 | 说明 |
|---|---|---|
| 导入已缓存模块 | O(1) 哈希查找 | sys.modules 是普通 dict,查找极快 |
| PathFinder 查找模块 | O(p) 最坏 | p = sys.path 条目数,找到即停 |
| 首次编译源码 | O(源码大小) | 解析 + 编译为字节码,写入 .pyc |
| 执行模块代码 | O(模块复杂度) | 取决于顶层代码量 |
| 相对导入解析 | O(1) | 点号计数 → 确定跳转层级 |
TYPE_CHECKING 跳过 | O(0) | 运行时完全跳过 if 块 |
| 方法内延迟导入 | O(1) 首次检查 + O(导入) | 首次调用时触发导入 |
知识关联
┌──────────────────────────────────────────────────────────────┐
│ 导入机制 知识关联图 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 本章(导入机制) │
│ │ │
│ ├── 绝对导入 ───────────► 03-包的结构(包的组织) │
│ │ │
│ ├── 相对导入 ───────────► 03-包的结构(__init__.py) │
│ │ │
│ ├── sys.meta_path ──────► 01-模块基础(import 流程) │
│ │ │
│ ├── Finder / Loader ────► 01-模块基础(sys.modules) │
│ │ │
│ ├── 循环导入解决 ───────► 02-自定义模块(懒加载) │
│ │ │
│ ├── -m 运行机制 ────────► 01-模块基础(__name__) │
│ │ │
│ └── 自定义 Import Hook ─► 进阶:插件系统、虚拟文件系统 │
│ │
└──────────────────────────────────────────────────────────────┘本章小结
┌──────────────────────────────────────────────────────────────┐
│ 导入机制 知识要点 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 绝对导入:from myshop.core.config import Config │
│ ✓ 从顶层包名开始写完整路径 │
│ ✓ 任何地方运行都正确,IDE 支持好 │
│ ✓ 入口文件和跨包调用的首选 │
│ │
│ 相对导入:from ..core.config import Config │
│ ✓ 点号表示相对当前包的位置 │
│ ✓ 只能在包内部使用,不能在顶层脚本用 │
│ ✓ 包内聚合和同包模块互引时使用 │
│ │
│ 三类常见陷阱: │
│ ✓ 直接运行包内文件 → 改用 -m 或 main.py │
│ ✓ 循环导入 → TYPE_CHECKING + 延迟导入 │
│ ✓ 模块名遮蔽标准库 → 避免与内置模块同名 │
│ │
└──────────────────────────────────────────────────────────────┘