到目前为止,我们所有的代码都写在了一个文件中。当程序变得庞大时,把所有代码塞在一个文件里会让维护变得痛苦不堪。

Python 通过**模块(Module)包(Package)**来解决这个问题。模块就是 .py 文件,包就是包含模块的目录。借助这套机制,你可以把代码拆分成逻辑单元,复用第三方库,并组织自己的项目结构。

模块的概念

一个 .py 文件就是一个模块。模块名就是文件名(去掉 .py)。

# 文件:greeting.py
"""问候模块"""

def say_hello(name):
    return f"Hello, {name}!"

def say_goodbye(name):
    return f"Goodbye, {name}!"

PI = 3.14159  # 模块中也可以定义变量

print(f"模块 {__name__} 已加载")  # 每个模块都有 __name__

在其他文件中导入它:

# 文件:main.py
import greeting  # 导入 greeting 模块

print(greeting.say_hello("Alice"))  # Hello, Alice!
print(greeting.PI)                  # 3.14159

import 的各种方式

基本导入

# 方式 1:导入整个模块
import math
print(math.sqrt(16))  # 4.0

# 方式 2:导入特定对象
from math import sqrt, pi
print(sqrt(25))       # 5.0
print(pi)             # 3.14159...

# 方式 3:导入并重命名
import numpy as np
from math import sqrt as square_root
print(square_root(100))  # 10.0

# 方式 4:导入所有(谨慎使用!)
from math import *  # 可能造成命名冲突
print(sin(0))       # 0.0

import * 的风险

# 假设你有两个模块:
# module_a.py
def fun():
    return "A"

# module_b.py
def fun():
    return "B"

# 在你的代码中:
from module_a import *
from module_b import *
print(fun())  # "B" —— module_b 的 fun 覆盖了 module_a 的

规则: 避免 from module import *。如果要用,在模块中通过 __all__ 精确控制导出内容。

导入模块时发生了什么?

Python 导入模块时会做三件事:

  1. 搜索模块(在 sys.path 中查找)
  2. 执行模块代码(从上到下运行)
  3. 缓存模块(sys.modules 中缓存,避免重复加载)
# sys.modules 中可以看到所有已加载的模块
import sys
print("math" in sys.modules)  # False——还没导入 math
import math
print("math" in sys.modules)  # True——已缓存

sys.path 与模块搜索路径

Python 从哪里寻找模块?答案在 sys.path 中:

import sys

for path in sys.path:
    print(path)

典型的 sys.path 包含:

  1. 当前脚本所在目录(或当前工作目录)
  2. PYTHONPATH 环境变量中指定的目录
  3. Python 安装目录中的 site-packages(第三方库所在位置)
  4. Python 标准库目录

添加自定义搜索路径

import sys

# 方法 1:追加到 sys.path
sys.path.append("/my/custom/path")

# 方法 2:使用 PYTHONPATH 环境变量(推荐)
# 在命令行中:
# export PYTHONPATH="/my/custom/path:$PYTHONPATH"  # Linux/macOS
# set PYTHONPATH=C:\my\custom\path;%PYTHONPATH%     # Windows

# 方法 3:使用 site-packages(通过 pip 安装)

if __name__ == "__main__" 惯用法

这是 Python 中最重要的惯用法之一。它的作用是:当模块被直接执行时才运行某些代码,被导入的时候不运行。

# 文件:calculator.py

def add(a, b):
    return a + b

def sub(a, b):
    return a - b

# 测试代码——只在直接运行时执行
if __name__ == "__main__":
    print("测试 calculator 模块:")
    print(f"add(3, 5) = {add(3, 5)}")
    print(f"sub(10, 4) = {sub(10, 4)}")
# 文件:main.py
import calculator  # 导入时不会触发测试代码

print(calculator.add(1, 2))  # 3
# "测试 calculator 模块:" 不会被打印

原理:

  • 当模块被直接执行时,__name__ 被设为 "__main__"
  • 当模块被导入时,__name__ 被设为模块名(如 "calculator"

这个惯用法让每个 .py 文件既可以作为可复用的模块,又可以作为独立的脚本运行。

创建包

包(Package)是一个包含 __init__.py 文件的目录。当 Python 导入包时,会执行 __init__.py 中的代码。

包的结构

myproject/
├── main.py
└── mypackage/
    ├── __init__.py
    ├── math_ops.py
    └── string_ops.py
# mypackage/__init__.py
print("正在初始化 mypackage 包...")

# 可以在 __init__.py 中导入子模块,方便外部访问
from .math_ops import add, multiply
from .string_ops import reverse

__all__ = ["add", "multiply", "reverse"]
# mypackage/math_ops.py
def add(a, b):
    return a + b

def multiply(a, b):
    return a * b
# mypackage/string_ops.py
def reverse(text):
    return text[::-1]

def capitalize(text):
    return text.capitalize()
# main.py
import mypackage  # 打印 "正在初始化 mypackage 包..."

# 通过 __init__.py 暴露的接口访问
print(mypackage.add(3, 5))       # 8
print(mypackage.reverse("abc"))  # cba

# 也可以直接访问子模块
from mypackage import math_ops
print(math_ops.multiply(4, 7))  # 28

__init__.py 的设计哲学

__init__.py 定义了包的公共接口。好的包设计应该:

  • 只暴露用户需要的部分
  • 隐藏内部实现细节
  • 提供便捷的顶层导入
# 好的 __init__.py —— 精选导出
from .core import Engine
from .utils import format_result
from .exceptions import EngineError

__all__ = ["Engine", "format_result", "EngineError"]

# 内部实现细节不导出
# from .internal import _process, _validate  # 不暴露

绝对导入 vs 相对导入

绝对导入

从包的根目录开始指定完整路径:

import mypackage.math_ops
from mypackage.math_ops import add

# 在子包中:
from mypackage.subpackage import something

相对导入

相对于当前模块的位置,使用 . 表示当前目录,.. 表示上级目录:

# 在 mypackage/math_ops.py 中

# 导入同级的 string_ops
from .string_ops import reverse

# 导入父级的 utils
from ..utils import format_result

# 导入兄弟包的模块
from ..other_package import helper

重要限制:

  • 相对导入只能在包内部使用
  • 直接运行包内的模块(python mypackage/math_ops.py)会导致相对导入失败
  • 相对导入的模块不能作为入口脚本运行
# 正确使用方式
python -m mypackage.math_ops  # 使用 -m 参数以模块方式运行

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

方式 优点 缺点
绝对导入 清晰、稳定、可重命名包 包名较长时繁琐
相对导入 简洁、重命名包时无需修改 只能用于包内部,容易混淆

建议: 大多数情况下使用绝对导入。只有在包内部深层引用时才用相对导入。

__all__ 变量

__all__ 控制 from module import * 的行为,也是一个模块公开 API 的文档:

# 文件:utils.py
__all__ = ["format_date", "parse_email"]

def format_date(date):
    return date.strftime("%Y-%m-%d")

def parse_email(email):
    return email.split("@")

def _internal_helper():  # 以下划线开头表示内部函数
    pass

# 外部使用 from utils import * 时,只能得到 format_date 和 parse_email
# 包级别的 __all__ 在 __init__.py 中
# mypackage/__init__.py
from .math_ops import add, multiply
from .string_ops import reverse

__all__ = ["add", "multiply", "reverse"]

第三方包与 pip

Python 的第三方生态是其最大优势之一。使用 pip 来安装和管理第三方包:

# 安装包
pip install requests

# 安装特定版本
pip install requests==2.31.0

# 升级包
pip install --upgrade requests

# 卸载包
pip uninstall requests

# 查看已安装的包
pip list

# 查看包的信息
pip show requests

requirements.txt

当你需要分享项目依赖时,使用 requirements.txt 文件记录所有依赖:

# requirements.txt
requests==2.31.0
numpy>=1.24.0
pandas<2.0.0
flask>=2.3.0,<3.0.0
# 导出当前环境的依赖
pip freeze > requirements.txt

# 安装所有依赖
pip install -r requirements.txt

虚拟环境

每个项目应该有自己的虚拟环境,避免依赖冲突:

# 创建虚拟环境(Python 3.3+ 内置 venv 模块)
python -m venv venv

# 激活虚拟环境
# Windows:
venv\Scripts\activate
# Linux/macOS:
source venv/bin/activate

# 停用虚拟环境
deactivate

循环导入(Circular Import)

当模块 A 导入了模块 B,而模块 B 又(直接或间接)导入模块 A,就形成了循环导入。这通常会导致 ImportError

示例

# 文件:a.py
from b import func_b

def func_a():
    print("A")
    func_b()
# 文件:b.py
from a import func_a

def func_b():
    print("B")
    func_a()
# 运行 a.py
import a  # ImportError: cannot import name 'func_b' from partially initialized module 'b'

解决方案

方案 1:将公共代码提取到第三个模块

# 文件:common.py
# 存放 a 和 b 都依赖的公共代码

方案 2:延迟导入(在函数内部导入)

# a.py
def func_a():
    from b import func_b  # 在函数内导入,避免导入时立即执行
    print("A")
    func_b()

方案 3:使用 import 而非 from

# a.py
import b  # 导入模块而不是具体的函数

def func_a():
    print("A")
    b.func_b()  # 通过模块名访问

命名空间包(Namespace Package)

命名空间包是 Python 3.3+ 引入的概念。它允许一个包分布在多个目录中,而不需要每个目录都有 __init__.py

# 目录结构:
# project/
# ├── package/
# │   └── sub_a/
# │       └── module_a.py
# └── extension/
#     └── package/
#         └── sub_b/
#             └── module_b.py
# 只要 package 目录中没有 __init__.py,它们就组成一个命名空间包
import package.sub_a.module_a
import package.sub_b.module_b

命名空间包的主要用途是让一个包的多个部分可以来自不同的地方(例如插件系统)。

标准库精选

Python 自带"电池"——标准库。以下是一些常用标准库模块:

import os       # 操作系统接口
import sys      # 系统相关功能
import json     # JSON 处理
import re       # 正则表达式
import math     # 数学函数
import random   # 随机数
import datetime # 日期和时间
import pathlib  # 路径操作(推荐替代 os.path)
import collections  # 额外的数据结构
import itertools    # 迭代器工具
import functools    # 函数式工具
import logging      # 日志
import unittest     # 单元测试

实战:构建一个小型工具包

综合运用本章知识,创建一个日期处理工具包:

dateutils/
├── __init__.py
├── parsing.py
├── formatting.py
└── calculations.py
# dateutils/__init__.py
"""日期处理工具包"""

from .parsing import parse_date, try_parse
from .formatting import format_iso, format_cn, format_short
from .calculations import days_between, add_days, is_weekend

__all__ = [
    "parse_date", "try_parse",
    "format_iso", "format_cn", "format_short",
    "days_between", "add_days", "is_weekend",
]
# dateutils/parsing.py
from datetime import datetime

DATE_FORMATS = [
    "%Y-%m-%d",
    "%Y/%m/%d",
    "%Y.%m.%d",
    "%m-%d-%Y",
]

def parse_date(date_str: str) -> datetime:
    """尝试多种格式解析日期字符串"""
    for fmt in DATE_FORMATS:
        try:
            return datetime.strptime(date_str, fmt)
        except ValueError:
            continue
    raise ValueError(f"无法解析日期: {date_str}")

def try_parse(date_str: str):
    """安全地尝试解析,失败返回 None"""
    try:
        return parse_date(date_str)
    except ValueError:
        return None
# dateutils/formatting.py
from datetime import datetime

def format_iso(dt: datetime) -> str:
    """格式化为 ISO 标准"""
    return dt.strftime("%Y-%m-%d")

def format_cn(dt: datetime) -> str:
    """格式化为中文格式"""
    return f"{dt.year}{dt.month}{dt.day}日"

def format_short(dt: datetime) -> str:
    """格式化为短格式"""
    return dt.strftime("%m/%d")
# dateutils/calculations.py
from datetime import datetime, timedelta

def days_between(d1: datetime, d2: datetime) -> int:
    """计算两个日期之间的天数差"""
    return abs((d2.date() - d1.date()).days)

def add_days(dt: datetime, days: int) -> datetime:
    """增加指定天数"""
    return dt + timedelta(days=days)

def is_weekend(dt: datetime) -> bool:
    """判断是否是周末"""
    return dt.weekday() >= 5  # 5=周六, 6=周日
# 使用示例
from dateutils import parse_date, format_cn, days_between, is_weekend

d1 = parse_date("2026-05-12")
print(format_cn(d1))             # 2026年5月12日

d2 = parse_date("2026/06/01")
print(f"相差 {days_between(d1, d2)} 天")  # 相差 20 天

print(f"是周末吗?{is_weekend(d1)}")      # 是周末吗?False

小结

本章我们学习了 Python 的模块和包系统:

  • 模块.py 文件,是包含 __init__.py 的目录
  • import 的四种方式:整体导入、从模块导入、重命名导入、通配符导入
  • 模块搜索路径sys.path 决定了 Python 去哪里找模块
  • if __name__ == "__main__":让一个文件既可被导入又可独立运行
  • __init__.py:包的初始化代码和公共接口定义
  • 绝对导入 vs 相对导入from . importfrom mypackage import
  • __all__:控制模块的公开接口
  • 第三方包管理:pip、requirements.txt、虚拟环境
  • 循环导入:原因与三种解决方案
  • 命名空间包:无 __init__.py 的多目录包

下一步: 有了函数和模块的基础,接下来我们来处理实际开发中最常见的任务之一——文件读写操作。

Summary: 模块与包的概念、导入方式、pip 与虚拟环境。