Python 模块与包

0 阅读17分钟

Python 模块与包

目录


模块基础

什么是模块?

模块是一个包含 Python 代码的文件(.py 文件),可以包含:

  • 变量
  • 函数
  • 可执行代码

模块的作用

  • 代码组织和复用
  • 命名空间管理
  • 提高代码可维护性
# math_operations.py - 一个简单的模块
"""数学运算模块"""

def add(a, b):
    """加法"""
    return a + b

def subtract(a, b):
    """减法"""
    return a - b

def multiply(a, b):
    """乘法"""
    return a * b

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

# 模块级别的变量
PI = 3.141592653589793
E = 2.718281828459045

使用模块

# main.py - 使用上面定义的模块
import math_operations

result = math_operations.add(10, 5)
print(result)  # 15

print(math_operations.PI)  # 3.141592653589793

导入模块

import 语句

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

# 方法2:从模块导入特定内容
from math import sqrt, pi
print(sqrt(16))  # 4.0
print(pi)        # 3.141592653589793

# 方法3:导入所有内容(不推荐)
from math import *
print(sqrt(16))
print(pi)

# 方法4:给模块起别名
import numpy as np
import pandas as pd

导入的区别

# 示例模块 my_module.py
VALUE = 100

def get_value():
    return VALUE

def set_value(new_value):
    global VALUE
    VALUE = new_value
# 测试不同的导入方式

# 方式1:import module
import my_module
print(my_module.VALUE)      # 100
my_module.set_value(200)
print(my_module.VALUE)      # 200(修改生效)

# 方式2:from module import name
from my_module import VALUE, get_value
print(VALUE)                # 100
# VALUE = 300               # 这只是创建了局部变量,不影响模块
print(get_value())          # 100(仍然是原始值)

条件导入

# 根据条件导入不同的模块
import sys

if sys.version_info >= (3, 9):
    from collections import OrderedDict
else:
    # 旧版本的兼容处理
    pass

# 尝试导入,失败则使用替代方案
try:
    import ujson as json
except ImportError:
    import json

延迟导入

# 在函数内部导入,减少启动时间
def heavy_operation():
    import numpy as np  # 只在需要时导入
    return np.array([1, 2, 3])

# 或者在函数开始时导入
def process_data(data):
    import pandas as pd
    df = pd.DataFrame(data)
    return df

模块搜索路径

sys.path

import sys

# 查看模块搜索路径
for path in sys.path:
    print(path)

# 输出示例:
# /Users/username/project
# /usr/local/lib/python3.9/site-packages
# /usr/local/lib/python3.9
# ...

添加搜索路径

import sys

# 方法1:运行时添加(临时)
sys.path.append('/path/to/my/modules')
sys.path.insert(0, '/path/to/my/modules')  # 优先搜索

# 方法2:设置环境变量(永久)
# export PYTHONPATH=/path/to/my/modules:$PYTHONPATH

# 方法3:使用 .pth 文件
# 在 site-packages 目录创建 mypath.pth 文件
# 文件中写入路径,每行一个

模块查找顺序

  1. 内置模块
  2. 当前目录
  3. PYTHONPATH 环境变量
  4. 安装目录(site-packages)
# 检查模块来源
import os
print(os.__file__)  # 显示模块文件路径

import sys
print(sys.__file__)  # None(内置模块)

创建模块

基本模块结构

# utils.py
"""
工具函数模块

这个模块提供常用的工具函数。
"""

# 模块文档字符串
__author__ = "Your Name"
__version__ = "1.0.0"

# 公开的内容
def format_date(date_obj):
    """格式化日期"""
    return date_obj.strftime("%Y-%m-%d")

def validate_email(email):
    """验证邮箱格式"""
    return "@" in email and "." in email

# 私有内容(约定:以下划线开头)
def _internal_helper():
    """内部辅助函数,不建议外部使用"""
    pass

# 控制 from module import * 导入的内容
__all__ = ['format_date', 'validate_email']

__name____main__

# calculator.py
"""计算器模块"""

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

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

# 当直接运行此文件时执行
if __name__ == "__main__":
    # 测试代码
    print("测试计算器模块")
    print(f"add(2, 3) = {add(2, 3)}")
    print(f"multiply(2, 3) = {multiply(2, 3)}")

# 当被导入时,上面的代码不会执行
# 直接运行
$ python calculator.py
测试计算器模块
add(2, 3) = 5
multiply(2, 3) = 6

# 导入使用
$ python
>>> import calculator
>>> calculator.add(2, 3)
5

模块缓存

# Python 会缓存已导入的模块
import sys

import my_module
print(id(my_module))  # 内存地址

import my_module  # 再次导入
print(id(my_module))  # 相同的内存地址(缓存)

# 查看已缓存的模块
print(sys.modules.keys())

# 强制重新加载(开发时使用)
import importlib
importlib.reload(my_module)

包基础

什么是包?

包是包含多个模块的目录,必须包含 __init__.py 文件(Python 3.3+ 可选)。

my_package/
├── __init__.py      # 包初始化文件
├── module1.py       # 模块1
├── module2.py       # 模块2
└── subpackage/      # 子包
    ├── __init__.py
    └── module3.py

创建包

math_utils/
├── __init__.py
├── arithmetic.py
├── geometry.py
└── statistics.py
# math_utils/__init__.py
"""数学工具包"""

__version__ = "1.0.0"

# 从子模块导入常用功能
from .arithmetic import add, subtract, multiply, divide
from .geometry import circle_area, rectangle_area

# 控制包的公开接口
__all__ = [
    'add', 'subtract', 'multiply', 'divide',
    'circle_area', 'rectangle_area'
]
# math_utils/arithmetic.py
"""算术运算"""

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

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

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

def divide(a, b):
    if b == 0:
        raise ValueError("除数不能为零")
    return a / b
# math_utils/geometry.py
"""几何计算"""

import math

def circle_area(radius):
    """圆的面积"""
    return math.pi * radius ** 2

def rectangle_area(width, height):
    """矩形面积"""
    return width * height

def triangle_area(base, height):
    """三角形面积"""
    return 0.5 * base * height

使用包

# 方法1:导入整个包
import math_utils
result = math_utils.add(10, 5)

# 方法2:从包导入特定模块
from math_utils import arithmetic
result = arithmetic.multiply(3, 4)

# 方法3:从包导入特定函数
from math_utils import add, circle_area
print(add(10, 5))
print(circle_area(5))

# 方法4:给包起别名
import math_utils as mu
result = mu.subtract(10, 5)

包的导入

绝对导入

# 项目结构
project/
├── main.py
└── package/
    ├── __init__.py
    ├── module_a.py
    └── module_b.py
# package/module_a.py
def func_a():
    return "Function A"
# package/module_b.py
# 绝对导入
from package.module_a import func_a

def func_b():
    return func_a() + " and Function B"
# main.py
from package.module_b import func_b
print(func_b())  # Function A and Function B

包的 __init__.py

# package/__init__.py
"""包的初始化文件"""

# 1. 包元信息
__version__ = "1.0.0"
__author__ = "Your Name"

# 2. 导入子模块,简化使用
from .module_a import func_a
from .module_b import func_b

# 3. 初始化代码
print("包已加载")

# 4. 控制公开接口
__all__ = ['func_a', 'func_b']
# 使用时更简洁
import package
print(package.func_a())
print(package.func_b())

子包结构

web_framework/
├── __init__.py
├── core/
│   ├── __init__.py
│   ├── router.py
│   └── middleware.py
├── templates/
│   ├── __init__.py
│   └── renderer.py
└── database/
    ├── __init__.py
    ├── models.py
    └── connection.py
# web_framework/core/router.py
class Router:
    def __init__(self):
        self.routes = {}

    def add_route(self, path, handler):
        self.routes[path] = handler
# web_framework/__init__.py
from .core.router import Router
from .database.connection import Database

__all__ = ['Router', 'Database']
# 使用
from web_framework import Router, Database

router = Router()
db = Database()

相对导入

相对导入语法

# 在项目内部使用相对导入

# package/subpackage/module.py

# 同级模块
from . import sibling_module
from .sibling_module import some_function

# 父级模块
from .. import parent_module
from ..parent_module import some_function

# 兄弟包的模块
from ..other_package import module

相对导入示例

project/
├── main.py
└── app/
    ├── __init__.py
    ├── models.py
    ├── views.py
    └── utils/
        ├── __init__.py
        └── helpers.py
# app/models.py
class User:
    def __init__(self, name):
        self.name = name
# app/utils/helpers.py
def format_user(user):
    return f"User: {user.name}"
# app/views.py
# 相对导入
from .models import User              # 同级模块
from .utils.helpers import format_user  # 子包模块

def show_user(name):
    user = User(name)
    return format_user(user)
# main.py
# 使用绝对导入
from app.views import show_user

print(show_user("Alice"))  # User: Alice

相对导入的限制

# ❌ 错误:不能在顶级脚本中使用相对导入
# main.py
from .app.models import User  # ImportError

# ✅ 正确:使用绝对导入
from app.models import User

命名空间包

什么是命名空间包?

命名空间包允许将同一个包分布在多个目录中。

# 目录结构
namespace_pkg/
├── part1/
│   └── namespace_pkg/
│       └── module1.py
└── part2/
    └── namespace_pkg/
        └── module2.py
# 注意:命名空间包不需要 __init__.py
# 使用
import namespace_pkg.module1
import namespace_pkg.module2

PEP 420 隐式命名空间包

Python 3.3+ 支持隐式命名空间包(没有 __init__.py 的目录)。


常用内置模块

os 模块 - 操作系统接口

import os

# 文件和目录操作
print(os.getcwd())              # 当前工作目录
os.chdir('/path/to/dir')        # 改变目录
print(os.listdir('.'))          # 列出目录内容

# 路径操作
path = os.path.join('dir', 'subdir', 'file.txt')
print(os.path.exists(path))     # 检查是否存在
print(os.path.isfile(path))     # 是否是文件
print(os.path.isdir(path))      # 是否是目录

# 环境变量
print(os.environ.get('HOME'))
os.environ['MY_VAR'] = 'value'

# 执行系统命令
os.system('ls -la')
result = os.popen('ls -la').read()

sys 模块 - 系统相关

import sys

# Python 版本
print(sys.version)
print(sys.version_info)

# 命令行参数
print(sys.argv)  # ['script.py', 'arg1', 'arg2']

# 标准输入输出
sys.stdout.write("Hello\n")
sys.stderr.write("Error\n")

# 退出程序
sys.exit(0)   # 正常退出
sys.exit(1)   # 异常退出

# 模块搜索路径
print(sys.path)

# 已加载的模块
print(list(sys.modules.keys())[:10])

datetime 模块 - 日期时间

from datetime import datetime, date, time, timedelta

# 当前时间
now = datetime.now()
print(now)                      # 2024-01-15 10:30:00.123456
print(now.strftime("%Y-%m-%d %H:%M:%S"))

# 创建日期时间
dt = datetime(2024, 1, 15, 10, 30, 0)
d = date(2024, 1, 15)
t = time(10, 30, 0)

# 日期运算
tomorrow = now + timedelta(days=1)
yesterday = now - timedelta(days=1)
next_hour = now + timedelta(hours=1)

# 解析字符串
date_str = "2024-01-15 10:30:00"
parsed = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")

# 时间差
diff = tomorrow - now
print(diff.days)        # 1
print(diff.seconds)     # 0

json 模块 - JSON 处理

import json

# Python 对象转 JSON
data = {
    "name": "张三",
    "age": 25,
    "city": "北京",
    "hobbies": ["读书", "游泳"]
}

json_str = json.dumps(data, ensure_ascii=False, indent=2)
print(json_str)

# JSON 转 Python 对象
parsed = json.loads(json_str)
print(parsed["name"])

# 读写 JSON 文件
with open('data.json', 'w', encoding='utf-8') as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

with open('data.json', 'r', encoding='utf-8') as f:
    loaded_data = json.load(f)

re 模块 - 正则表达式

import re

# 匹配
text = "我的电话是 138-1234-5678"
pattern = r"\d{3}-\d{4}-\d{4}"
match = re.search(pattern, text)
if match:
    print(match.group())  # 138-1234-5678

# 查找所有
emails = re.findall(r'\w+@\w+\.\w+', "联系: test@example.com 或 admin@test.org")
print(emails)  # ['test@example.com', 'admin@test.org']

# 替换
text = "颜色: red, green, blue"
result = re.sub(r'red|green|blue', 'COLOR', text)
print(result)  # 颜色: COLOR, COLOR, COLOR

# 分割
parts = re.split(r'[,\s]+', "apple,banana orange grape")
print(parts)  # ['apple', 'banana', 'orange', 'grape']

# 编译正则(重复使用时更高效)
pattern = re.compile(r'\d+')
numbers = pattern.findall("我有 3 个苹果和 5 个橙子")
print(numbers)  # ['3', '5']

collections 模块 - 容器数据类型

from collections import Counter, defaultdict, OrderedDict, deque, namedtuple

# Counter - 计数器
words = ["apple", "banana", "apple", "orange", "banana", "apple"]
counter = Counter(words)
print(counter)           # Counter({'apple': 3, 'banana': 2, 'orange': 1})
print(counter.most_common(2))  # [('apple', 3), ('banana', 2)]

# defaultdict - 默认字典
dd = defaultdict(list)
dd['fruits'].append('apple')
dd['fruits'].append('banana')
print(dd)  # defaultdict(<class 'list'>, {'fruits': ['apple', 'banana']})

# OrderedDict - 有序字典(Python 3.7+ dict 已有序)
od = OrderedDict()
od['first'] = 1
od['second'] = 2
od['third'] = 3

# deque - 双端队列
dq = deque([1, 2, 3])
dq.appendleft(0)    # [0, 1, 2, 3]
dq.append(4)        # [0, 1, 2, 3, 4]
dq.popleft()        # [1, 2, 3, 4]

# namedtuple - 命名元组
Point = namedtuple('Point', ['x', 'y'])
p = Point(10, 20)
print(p.x, p.y)  # 10 20

itertools 模块 - 迭代器工具

from itertools import count, cycle, repeat, chain, combinations, permutations

# 无限迭代器
for i in count(10, 2):  # 从10开始,步长2
    if i > 20:
        break
    print(i, end=" ")  # 10 12 14 16 18 20

# 循环迭代器
colors = cycle(['red', 'green', 'blue'])
for _ in range(5):
    print(next(colors), end=" ")  # red green blue red green

# 组合和排列
items = [1, 2, 3]
print(list(combinations(items, 2)))  # [(1,2), (1,3), (2,3)]
print(list(permutations(items, 2)))  # [(1,2), (1,3), (2,1), (2,3), (3,1), (3,2)]

# 链式迭代
combined = chain([1, 2], [3, 4], [5, 6])
print(list(combined))  # [1, 2, 3, 4, 5, 6]

functools 模块 - 高阶函数

from functools import lru_cache, wraps, partial, reduce

# 缓存装饰器
@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(100))  # 快速计算

# 偏函数
def power(base, exponent):
    return base ** exponent

square = partial(power, exponent=2)
cube = partial(power, exponent=3)

print(square(5))  # 25
print(cube(5))    # 125

# reduce
from functools import reduce
numbers = [1, 2, 3, 4, 5]
product = reduce(lambda x, y: x * y, numbers)
print(product)  # 120

第三方包管理

pip 基本用法

# 安装包
pip install requests
pip install numpy==1.21.0  # 指定版本
pip install "django>=3.0,<4.0"  # 版本范围

# 卸载包
pip uninstall requests

# 查看已安装的包
pip list
pip freeze

# 导出依赖
pip freeze > requirements.txt

# 从文件安装
pip install -r requirements.txt

# 升级包
pip install --upgrade requests

# 查看包信息
pip show requests

requirements.txt

# requirements.txt
requests==2.28.0
numpy>=1.21.0
pandas~=1.4.0
flask>=2.0,<3.0
# 安装所有依赖
pip install -r requirements.txt

pyproject.toml(现代方式)

# pyproject.toml
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.backends._legacy:_Backend"

[project]
name = "my-package"
version = "1.0.0"
description = "我的 Python 包"
authors = [
    {name = "Your Name", email = "your@email.com"}
]
dependencies = [
    "requests>=2.28.0",
    "numpy>=1.21.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0",
    "black>=22.0",
]

Poetry(依赖管理工具)

# 初始化项目
poetry init

# 添加依赖
poetry add requests
poetry add numpy --group dev

# 安装依赖
poetry install

# 运行命令
poetry run python main.py

# 构建包
poetry build
poetry publish

虚拟环境

为什么需要虚拟环境?

  • 隔离不同项目的依赖
  • 避免版本冲突
  • 便于部署和分享

venv(内置)

# 创建虚拟环境
python -m venv myenv

# 激活虚拟环境
# macOS/Linux
source myenv/bin/activate

# Windows
myenv\Scripts\activate

# 退出虚拟环境
deactivate

# 在虚拟环境中安装包
pip install requests
pip freeze > requirements.txt

virtualenv(第三方)

# 安装
pip install virtualenv

# 创建
virtualenv myenv

# 指定 Python 版本
virtualenv -p python3.9 myenv

conda

# 创建环境
conda create -n myenv python=3.9

# 激活
conda activate myenv

# 安装包
conda install numpy
conda install -c conda-forge requests

# 导出环境
conda env export > environment.yml

# 从文件创建
conda env create -f environment.yml

虚拟环境最佳实践

# 1. 每个项目使用独立的虚拟环境
project1/
├── venv/
├── src/
└── requirements.txt

project2/
├── venv/
├── src/
└── requirements.txt

# 2. 将虚拟环境目录加入 .gitignore
echo "venv/" >> .gitignore
echo ".env/" >> .gitignore

# 3. 始终记录依赖
pip freeze > requirements.txt

模块最佳实践

1. 模块化设计原则

# ❌ 不推荐:一个大文件做所有事
# app.py (1000+ 行)

# ✅ 推荐:拆分成多个模块
myapp/
├── __init__.py
├── models.py      # 数据模型
├── views.py       # 视图逻辑
├── services.py    # 业务逻辑
├── utils.py       # 工具函数
└── config.py      # 配置

2. 清晰的导入组织

# ✅ 推荐的导入顺序

# 1. 标准库
import os
import sys
from datetime import datetime

# 2. 第三方库
import requests
import numpy as np

# 3. 本地应用/库
from myapp.models import User
from myapp.utils import helper

# 每组之间空一行

3. 避免循环导入

# ❌ 循环导入
# module_a.py
from module_b import func_b  # 导入 module_b

def func_a():
    return "A"

# module_b.py
from module_a import func_a  # 导入 module_a(循环!)

def func_b():
    return "B"

# ✅ 解决方案1:延迟导入
# module_b.py
def func_b():
    from module_a import func_a  # 在函数内导入
    return func_a()

# ✅ 解决方案2:重构代码,提取共同依赖
# common.py
def shared_function():
    pass

# module_a.py
from common import shared_function

# module_b.py
from common import shared_function

4. 使用 __all__ 控制公开接口

# my_module.py
__all__ = ['public_func1', 'public_func2']

def public_func1():
    """公开函数"""
    pass

def public_func2():
    """公开函数"""
    pass

def _private_func():
    """私有函数,不会被 import * 导入"""
    pass

5. 编写模块文档

"""
模块名称:数据处理工具

功能描述:
    提供常用的数据处理和转换功能。

主要功能:
    - 数据清洗
    - 格式转换
    - 统计分析

使用示例:
    >>> from data_utils import clean_data
    >>> clean_data([1, None, 3])
    [1, 3]

作者:Your Name
版本:1.0.0
"""

__author__ = "Your Name"
__version__ = "1.0.0"

6. 懒加载

# 大型模块可以懒加载以提高启动速度

class LazyModule:
    def __init__(self, module_name):
        self._module_name = module_name
        self._module = None

    def __getattr__(self, name):
        if self._module is None:
            self._module = __import__(self._module_name)
        return getattr(self._module, name)

# 使用
numpy = LazyModule('numpy')
# 只有在首次使用时才真正导入
array = numpy.array([1, 2, 3])

综合实战

实战1: 插件系统

"""
插件系统示例
展示如何使用动态导入实现可扩展的插件架构
"""

import os
import importlib
import inspect
from typing import Dict, List, Callable

class PluginManager:
    """插件管理器"""

    def __init__(self, plugin_dir: str = "plugins"):
        self.plugin_dir = plugin_dir
        self.plugins: Dict[str, object] = {}
        self.hooks: Dict[str, List[Callable]] = {}

    def discover_plugins(self):
        """自动发现并加载插件"""
        if not os.path.exists(self.plugin_dir):
            print(f"插件目录不存在: {self.plugin_dir}")
            return

        for filename in os.listdir(self.plugin_dir):
            if filename.endswith('.py') and not filename.startswith('_'):
                module_name = filename[:-3]
                self.load_plugin(module_name)

    def load_plugin(self, module_name: str):
        """加载单个插件"""
        try:
            # 动态导入模块
            module_path = f"{self.plugin_dir}.{module_name}"
            module = importlib.import_module(module_path)

            # 查找插件类
            for name, obj in inspect.getmembers(module):
                if (inspect.isclass(obj) and
                    hasattr(obj, 'name') and
                    hasattr(obj, 'execute')):

                    plugin = obj()
                    self.plugins[plugin.name] = plugin

                    # 注册钩子
                    if hasattr(plugin, 'hooks'):
                        for hook_name in plugin.hooks:
                            if hook_name not in self.hooks:
                                self.hooks[hook_name] = []
                            self.hooks[hook_name].append(plugin.execute)

                    print(f"✓ 插件加载成功: {plugin.name}")

        except Exception as e:
            print(f"✗ 插件加载失败: {module_name}, 错误: {e}")

    def execute_hook(self, hook_name: str, *args, **kwargs):
        """执行钩子"""
        if hook_name not in self.hooks:
            return []

        results = []
        for callback in self.hooks[hook_name]:
            try:
                result = callback(*args, **kwargs)
                results.append(result)
            except Exception as e:
                print(f"钩子执行失败: {e}")

        return results

    def list_plugins(self) -> List[str]:
        """列出所有已加载的插件"""
        return list(self.plugins.keys())

# 插件基类
class Plugin:
    """插件基类"""
    name = "base_plugin"
    hooks = []

    def execute(self, *args, **kwargs):
        raise NotImplementedError

# 示例插件目录结构
"""
plugins/
├── __init__.py
├── greeting_plugin.py
└── logging_plugin.py
"""

# plugins/greeting_plugin.py
"""
from plugin_system import Plugin

class GreetingPlugin(Plugin):
    name = "greeting"
    hooks = ['on_start', 'on_stop']

    def execute(self, *args, **kwargs):
        event = kwargs.get('event', '')
        if event == 'on_start':
            return "欢迎使用系统!"
        elif event == 'on_stop':
            return "再见!"
        return ""
"""

# 使用示例
def main():
    manager = PluginManager("plugins")

    # 自动发现插件
    manager.discover_plugins()

    # 列出插件
    print(f"已加载插件: {manager.list_plugins()}")

    # 执行钩子
    results = manager.execute_hook('on_start', event='on_start')
    for result in results:
        print(result)

if __name__ == "__main__":
    main()

实战2: 配置管理系统

"""
配置管理系统
展示如何组织和管理应用程序配置
"""

import os
import json
import yaml
from pathlib import Path
from typing import Any, Dict, Optional

class ConfigError(Exception):
    """配置错误"""
    pass

class Config:
    """配置管理类"""

    def __init__(self, config_dir: str = "config"):
        self.config_dir = Path(config_dir)
        self._config: Dict[str, Any] = {}
        self._loaded_files: list = []

    def load(self, filename: str, required: bool = True):
        """加载配置文件"""
        filepath = self.config_dir / filename

        if not filepath.exists():
            if required:
                raise ConfigError(f"配置文件不存在: {filepath}")
            return

        # 根据扩展名选择加载器
        if filepath.suffix == '.json':
            self._load_json(filepath)
        elif filepath.suffix in ['.yaml', '.yml']:
            self._load_yaml(filepath)
        elif filepath.suffix == '.py':
            self._load_python(filepath)
        else:
            raise ConfigError(f"不支持的配置格式: {filepath.suffix}")

        self._loaded_files.append(str(filepath))

    def _load_json(self, filepath: Path):
        """加载 JSON 配置"""
        with open(filepath, 'r', encoding='utf-8') as f:
            data = json.load(f)
            self._merge_config(data)

    def _load_yaml(self, filepath: Path):
        """加载 YAML 配置"""
        try:
            import yaml
        except ImportError:
            raise ConfigError("需要安装 PyYAML: pip install pyyaml")

        with open(filepath, 'r', encoding='utf-8') as f:
            data = yaml.safe_load(f)
            if data:
                self._merge_config(data)

    def _load_python(self, filepath: Path):
        """加载 Python 配置"""
        import importlib.util

        spec = importlib.util.spec_from_file_location("config_module", filepath)
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)

        # 提取大写的变量作为配置
        data = {
            key: value
            for key, value in vars(module).items()
            if key.isupper() and not key.startswith('_')
        }
        self._merge_config(data)

    def _merge_config(self, new_config: Dict[str, Any]):
        """合并配置(新配置覆盖旧配置)"""
        self._deep_merge(self._config, new_config)

    def _deep_merge(self, base: dict, override: dict):
        """深度合并字典"""
        for key, value in override.items():
            if (key in base and
                isinstance(base[key], dict) and
                isinstance(value, dict)):
                self._deep_merge(base[key], value)
            else:
                base[key] = value

    def get(self, key: str, default: Any = None) -> Any:
        """获取配置值"""
        keys = key.split('.')
        value = self._config

        for k in keys:
            if isinstance(value, dict):
                value = value.get(k)
                if value is None:
                    return default
            else:
                return default

        return value

    def set(self, key: str, value: Any):
        """设置配置值"""
        keys = key.split('.')
        config = self._config

        for k in keys[:-1]:
            if k not in config:
                config[k] = {}
            config = config[k]

        config[keys[-1]] = value

    def load_environment(self, prefix: str = "APP_"):
        """从环境变量加载配置"""
        env_config = {}

        for key, value in os.environ.items():
            if key.startswith(prefix):
                # 转换环境变量名为配置键
                config_key = key[len(prefix):].lower().replace('_', '.')

                # 尝试转换类型
                typed_value = self._parse_value(value)
                env_config[config_key] = typed_value

        self._merge_config(env_config)

    def _parse_value(self, value: str) -> Any:
        """解析字符串值为适当类型"""
        # 布尔值
        if value.lower() in ('true', 'yes', '1'):
            return True
        if value.lower() in ('false', 'no', '0'):
            return False

        # 整数
        try:
            return int(value)
        except ValueError:
            pass

        # 浮点数
        try:
            return float(value)
        except ValueError:
            pass

        # JSON 数组或对象
        if value.startswith('[') or value.startswith('{'):
            try:
                return json.loads(value)
            except json.JSONDecodeError:
                pass

        # 默认返回字符串
        return value

    def validate(self, schema: Dict[str, type]):
        """验证配置"""
        errors = []

        for key, expected_type in schema.items():
            value = self.get(key)

            if value is None:
                errors.append(f"缺少必需配置: {key}")
            elif not isinstance(value, expected_type):
                errors.append(
                    f"配置 '{key}' 类型错误: "
                    f"期望 {expected_type.__name__}, "
                    f"实际 {type(value).__name__}"
                )

        if errors:
            raise ConfigError("配置验证失败:\n" + "\n".join(errors))

    def save(self, filepath: str):
        """保存配置到文件"""
        path = Path(filepath)
        path.parent.mkdir(parents=True, exist_ok=True)

        with open(path, 'w', encoding='utf-8') as f:
            json.dump(self._config, f, indent=2, ensure_ascii=False)

    def __getitem__(self, key: str) -> Any:
        return self.get(key)

    def __setitem__(self, key: str, value: Any):
        self.set(key, value)

    def __repr__(self):
        return f"Config({self._config})"

# 使用示例
def main():
    # 创建配置管理器
    config = Config("config")

    # 加载配置文件(按优先级)
    config.load("default.yaml", required=False)
    config.load("production.yaml", required=False)

    # 加载环境变量(最高优先级)
    config.load_environment("APP_")

    # 获取配置
    db_host = config.get("database.host", "localhost")
    db_port = config.get("database.port", 5432)
    debug = config.get("app.debug", False)

    print(f"数据库: {db_host}:{db_port}")
    print(f"调试模式: {debug}")

    # 验证配置
    schema = {
        "database.host": str,
        "database.port": int,
        "app.debug": bool,
    }

    try:
        config.validate(schema)
        print("✓ 配置验证通过")
    except ConfigError as e:
        print(f"✗ 配置验证失败: {e}")

if __name__ == "__main__":
    main()

实战3: 日志系统封装

"""
日志系统封装
展示如何创建统一的日志模块
"""

import os
import sys
import logging
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
from pathlib import Path
from typing import Optional

class LoggerFactory:
    """日志工厂"""

    _loggers = {}

    @classmethod
    def get_logger(
        cls,
        name: str,
        level: int = logging.INFO,
        log_dir: str = "logs",
        max_bytes: int = 10 * 1024 * 1024,  # 10MB
        backup_count: int = 5,
        console_output: bool = True,
        file_output: bool = True,
    ) -> logging.Logger:
        """获取或创建 logger"""

        if name in cls._loggers:
            return cls._loggers[name]

        logger = logging.getLogger(name)
        logger.setLevel(level)

        # 避免重复添加 handler
        if logger.handlers:
            cls._loggers[name] = logger
            return logger

        # 格式化器
        formatter = logging.Formatter(
            '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S'
        )

        # 控制台处理器
        if console_output:
            console_handler = logging.StreamHandler(sys.stdout)
            console_handler.setLevel(level)
            console_handler.setFormatter(formatter)
            logger.addHandler(console_handler)

        # 文件处理器
        if file_output:
            log_path = Path(log_dir)
            log_path.mkdir(parents=True, exist_ok=True)

            # 按大小轮转
            log_file = log_path / f"{name}.log"
            file_handler = RotatingFileHandler(
                log_file,
                maxBytes=max_bytes,
                backupCount=backup_count,
                encoding='utf-8'
            )
            file_handler.setLevel(level)
            file_handler.setFormatter(formatter)
            logger.addHandler(file_handler)

            # 错误日志单独文件
            error_file = log_path / f"{name}_error.log"
            error_handler = RotatingFileHandler(
                error_file,
                maxBytes=max_bytes,
                backupCount=backup_count,
                encoding='utf-8'
            )
            error_handler.setLevel(logging.ERROR)
            error_handler.setFormatter(formatter)
            logger.addHandler(error_handler)

        cls._loggers[name] = logger
        return logger

    @classmethod
    def cleanup(cls):
        """清理所有 logger"""
        for logger in cls._loggers.values():
            for handler in logger.handlers[:]:
                handler.close()
                logger.removeHandler(handler)
        cls._loggers.clear()

# 便捷函数
def get_logger(name: Optional[str] = None, **kwargs) -> logging.Logger:
    """快速获取 logger"""
    if name is None:
        # 使用调用者的模块名
        import inspect
        frame = inspect.currentframe().f_back
        name = frame.f_globals.get('__name__', 'unknown')

    return LoggerFactory.get_logger(name, **kwargs)

# 使用示例
def main():
    # 获取 logger
    logger = get_logger("myapp", level=logging.DEBUG)

    # 记录日志
    logger.debug("调试信息")
    logger.info("一般信息")
    logger.warning("警告信息")
    logger.error("错误信息")

    try:
        result = 1 / 0
    except Exception as e:
        logger.exception("发生异常")  # 自动包含堆栈跟踪

    # 不同模块使用不同的 logger
    db_logger = get_logger("myapp.database")
    db_logger.info("数据库连接成功")

    api_logger = get_logger("myapp.api")
    api_logger.info("API 请求处理完成")

if __name__ == "__main__":
    main()

小结

概念说明使用场景
模块.py 文件代码组织、复用
包含模块的目录大型项目管理
import导入模块使用其他模块的功能
__init__.py包初始化文件定义包接口
相对导入from . import包内部模块引用
虚拟环境隔离依赖多项目管理
pip包管理工具安装第三方包
__name__模块名称判断是否主程序
__all__公开接口控制 import *

核心要点

  • 模块是代码复用的基本单位
  • 包用于组织相关模块
  • 合理使用导入方式避免循环依赖
  • 使用虚拟环境隔离项目依赖
  • 遵循 PEP 8 导入规范
  • 使用 __all__ 明确公开接口
  • 编写清晰的模块文档
  • 利用内置模块提高效率

掌握模块与包的管理是构建大型 Python 项目的基础!