Python · import 机制与模块系统

一行 import 的真面目: sys.modules 缓存 → sys.path 搜索 → 字节码编译 → 模块对象绑定 — 模块是天然单例, 导入即执行

执行到这条语句 缓存未命中 ↓ 定位到 a/b.py 字节码就绪 命中 → 跳过 ②③④, 直接返回模块对象 ① import b ② 反向 import a → 命中半初始化的 A import a.b — 一行语句, 一套流水线 词法阶段只登记模块名; 执行到这条语句才真正开始加载 from a.b import x 多一步: 从模块对象上取属性 x 绑定到本地 ① 查 sys.modules 缓存 (key = 'a.b') 命中 → 直接返回缓存里的模块对象, ②③④ 全部跳过 import 幂等性的来源: 同一模块的顶层代码只执行一次 monkey patch 改的就是缓存里这个对象 — 补丁必须打在使用之前 ② 沿 sys.path 逐项搜索 (先到先得) [0] 脚本所在目录 (直接执行时) — 同名影子覆盖高发位 [1] PYTHONPATH 环境变量目录 — 生产污染事故高发位 [2] 虚拟环境 site-packages, 之后是标准库 逐项找 a/b.py, 找到即停; 全部未命中 → ModuleNotFoundError print(module.__file__) 是"到底导了谁"的现场第一问 ③ 编译字节码 → 写 __pycache__ a/__pycache__/b.cpython-312.pyc — 下个进程直接加载字节码 以源文件 mtime+size 校验, 源码变了自动重编 ④ 执行模块体 → 注册回 sys.modules → 绑定名字 顶层代码此刻运行: 连库/读配置/注册路由 — import 即执行 模块对象写入 sys.modules['a.b'], 名字 a/b 绑定到当前命名空间 * 先注册后执行: 循环导入时对方能拿到"半成品"模块 — 这正是环的引爆点 循环导入事故现场 — A ↔ B A.py (执行中) import b NAME = 'a' ← 还没执行到 B.py from a import NAME 启动即反向要 A 的东西 ③ 拿到的是残缺模块对象: NAME 尚未定义 → ImportError: cannot import name 'NAME' from partially initialized module 'a' (most likely due to a circular import) 环炸不炸取决于首个 import 的入口 — 测试全绿, 生产偶发 三种解法 — 打断初始化期的环 ① 依赖下沉 (治本): 双方共用的类型/常量抽 到 schemas.py, A/B 都只依赖它 → 环消失 ② import 移入函数: def handle(): import b — 运行时才触发, 错开两个模块的初始化期 ③ TYPE_CHECKING: if TYPE_CHECKING: import a 只给 mypy 看, 运行时不导入 (注解需字符串) 首选① — 依赖图必须是 DAG; ②③ 是止血, 记 TODO 重构 Legend import 语句 sys.modules / 模块对象 sys.path 搜索 循环导入 / 报错 字节码缓存 / 解法

sys.modules = 单例注册表

  • • 第二次 import 只是字典命中, 零成本
  • • 顶层代码只执行一次 — 天然的应用级单例
  • • monkey patch 改的就是缓存里那个对象, 顺序决定成败
  • • 测试想"重来一次"得 sys.modules.pop 后再 import

导入即执行

  • • 首次 import 时模块体完整跑一遍: 配置/连接/注册都在这发生
  • • __init__.py 是包门面, 做重活拖慢所有子模块
  • • SDK 模块级初始化可能触发网络 IO, 容器没就绪就卡死
  • • 想晚点付钱: 延迟 import / PEP 562 模块级 __getattr__

sys.path 顺序即命运

  • • 脚本目录 → PYTHONPATH → venv site-packages, 先到先得
  • • 项目内同名模块影子覆盖第三方库 — __file__ 一查便知
  • • python -m 与直接执行, sys.path[0] 不同, 相对导入命运不同
  • • -X importtime 是冷启动优化的第一工具

💡 一句话理解

import 不是"把代码贴进来", 而是一次运行时查找 + 执行: 先查 sys.modules 缓存, 没有就沿 sys.path 找到源文件, 编译成 .pyc, 然后把模块体当脚本执行一遍, 把造出来的模块对象登记进缓存。所以同一个模块不管被 import 多少次, 顶层代码只跑一次 — 这既是"天然单例"的来源, 也是"导入即执行"一切副作用 (连库/读配置/耗时初始化) 的来源。

生产里 90% 的 import 事故是三件事: 搜索路径决定"到底导入了谁" (影子覆盖/PYTHONPATH 污染), 导入时机决定"什么时候炸" (循环导入/冷启动), 模块单例决定"状态被谁共享" (monkey patch/测试污染)。把这三件事想在前面, import 就从玄学变成可控的依赖装配。

🧠 必知必会 必考 & 必会

sys.modules
导入缓存/单例注册表: key 是模块全名字符串, import 第一步就是查它; 命中即返回, 模块体不再执行。sys.modules.pop(m) 是测试里"强制重新初始化"的手段。
import json
import json                     # 第二次: 只查缓存, 模块体不重跑
json is sys.modules["json"]     # 关键: 全进程同一对象
sys.modules.pop("app.cfg", None) # 测试强制"重新初始化"
sys.path
搜索路径列表, 顺序即优先级: [0] 是脚本所在目录 (或 -m 时的 cwd), 之后 PYTHONPATH, 再到虚拟环境 site-packages 与标准库。谁排前面谁赢。
import sys
sys.path
# ['/app', '/opt/legacy', '.venv/.../site-packages', ...]
# 关键: 顺序即优先级 — [0] 脚本目录, 再 PYTHONPATH, 再 site-packages
__pycache__ / .pyc
字节码缓存: 首次导入把源码编译成 b.cpython-312.pyc 存盘, 用源文件 mtime+size 校验失效。它只省编译时间, 不省模块体执行时间。
# 首次: 编译源码 → pkg/__pycache__/b.cpython-312.pyc
# 校验: 源文件 mtime + size, 源码变了自动重编
# 关键: 只省编译时间, 模块体每个新进程都照跑一遍
包与 __init__.py
目录 + __init__.py 才是常规包; 导入包先执行 __init__.py 再导入子模块。它是包的公共门面: 导出什么、隐藏什么、是否做 lazy 都在这控制。
# pkg/__init__.py — 包门面
from .core import Service       # 导出公共 API
__all__ = ["Service"]           # 控制 import * 的导出面
# 关键: import pkg 先执行 __init__.py, 再进子模块
绝对 vs 相对导入
from pkg.mod import x 绝对路径最稳; from . import x 的锚点是 __package__ 而不是 cwd, 只在包内合法 — 直跑脚本时没有包上下文就报错。
from pkg.mod import x      # 对: 绝对路径最稳
from . import sibling      # 锚点是 __package__, 不是 cwd
python pkg/mod.py          # 直跑 → ImportError: no known parent package
from M import x 两步
先把模块 M 完整导入 (执行一遍), 再从模块对象取属性 x 绑定到本地; x 不存在抛 ImportError: cannot import name。
import json
from json import dumps        # ① 导入 json ② 取属性绑定
dumps is json.dumps         # → True, 同一函数对象
from json import nope        # → ImportError: cannot import name
importlib.import_module
运行时按字符串动态导入; import 语句是静态字面量。插件加载、配置驱动的策略选择都走它, 配合 try/except 兜底。
from importlib import import_module
try:
    Handler = import_module(f"jobs.handlers.{name}")
except ImportError:
    Handler = DefaultHandler   # 关键: 动态导入必须配兜底
monkey patch 与顺序
patch 改的是模块对象的属性; 但已被引用走的对象不会被追改 — patch 必须发生在目标模块被使用之前, 否则补丁打了个寂寞。
import requests
requests.get = fake_get       # patch 打在模块对象属性上
# 错时机: 别处早已 from requests import get → 旧引用不改
# 关键: patch 必须发生在任何使用之前 (conftest 最顶层)
延迟导入 lazy import
把重库 import 挪进函数/方法体, 冷启动时间换首次调用延迟; serverless 与 CLI 的标准优化。注意别放进热点循环。
def handler(event, ctx):
    import boto3             # 关键: 冷启动省 1.3s, 首调再付
    return boto3.client("s3").list_buckets()
# 注意: 别放进每秒百万次调用的热点循环
-X importtime
每个 import 的 self/cumulative 耗时 profile, 输出自缩进树 — 回答"启动这 3 秒花在哪"的第一工具。
$ python -X importtime app.py 2> import.log
# import time: self [us] | cumulative | imported package
# import time: _ 128450 | 1327800 | boto3   ← 累计 1.3s
# 关键: 自缩进树直接指出冷启动花钱大户
__file__ 与 metadata
排查"导了哪个文件/哪个版本"的现场组合拳: mod.__file__ 看物理路径, importlib.metadata.version() 看安装版本。
import requests
requests.__file__             # 物理路径: 到底导了哪个文件
from importlib.metadata import version
version("requests")          # → '2.31.0', 安装版本
namespace package
无 __init__.py 的目录也能成包 (PEP 420), 多个目录可拼出同一个包名 — 也是某些"幽灵模块/导入到意外目录"事故的来源。
# 无 __init__.py 的目录 = namespace 包 (PEP 420)
# 多个目录可拼出同一个包名:
#   /opt/a/plugins/x/  +  site-packages/plugins/y/
# 关键: 两处都算 plugins.* — "幽灵模块"事故的来源
importlib.reload
重跑模块体并原地更新模块对象; 但 from import 拿到的旧引用不回溯更新, 类对象身份会变 — 热更新没看起来那么美。
import importlib, mymod
old = mymod.Task()
importlib.reload(mymod)       # 重跑模块体, 原地更新
isinstance(old, mymod.Task)   # 关键: → False, 类身份变了

🏭 生产实战 real world

场景 1 · Lambda 冷启动 3.2s → 800ms: importtime 定位最贵 import

函数平台按冷启动收体验费, 而 90% 的冷启动就是 import: 用 -X importtime 找出花钱大户, 延迟到真正用它的那条路由里:

# $ python -X importtime app.py 2> import.log   ← 分析树走 stderr
# import time: self [us] | cumulative | imported package
# import time: _   9157 |    81240 | socket
# import time: _ 128450 | 1327800 | boto3     ← 累计 1.3s, 一家占 40%

def handler(event, ctx):               # 冷启动 = 所有顶层 import 的总价
    import boto3                        # 惰性导入: 只在真正用到时付这 1.3s
    s3 = boto3.client("s3")
    return {"buckets": [b["Name"] for b in s3.list_buckets()["Buckets"]]}

# 健康检查/预热路由绝不 import 重库 — 指标采不到"没发生的导入"

改完冷启动 -1.3s; 代价是首次业务请求慢约 50ms — 对低频管理类函数稳赚。

场景 2 · 模型层互相引用成环, 抽公共 schemas 打断

user.py 和 order.py 彼此 import 对方的类型做注解, 某次入口变更后开始偶发 partially initialized module。治本解法是让公共类型下沉, 依赖图恢复成 DAG:

# 重构前: models/user.py:    from .order import Order
#          models/order.py:  from .user import User     ← 成环
# 重构后: 新建 models/schemas.py, 只放双方都需要的"薄"类型
from dataclasses import dataclass

@dataclass(frozen=True)
class Ref:
    id: int
    kind: "str"                        # 跨模块只传引用, 不传行为

# user.py / order.py 都只 import schemas → 单向依赖, 环消失
# 验收: python -c "import app.main" 在任意入口下都必须稳定通过

场景 3 · 插件系统: entry points 协议 + importlib 动态加载

报表导出格式要做插件化: 装了就能用, 没装不报错。用标准库入口点协议注册, 主程序按名动态加载:

# 插件侧声明 (插件的 pyproject.toml, pip 装上即注册):
# [project.entry-points."report.exporters"]
# csv  = "report_csv:export_csv"
# json = "report_json:export_json"

from importlib.metadata import entry_points

def load_exporters():
    return {ep.name: ep.load() for ep in entry_points(group="report.exporters")}

def export(fmt, rows):
    try:
        return load_exporters()[fmt](rows)    # 插件缺依赖在这里炸, 拦得住
    except KeyError:
        raise ValueError(f"unknown exporter: {fmt}") from None
    except Exception as e:                   # 插件失败不拖垮主流程
        log.warning("plugin failed", fmt=fmt, exc_info=True); raise

场景 4 · 测试隔离: sys.modules.pop 防单例串味

被测模块在 import 时读环境变量并缓存成单例, 测试之间互相污染。fixture 里把单例丢弃, 让模块体按新环境重跑:

# conftest.py — 模块单例让"改环境再测"变成不可能, 必须能重置
import sys, pytest

@pytest.fixture
def fresh_settings(monkeypatch):
    monkeypatch.setenv("APP_ENV", "test")      # 先改环境再谈导入
    sys.modules.pop("app.settings", None)      # 丢弃单例, 下次 import 重跑模块体
    import app.settings as s               # 用新环境重新初始化
    yield s
    sys.modules.pop("app.settings", None)      # 不把状态漏给下一个用例
    monkeypatch.undo()                        # 顺手还原被 patch 的属性

场景 5 · __init__.py 门面: __all__ + 惰性属性加速包导入

geo 包里 reverse 模块拖着 800ms 的 shapely 链, 但多数调用方只用 distance。门面只导出公共 API, 重子模块延迟到首次点名:

# geo/__init__.py — 公共 API 收口, 重库延迟到首次属性访问
__all__ = ["reverse_geocode", "distance"]

def __getattr__(name):                     # PEP 562 模块级惰性属性
    if name == "reverse_geocode":
        from .reverse import reverse_geocode  # 800ms 的链此刻才进内存
        return reverse_geocode
    raise AttributeError(f"module {__name__!r} has no {name!r}")

# 调用方无感: from geo import reverse_geocode — 包导入 3ms 不变

import geo 从 823ms → 3ms; 只有真正调用 reverse 的路径付那 800ms。

场景 6 · 脚本直跑相对导入报错: -m 与直跑的部署差异

同一个文件, 直跑报 ImportError, -m 却正常 — 差异全在 sys.path[0] 与包上下文。统一入口 + 护栏提示:

# ImportError: attempted relative import with no known parent package
# ✗ python geo/reverse.py   → sys.path[0]='geo/', __package__=None, 相对导入无锚点
# ✓ python -m geo.reverse   → sys.path[0]=cwd,   __package__='geo',  相对导入成立

# 部署脚本/systemd/Dockerfile CMD 统一走 -m; 文件尾加护栏防手滑直跑:
if __name__ == "__main__" and __package__ is None:
    raise SystemExit("run as: python -m geo.reverse")

场景 7 · 灰度机 PYTHONPATH 污染, 导入了旧版同名包

同一镜像的代码在灰度机行为不同: 新参数直接 TypeError。三行排查确认导的不是同一个包:

# 症状: 灰度机 requests=2.6.0, 其余机器 2.31 — 用到新参数直接 TypeError
import requests, os
print(requests.__file__)              # /opt/legacy/.../requests/__init__.py ← 导的不是 venv 里那个!
from importlib.metadata import version
print(version("requests"))          # 2.6.0 — 真身版本现形
print(os.environ.get("PYTHONPATH"))    # /opt/legacy:... ← 污染源
# 修复: 清掉 systemd 单元 Environment=PYTHONPATH; requirements 锁版 + pip check 进 CI

根因: 早年运维往 /etc/profile 写的 PYTHONPATH 让 /opt/legacy 排到了 venv 前面 — 先到先得, 版本漂移。

场景 8 · pickle 存档遭遇模块路径迁移, meta_path 垫片续命

工具模块从 utils.geo 迁到 geo.core, 三年的二进制存档 unpickle 全部 ModuleNotFoundError。迁移期加一个查找器垫片:

# 旧存档里类的路径是 utils.geo.Point — 模块没了, pickle 按路径找类
import sys, pickle
from importlib.abc import MetaPathFinder
from importlib.util import find_spec

class LegacyAlias(MetaPathFinder):
    OLD2NEW = {"utils.geo": "geo.core"}          # 旧路径 → 新家
    def find_spec(self, name, path=None, target=None):
        if name in self.OLD2NEW:
            return find_spec(self.OLD2NEW[name])     # 让新模块顶上

sys.meta_path.insert(0, LegacyAlias())            # 迁移期垫片, 存档迁完即删
records = pickle.load(open("archive-2023.pkl", "rb"))

场景 9 · 模块级惰性单例 vs 显式依赖注入的取舍

连接持有者怎么拿? 小服务用模块级惰性单例最省事; 多环境/重测试的 服务用显式注入, 依赖看得见才好替换:

# 写法A: 模块级惰性单例 — import 期零 IO, 首次调用才连接
_db = None
def get_db():
    global _db
    if _db is None:
        _db = create_engine(DB_URL, pool_size=10)
    return _db

# 写法B: 显式注入 — 依赖出现在签名里, 测试不用 hack sys.modules
# app = create_app(db=engine(DB_URL), cache=Redis("redis://cache:6379/0"))
# 取舍: 单服务单环境用A; SaaS 多租户/多配置用B, 组合根统一装配

场景 10 · 依赖环境漂移: 锁版 + src 布局让 sys.path 只有一条可信来源

47 台机器 pip 装出 3 个小版本, 同一段代码行为漂移查无此案。用镜像锁死依赖层, 用 src 布局防 cwd 影子覆盖:

# Dockerfile — 依赖层与代码层分离, requirements 锁到精确版本
# COPY requirements.txt .
# RUN pip install --no-cache-dir -r requirements.txt   # orjson==3.10.7
# COPY src/ ./src/
# CMD ["python", "-m", "app.main"]                      # 入口统一 -m

# pyproject.toml — src 布局: 项目包只在安装后可见, cwd 里的同名文件骗不到人
# [tool.pytest.ini_options]
# pythonpath = ["src"]
# 铁律: 生产进程的 PYTHONPATH 必须为空, sys.path 只有一条可信来源

⚠️ 编码注意与常见坑 pitfalls

坑 1 · 函数内 import 掩盖真实依赖 — 依赖缺失直到某条路径第一次执行才 ModuleNotFoundError, 上线当晚才炸, 本地测试根本走不到。正解: 必需依赖留在模块顶部; 函数内 import 只用于可选依赖并配 try/except ImportError。
def export_pdf(rows):
    import weasyprint        # 错: 缺依赖首次调用才炸
import weasyprint            # 对: 必需依赖放顶部, 启动即报
# 可选依赖才进函数体 + try/except ImportError 兜底
坑 2 · except 把 ImportError 一起吞 — 可选依赖没装, 被 except Exception 当成业务错误静默降级, 功能悄悄消失还没日志。正解: 先单独 except ImportError 记日志再接业务异常; 或用 importlib.util.find_spec() 启动期探测。
try:
    import ujson as json
except Exception:              # 错: ImportError 被吞, 无日志
    pass
try:
    import ujson as json
except ImportError:            # 对: 单独接住并记日志
    log.warning("ujson missing"); import json
坑 3 · 同一模块两个名字 — import a.b 与 from a import b 并存, reload/热更新后一个引用更新一个不更新, 两处行为分叉。正解: 团队统一引用风格; 需要 reload 的场景一律 sys.modules['a.b'] 取同一个对象。
import a.b; from a import b
sys.modules.pop("a.b"); import a.b   # 模拟热更新
a.b is b                         # 错: → False, b 还是旧对象
use(sys.modules["a.b"])             # 对: 永远取当前对象
坑 4 · reload 后 isinstance 失败 — importlib.reload() 重跑模块体生成新的类对象, 旧实例还指着旧类, isinstance(x, NewCls) 返回 False, 分派逻辑全乱。正解: reload 后重建实例; from import 拿到的旧类要重新 import 再用。
import importlib, plugin
old = plugin.Task()
importlib.reload(plugin)       # 模块体重跑, 类是新的
isinstance(old, plugin.Task)   # 错: → False, 分派全乱
# 对: reload 后重建实例, 要用的类重新 import
坑 5 · 自写模块名撞标准库 — 本地 requests.py/email.py/uuid.py 排在搜索路径前面把真库影子覆盖, 报莫名其妙的 AttributeError。正解: 起名先过 stdlib 清单; 报错第一反应打印 module.__file__ 看物理路径。
import email                  # 错: 导到的是你自己的 email.py
# 真库的属性全没了 → AttributeError 诡异地炸
# 对: 起名避开 stdlib; 现场打印验明正身
print(email.__file__)          # → /app/email.py ← 露馅
坑 6 · 字符串拼出来的 import — __import__(f"pkg.{mode}") 拼错模块名运行时才炸, 静态检查全瞎; 用户输入进 import 路径等于任意代码执行。正解: 白名单 dict 映射 name 到 callable; 动态加载必须 try/except 并翻译成业务错误码。
mod = __import__(f"pkg.{mode}")   # 错: 拼错运行时才炸, 输入=RCE
# 对: 白名单映射, 不让用户碰 import 路径
HANDLERS = {"csv": csv_export, "pdf": pdf_export}
h = HANDLERS.get(fmt) or fail(400, f"unknown {fmt}")
坑 7 · from m import * — 命名空间被无差别灌入, 本地名字被覆盖还找不到来源, IDE 和 code review 都分析不了。正解: 显式列名 from m import a, b; 库作者维护 __all__ 控制导出面。
from utils import *          # 错: 名字灌入, 本地被覆盖
# 对: 显式列名
from utils import parse_ts, fmt_money
# 库作者: __all__ = [...] 控制 import * 的导出面
坑 8 · 循环导入只在特定调用路径才炸 — 首个 import 的入口决定谁处于半初始化, 测试全绿生产偶发 ImportError: partially initialized module。正解: 依赖必须是无环 DAG, 公共部分下沉; 函数内 import 只是止血手段。
# a.py: import b      b.py: from a import NAME  ← 成环
# 错: 入口不同谁半初始化就不同, 生产偶发 ImportError
# 对: 公共类型下沉 schemas, A/B 单向依赖
from .schemas import Ref    # 依赖图恢复成 DAG
坑 9 · .pyc 不更新 — 改了源码行为没变: 部署只换了源却沿用旧缓存目录、时钟回拨骗过 mtime 校验、__pycache__ 属主是 root 写不进。正解: 发布脚本统一清 __pycache__; 镜像内构建期跑一次 import 让缓存与源对齐。
# 错: 只换源不换缓存 / 时钟回拨 / 属主 root 写不进
python app.py                 # 行为没变? 大概率旧 .pyc
# 对: 发布脚本统一清缓存
find . -name __pycache__ -exec rm -rf {} +
坑 10 · __init__.py 里做重活 — 连 DB/发 HTTP/读大配置, 任何子模块 import 都要付一遍, 包导入从 5ms 涨到 2s, 所有人跟着慢。正解: __init__.py 只放导出与常量; 重活下沉到子模块并延迟到首次使用。
# 错: pkg/__init__.py 顶层连 DB
db = create_engine(URL)       # import pkg 就付一遍, 全员变慢
# 对: 门面只放导出; 重活下沉子模块延迟到首次用
from .core import get_db    # 用到才连
坑 11 · 动态 import 无兜底直接 500 — 插件 ep.load() 抛异常没人接, Web 进程直接崩全局, 一个坏插件带走整站。正解: 插件加载循环 try/except, 失败记插件名+异常并跳过, 缺插件降级不拖垮主流程。
# 错: 一个坏插件抛异常, 全站 500
plugins = {ep.name: ep.load() for ep in entry_points(group=G)}
# 对: 逐个 try/except, 记名跳过
for ep in entry_points(group=G):
    try: plugins[ep.name] = ep.load()
    except Exception: log.warning("plugin failed", ep.name)
坑 12 · 条件 import 分支不完整 — if sys.platform == 'linux' 里 import 的依赖, 其他分支没覆盖, 换平台部署就 ModuleNotFoundError。正解: 各平台依赖进各自 extras, CI 矩阵全平台装一遍; 缺失时显式 raise 并带安装提示。
if sys.platform == "linux":
    import uvloop             # 错: 其他平台部署直接缺
# 对: 平台依赖进各自 extras, 缺失显式提示
try:
    import uvloop
except ImportError:
    uvloop = None            # 或 raise 提示 pip install app[linux]
坑 13 · import 触发网络 IO — SDK 在模块级读凭证/连云元数据服务, 容器网络未就绪时 import 卡死 30s 后崩溃。正解: SDK 初始化推迟到显式 init() 或首次请求; 探活逻辑与业务导入解耦。
client = CloudClient()        # 错: 模块顶层连云, 网未就绪卡死
# 对: 初始化推迟到显式 init()/首次请求
_client = None
def client():
    global _client
    _client = _client or CloudClient(); return _client
坑 14 · 项目内同名模块影子覆盖 — 本地 json.py/typing.py 排在 site-packages 前面, 第三方库 import 到你的文件后行为错乱且极难定位。正解: src 布局 + 命名查重; 现场用 __file__ 一眼识破。
import json
json.__file__
# 错: → '/app/json.py' ← 你的文件, 不是 stdlib!
# 对: src 布局 + 命名查重, __file__ 一眼识破
坑 15 · pickled 对象依赖被删掉的模块 — 反序列化按类路径找模块, 模块下线即 ModuleNotFoundError, 历史存档全废。正解: 迁移期保留 alias 模块或 sys.meta_path 垫片; 长期数据用版本化 schema 而非 pickle 裸类。
pickle.load(open("a.pkl", "rb"))
# 错: 类路径 utils.geo.Point 已下线 → ModuleNotFoundError
# 对: 迁移期留 alias 或 meta_path 垫片 (旧路径→新家)
sys.meta_path.insert(0, LegacyAlias())  # 存档迁完即删
坑 16 · python -m 与直接执行的差异 — 直接 python mod.py 时 sys.path[0] 是文件目录且无包上下文, 相对导入直接报错; python -m pkg.mod 才有 __package__。正解: 入口统一 -m; 脚本内检测 __package__ is None 给出可执行提示。
python geo/reverse.py   # 错: sys.path[0]=文件目录, 无包上下文
# → ImportError: no known parent package
python -m geo.reverse   # 对: sys.path[0]=cwd, __package__='geo'
# 脚本内: if __package__ is None: 提示改用 -m
坑 17 · TYPE_CHECKING 里的 import 运行时用 — if TYPE_CHECKING: from .models import User 之后运行时引用 User 抛 NameError。正解: 文件头 from __future__ import annotations 让注解全变字符串; 运行时真要用就函数内局部 import。
if TYPE_CHECKING:
    from .models import User
def find(u: User): ...      # 错: 运行时 NameError
# 对: 文件头 from __future__ import annotations
# 运行时真要用 → 函数内局部 import
坑 18 · 模块级全局被并发共享 — STATE = {} 被多线程/asyncio 任务同时读写, import 期看着无害运行期数据竞争, 计数丢更新。正解: 加锁或 threading.local/contextvars; 多进程下模块状态根本不共享, 别拿来当进程间通信。
STATE = {}                    # 错: 多线程裸读写, 计数丢更新
STATE = threading.local()     # 对: 线程各自一份
# 多进程下模块状态根本不共享, 别当 IPC 用
坑 19 · 延迟 import 放进热点循环 — for 循环内每次 import 虽命中缓存也要走 sys.modules 查找, 每秒百万次调用白烧几个点 CPU。正解: 循环外 import 或首次用时缓存到局部变量; 惰性只该发生一次。
for row in rows:
    import json             # 错: 每次都查 sys.modules, 白烧 CPU
import json                  # 对: 循环外导入一次
for row in rows: process(row)  # 惰性只该发生一次
坑 20 · 基础设施反向 import 业务模块 — 日志配置为了"记录谁导入了"去 import 业务模块, 反手把半初始化对象拖进格式化, 循环导入提前引爆。正解: 基础设施模块零业务依赖; 分析导入链用 -X importtime, 别用业务代码。
# 错: logging_conf.py 反手 import 业务模块
from app.models import User   # 半初始化对象入局, 环提前引爆
# 对: 基础设施零业务依赖
python -X importtime app.py   # 分析导入链用它, 不用业务代码