飞翔飞翔
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
  • 学习路径
  • 第1章 Python简介

    • Python是什么
    • 安装与运行
    • 交互式解释器
    • 注释与编码规范
  • 第2章 变量与数据类型

    • 变量与对象
    • 整数 int
    • 浮点数 float
    • 复数 complex
    • 布尔值 bool
    • 字符串 str
    • 空值 None
    • 类型转换
  • 第3章 运算符与表达式

    • 算术运算符
    • 比较运算符
    • 赋值运算符
    • 逻辑运算符
    • 位运算符
    • 身份与成员运算符
    • 海象运算符
    • 运算符优先级
  • 第4章 流程控制

    • if 语句
    • if-else 语句
    • if-elif-else 语句
    • match-case 语句
    • 条件表达式(三元运算符)
    • while 循环
    • for 循环
    • range 函数
    • break 与 continue
    • 循环的 else 子句
    • pass 语句
  • 第5章 数据结构

    • 列表创建与索引
    • 列表方法
    • 列表推导式
    • 元组
    • 序列解包
    • 集合
    • 字典创建与访问
    • 字典方法
    • 字典推导式
    • range 对象
  • 第6章 函数

    • 定义函数
    • 位置参数与关键字参数
    • 默认参数
    • 可变参数
    • 解包实参
    • 函数返回值
    • lambda 表达式
    • 文档字符串与注解
    • 作用域与命名空间
    • global 与 nonlocal
  • 第7章 模块与包

    • 模块导入
    • 模块搜索路径
    • 包与相对导入
    • 标准库概览
  • 第8章 文件与输入输出

    • 文件读写
    • 上下文管理器
    • 字符串格式化
    • JSON 与 CSV
  • 第9章 面向对象

    • 类与对象
    • 方法
    • 实例变量与类变量
    • 私有变量
    • 继承
    • 多重继承
    • 魔术方法
    • 属性装饰器
    • 数据类 dataclass
  • 第10章 异常处理

    • 语法错误与异常
    • try-except
    • 异常链与 raise
    • 清理操作
    • 自定义异常
  • 第11章 迭代器与生成器

    • 迭代器协议
    • 生成器
    • 生成器表达式
    • 迭代工具
  • 第12章 高级特性

    • 装饰器
    • 函数式编程
  • 第13章 工程实践

    • 测试与调试
    • 代码质量
    • 虚拟环境

模块搜索路径

当代码中写下 import spam 时,Python 解释器需要定位名为 spam 的模块。这个过程遵循一套明确的搜索规则,理解这些规则有助于排查 ModuleNotFoundError,也能避免本地文件意外覆盖标准库。

搜索顺序:内置优先,目录次之

模块查找分为两步。第一步检查内置模块——那些直接编译进解释器、没有对应 .py 文件的模块,例如 sys、builtins。它们的名称可以通过 sys.builtin_module_names 查看:

import sys
print('sys' in sys.builtin_module_names)      # True
print('os' in sys.builtin_module_names)         # False,os 是标准库而非内置
print('json' in sys.builtin_module_names)       # False

如果目标不是内置模块,解释器进入第二步:在 sys.path 列表中按顺序搜索 spam.py(或包目录)。sys.path 在解释器启动时从以下位置初始化:

  1. 被命令行直接运行的脚本所在目录(或未指定文件时的当前工作目录)。
  2. PYTHONPATH 环境变量中列出的目录,格式与系统 PATH 相同,用分号(Windows)或冒号(Unix)分隔。
  3. 依赖于安装的默认值,通常包含标准库目录和 site-packages 目录(由 site 模块处理)。
import sys
for i, p in enumerate(sys.path, 1):
    print(f"{i}. {p}")

这个顺序有一个重要后果:如果你在脚本所在目录创建了一个与标准库同名的文件(比如 json.py),那么 import json 会优先加载你的本地文件,而不是标准库。这通常是个错误,除非你有意替换该模块。

PYTHONPATH 与动态修改

PYTHONPATH 是用户扩展模块搜索路径的主要手段。假设项目结构如下:

/home/user/
  project/
    main.py
  libs/
    helper.py

为了让 main.py 能导入 libs/helper.py,可以在启动前设置环境变量:

# Linux/macOS
export PYTHONPATH=/home/user/libs
python project/main.py

# Windows
set PYTHONPATH=C:\Users\user\libs
python project\main.py

也可以在程序运行期间直接修改 sys.path。由于它是普通列表,支持 append、insert 等操作:

import sys
sys.path.append('/home/user/libs')
sys.path.insert(0, '/home/user/experimental')  # 插入到最前面,优先级最高

import helper  # 现在可以找到了

但运行时修改 sys.path 通常只应在特殊场景(如插件系统)中使用,常规项目应通过虚拟环境或安装包来管理依赖。

符号链接与目录解析

在支持符号链接的文件系统中,"被命令行直接运行的脚本所在目录"指的是符号链接最终指向的目录,而不是链接本身所在的目录。例如:

ln -s /real/project/main.py /shortcut/main.py
cd /shortcut
python main.py

此时 sys.path[0] 是 /real/project,而不是 /shortcut。这一行为防止了符号链接目录被误加入搜索路径,但也意味着如果链接和源文件不在同一目录结构下,相对导入的行为可能与预期不同。

.pyc 缓存与 pycache

为了加速模块加载,Python 会把源码编译成字节码,缓存在 __pycache__ 目录中。文件名遵循 module.cpython-版本.pyc 的格式,例如 spam.cpython-312.pyc。这种命名允许不同 Python 版本的编译文件共存于同一目录,而不会互相覆盖。

myproject/
  spam.py
  __pycache__/
    spam.cpython-312.pyc
    spam.cpython-311.pyc

Python 在导入时会自动比较 .py 源码与 .pyc 缓存的修改时间。如果源码更新,则重新编译;如果只有 .pyc 而无 .py,则直接加载缓存。整个过程完全自动,无需手动干预。

但有两种情况不检查缓存:一是从命令行直接载入的模块(非导入),每次都会重新编译且不保存结果;二是只有 .pyc 没有 .py 时,不会验证缓存是否过期。后一种情况常用于以编译后形式分发代码,此时 .pyc 应放在源码目录而非 __pycache__ 中,且该目录不能存在同名的未编译源文件。

优化编译:-O 与 -OO

使用 -O 或 -OO 命令行开关可以生成优化后的字节码,减小 .pyc 文件体积:

  • -O:去除 assert 语句。
  • -OO:去除 assert 语句和文档字符串(__doc__)。

优化后的缓存文件带有 opt- 标签,例如 spam.cpython-312.opt-1.pyc(对应 -O)或 spam.cpython-312.opt-2.pyc(对应 -OO)。需要注意的是,.pyc 文件只是加载更快,运行时执行速度并不会比直接从 .py 读取更快。

python -O script.py          # 使用 assert 去除后的字节码
python -OO script.py         # 同时去除 __doc__

如果程序依赖 assert 做运行时检查,或依赖 __doc__ 生成文档,不应使用这些选项。

批量编译:compileall

compileall 模块可以为整个目录树预生成 .pyc 文件,常用于部署时加速首次启动:

import compileall
import sys

# 编译当前目录下所有模块
compileall.compile_dir('.', force=True)

# 或从命令行执行
# python -m compileall /path/to/project

force=True 会忽略时间戳检查,强制重新编译。在 CI/CD 流程中,可以在打包阶段运行 python -m compileall,确保生产环境无需在首次导入时现场编译。

内置模块与标准库的区别

初学者常混淆"内置模块"和"标准库"。内置模块如 sys、builtins 直接链接进解释器二进制,没有独立的 .py 文件,因此不受 sys.path 影响,也无法通过修改路径来替换。标准库模块如 os、json 则以 .py 文件形式存放在安装目录中,理论上可以被同名本地文件覆盖。

import sys
print(sys.builtin_module_names[:5])  # ('_abc', '_ast', '_codecs', ...)

总结

模块搜索路径的优先级是:内置模块 > sys.path 中的目录列表。sys.path 本身由脚本目录、PYTHONPATH 和安装默认值组成,且可以在运行时修改。__pycache__ 和 .pyc 文件是自动管理的字节码缓存,用于加速加载而非加速执行。理解这些机制,就能准确定位 ModuleNotFoundError 的根源,也能避免本地文件意外屏蔽标准库。

上一页
模块导入
下一页
包与相对导入