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

    • 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章 工程实践

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

包与相对导入

包(Package)是通过"带点号的模块名"来构建 Python 模块命名空间的一种机制。模块名 A.B 表示包 A 中的子模块 B。就像模块解决了不同作者之间的全局变量名冲突一样,包解决了不同项目之间的模块名冲突——NumPy、Pillow 等大型库都依赖包机制来组织成百上千个子模块。

包的结构与 init.py

一个包在文件系统中表现为包含 __init__.py 文件的目录。这个文件可以是空的,也可以执行包的初始化代码。假设要设计一个处理声音数据的包 sound,其结构如下:

sound/                          # 顶层包
    __init__.py                 # 初始化 sound 包
    formats/                    # 子包:文件格式
        __init__.py
        wavread.py
        wavwrite.py
    effects/                    # 子包:音效
        __init__.py
        echo.py
        surround.py
    filters/                    # 子包:过滤器
        __init__.py
        equalizer.py

__init__.py 的存在让 Python 将该目录识别为包而非普通文件夹。在 Python 3.3 之前,没有 __init__.py 的目录不会被当作包;Python 3.3 引入了命名空间包(namespace package)机制,允许无 __init__.py 的目录参与包结构,但这主要用于跨目录拆分大型包。对于常规项目,仍建议保留 __init__.py,以明确标识包边界并兼容旧版本。

__init__.py 中常放置版本号、公共 API 导入和初始化逻辑:

# sound/__init__.py
__version__ = "1.0.0"

# 让 from sound import effects 直接可用
from . import effects

导入包的多种方式

导入子模块时,可以使用完整路径:

import sound.effects.echo
sound.effects.echo.echofilter(input, output, delay=0.7)

这种写法加载了 echo 子模块,但调用时必须使用完整前缀。更简洁的方式是:

from sound.effects import echo
echo.echofilter(input, output, delay=0.7)

如果只需要模块中的某个函数,可以直接导入:

from sound.effects.echo import echofilter
echofilter(input, output, delay=0.7)

注意 from package import item 的解析规则:解释器首先检查 item 是否是包中定义的名称(由 __init__.py 定义);如果不是,则假定 item 是子模块并尝试加载。如果两者都找不到,抛出 ImportError。

相反,import item.subitem.subsubitem 要求除最后一项外,每个 item 都必须是包;最后一项可以是模块或包,但不能是类、函数或变量。下面的写法是错误的:

import sound.effects.echo.echofilter  # ImportError: No module named 'sound.effects.echo.echofilter'

from package import * 与 all

from sound.effects import * 的行为与直觉不同:它不会自动导入包的所有子模块,因为遍历并加载所有子模块可能耗时很长,且可能触发不期望的副作用。正确的做法是在包的 __init__.py 中定义 __all__ 列表,显式声明通配导入时应暴露哪些名称:

# sound/effects/__init__.py
__all__ = ["echo", "surround", "reverse"]

这样 from sound.effects import * 只会导入 echo、surround 和 reverse 三个子模块。如果 __init__.py 中同时定义了与某个子模块同名的函数,该函数会遮蔽子模块:

# sound/effects/__init__.py
__all__ = ["echo", "surround", "reverse"]

def reverse(msg: str):       # 同名函数遮蔽了 reverse.py 子模块
    return msg[::-1]

此时 from sound.effects import * 导入的 reverse 是上述函数,而不是 reverse.py 模块。

如果没有定义 __all__,from package import * 不会导入任何子模块,只会导入包中已定义的名称(包括 __init__.py 中显式加载的子模块)。例如:

import sound.effects.echo      # 显式加载 echo
import sound.effects.surround  # 显式加载 surround
from sound.effects import *    # 此时 echo 和 surround 已进入当前命名空间

相对导入:. 与 ..

当包结构复杂时,子模块之间经常需要相互引用。相对导入使用前导点号表示当前包和上级包,避免了硬编码包名:

# sound/effects/surround.py
from . import echo              # 导入同级模块 echo
from .echo import echofilter    # 从同级模块导入具体函数
from .. import formats          # 导入上级包 sound 下的 formats 子包
from ..filters import equalizer # 导入上级包下的兄弟子包 filters

点号的含义如下:

  • from .module import name:当前包的同级模块。
  • from .. import module:上级包中的模块。
  • from ... import module:上两级包(以此类推)。

相对导入基于当前模块的 __name__ 来解析位置。如果计划将某个模块作为应用程序的入口直接运行(python module.py),其 __name__ 会被设为 "__main__",此时相对导入无法确定包层级,会抛出 ImportError: attempted relative import with no known parent package。因此,入口模块应始终使用绝对导入,或者通过 python -m package.module 方式运行。

# 错误:直接运行含相对导入的文件
python sound/effects/surround.py

# 正确:作为模块运行
python -m sound.effects.surround

绝对导入与相对导入的选择

绝对导入使用完整包路径,如 from sound.effects import echo。它的优点是清晰、不易因文件移动而失效,推荐用于项目入口和对外 API。相对导入的优点是包名变更时无需修改内部引用,适合大型包内部的模块间调用。PEP 8 建议:项目内部优先使用绝对导入;当包结构很深、导入路径冗长时,可以使用相对导入简化代码。

多目录中的包:path

包对象有一个特殊属性 __path__,它是一个字符串序列,包含 __init__.py 所在的目录。在导入子模块或子包时,Python 会搜索 __path__ 中的所有目录。这个属性可以在运行时修改,从而实现将模块动态分散到多个目录:

# sound/__init__.py
import os
__path__.append(os.path.join(os.path.dirname(__file__), 'plugins'))

修改后,位于 sound/plugins/ 下的子模块也能被当作 sound 包的一部分导入。虽然不常用,但这是实现插件系统的底层机制之一。

常见错误与排查

  1. 忘记 __init__.py:在旧版 Python 中会导致目录不被识别为包;新版中则成为命名空间包,某些行为可能不同。
  2. 循环导入:包内模块 A 导入 B,B 又导入 A。虽然不会无限递归,但如果 A 在初始化期间访问 B 的未完成属性,会抛出 AttributeError。
  3. 相对导入在顶层脚本中失败:直接运行 python script.py 时,__name__ 为 "__main__",相对导入无法解析。应改用 python -m package.script。
  4. from package import * 不加载子模块:必须通过 __all__ 或显式 import 预先加载。

总结

包是模块的层级组织方式,__init__.py 是包的入口和初始化点。from package import * 依赖 __all__ 控制暴露范围;相对导入用 . 和 .. 简化内部引用,但不能用于直接运行的顶层脚本。理解这些规则,就能设计出结构清晰、易于维护的多模块项目。

上一页
模块搜索路径
下一页
标准库概览