Skip to content
This page has been auto-translated and may contain errors.View in English

模块与标准库

Python 自带了大量可以直接使用的工具:随机数、数学、日期、文件路径等等。这些工具存在于模块中,你可以用 import 把它们引入代码。在上一章里,你已经用过 import json。本章将完整介绍导入机制,并带你了解标准库中最有用的部分。

Python 的标准库为常见问题提供了经过测试且有文档的解决方案。模块是代码组织的基本单元:每个文件都是一个模块,每个包含 __init__.py 的目录都是一个包。import 系统会查找模块、必要时编译它们,并把结果缓存到 sys.modules 中,这样它们只会被加载一次。

导入系统是一套分层机制:finder 负责定位模块,loader 负责编译并执行它们。结果会被缓存到 sys.modules 中。import foo 会执行一次 foo.py,并将该模块对象绑定到当前命名空间中的 foofrom foo import bar 只会绑定 bar。理解 sys.path__init__.py 和相对导入,对于构建包来说至关重要。

导入模块

最简单的导入方式是引入整个模块,然后用点号访问它的内容。你也可以从模块中导入特定的名字,直接使用它们而不需要前缀。别名可以缩短较长的名字。

import module 会将模块对象绑定到当前作用域中的 module 名字。from module import name 只绑定 name。别名(import module as alias)在第三方库中很常见。避免使用 from module import *:它会污染命名空间,并使名字的来源变得不清晰。

import module 会触发完整的导入机制,将结果缓存到 sys.modules 中,并绑定模块对象。from module import name 是语法糖:它仍然会完整导入模块,然后提取 name。循环导入是一个常见的陷阱;解决方法通常是把导入移到函数内部,或者重新组织模块依赖。importlib.import_module() 允许以编程方式导入。

python
import math

math.sqrt(16)     # 4.0
math.pi           # 3.141592653589793
math.floor(3.9)   # 3
math.ceil(3.1)    # 4

从模块中导入特定的名字,这样你可以直接使用它们:

python
from math import sqrt, pi

sqrt(16)    # 4.0(不需要 "math." 前缀)
pi          # 3.141592653589793

为模块或名字起一个别名以缩短它:

python
import math as m

m.sqrt(16)    # 4.0

from math import sqrt as square_root
square_root(25)    # 5.0

别名在流行的第三方库中很常见(import numpy as npimport pandas as pd)。对于标准库模块,优先使用完整名字;这样代码更容易阅读。

random

random 模块用于生成随机数和做随机选择。它可用于游戏、模拟、随机抽样,以及任何需要不可预测性的场景。设置种子可以让结果可复现:同样的种子每次都会产生同样的序列。

random 使用梅森旋转伪随机数生成器。种子决定整个序列;同样的种子总是产生同样的输出。.choice() 选一个元素,.choices() 是有放回抽取,.sample() 是无放回抽取。.shuffle() 就地修改列表并返回 None

random 使用梅森旋转(MT19937)PRNG,状态为 624 字。random.seed() 用于初始化状态;不调用时,状态会从 os.urandom() 取得种子。对于加密用途,请使用 secrets:random 不是密码学安全的。random.SystemRandom() 封装了 os.urandom(),提供与 random 相同 API 的安全替代方案。

python
import random

random.random()              # 0 到 1 之间的浮点数(不含 1)
random.randint(1, 10)        # 1 到 10 的整数(两端都包含)
random.uniform(1.0, 10.0)    # 1.0 到 10.0 之间的浮点数

colours = ["red", "green", "blue"]
random.choice(colours)       # 选一个元素
random.choices(colours, k=3) # 选 k 个元素(有放回)
random.sample(colours, k=2)  # 选 k 个元素(无放回)

numbers = [1, 2, 3, 4, 5]
random.shuffle(numbers)      # 就地打乱,返回 None

为了让结果可复现(在测试和数据科学中很有用),在生成之前设置种子:

python
random.seed(42)
random.randint(1, 100)   # 种子为 42 时,总是返回相同的值

同样的种子在任何机器上每次都会产生同样的序列。

math

math 模块在基本算术运算符之外提供了更高级的数学运算。平方根、幂、对数、三角函数,以及 pi 和无穷大等特殊值,都在这里。

math 提供了标准数学函数的 C 层实现。注意 math.pow() 总是返回 float,而 Python 的 ** 运算符对整数底数和指数返回 intmath.log(x, base) 计算任意底数的对数;math.log(x) 计算自然对数。

math 封装了 C <math.h> 库函数。它们比纯 Python 实现更快,并能正确处理边界情况(NaN、无穷大)。math.isnan()math.isinf() 检查 IEEE 754 特殊值。对于复数,cmath 提供了对应的函数。对于数组级数学运算,numpy 是标准工具。

python
import math

math.sqrt(25)        # 5.0
math.pow(2, 10)      # 1024.0(等同于 2 ** 10,但总是返回 float)
math.log(100, 10)    # 2.0(以 10 为底的对数)
math.log(math.e)     # 1.0(自然对数)

math.sin(math.pi / 2)   # 1.0
math.cos(0)             # 1.0

math.ceil(3.2)    # 4
math.floor(3.9)   # 3
math.trunc(3.9)   # 3(对正数与 int() 等同)

math.inf          # 无穷大
math.isnan(float("nan"))   # True
math.isinf(math.inf)       # True

datetime

datetime 模块用于处理日期和时间。datetime.now() 给你当前的日期和时间。strftime() 将其格式化为字符串。strptime() 把字符串解析成 datetime。timedelta 表示一段时长,你可以做加减运算。

datetimedatetimedelta 是主要的类。strftime() 使用格式代码把 datetime 格式化为字符串。strptime() 根据给定的格式模式解析字符串。timedelta 支持算术运算:你可以在日期上加减时长,并使用 <>- 比较 datetime。

datetime 对象默认是 naive 的(没有时区)。对于带时区的 datetime,使用 datetime.now(tz=timezone.utc) 或带偏移量的 datetime.fromisoformat()strftime/strptime 使用 C 库的格式代码;%f 表示微秒。对于高精度计时,优先使用 time.perf_counter() 而不是 datetime.now()zoneinfo 模块(Python 3.9+)提供 IANA 时区支持。

python
from datetime import datetime, date, timedelta

now   = datetime.now()           # 当前日期和时间
today = date.today()             # 仅当前日期

print(now.year, now.month, now.day)
print(now.hour, now.minute, now.second)

# 格式化
print(now.strftime("%Y-%m-%d"))           # "2024-01-15"
print(now.strftime("%d %B %Y, %H:%M"))   # "15 January 2024, 09:42"

# 解析
deadline = datetime.strptime("2024-12-31", "%Y-%m-%d")

# 算术运算
tomorrow    = today + timedelta(days=1)
next_week   = today + timedelta(weeks=1)
diff        = deadline - now
print(f"{diff.days} days until deadline")

常用的 strftime 代码:

代码含义示例
%Y4 位年份2024
%m月份(补零)01
%d日(补零)15
%H小时(24 小时制)09
%M分钟42
%B完整月份名January

os 和 pathlib

pathlib 是处理文件路径的现代方式。Path 对象让你可以使用 / 运算符来构建、检查和导航路径。os 提供对环境变量和较低层 OS 操作的访问。新代码建议优先使用 pathlib

pathlib.Path 把文件系统路径表示为带有查询和导航方法的对象。/ 运算符可以干净地拼接路径组件,自动处理操作系统特定的分隔符。os.environ 是一个类字典对象,用于访问环境变量;对于可能缺失的变量,os.environ.get("KEY", "default") 是安全的写法。

pathlib.Path 是抽象基类,PurePosixPathPureWindowsPath 是各操作系统的具体实现。像 .glob().rglob().iterdir() 这样的方法返回生成器。.stat() 调用 os.stat() 并返回一个 stat_result。自 Python 3.6 起,os.path 函数同时接受字符串和 Path 对象。新代码优先使用 pathlib;在调用不接受 Path 的 API 时,使用 os.fspath()Path 转换为 str

python
from pathlib import Path

p = Path("data/reports")

p.exists()           # 路径存在则为 True
p.is_dir()           # 是目录则为 True
p.is_file()          # 是文件则为 True

p.mkdir(parents=True, exist_ok=True)   # 创建目录

for f in p.glob("*.csv"):              # 目录中所有 CSV 文件
    print(f.name)                      # 只显示文件名

report = p / "report_jan.csv"          # / 运算符用于拼接路径
report.stem       # "report_jan"(不含扩展名的名称)
report.suffix     # ".csv"
report.parent     # Path("data/reports")

content = report.read_text()           # 直接读取文件内容
report.write_text("new content\n")    # 直接写入

对于 os 模块:

python
import os

os.getcwd()                        # 当前工作目录
os.listdir(".")                    # 列出目录内容
os.path.exists("data.txt")        # 路径存在则为 True
os.path.join("data", "file.txt")  # "data/file.txt"(跨平台)
os.environ.get("HOME")            # 读取一个环境变量

新代码优先使用 pathlib。当你需要环境变量或处理只接受字符串的老式 API 时,使用 os

timeit

timeit 测量代码运行所需的时间。当你想比较两种实现并选择更快的那一种时,它很有用。多次运行代码以获得稳定的测量值。

timeit.timeit(stmt, setup, number) 通过运行 stmtnumber 次,并返回总耗时(秒)来计时。setup 字符串在计时循环之前运行一次。将结果除以 number 可得到单次调用的耗时。重复次数越多,系统调度带来的噪声越小。

timeit 在计时期间禁用垃圾回收以减少噪声。它使用 time.perf_counter() 进行高分辨率测量。globals 参数将命名空间传递给被计时的语句。对于微基准测试,timeit 是标准工具;若要分析时间花在大型程序的哪些地方,请使用 cProfile

python
import timeit

# 给单条语句计时
timeit.timeit("sum(range(1000))", number=10000)

# 给更复杂的代码块计时
setup = "data = list(range(1000))"
code  = "[x * 2 for x in data]"
time  = timeit.timeit(code, setup=setup, number=10000)
print(f"{time:.4f} seconds for 10,000 runs")

number 是重复次数。重复次数越多,测量结果越稳定。

string

string 模块提供了预先定义好的字符串常量,包括字母、数字和标点符号。当你需要检查字符或从特定字母表生成随机字符串时很有用。

string 模块常量(ascii_lettersdigitspunctuation)是普通字符串,你可以索引、迭代或用 in 操作。把它们与 random.choices() 结合是生成随机 token 或密码的标准方法。

string 模块常量是纯 Python 字符串字面量,没有特殊行为。它们不是集合,所以 in 的复杂度是 O(n);若需要频繁的成员判断,使用 set(string.digits)string.Formatterstring.Template 分别是 str.format()$ 风格替换的底层机制。

python
import string

string.ascii_lowercase   # "abcdefghijklmnopqrstuvwxyz"
string.ascii_uppercase   # "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
string.ascii_letters     # 两者合并
string.digits            # "0123456789"
string.punctuation       # 所有标点符号

当你需要检查字符或生成随机字符串时很有用:

python
import string, random

chars    = string.ascii_letters + string.digits
password = "".join(random.choices(chars, k=12))

创建自己的模块

任何 Python 文件都是一个模块。要在另一个文件中使用它,按文件名(去掉 .py)导入即可。你可以导入整个模块,通过点号访问其内容;也可以直接导入特定的名字。

当 Python 导入一个模块时,它会从上到下执行该文件一次,并将结果缓存到 sys.modules 中。同一模块后续的导入会返回缓存的对象,而不会重新执行文件。对于较大的项目,模块会被组织成:包含 __init__.py 文件的目录。

导入解析依赖于 sys.path:一个按顺序搜索的目录列表。sys.path[0] 是脚本所在目录。PYTHONPATH 环境变量会在前面添加额外目录。包需要 __init__.py(可以为空)才能被识别。相对导入(from . import module)在包内是有效的。importlib.reload() 会重新执行一个模块,但对旧对象的现有引用不会被更新。

python
# utils.py
def clamp(value, lo, hi):
    return max(lo, min(value, hi))

PI = 3.14159
python
# main.py
import utils

utils.clamp(150, 0, 100)   # 100
utils.PI                    # 3.14159

from utils import clamp
clamp(50, 0, 100)           # 50

Python 会在导入文件所在的同一目录(以及其他一些位置)查找模块。对于较大的项目,模块会被组织成:包含 __init__.py 文件的目录。

__name__ == "__main__"

当 Python 直接运行一个文件时,__name__ 被设为 "__main__"。当同一文件作为模块被导入时,__name__ 是模块名。这种写法让你可以编写直接执行文件时运行的代码,而当该文件被其他模块导入时这些代码会被跳过。

if __name__ == "__main__": 是可执行模块代码的标准守卫。它让一个模块既可以被导入(暴露其函数),也可以被直接运行(带有测试或演示代码)。如果没有它,导入该模块会执行任何顶层代码,而这几乎从来都不是你想要的。

__name__ 由导入机制设置:入口脚本为 "__main__",其他情况下是模块的点分名称。这个守卫可以防止副作用(启动代码、参数解析、测试运行)在导入时被执行。对于命令行工具,把入口逻辑放在 main() 函数中并在守卫下调用它,是惯用的写法。

python
# utils.py
def clamp(value, lo, hi):
    return max(lo, min(value, hi))

if __name__ == "__main__":
    # 这只在你执行 python utils.py 时运行
    # 当执行 import utils 时不会运行
    print(clamp(150, 0, 100))   # 100

这是任何同时也可以作为独立脚本运行的模块的标准写法。

标准库精华

这里再介绍几个值得了解的模块。每一个都解决了一个常见问题,如果自己实现需要花费大量工作。

标准库非常庞大;下面列举的精华是你在生产代码中最常遇到的。完整参考请查阅权威来源 docs.python.org/3/library

标准库是一组精挑细选、经过良好测试且有文档的模块。在求助于第三方包之前,先看看标准库是否已有解决方案:functoolsitertoolscontextlibdataclassestypingabc 都提供了第三方包经常重新发明的工具。

collections:特殊的容器类型:

python
from collections import Counter, defaultdict, deque

Counter(["a", "b", "a", "c", "a"])   # Counter({'a': 3, 'b': 1, 'c': 1})
defaultdict(list)                      # 自动为缺失键创建值的字典
deque([1, 2, 3], maxlen=5)            # 两端都能快速追加/弹出

itertools:处理可迭代对象的工具:

python
import itertools

list(itertools.chain([1, 2], [3, 4]))          # [1, 2, 3, 4]
list(itertools.islice(range(100), 5))          # [0, 1, 2, 3, 4]
list(itertools.combinations([1, 2, 3], 2))     # [(1, 2), (1, 3), (2, 3)]
list(itertools.product([0, 1], repeat=2))      # [(0,0), (0,1), (1,0), (1,1)]

sys:访问 Python 解释器:

python
import sys

sys.argv        # 命令行参数列表
sys.exit(1)     # 以状态码退出
sys.version     # Python 版本字符串

第三方包:除了标准库,pip 可以安装社区包:

bash
pip install requests    # HTTP 库
pip install pandas      # 数据处理
pip install numpy       # 数值计算

第三方包不在本指南的讨论范围内,但模式总是一样的:pip install,然后 import

实战示例

结合 randomstringdatetime 生成带时间戳的唯一游戏 ID:

python
import random
import string
from datetime import datetime

def generate_game_id(length: int = 8) -> str:
    chars = string.ascii_uppercase + string.digits
    return "".join(random.choices(chars, k=length))

def timestamp() -> str:
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

game_id = generate_game_id()
print(f"[{timestamp()}] Starting game {game_id}")

scores = [random.randint(50, 100) for _ in range(5)]
print(f"Round scores: {scores}")
print(f"Best: {max(scores)}")

使用 pathlibdatetime 在目录中查找文件并报告它们的大小:

python
from pathlib import Path
from datetime import datetime

def find_files(directory: str, pattern: str = "*.csv") -> list[Path]:
    return sorted(Path(directory).glob(pattern))

def timestamp() -> str:
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

files = find_files(".", "*.md")[:3]
print(f"[{timestamp()}] Found {len(files)} file(s)")
for f in files:
    size = f.stat().st_size if f.exists() else 0
    print(f"  {f.name} ({size} bytes)")

从环境变量中读取应用配置(带有带类型的默认值),并以换行分隔的 JSON 形式写入结构化的访问日志:

python
import os
import json
from datetime import datetime
from pathlib import Path

def load_env_config() -> dict:
    return {
        "debug":     os.environ.get("DEBUG", "false").lower() == "true",
        "port":      int(os.environ.get("PORT", "8080")),
        "log_level": os.environ.get("LOG_LEVEL", "INFO"),
    }

def write_access_log(method: str, path: str, status: int) -> None:
    log_dir = Path("logs")
    log_dir.mkdir(exist_ok=True)
    entry = {
        "ts":     datetime.now().isoformat(),
        "method": method,
        "path":   path,
        "status": status,
    }
    with open(log_dir / "access.jsonl", "a") as f:
        f.write(json.dumps(entry) + "\n")

config = load_env_config()
print(f"Starting on port {config['port']}, debug={config['debug']}")
write_access_log("GET", "/users", 200)

换行分隔的 JSON(.jsonl)是一种常见的日志格式:每行都是一个有效的 JSON 对象,这使得它易于流式处理、追加,以及逐行解析而无需加载整个文件。