文章
合集Python 语言基础第 21 / 21 篇

Python 模块系统:多文件协同与包管理

摘要
模块系统核心概念:
├── 模块 = .py 文件
├── 包 = 目录 + __init__.py
├── import 五种方式(推荐 import 模块名)
├── __name__ == "__main__"(模块自测)
└── 项目结构(src/包名/模块.py)

建议:

  1. 先学会拆分两个文件,再逐步引入包结构
  2. 善用 if __name__ == "__main__" 编写模块自测代码
  3. 避免 from xxx import *
  4. 遇到循环导入,说明模块职责划分需要重新设计

第 1 章:为什么需要多文件?

1.1 单文件的局限

想象你写了一个学生管理系统,所有代码都在一个文件中:

# student_system.py(1000+ 行)
def add_student(): ...      # 添加学生
def delete_student(): ...   # 删除学生
def update_student(): ...   # 更新学生
def query_student(): ...    # 查询学生
def save_to_file(): ...     # 文件操作
def load_from_file(): ...   # 文件操作
def print_menu(): ...       # 界面显示
def main(): ...             # 主程序

# 还有工具函数、常量定义、类定义...

问题:

  • 文件过长,查找代码需要大量滚动
  • 多人协作时同一文件频繁产生冲突
  • 想复用文件操作功能,只能复制粘贴

1.2 多文件的优势

student_system/
├── main.py        # 程序入口
├── models.py      # 数据模型(学生类)
├── storage.py     # 文件存储
├── ui.py          # 界面交互
└── utils.py       # 工具函数
  • 逻辑分离,每个文件职责单一
  • storage.py 可以在其他项目中直接复用
  • 团队协作,每人负责不同文件
  • 可以单独测试每个模块

第 2 章:模块基础

2.1 什么是模块?

模块就是一个 .py 文件。

# math_utils.py
def add(a, b):
    return a + b

def multiply(a, b):
    return a * b

PI = 3.14159
# main.py
import math_utils

result = math_utils.add(2, 3)
print(math_utils.PI)

2.2 import 的五种方式

方式 1:import 模块名(推荐)

import math_utils

print(math_utils.add(1, 2))
print(math_utils.PI)

命名空间清晰,一眼可知函数来自哪个模块。

方式 2:from 模块 import 名称

from math_utils import add, PI

print(add(1, 2))
print(PI)

代码更简洁,适合频繁使用的少量名称。

方式 3:from 模块 import *(不推荐)

from math_utils import *

print(add(1, 2))
print(multiply(3, 4))
print(PI)

方式 4:导入别名

import math_utils as mu
from math_utils import add as addition

print(mu.add(1, 2))
print(addition(1, 2))

适用场景:模块名过长,或避免与内置名称冲突。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

方式 5:相对导入(包内使用)

from . import module          # 同级目录
from .. import parent         # 上级目录
from .utils import helper     # 同级子模块

相对导入只能在包内部使用,直接运行文件(python module.py)会报错,必须用 python -m package.module 方式运行。

2.3 模块搜索路径

当执行 import xxx 时,Python 按以下顺序查找:

  1. 当前目录(运行脚本所在目录)
  2. PYTHONPATH 环境变量中的目录
  3. 标准库目录
  4. 第三方包目录(site-packages)
import sys
print(sys.path)   # 查看完整搜索路径列表

添加自定义路径:

import sys
sys.path.append('/path/to/your/modules')
import my_module

第 3 章:创建自己的模块

3.1 最简单的模块

my_project/
├── greeting.py
└── main.py
# greeting.py
def say_hello(name):
    return f"你好,{name}!"

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

if __name__ == "__main__":
    print(say_hello("世界"))
    print("这是 greeting 模块的测试输出")
# main.py
import greeting

message = greeting.say_hello("小明")
print(message)
python main.py
# 输出:你好,小明!

3.2 __name__ == "__main__" 的作用

这是模块系统中最重要的约定之一。

规则:

  • 文件被直接运行时,__name__ 的值是 "__main__"
  • 文件被导入时,__name__ 的值是模块名
# my_module.py
print(f"模块名: {__name__}")

if __name__ == "__main__":
    print("直接运行")
else:
    print("被导入")
# 直接运行
python my_module.py
# 输出:模块名: __main__
#       直接运行

# 作为模块导入
python -c "import my_module"
# 输出:模块名: my_module
#       被导入

用途:在 if __name__ == "__main__": 块内编写测试代码,导入时不会执行:

# calculator.py
def add(a, b):
    return a + b

def divide(a, b):
    if b == 0:
        raise ValueError("除数不能为零")
    return a / b

if __name__ == "__main__":
    assert add(2, 3) == 5
    assert divide(10, 2) == 5.0
    print("所有测试通过")

3.3 模块只执行一次

Python 会缓存已导入的模块,重复导入不会重新执行模块代码:

# config.py
print("config 模块已加载")
DB_HOST = "localhost"
# main.py
import config   # 输出:config 模块已加载
import config   # 无输出(从缓存中取)
import config   # 无输出

print(config.DB_HOST)

第 4 章:从模块到包

4.1 什么是包?

包是包含 __init__.py 文件的目录,用于组织多个模块。

mypackage/
├── __init__.py
├── module_a.py
├── module_b.py
└── subpackage/
    ├── __init__.py
    └── module_c.py

4.2 __init__.py 的作用

# mypackage/__init__.py

# 控制对外暴露的名称
__all__ = ['func_a', 'func_b', 'ClassA']

# 简化导入路径:让使用者可以直接 from mypackage import func_a
from .module_a import func_a
from .module_b import ClassB

# 包级别初始化
PACKAGE_VERSION = "1.0.0"
import mypackage

mypackage.func_a()
print(mypackage.PACKAGE_VERSION)

4.3 包的多层级导入

# 方式1:导入子包
from mypackage import subpackage
subpackage.module_c.some_func()

# 方式2:导入深层模块
from mypackage.subpackage import module_c
module_c.some_func()

# 方式3:直接导入具体函数
from mypackage.subpackage.module_c import some_func
some_func()

4.4 相对导入 vs 绝对导入

绝对导入(推荐):

from mypackage.module_a import func_a
from mypackage.subpackage.module_c import MyClass

相对导入(包内使用):

# mypackage/module_b.py
from .module_a import func_a        # 同级模块
from . import module_a              # 导入整个同级模块
from ..other_package import thing   # 上级包
from .subpackage import module_c    # 子包

第 5 章:项目结构规范

5.1 按规模选择结构

阶段 1:单文件(< 200 行)

hello.py

阶段 2:主程序 + 工具模块(200–500 行)

myproject/
├── main.py
├── utils.py
└── config.py

阶段 3:功能模块化(500–2000 行)

myproject/
├── main.py
├── requirements.txt
├── core/
│   ├── __init__.py
│   ├── models.py
│   ├── database.py
│   └── business.py
├── utils/
│   ├── __init__.py
│   ├── helpers.py
│   └── validators.py
└── config.py

5.2 中型项目标准结构

my_project/
├── README.md
├── LICENSE
├── requirements.txt
├── setup.py
├── .gitignore
│
├── src/
│   └── my_project/
│       ├── __init__.py
│       ├── __main__.py        # python -m my_project 入口
│       ├── cli.py
│       ├── core/
│       │   ├── __init__.py
│       │   ├── models.py
│       │   └── engine.py
│       ├── utils/
│       │   ├── __init__.py
│       │   └── helpers.py
│       └── config/
│           ├── __init__.py
│           └── settings.py
│
├── tests/
│   ├── __init__.py
│   ├── test_core.py
│   └── test_utils.py
│
└── docs/
    └── index.md

5.3 命名规范

类型规范示例
模块名小写,可用下划线my_module.py、string_utils.py
包名小写,简短core、utils
类名大驼峰StudentManager
函数名小写,下划线分隔calculate_score
常量大写,下划线分隔MAX_SIZE = 100
私有名称下划线前缀_internal_func、_private_var

常见命名错误:

不要用 Python 关键字:for.py, class.py, return.py
不要与标准库同名:random.py, json.py, sys.py
不要有大写或连字符:MyModule.py, my-module.py
推荐:my_module.py, data_processor.py

第 6 章:高级话题

6.1 循环导入问题

# a.py
from b import func_b

def func_a():
    return "A"

# b.py
from a import func_a

def func_b():
    return "B"

运行会报错:ImportError: cannot import name 'func_a'

解决方案:

方案 1:重构代码(推荐)

将共享代码提取到第三个模块:

common.py  <- 放共享函数
a.py       <- 导入 common
b.py       <- 导入 common

方案 2:延迟导入

# a.py
def func_a():
    from b import func_b   # 在使用时才导入
    return func_b() + "A"

方案 3:合并模块

如果两个模块高度耦合,考虑合并为一个文件。

6.2 __all__ 控制导出接口

# mymodule.py
__all__ = ['public_func', 'PublicClass']

def public_func():      # 外部可用
    pass

def _private_func():    # 约定内部使用,不在 __all__ 中
    pass

class PublicClass:      # 外部可用
    pass
from mymodule import *

public_func()           # 可以调用
_private_func()         # NameError:未被导入(未在 __all__ 中列出)

6.3 动态导入

import importlib

module_name = "math"
math = importlib.import_module(module_name)
print(math.sqrt(16))   # 4.0

应用场景:插件系统、根据配置加载不同后端实现。

def load_backend(name):
    module = importlib.import_module(f"backends.{name}")
    return module.Database()

db = load_backend("sqlite")   # 加载 backends/sqlite.py
db = load_backend("mysql")    # 加载 backends/mysql.py