02-自定义模块
Python 3.11+
概念铺垫
2.1 创建自己的模块
实际场景
你在多个项目中都要处理用户数据:验证邮箱、格式化手机号、计算年龄。每次都复制粘贴代码很麻烦,而且容易出错。
问题:如何把常用功能打包成模块,方便复用?
概念说明
任何 .py 文件都可以作为模块被导入。
2.2 模块的结构
┌─────────────────────────────────────────────────────────────┐
│ 推荐的模块结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ # 1. 模块文档字符串 │
│ """模块说明""" │
│ │
│ # 2. 导入语句 │
│ from __future__ import annotations │
│ import os │
│ from typing import List, Optional │
│ │
│ # 3. 模块级常量 │
│ VERSION = '1.0.0' │
│ DEFAULT_TIMEOUT = 30 │
│ │
│ # 4. 类定义 │
│ class MyClass: │
│ pass │
│ │
│ # 5. 函数定义 │
│ def my_function() -> None: │
│ pass │
│ │
│ # 6. 测试代码 │
│ if __name__ == '__main__': │
│ # 测试逻辑 │
│ pass │
│ │
└─────────────────────────────────────────────────────────────┘模块搜索路径
sys.path
Python 按照特定顺序搜索模块。
搜索顺序:
┌─────────────────────────────────────────────────────────────┐
│ 模块搜索顺序 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 当前目录 │
│ 运行脚本的所在目录 │
│ │
│ 2. PYTHONPATH 环境变量 │
│ 系统环境变量指定的目录 │
│ │
│ 3. 标准库目录 │
│ Python 安装目录下的库 │
│ │
│ 4. site-packages │
│ pip 安装的第三方库 │
│ │
│ 5. .pth 文件 │
│ site-packages 中的路径配置文件 │
│ │
└─────────────────────────────────────────────────────────────┘模块的私有成员
python
# my_module.py
# 约定的私有变量(单下划线)
_internal_var: str = "内部使用"
# 强私有变量(双下划线,会被名称改写)
__private_var: str = "强私有"
# 约定的私有函数
def _internal_function() -> None:
"""内部函数,不建议外部调用"""
pass
# 公开函数
def public_function() -> str:
"""公开函数"""
return "public"使用建议:
python
import my_module
# 可以访问但不推荐
print(my_module._internal_var)
# 公开接口
my_module.public_function()模块的懒加载
python
# lazy_module.py
from __future__ import annotations
from typing import Any
import time
def expensive_initialization() -> dict[str, Any]:
"""耗时的初始化"""
time.sleep(2)
return {"data": "initialized"}
# 懒加载:只在需要时初始化
_cache: dict[str, Any] | None = None
def get_data() -> dict[str, Any]:
global _cache
if _cache is None:
_cache = expensive_initialization()
return _cacheL1 理解层:会用
基本模块
python
# user_utils.py
"""用户数据处理工具模块"""
from __future__ import annotations
from typing import Optional
import re
from datetime import datetime
def validate_email(email: str) -> bool:
"""验证邮箱格式
Args:
email: 邮箱地址
Returns:
True 如果格式正确,否则 False
"""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
def format_phone(phone: str) -> str:
"""格式化手机号为标准格式
Args:
phone: 手机号码字符串
Returns:
格式化后的手机号,如 "138-1234-5678"
"""
digits = ''.join(filter(str.isdigit, phone))
if len(digits) == 11:
return f"{digits[:3]}-{digits[3:7]}-{digits[7:]}"
return phone
def calculate_age(birth_year: int) -> int:
"""根据出生年份计算年龄
Args:
birth_year: 出生年份
Returns:
当前年龄
"""
current_year = datetime.now().year
return current_year - birth_year
if __name__ == '__main__':
# 测试代码
print(validate_email("test@example.com")) # True
print(format_phone("13812345678")) # 138-1234-5678
print(calculate_age(1990)) # 36 (2026年)导入使用:
python
# main.py
import user_utils
result = user_utils.validate_email("test@example.com")
print(result) # True
age = user_utils.calculate_age(1990)
print(age) # 36sys.path 使用
python
import sys
# 查看模块搜索路径
for i, path in enumerate(sys.path):
print(f"{i}: {path}")输出示例:
0: /Users/user/project # 当前目录
1: /usr/lib/python3.11 # 标准库目录
2: /usr/lib/python3.11/site-packages # 第三方库目录
3: /Users/user/.local/lib/python3.11/site-packages添加自定义路径
python
import sys
# 方式 1:临时添加
sys.path.append('/path/to/my/modules')
import my_module
# 方式 2:使用 .pth 文件
# 在 site-packages 目录创建 my_paths.pth
# 内容:/path/to/my/modules
# 方式 3:设置 PYTHONPATH 环境变量
# export PYTHONPATH=/path/to/my/modules:$PYTHONPATH__all__ 变量
__all__ 定义了 from module import * 时导入的内容。
python
# my_module.py
from __future__ import annotations
__all__ = ['func_a', 'func_b', 'MyClass']
def func_a() -> str:
return "A"
def func_b() -> str:
return "B"
def _internal_func() -> str: # 不在 __all__ 中
return "internal"
class MyClass:
pass
# 使用
from my_module import *
print(func_a()) # A
print(func_b()) # B
# _internal_func() # NameError: 未导入文档字符串
python
"""计算器模块
提供基本的数学运算功能。
Example:
>>> from calculator import add
>>> add(1, 2)
3
Attributes:
PI (float): 圆周率常量
E (float): 自然常数
"""
def add(a: int, b: int) -> int:
"""两数相加
Args:
a: 第一个数
b: 第二个数
Returns:
两数之和
Raises:
TypeError: 参数类型错误
Example:
>>> add(1, 2)
3
"""
return a + b类型提示
python
from __future__ import annotations
from typing import Optional
def process_data(
items: list[dict[str, str | int]],
threshold: int | None = None
) -> dict[str, int]:
"""处理数据"""
result: dict[str, int] = {}
for item in items:
key = item.get('name', '')
value = item.get('value', 0)
if threshold is None or value >= threshold:
result[key] = value
return resultL2 实践层:用好
推荐做法
| 做法 | 原因 | 示例 |
|---|---|---|
| 模块顶部写文档字符串 | 帮助自己和他人理解模块用途 | """用户数据处理工具模块""" |
| 使用类型提示标注参数和返回值 | 提高代码可读性和 IDE 支持 | def validate_email(email: str) -> bool: |
| 用单下划线约定私有成员 | 告知使用者这是内部实现 | def _internal_helper(): |
| 模块级常量用大写命名 | 区分常量与变量 | VERSION = "1.0.0" |
| 懒加载昂贵初始化 | 减少导入时的开销 | 延迟到函数调用时初始化 |
测试代码放在 if __name__ == "__main__" | 模块被导入时不执行测试 | 见上方 user_utils.py 示例 |
使用 from __future__ import annotations | 避免类型注解的循环依赖和运行时开销 | Python 3.11+ 建议启用 |
反模式:不要这样做
python
# ❌ 没有模块文档
# mysterious.py
def f(x):
return x * 2
# ✅ 添加文档,说明模块和函数的功能
"""数据处理工具模块"""
def double_value(x: int) -> int:
"""将输入值翻倍"""
return x * 2
# ---
# ❌ 模块顶层执行副作用
# bad_module.py
import requests
CONFIG = requests.get("https://api.example.com/config").json()
# 每次导入都会发起网络请求!慢且不可靠
# ✅ 延迟获取配置
# good_module.py
import requests
from functools import lru_cache
@lru_cache(maxsize=1)
def get_config() -> dict:
return requests.get("https://api.example.com/config").json()
# ---
# ❌ 忽略 `if __name__ == "__main__"` 保护
# no_guard.py
def setup():
print("初始化资源...")
setup() # 导入时就执行,可能破坏导入者的预期
# ✅ 加上保护
# with_guard.py
def setup():
print("初始化资源...")
if __name__ == "__main__":
setup()
# ---
# ❌ 模块中混用不同职责的功能
# kitchen_sink.py
def validate_email(email): ... # 验证
def format_phone(phone): ... # 格式化
def connect_database(): ... # 数据库
def send_notification(): ... # 通知
# 什么都做,难以维护和复用
# ✅ 按职责拆分
# validators.py — 验证相关
# formatters.py — 格式化相关
# database.py — 数据库相关
# notifications.py — 通知相关适用场景
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 多个项目共享工具函数 | 打包为独立模块,放在公共路径下 | 避免复制粘贴,统一维护 |
| 模块需要配置文件 | 使用懒加载或依赖注入 | 避免导入时读取文件阻塞 |
| 开发阶段频繁修改模块 | 使用 importlib.reload() | 无需重启 Python 进程 |
| 发布为第三方库 | 设置 __all__ 控制公开 API | 明确对外接口,隐藏实现细节 |
| 大型模块(>500行) | 拆分为多个子模块,用包组织 | 提高可维护性和可读性 |
| 类型检查需要跨模块引用 | 使用 TYPE_CHECKING + 延迟导入 | 避免循环导入 |
L3 专家层:深入
Python 如何实现:模块对象的内部结构
CPython 中每个模块在运行时都有一个 PyModuleObject 结构体:
┌──────────────────────────────────────────────────────────────┐
│ CPython 模块对象 (PyModuleObject) │
├──────────────────────────────────────────────────────────────┤
│ │
│ typedef struct { │
│ PyObject_HEAD │
│ PyObject *md_dict; // __dict__ → 模块命名空间 │
│ } PyModuleObject; │
│ │
│ 模块的执行过程: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ import user_utils │ │
│ │ │ │ │
│ │ ├── 1. 创建空模块对象 module │ │
│ │ │ module = PyModule_New("user_utils") │ │
│ │ │ │ │
│ │ ├── 2. 设置模块属性 │ │
│ │ │ module.__name__ = "user_utils" │ │
│ │ │ module.__file__ = "user_utils.py" │ │
│ │ │ module.__package__ = "" or "pkg_name" │ │
│ │ │ module.__loader__ = SourceFileLoader() │ │
│ │ │ │ │
│ │ ├── 3. 初始化模块的 __dict__ │ │
│ │ │ PyModule_GetDict(module) → md_dict │ │
│ │ │ │ │
│ │ ├── 4. 执行模块源代码 │ │
│ │ │ exec(code_object, module.__dict__) │ │
│ │ │ # 所有顶层定义的变量/函数/类都写入 md_dict│ │
│ │ │ │ │
│ │ └── 5. 存入缓存 │ │
│ │ sys.modules["user_utils"] = module │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘__name__ == "__main__" 的深入机制
┌──────────────────────────────────────────────────────────────┐
│ __name__ 在直接运行 vs 导入时的不同值 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 直接运行:python my_module.py │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. Python 解释器启动 │ │
│ │ 2. 创建一个主模块 (__main__) │ │
│ │ 3. 读取 my_module.py 源码 │ │
│ │ 4. 编译并在 __main__ 的命名空间中执行 │ │
│ │ 5. module.__name__ = "__main__" ← 关键! │ │
│ │ 6. if __name__ == "__main__": → True │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 作为模块导入:import my_module │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. import 语句触发 │ │
│ │ 2. 创建模块对象 module = PyModule_New(...) │ │
│ │ 3. module.__name__ = "my_module" ← 关键! │ │
│ │ 4. 编译并执行 my_module.py │ │
│ │ 5. if __name__ == "__main__": → False │ │
│ │ 6. 测试代码被跳过 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘验证代码:
python
# test_name.py
import sys
print(f"__name__ = {__name__!r}")
print(f"__file__ = {__file__!r}")
print(f"__package__ = {__package__!r}")
# 检查 sys.modules 中的主模块
main_module = sys.modules.get("__main__")
if main_module:
print(f"sys.modules['__main__'] 存在")
print(f" __name__ = {main_module.__name__!r}")
print(f" __file__ = {getattr(main_module, '__file__', None)!r}")
# 直接运行输出:
# __name__ = '__main__'
# __file__ = 'test_name.py'
# __package__ = None
# import test_name 输出:
# __name__ = 'test_name'
# __file__ = '.../test_name.py'
# __package__ = ''模块对象的完整属性
python
import importlib
import inspect
# 创建一个简单的模块来观察其内部
import types
mod = types.ModuleType("my_dynamic_module")
mod.__doc__ = "动态创建的模块"
mod.__package__ = None
# 模块的关键属性
print("模块属性:")
for attr in [
"__name__", "__doc__", "__file__", "__dict__",
"__package__", "__loader__", "__spec__",
"__path__", "__annotations__"
]:
value = getattr(mod, attr, "<未设置>")
print(f" {attr:20s} = {value!r}")
# 观察实际模块
import math
print(f"\nmath 模块的类型: {type(math)}")
print(f"math 是 ModuleType: {isinstance(math, types.ModuleType)}")
print(f"math.__dict__ 键数量: {len(math.__dict__)}")
print(f"math.__dict__ 前5个键: {list(math.__dict__.keys())[:5]}")性能考量
| 操作 | 复杂度 | 说明 |
|---|---|---|
| 创建空模块对象 | O(1) | PyModule_New() 只是分配一个结构体 |
| 执行模块代码 | O(代码行数 + 执行逻辑) | 取决于模块中顶层代码的复杂度 |
module.__dict__ 查找 | O(1) 平均 | 底层是哈希字典 |
dir(module) | O(n) | n = 模块中定义的名称数量 |
| 懒加载初始化 | O(1) 首次检查 + O(初始化) 按需 | 用 if _cache is None 模式 |
| 模块级常量赋值 | O(1) | 导入时立即赋值,无额外开销 |
知识关联
┌──────────────────────────────────────────────────────────────┐
│ 自定义模块 知识关联图 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 本章(自定义模块) │
│ │ │
│ ├── 模块结构规范 ────────► 06-发布自己的包(pyproject) │
│ │ │
│ ├── sys.path ───────────► 01-模块基础(搜索路径) │
│ │ │
│ ├── __all__ ────────────► 03-包的结构(包级 __all__) │
│ │ │
│ ├── 私有成员 _name ─────► OOP(封装原则) │
│ │ │
│ ├── 懒加载模式 ─────────► 性能优化(延迟初始化) │
│ │ │
│ ├── __name__ =="__main__" → 01-模块基础(__name__) │
│ │ │
│ └── 模块对象类型 ────────► 01-模块基础(元类/反射) │
│ │
└──────────────────────────────────────────────────────────────┘本章小结
┌─────────────────────────────────────────────────────────────┐
│ 自定义模块 知识要点 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 创建模块: │
│ ✓ 任何 .py 文件都是模块 │
│ ✓ 推荐结构:文档 → 导入 → 常量 → 类 → 函数 → 测试 │
│ │
│ 搜索路径: │
│ ✓ sys.path 列表 │
│ ✓ 顺序:当前目录 → PYTHONPATH → 标准库 → site-packages │
│ ✓ 添加路径:sys.path.append()、.pth 文件 │
│ │
│ 高级特性: │
│ ✓ __all__:控制 import * 的内容 │
│ ✓ _name:约定的私有成员 │
│ ✓ 懒加载:延迟初始化 │
│ │
│ 最佳实践: │
│ ✓ 文档字符串 │
│ ✓ 类型提示 │
│ ✓ 测试代码放在 if __name__ == '__main__' │
│ │
└─────────────────────────────────────────────────────────────┘