返回博客

24. 模块系统:导入、作用域与包结构

24. 模块系统:导入、作用域与包结构

第二十四篇:模块系统——导入、作用域与包结构

前言

**模块(Module)和包(Package)**是 Python 代码组织的核心机制。本文深入讲解 import 的各种用法、作用域规则、包结构设计,以及 __init__.py 的实际用途。

模块基础

什么是模块?

Python 中每个 .py 文件都是一个模块——一个包含函数、类、变量的独立代码单元。

# math_module.py
PI = 3.14159

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

class Calculator:
    def multiply(self, x, y):
        return x * y

# 在其他文件中导入
import math_module
print(math_module.PI)  # 3.14159
print(math_module.add(3, 5))  # 8

calc = math_module.Calculator()
print(calc.multiply(4, 5))  # 20

as 别名导入

import json as simple_json  # 简短别名
from datetime import datetime as DT

data = simple_json.loads('{"name": "小明"}')
now = DT.now()
print(f"当前时间:{now}")

from...import 语法

# 导入特定成员
from math import pi, sin, cos

print(pi)       # 3.14159...
print(sin(0))   # 0.0

# ⚠️ 避免通配符导入(污染命名空间)
# from module import *  ← 不推荐!

name 与主程序判断

模块化测试

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

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

# 仅当直接运行时才执行此块
if __name__ == "__main__":
    print("Calculator 模块被直接运行")
    print(f"2+3={add(2, 3)}")
    print(f"5-2={subtract(5, 2)}")
else:
    print("Calculator 模块被导入")

# ✅ 作为库使用时:不会打印任何东西
# ❌ 直接运行 python calculator.py:会打印两行

模块初始化顺序

# a.py
print("A 正在加载...")
msg_a = "hello from A"

# b.py  
import a  # ← 触发加载 a.py
print("B 正在加载...")
print(f"B 拿到 A 的消息:{a.msg_a}")

执行 b.py 的输出:

A 正在加载...
B 正在加载...
B 拿到 A 的消息:hello from A

关键规则:

  1. Python 缓存已导入的模块(sys.modules),防止重复加载
  2. 模块级代码在第一次 import 时自动执行
  3. 后续 import 同一个模块返回已有引用(零开销)

包(Package)结构

包的目录布局

my_package/                ← 顶层包
├── __init__.py           ← 必需!标识为包
├── module_a.py           ← 子模块
├── module_b.py
└── subpackage/           ← 嵌套子包
    ├── __init__.py
    └── module_c.py

init.py 的作用

三种典型用法:

# 用法一:空文件(仅标记包存在)
# my_package/__init__.py

# 用法二:暴露公共 API(简化导入路径)
from .module_a import ClassA, function_a
from .subpackage.module_c import CLASS_C

# 用法三:包级初始化逻辑
import os, sys

PACKAGE_VERSION = "1.0.0"
DEBUG_MODE = os.getenv("MY_PACKAGE_DEBUG", "false").lower() == "true"

if DEBUG_MODE:
    logging.basicConfig(level=logging.DEBUG)

相对导入 vs 绝对导入

# 假设项目结构:
# src/package/__init__.py
# src/module_a.py
# src/subpackage/module_b.py

# 绝对导入(推荐)
from package import module_a
from package.subpackage import module_b

# 相对导入(包内部使用)
from . import module_a              # 同级别
from .. import parent_module        # 父级别
from .subpackage.module_b import func

# ⚠️ 相对导入仅在包内有效,不能直接在脚本中运行

搜索路径与环境变量

sys.path 详解

import sys

# 查看模块搜索路径
print(sys.path)
# ['', '/usr/lib/python3.x', '/home/user/.local/lib/python3.x', ...]

# 动态添加搜索路径(临时方案)
sys.path.append("/custom/path/to/my/modules")

# ⚠️ 修改 sys.path 通常不是最佳实践
# 更好的做法:安装开发包或使用 pip install -e .

PYTHONPATH 环境变量

# Linux/macOS
export PYTHONPATH="/opt/custom:$PYTHONPATH"
python script.py

# Windows
set PYTHONPATH=C:\Custom;C:\Libraries
python script.py

实用技巧与最佳实践

依赖检查装饰器

import functools

def requires(package, version="*"):
    """装饰器检查第三方包是否可用"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            try:
                pkg = __import__(package)
                # 版本检查
                if hasattr(pkg, '__version__'):
                    import packaging.version
                    actual = packaging.version.parse(pkg.__version__)
                    required = packaging.version.parse(version)
                    if actual < required:
                        raise ImportError(
                            f"{package} >= {version} 但发现 {pkg.__version__}"
                        )
            except ImportError:
                raise ImportError(f"缺少依赖:请安装 {package}")
            
            return func(*args, **kwargs)
        return wrapper
    return decorator

@requires("numpy", "1.20")
def numpy_heavy_function():
    import numpy as np
    return np.random.rand(100, 100)

try:
    result = numpy_heavy_function()
except ImportError as e:
    print(e)

LazyImportManager — 懒加载模块

import importlib

class LazyImportManager:
    """延迟导入模块以加快程序启动速度"""
    
    def __init__(self, lazy_modules):
        self._modules = {}
        for name in lazy_modules:
            object.__setattr__(self, name, None)
            self._modules[name] = importlib.import_module(name)
    
    def __getattribute__(self, name):
        if name.startswith("_"):
            return object.__getattribute__(self, name)
        
        modules = object.__getattribute__(self, "_modules")
        if name not in modules:
            raise AttributeError(f"'该类型' has no attribute '{name}'")
        
        return modules[name]
    
    def __setattr__(self, name, value):
        if name.startswith("_"):
            object.__setattr__(self, name, value)
        else:
            modules = object.__getattribute__(self, "_modules")
            modules[name] = value
    
    def __delattr__(self, name):
        if name.startswith("_"):
            object.__delattr__(self, name)
        else:
            modules = object.__getattribute__(self, "_modules")
            del modules[name]

# 使用
lazy = LazyImportManager(["pandas", "numpy"])
print(lazy.pandas.__name__)  # 'pandas' — 此时才真正导入
print(lazy.numpy.__name__)   # 'numpy'

常见问题排查

问题一:循环导入(Circular Import)

# ❌ 错误示例:a.py 和 b.py 互相导入导致 NameError
# a.py
from b import func_b
def func_a():
    return func_b()

# b.py
from a import func_a
def func_b():
    return func_a()

# ✅ 修复:延迟导入
def func_a():
    from b import func_b  # 只在调用时才导入
    return func_b()

问题二:找不到模块(ModuleNotFoundError)

# 常见原因:
# 1. 路径不在 sys.path 中 → 检查 PYTHONPATH
# 2. 拼写错误 → 仔细核对文件名
# 3. 虚拟环境未激活 → 确保激活正确的 venv

# 诊断方法
import sys
print(sys.path)
import importlib.util
spec = importlib.util.find_spec("target_module")
print(spec)  # None 表示找不到

问题三:不同版本的包冲突

# 解决方案一:使用虚拟环境隔离
python -m venv my_project_env
source my_project_env/bin/activate  # Linux/Mac
my_project_env\Scripts\activate     # Windows

# 解决方案二:pip 冻结依赖
pip freeze > requirements.txt
pip install -r requirements.txt

# 解决方案三:conda 管理
conda create -n myenv python=3.9
conda activate myenv

实战示例:创建一个可安装的包

项目结构

my_library/
├── setup.py          # 旧式打包
├── pyproject.toml    # 新式打包推荐
├── README.md
└── my_library/
    ├── __init__.py   # 暴露公共 API
    ├── core.py       # 核心功能
    ├── utils.py      # 工具函数
    └── cli.py        # 命令行接口

core.py:

class DataProcessor:
    def __init__(self, source):
        self.source = source
        self._cache = {}
    
    def process(self, data):
        key = hash(data)
        if key not in self._cache:
            self._cache[key] = self._transform(data)
        return self._cache[key]
    
    def _transform(self, data):
        # 具体变换逻辑
        return data.upper() if isinstance(data, str) else data

class StreamHandler:
    def __init__(self, config):
        self.config = config
        self.buffer = []
    
    def write(self, item):
        self.buffer.append(item)
        if len(self.buffer) >= self.config.get("buffer_size", 100):
            self.flush()
    
    def flush(self):
        if self.buffer:
            self._process_batch(self.buffer)
            self.buffer.clear()
    
    def _process_batch(self, items):
        print(f"处理批次:共 {len(items)} 项")

utils.py:

from functools import lru_cache
import hashlib
import time

@lru_cache(maxsize=None)
def compute_hash(text):
    return hashlib.sha256(text.encode()).hexdigest()

def retry(func, max_attempts=3, delay=1):
    for i in range(max_attempts):
        try:
            return func()
        except Exception:
            if i < max_attempts - 1:
                time.sleep(delay)
    return func()

init.py:

"""My Library — 高性能数据处理工具集"""

__version__ = "1.0.0"
__author__ = "开发者"

from .core import DataProcessor, StreamHandler
from .utils import compute_hash, retry

__all__ = [
    "DataProcessor",
    "StreamHandler",
    "compute_hash", 
    "retry",
    "__version__",
]

setup.py:

from setuptools import setup, find_packages

setup(
    name="my-library",
    version="1.0.0",
    packages=find_packages(),
    description="高性能数据处理工具集",
    python_requires=">=3.8",
)

总结

本文涵盖了 Python 模块系统的核心知识:

  1. 模块基础 — .py 文件即模块、as 别名、from...import
  2. name 守卫 — if __name__ == "__main__" 实现可测试模块
  3. init.py — 空标记、API 暴露、包级初始化
  4. 相对/绝对导入 — 包内外不同的导入风格
  5. sys.path/PYTHONPATH — 自定义模块搜索路径
  6. 循环导入 — 延迟导入修复技巧
  7. 虚拟环境 — 隔离依赖的最佳实践
  8. 延迟加载 — LazyImportManager 加速启动
  9. 可安装包结构 — setup.py/pyproject.toml 规范
  10. 最佳实践 — 避免通配符导入、使用类型注解增强可读性

下一篇文章将深入讲解第三方模块与虚拟环境。


本文是 Python 教程系列的第二十四篇。