(十八)综合示例:可以导入的阅读统计包
1. 任务、数据流与文件职责
任务(自拟):从启动参数接收每天的非负整数分钟数,输出统计天数、总时长、平均时长、达标天数和最长时长。--goal 设置非负整数目标,默认 30;--only-met 表示先保留达标记录,再统计保留下来的数据。没有选中记录时,各项计数与总量为 0,平均值显示为 0.00。
先用 20、35、0、45 手算:全部保留时,总量 100、条数 4、平均 25.00、达标 2、最长 45;打开 --only-met 后,剩下 35、45,总量 80、条数 2、平均 40.00、达标 2、最长 45。
把目录保存为 reading_project:
reading_project/
└── readingkit/
├── __init__.py
├── __main__.py
├── cli.py
└── core.py
core 负责筛选和统计,cli 负责启动参数和输出,__main__ 负责调用入口。cli 可以导入 core,计算模块不需要反过来导入命令行入口。
图中的筛选与统计沿同一来源前进。统计函数每请求下一项,筛选函数才继续读取输入;筛选掉 20 和 0 后,统计状态只更新两次。
2. 计算模块:筛选与统计
# 项目文件:reading_project/readingkit/core.py
# Iterable 表示可遍历的输入,Iterator 表示函数调用返回的逐项来源。
from collections.abc import Iterable, Iterator
type Summary = dict[str, int | float]
def select_minutes(
values: Iterable[int], *, goal: int = 30, only_met: bool = False
) -> Iterator[int]:
# 关闭筛选时交出全部数据,打开时只交出达标项。
for minutes in values:
if not only_met or minutes >= goal:
yield minutes
def summarize(values: Iterable[int], *, goal: int = 30) -> Summary:
"""在一次遍历中生成阅读统计。"""
count = total = completed = longest = 0
for minutes in values:
count += 1
total += minutes
if minutes >= goal:
completed += 1
longest = max(longest, minutes)
# 空来源也直接形成一份完整报告,显示层不需要补齐字段。
average = total / count if count else 0.0
return {
"count": count,
"total": total,
"average": average,
"completed": completed,
"longest": longest,
}select_minutes 不建立筛选结果列表,summarize 不会先用 sum 消耗来源、再重新遍历。四个累计状态与平均值已经对应第二章的手算表;函数包装没有改变统计规则,只把数据交入和结果交出的地方写明确。
从这一接口看,Summary 是字符串键、整数或浮点值的字典。每个键的具体含义由返回内容说明;后面的面向对象章节会用数据类把一条报告的各字段集中起来。
3. 命令行模块与包入口
# 项目文件:reading_project/readingkit/cli.py
# 命令行层依赖计算层;计算层不依赖输入方式。
import argparse
from .core import select_minutes, summarize
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="阅读时长统计")
parser.add_argument("minutes", nargs="*", type=int, help="每天的分钟数")
parser.add_argument("--goal", type=int, default=30, help="达标分钟数")
parser.add_argument("--only-met", action="store_true", help="只统计达标记录")
return parser
def main() -> None:
parser = build_parser()
args = parser.parse_args()
# 解析器负责把错误反馈给命令行用户;下面继续使用转换后的整数。
if args.goal < 0:
parser.error("目标分钟数不能为负数")
if any(minutes < 0 for minutes in args.minutes):
parser.error("每天的分钟数不能为负数")
selected = select_minutes(
args.minutes, goal=args.goal, only_met=args.only_met
)
report = summarize(selected, goal=args.goal)
print("统计天数", report["count"])
print("总时长", report["total"])
print(f"平均时长 {report['average']:.2f}")
print("达标天数", report["completed"])
print("最长时长", report["longest"])build_parser 返回解析器,让配置启动规则与实际执行分开。main() -> None 表示入口通过显示结果完成工作,不返回一份报告;真正要复用报告时,直接调用 summarize。
# 项目文件:reading_project/readingkit/__init__.py
# 导入 readingkit 时只建立包,启动动作由 __main__ 承担。
"""阅读时长统计包。"""# 项目文件:reading_project/readingkit/__main__.py
# 包入口只调用命令行层,不重复计算或解析逻辑。
from .cli import main
if __name__ == "__main__":
main()4. 运行、复用与工作量
在 reading_project 文件夹中,用第一章准备的 Python 3.12 环境执行:
# 先统计全部四天,再只统计达标记录,比较相同数据的两种结果。
python -m readingkit 20 35 0 45
python -m readingkit 20 35 0 45 --only-met
# 调高目标,或查看解析器生成的帮助。
python -m readingkit 20 35 0 45 --goal 40
python -m readingkit --help
第一条输出:
统计天数 4
总时长 100
平均时长 25.00
达标天数 2
最长时长 45
第二条输出:
统计天数 2
总时长 80
平均时长 40.00
达标天数 2
最长时长 45
在同一目录还可以另建 use_core.py,只导入计算函数:
# 项目文件:reading_project/use_core.py
# 导入计算模块不会解析启动参数,也不会主动显示命令行报告。
from readingkit.core import summarize
report = summarize(iter([20, 35, 0, 45]))
print(report["total"], report["completed"])执行 python use_core.py 得到 100 2。后面从文件、数据库或网页取得数据,都可以继续调用计算函数,不必同时启动命令行。
设有 n 条输入,解析、检查与筛选分别线性读取,合计工作量仍是 。命令行解析器保存的分钟数列表占 空间;筛选生成器和统计函数自身只额外保存当前进度与几个累计值,为 。逐项处理减少的是中间结果存储,整条命令并没有因此变成常数空间。