Python 模块与包

0 阅读3分钟

这篇文章会系统、完整地讲清:

  1. Python 模块与包的概念
  2. 导入机制的底层原理
  3. 名称冲突的根本原因
  4. 如何区分同名模块
  5. 工程级最佳实践

ChatGPT Image 2026年3月3日 18_21_40.png

一、Python 模块系统概述

Python 的模块系统建立在三个核心概念之上:

  • module(模块)
  • package(包)
  • import system(导入系统)

1. 模块(Module)

一个 .py 文件就是一个模块。

math_utils.py

导入:

import math_utils

此时:

math_utils.__name__ == "math_utils"

2. 包(Package)

一个目录如果包含:

  • __init__.py(传统包, 小项目推荐这种方式)
  • 或没有 __init__.py 但符合 PEP 420(命名空间包)

则它是一个包。

结构:

project/
    app/
        __init__.py
        service.py

导入:

from app import service

3. 模块系统的本质

Python 的模块系统是:

基于文件路径 + sys.path 查找顺序 + 缓存机制 的动态加载系统


二、import 的完整执行流程

当你执行:

import foo

Python 实际执行流程是:


1️⃣ 检查缓存

sys.modules

如果已加载,直接返回缓存。


2️⃣ 按顺序搜索 sys.path

import sys
print(sys.path)

典型顺序:

  1. 当前运行目录
  2. PYTHONPATH
  3. site-packages
  4. 标准库目录

⚠ 搜索是顺序匹配,第一个匹配成功的模块即被加载。


3️⃣ 使用 finder + loader 加载

基于 PEP 302 / PEP 451:

  • finder:查找模块
  • loader:加载模块

核心接口在:

importlib

三、为什么会发生“模块名冲突”?

举例:你有一个文件,

random.py

然后:

import random

你期望导入标准库 random,但实际上导入的是你自己目录下的 random.py。

原因:当前目录在 sys.path 的最前面, 这叫做Shadowing(遮蔽)


四、如何区分不同来源的同名模块?

假设场景:

  • 你有本地包 json
  • Python 标准库也有 json

方法一:查看模块路径

import json
print(json.__file__)

可以看到加载来源。


方法二:避免顶层重名(最推荐)

不要这样:

json/
random/
sys/
email/

使用项目相关的更具体的名字:

myproject_json/
core_random/

这是最彻底的解决方案。


方法三:使用绝对导入(推荐)

项目结构:

project/
    mypkg/
        __init__.py
        json.py

在内部引用:

from mypkg import json

不要:

import json

方法四:使用相对导入

在包内部:

from . import json

或:

from .json import parse

相对导入只在包内部有效。


方法五:手动修改 sys.path(不推荐)

import sys
sys.path.insert(0, "/custom/path")

⚠ 这样会破坏可维护性。


方法六:使用 importlib 精确加载

import importlib.util
spec = importlib.util.spec_from_file_location(
    "myjson", "/path/to/my/json.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)

适合插件系统,不适合普通工程。


五、深入理解 Python 模块查找机制

核心结构:

sys.meta_path

它是一个 finder 列表:

  • BuiltinImporter
  • FrozenImporter
  • PathFinder

PathFinder 工作原理

  1. 遍历 sys.path

  2. 在每个目录下查找:

    • foo.py
    • foo/init.py
    • 扩展模块
  3. 找到即返回


六、模块缓存机制

所有加载的模块都会进入:

sys.modules

键是模块名,值是模块对象。

这意味着:

import foo
import foo

只会执行一次文件代码。


七、包的命名空间(PEP 420)

没有 __init__.py 也可以成为包:

namespace_pkg/
    module1.py

多个目录可以共享同一命名空间。

这在大型项目(如插件系统)中常见。


八、工程实践建议

1️⃣ 永远使用“项目顶级包名”

正确结构:

project/
    myproject/
        __init__.py
        service.py

然后:

from myproject.service import run

不要直接:

import service

2️⃣ 避免使用标准库名字

不要创建:

  • sys.py
  • json.py
  • typing.py
  • asyncio.py
  • random.py
  • email.py
  • string.py

这些都会破坏导入行为。


3️⃣ 使用虚拟环境隔离依赖

避免第三方库污染系统环境。


4️⃣ 使用 python -m 运行模块

正确:

python -m myproject.main

避免路径问题。


九、一个完整冲突示例

结构:

project/
    json.py
    main.py

main.py:

import json
print(json.dumps({"a": 1}))

运行报错:

因为导入的是你自己的 json.py。

修复方式:

  1. 重命名
  2. 或使用包结构
  3. 或调整项目结构

十、总结:Python 模块系统本质

可以概括为一句话:

Python 模块系统是基于 sys.path 顺序查找 + 首次匹配加载 + sys.modules 缓存 的动态加载机制。

名称冲突的根本原因:

搜索路径优先级导致的 shadowing。