首页 / 知识库 / 0基础入门-阅读资料 / 0基础-python入门到精通

第 17 章 模块与包

17.1 模块的创建与导入

模块就是一个 .py 文件,是组织代码的基本单位。

# math_utils.py — 这就是一个模块
def add(a, b):
    return a + b

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

PI = 3.14159
# 导入方式
import math_utils
print(math_utils.add(1, 2))       # 3
print(math_utils.PI)               # 3.14159

# 导入特定名字
from math_utils import add, PI
print(add(1, 2))                   # 3

# 别名
import math_utils as mu
from math_utils import multiply as mul

# 导入所有(不推荐,污染命名空间)
from math_utils import *

模块是对象

import math_utils

print(type(math_utils))            # <class 'module'>
print(math_utils.__name__)         # math_utils
print(math_utils.__file__)         # /path/to/math_utils.py
print(dir(math_utils))             # 查看模块所有属性

# 模块只在第一次导入时执行,之后使用缓存
import sys
print("math_utils" in sys.modules)  # True

模块的执行

# greet.py
print("模块被加载了")  # 每次首次 import 时执行

def hello():
    return "Hello!"

# 第一次
import greet   # 打印 "模块被加载了"
# 第二次
import greet   # 无输出(使用缓存)

# 强制重新加载
import importlib
importlib.reload(greet)  # 打印 "模块被加载了"

17.2 __name__if __name__ == "__main__"

每个模块都有 __name__ 属性:

  • 作为主程序运行时:__name__"__main__"
  • 被导入时:__name__ 为模块名
# calculator.py
def add(a, b):
    return a + b

def main():
    print(f"1 + 2 = {add(1, 2)}")

if __name__ == "__main__":
    # 只在直接运行时执行,被导入时不执行
    main()
python calculator.py     # 输出: 1 + 2 = 3
import calculator        # 不输出任何东西
calculator.add(1, 2)     # 3

最佳实践:所有可执行脚本都应该有 if __name__ == "__main__" 守卫。

17.3 包与 __init__.py

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

mypackage/
├── __init__.py         # 包的初始化文件
├── core.py
├── utils.py
└── sub/
    ├── __init__.py
    └── helper.py
# mypackage/__init__.py
"""mypackage — 我的工具包"""
from .core import main_function
from .utils import helper

__version__ = "1.0.0"
# 使用
import mypackage
print(mypackage.__version__)
mypackage.main_function()

from mypackage.utils import helper
from mypackage.sub.helper import some_func

__init__.py 的作用

  1. 标识目录是一个 Python 包
  2. 包被导入时自动执行
  3. 控制 from package import * 的行为
  4. 提供包级别的 API
# mypackage/__init__.py
# 让用户可以直接 from mypackage import XXX
from .core import Engine
from .utils import format_data, validate

# 可以为空文件,仅标识为包

17.4 相对导入与绝对导入

project/
├── main.py
└── mypackage/
    ├── __init__.py
    ├── core.py
    ├── utils.py
    └── sub/
        ├── __init__.py
        └── helper.py
# mypackage/core.py

# 绝对导入 — 从项目根目录开始
from mypackage.utils import helper
from mypackage.sub.helper import some_func

# 相对导入 — 以当前包为基准
from .utils import helper          # 同级模块
from .sub.helper import some_func  # 子包
from ..other_package import foo    # 上级包的兄弟包

规则

  • 相对导入只能在包内使用,不能在顶级脚本中使用
  • 绝对导入更清晰,一般推荐使用
  • 相对导入在包重构时更灵活
# 相对导入的 . 含义
from . import utils          # 当前包
from .. import parent_module # 父级包
from .sub import helper      # 当前包的子包

17.5 __all__ 与命名空间控制

__all__ 控制 from module import * 导出哪些名字:

# utils.py
__all__ = ["public_func", "PublicClass"]

def public_func():
    pass

def _private_func():
    pass

class PublicClass:
    pass

class _InternalClass:
    pass
from utils import *
# 只导入了 public_func 和 PublicClass
# _private_func 和 _InternalClass 不会被导入

# 但显式导入仍然可以
from utils import _private_func  # 可以

在包的 __init__.py 中:

# mypackage/__init__.py
__all__ = ["core", "utils"]  # from mypackage import * 时导入的子模块

17.6 模块搜索路径与 sys.path

Python 按以下顺序搜索模块:

import sys
print(sys.path)
# [
#   '',                           # 当前目录
#   '/usr/lib/python3.13',        # 标准库
#   '/usr/lib/python3.13/lib-dynload',
#   '/home/user/.local/lib/python3.13/site-packages',  # 第三方包
# ]
# 动态添加搜索路径
import sys
sys.path.insert(0, "/path/to/my/modules")

# 或使用环境变量
# export PYTHONPATH="/path/to/my/modules:$PYTHONPATH"
# 查看模块来源
import json
print(json.__file__)  # /usr/lib/python3.13/json/__init__.py

import os
print(os.__file__)    # /usr/lib/python3.13/os.py

17.7 importlib 与动态导入

import importlib

# 动态导入模块(模块名是字符串变量时)
module_name = "json"
mod = importlib.import_module(module_name)
print(mod.dumps({"key": "value"}))

# 导入子模块
sub = importlib.import_module("os.path")
print(sub.exists("/tmp"))

# 相对导入
sub = importlib.import_module(".utils", package="mypackage")

插件系统示例

import importlib
import os

def load_plugins(plugin_dir):
    plugins = {}
    for filename in os.listdir(plugin_dir):
        if filename.endswith(".py") and not filename.startswith("_"):
            module_name = filename[:-3]
            spec = importlib.util.spec_from_file_location(
                module_name,
                os.path.join(plugin_dir, filename),
            )
            module = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(module)
            
            if hasattr(module, "register"):
                plugins[module_name] = module.register()
    return plugins

17.8 命名空间包(PEP 420)

命名空间包不需要 __init__.py,允许同一个包分布在多个目录中:

# 目录结构
path1/
└── mypkg/
    └── module_a.py

path2/
└── mypkg/
    └── module_b.py
import sys
sys.path.extend(["path1", "path2"])

# 两个目录的 mypkg 合并为一个命名空间包
from mypkg import module_a
from mypkg import module_b

import mypkg
print(mypkg.__path__)  # ['path1/mypkg', 'path2/mypkg']

使用场景:大型组织中不同团队维护同一个包的不同子模块。

注意:如果目录中有 __init__.py,它就是普通包而不是命名空间包。


本章小结:模块和包是 Python 代码组织的基础。关键要点:用 if __name__ == "__main__" 区分脚本和库、用 __all__ 控制公共 API、理解 sys.path 搜索顺序、优先使用绝对导入。importlib 为动态场景提供了灵活的导入机制。