跳到主要内容

一个分布式系统就是这样的一种系统:你甚至不知道存在的一台电脑的故障,能把你自己电脑的故障搞出来。

Leslie Lamport分布式系统先驱

🧩 模块

随着代码量增长,把所有逻辑塞进一个 .py 文件会变得难以维护。Python 通过模块(module)机制,将代码按文件拆分、组织、复用。一个 .py 文件就是一个模块,文件名即模块名(去掉 .py 后缀)。模块让代码具备了复用、隔离、命名空间三大能力——这是从中型项目走向大型项目的第一步。

📌 本节要点

  • 一个 .py 文件就是一个模块,文件名即模块名
  • import module 引入完整模块,from module import name 引入部分名称
  • __name__ == "__main__" 让模块既能直接运行,又能被导入而不产生副作用
  • __all__ 显式声明公开 API,控制 from module import * 行为
  • sys.path 决定模块查找顺序,优先使用包结构和 pip install -e . 而非手动修改
  • __pycache__ 缓存字节码加快启动,可放心删除
  • 模块导入时会执行顶层代码一次,后续导入命中缓存

import 语句

最基础的导入方式是 import 模块名。导入后,通过 模块名.属性 访问其中的函数、类、变量:

Python
# 标准库 math 模块
import math

print(math.pi) # 输出:3.141592653589793
print(math.sqrt(16)) # 输出:4.0
print(math.factorial(5)) # 输出:120

导入时会完整执行模块顶层代码一次,后续再次 import 同一模块直接命中缓存(见 sys.modules),不会重复执行。

Python
import math
import math # 第二次不会重新执行 math 模块

import sys
print("math" in sys.modules) # 输出:True

from...import

如果只需要模块中的部分内容,使用 from 模块名 import 名称,可以直接用名称,免去模块名前缀:

Python
from math import pi, sqrt, factorial

print(pi) # 输出:3.141592653589793
print(sqrt(16)) # 输出:4.0
print(factorial(5)) # 输出:120
谨慎使用 from ... import *

from math import * 会把模块所有公开名称导入当前命名空间,容易覆盖同名变量且难以追溯来源,PEP 8 明确不推荐在生产代码中使用。

Python
from math import *
from os import * # 危险:os.open 会覆盖内置 open

# 不要这样写!

import as 别名

当模块名较长或与本地变量冲突时,使用 as 起别名:

Python
import numpy as np # 第三方库常用别名(约定俗成)
import pandas as pd
import matplotlib.pyplot as plt

# 也适用于 from...import
from math import factorial as fact

print(fact(5)) # 输出:120
主流库的别名约定

数据科学生态有一套事实上的命名约定,遵守它们能让代码更易读:

  • import numpy as np
  • import pandas as pd
  • import matplotlib.pyplot as plt
  • import seaborn as sns
  • import tensorflow as tf

name == 'main'

每个模块都有一个特殊属性 __name__

  • 当模块被直接运行时,__name__ 等于 "__main__"
  • 当模块被导入时,__name__ 等于模块名

这一机制常用于在文件中编写"既能作为脚本执行,又能作为模块导入"的代码:

Python
# greeter.py
def greet(name: str) -> str:
return f"你好,{name}!"


def main() -> None:
name = input("请输入姓名:")
print(greet(name))


if __name__ == "__main__":
# 只有直接运行 greeter.py 才会执行
# 被 import 时不执行,避免副作用
main()
Shell
python greeter.py
# 请输入姓名:Alice
# 你好,Alice!
Python
# 在另一个文件中
import greeter
print(greeter.greet("Bob")) # 输出:你好,Bob!
# 不会触发 input(),因为 __name__ != "__main__"
何时使用 name 守卫
  • 模块既作为脚本运行,又作为库被导入
  • 包含示例代码自测代码CLI 入口
  • 避免导入时产生副作用(如启动服务、写入文件、连接数据库)

all 控制导出

__all__ 是一个列表,明确声明模块的公开 API。它影响两件事:

  1. from module import * 时只会导入 __all__ 中的名称
  2. 文档工具(如 Sphinx)据此生成 API 文档
Python
# string_utils.py
__all__ = ["capitalize_words", "reverse_string"]


def capitalize_words(text: str) -> str:
"""将每个单词首字母大写。"""
return " ".join(w.capitalize() for w in text.split())


def reverse_string(text: str) -> str:
"""反转字符串。"""
return text[::-1]


def _internal_helper(text: str) -> str:
"""内部辅助函数,不希望被外部使用。"""
return text.strip().lower()


# 使用示例
if __name__ == "__main__":
print(capitalize_words("hello world")) # 输出:Hello World
print(reverse_string("Python")) # 输出:nohtyP
Python
from string_utils import *

print(capitalize_words("hi there")) # 输出:Hi There
print(reverse_string("abc")) # 输出:cba
# print(_internal_helper("X")) # NameError: 未导出
单下划线约定

即便没有 __all__,以单下划线开头的名称(如 _internal_helper)也被视为"模块私有",from module import * 不会导入它们。这是约定,不是强制——你仍可以显式 from string_utils import _internal_helper

标准库模块示例

Python 标准库提供了 200+ 个模块,覆盖了日常开发的绝大部分需求。

os — 操作系统接口

Python
import os

# 当前工作目录
print(os.getcwd()) # 输出示例:/home/user/project

# 环境变量
print(os.environ.get("HOME", "/home/user"))

# 拼接路径(跨平台安全)
config_path = os.path.join("config", "settings.json")
print(config_path) # Linux: config/settings.json Windows: config\settings.json

# 列出目录内容
for entry in os.listdir("."):
print(entry)

pathlib — 现代路径操作(推荐)

Python
from pathlib import Path

# 比 os.path 更优雅
home = Path.home()
print(home) # 输出示例:/home/user

config = home / "config" / "settings.json"
print(config) # 输出示例:/home/user/config/settings.json

# 递归查找所有 Python 文件
for py_file in Path(".").rglob("*.py"):
print(py_file)

datetime — 日期与时间

Python
from datetime import datetime, date, timedelta

now = datetime.now()
print(now.strftime("%Y-%m-%d %H:%M:%S")) # 输出示例:2026-07-10 14:30:00

today = date.today()
yesterday = today - timedelta(days=1)
print(yesterday) # 输出示例:2026-07-09

collections — 高级容器

Python
from collections import Counter, defaultdict, deque

# 计数
words = "the quick brown fox jumps over the lazy dog the".split()
counter = Counter(words)
print(counter.most_common(2))
# 输出:[('the', 3), ('quick', 1)]

# 默认值字典
word_len = defaultdict(list)
for w in words:
word_len[len(w)].append(w)
print(word_len[3]) # 输出:['the', 'fox', 'the', 'dog', 'the']

# 双端队列
queue = deque([1, 2, 3], maxlen=5)
queue.appendleft(0)
queue.append(4)
print(queue) # 输出:deque([0, 1, 2, 3, 4], maxlen=5)

itertools — 迭代器工具

Python
from itertools import chain, combinations, islice

# 拼接多个迭代器
for item in chain([1, 2], ["a", "b"], (True, False)):
print(item, end=" ")
# 输出:1 2 a b True False

# 所有组合
for combo in combinations("ABC", 2):
print(combo)
# 输出:('A', 'B') / ('A', 'C') / ('B', 'C')

# 切片迭代器
first_three = list(islice(range(100), 3))
print(first_three) # 输出:[0, 1, 2]

模块搜索路径 sys.path

当执行 import foo 时,Python 解释器按以下顺序查找 foo 模块:

  1. 内置模块(如 sysbuiltins
  2. 当前脚本所在目录(或交互式解释器的当前目录)
  3. PYTHONPATH 环境变量中的目录
  4. 标准库目录
  5. 第三方包目录(site-packages

这些路径存储在 sys.path 列表中:

Python
import sys

for i, path in enumerate(sys.path):
print(f"{i}: {path}")

输出示例:

输出
0: /home/user/project
1: /usr/lib/python312.zip
2: /usr/lib/python3.12
3: /usr/lib/python3.12/lib-dynload
4: /home/user/.venv/lib/python3.12/site-packages

临时添加搜索路径

Python
import sys
from pathlib import Path

# 在程序中动态添加(不推荐作为长期方案)
sys.path.insert(0, str(Path(__file__).parent / "libs"))

import my_custom_module # 现在可以找到 libs/my_custom_module.py
不要滥用 sys.path 修改

直接修改 sys.path 会让项目难以维护和部署。更好的方式:

  • 把代码组织成(下一节会讲)
  • pip install -e . 以可编辑模式安装项目
  • 使用 pyproject.toml 声明项目结构

模块缓存 sys.modules

Python 维护一个全局字典 sys.modules,记录所有已加载的模块。再次 import 同一模块时直接从这里取,避免重复执行:

Python
import sys
import math

print(id(sys.modules["math"])) # 模块对象的内存地址

# 再次 import 不会重新加载
import math
print(id(math)) # 与上面相同

# 强制重新加载(开发调试用)
import importlib
importlib.reload(math) # 注意:不会更新已 from...import 的引用

pycache 与字节码

首次导入模块时,Python 会将源码编译为字节码(.pyc 文件)缓存到 __pycache__ 目录,下次导入若源码未修改则直接加载字节码,加快启动速度。

输出
project/
├── greeter.py
└── __pycache__/
└── greeter.cpython-312.pyc

文件名格式:模块名.cpython-版本号.pyc

Python
# 查看字节码
import dis

def add(a: int, b: int) -> int:
return a + b

dis.dis(add)

输出示例:

输出
1 RESUME 0
2 LOAD_FAST 0 (a)
LOAD_FAST 1 (b)
BINARY_OP 0 (+)
RETURN_VALUE
关于 pycache
  • 可以随时删除 __pycache__ 目录,Python 会在下次运行时重新生成
  • .gitignore 中加入 __pycache__/*.pyc
  • 发布到生产环境时通常不需要 .pyc,部署时会重新生成
  • 设置环境变量 PYTHONDONTWRITEBYTECODE=1 可禁止生成

实战:自定义工具模块

下面我们创建一个完整的字符串处理工具模块,包含导出控制、文档字符串、__main__ 自测:

Python
# string_kit.py
"""字符串处理工具箱。

提供常用字符串操作,遵循 Python 3.12+ 类型注解规范。
"""

from __future__ import annotations

__version__ = "1.0.0"
__author__ = "study-python"

__all__ = [
"slugify",
"truncate",
"count_words",
"camel_to_snake",
]


def slugify(text: str, separator: str = "-") -> str:
"""将文本转为 URL 友好的 slug。

>>> slugify("Hello World!")
'hello-world'
"""
import re
text = text.lower().strip()
text = re.sub(r"[^\w\s-]", "", text)
text = re.sub(r"[\s_-]+", separator, text)
return text.strip(separator)


def truncate(text: str, max_len: int, suffix: str = "...") -> str:
"""截断字符串到指定长度,超出部分用 suffix 替代。

>>> truncate("Hello World", 8)
'Hello...'
"""
if len(text) <= max_len:
return text
return text[: max_len - len(suffix)] + suffix


def count_words(text: str) -> dict[str, int]:
"""统计每个单词出现次数。

>>> count_words("a a b")
{'a': 2, 'b': 1}
"""
from collections import Counter
return dict(Counter(text.split()))


def camel_to_snake(name: str) -> str:
"""驼峰命名转蛇形命名。

>>> camel_to_snake("camelCaseName")
'camel_case_name'
"""
import re
s1 = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name)
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1).lower()


def _internal_debug(text: str) -> str:
"""内部调试函数,不会通过 from ... import * 导出。"""
return repr(text)


if __name__ == "__main__":
# 模块自测:python -m string_kit 或 python string_kit.py
import doctest
failures, tests = doctest.testmod(verbose=True)
if failures == 0:
print(f"\n✓ 所有 {tests} 个测试通过")

运行自测:

Shell
python string_kit.py

输出示例:

输出
Trying:
slugify("Hello World!")
Expecting:
'hello-world'
ok
Trying:
truncate("Hello World", 8)
Expecting:
'Hello...'
ok
...
2 items passed all tests:
4 tests in __main__
4 tests in 2 items.
4 passed and 0 failed.
Test passed.
✓ 所有 4 个测试通过

在其他文件中使用:

Python
# main.py
import string_kit
from string_kit import slugify, camel_to_snake

print(string_kit.__version__) # 输出:1.0.0
print(slugify("Hello World! 你好")) # 输出:hello-world-你好
print(camel_to_snake("getUserById")) # 输出:get_user_by_id
模块自测三件套
  • 文档字符串中写 >>> 测试用例
  • if __name__ == "__main__" 块中调用 doctest.testmod()
  • python -m 模块名 形式运行(推荐,等价于直接运行)

🎯 动手练习

  1. 工具模块:创建 math_utils.py,包含 factorialfibonacciis_prime 函数,添加 __all__ 控制导出
  2. 模块自测:为每个函数编写 doctest,使用 if __name__ == "__main__" 运行自测
  3. 导入实验:尝试 importfrom...importimport as 三种方式,观察 sys.modules 缓存
  4. 路径探索:打印 sys.path,理解模块搜索顺序,尝试临时添加路径导入自定义模块

📚 延伸阅读

  • 包(package):把多个模块组织成层级化的目录结构
  • 相对导入:包内模块使用 from . import module 导入
  • 命名空间包:跨多个目录分布的同名包
  • importlib:动态导入模块,importlib.import_module() 运行时导入

📊 速查表

操作语法示例
导入模块import moduleimport math
导入名称from module import namefrom math import pi
别名导入import module as aliasimport numpy as np
多名称导入from module import a, bfrom math import pi, sqrt
全部导入from module import *❌ 不推荐
公开 API__all__ = ["func1"]控制 import * 行为
模块名__name__直接运行时为 "__main__"
入口守卫if __name__ == "__main__":避免导入副作用
模块缓存sys.modules记录已加载模块
搜索路径sys.path模块查找顺序
重新加载importlib.reload(mod)开发调试用
模块版本module.__version__查看版本号
模块路径module.__file__查看文件位置
运行模块python -m module推荐方式
模块自测doctest.testmod()文档字符串测试

✅ 本节总结

本节我们学习了 Python 模块机制,核心要点包括:

  • 模块即文件:一个 .py 文件就是一个模块,文件名即模块名
  • 导入方式import module 引入完整模块,from module import name 引入部分名称
  • 别名import module as alias 简化长模块名,遵循主流库约定
  • 入口守卫if __name__ == "__main__": 让模块既能运行又能被导入
  • 导出控制__all__ 声明公开 API,单下划线 _name 约定为私有
  • 模块缓存:首次导入执行顶层代码,后续导入命中 sys.modules
  • 搜索路径sys.path 决定查找顺序,优先用包结构而非手动修改
  • 字节码缓存__pycache__ 加快启动,可随时删除

下一节将介绍包(package)——把多个模块组织成层级化的目录结构,进一步管理大型项目的代码。