第 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 的作用
- 标识目录是一个 Python 包
- 包被导入时自动执行
- 控制
from package import *的行为 - 提供包级别的 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为动态场景提供了灵活的导入机制。