feat(core): 实现 Result 的 __repr__ 与 summary
L0 第 1 项落地。 - __repr__:单行紧凑;data 只给类型与规模(ndarray(100000)), 绝不整数组打印;__repr__ 永不抛异常 - summary():可直接贴进论文的 Markdown 表格, 行序 success → params.* → metrics.* → extras.*(按插入顺序,不排序), float 走 .6g,竖线转义,message 以引用行附在表后 - 新增 5 个模块级私有辅助:_fmt_scalar / _fmt_cell / _fmt_mapping / _describe_data / _truncate - tests/test_core.py:去掉 test_result_repr_is_readable 的 skip, 并补 3 条行为测试(默认值与失败路径与十万级数组、Markdown 结构、幂等) - STATUS.md:L0 进度 1/12;新增决策 21(repr 不打印 data 内容)、 22(summary 行序不排序);CHANGELOG.md 同步
This commit is contained in:
1 parent
bcdbdd35a3
commit
cb96c847dd
4 files changed
+162
-13
No files matched your search
@@ -6,9 +6,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
# 单个值在摘要里的最大字符数,超了就截断。
|
||||
_MAX_TEXT = 60
|
||||
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
@@ -29,6 +33,8 @@ class Result:
|
||||
message:
|
||||
给人看的一句话说明,可直接抄进论文的"方法说明"。
|
||||
|
||||
两个出口::meth:`summary` 给论文用(Markdown 表格),`repr` 给调试用(紧凑单行)。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> r = Result("demo", params={"alpha": 0.5}, metrics={"r2": 0.9})
|
||||
@@ -44,8 +50,92 @@ class Result:
|
||||
message: str = ""
|
||||
extras: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# TODO(L0): 实现 __repr__,打印成"方法名 + 指标表"的紧凑形式,
|
||||
# 方便在 notebook 里直接肉眼检查、或复制进论文草稿。
|
||||
# TODO(L0): 实现 summary() -> str,返回可直接贴进论文的 Markdown 表格。
|
||||
def __repr__(self) -> str: # pragma: no cover - 待实现
|
||||
raise NotImplementedError
|
||||
def __repr__(self) -> str:
|
||||
"""紧凑的单行摘要,方便在 notebook / 调试器里一眼确认结果。
|
||||
|
||||
`data` 只给类型和规模,**绝不**整个打印——模拟轨迹动辄十万个数,
|
||||
repr 一旦刷屏,traceback 和调试器就都没法看了。
|
||||
`__repr__` 还必须永不抛异常,否则报错信息本身会变成报错源。
|
||||
"""
|
||||
parts = [f"success={self.success}"]
|
||||
if self.params:
|
||||
parts.append(f"params={_fmt_mapping(self.params)}")
|
||||
if self.metrics:
|
||||
parts.append(f"metrics={_fmt_mapping(self.metrics)}")
|
||||
if self.data is not None:
|
||||
parts.append(f"data={_describe_data(self.data)}")
|
||||
if self.message:
|
||||
parts.append(f"message={self.message!r}")
|
||||
return f"Result({self.name}, {', '.join(parts)})"
|
||||
|
||||
def summary(self) -> str:
|
||||
"""返回可直接贴进论文的 Markdown 表格。
|
||||
|
||||
行顺序为 `success` → `params.*` → `metrics.*` → `extras.*`,
|
||||
按字典插入顺序输出(比赛时按自己想要的阅读顺序建字典即可)。
|
||||
`message` 非空时以引用行附在表后。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> print(Result("demo", metrics={"r2": 0.9}).summary())
|
||||
### demo
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| success | True |
|
||||
| metrics.r2 | 0.9 |
|
||||
"""
|
||||
rows: list[tuple[str, Any]] = [("success", self.success)]
|
||||
for prefix, mapping in (
|
||||
("params", self.params),
|
||||
("metrics", self.metrics),
|
||||
("extras", self.extras),
|
||||
):
|
||||
for key, value in (mapping or {}).items():
|
||||
rows.append((f"{prefix}.{key}", value))
|
||||
|
||||
lines = [f"### {self.name}", "", "| 项 | 值 |", "|---|---|"]
|
||||
lines += [f"| {key} | {_fmt_cell(value)} |" for key, value in rows]
|
||||
if self.message:
|
||||
lines += ["", f"> {self.message}"]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _fmt_scalar(value: Any) -> str:
|
||||
"""把一个值格式化成短、稳、论文友好的字符串。
|
||||
|
||||
`bool` 必须在 `int` 之前判断——`True` 同时是 `int` 的实例,
|
||||
顺序反了就会把 `True` 打印成 `1`。
|
||||
"""
|
||||
if isinstance(value, bool):
|
||||
return str(value)
|
||||
if isinstance(value, float):
|
||||
# 6 位有效数字:够论文用,又不会出现 0.30000000000000004。
|
||||
return f"{value:.6g}"
|
||||
if isinstance(value, int):
|
||||
return str(value)
|
||||
# 其他类型先归一化空白(换行会把单行 repr 和 Markdown 表格都撑破),再截断。
|
||||
return _truncate(" ".join(str(value).split()))
|
||||
|
||||
|
||||
def _fmt_cell(value: Any) -> str:
|
||||
"""Markdown 表格单元格:值里出现竖线会把表格顶散,必须转义。"""
|
||||
return _fmt_scalar(value).replace("|", r"\|")
|
||||
|
||||
|
||||
def _fmt_mapping(mapping: Mapping[str, Any]) -> str:
|
||||
return "{" + ", ".join(f"{key}: {_fmt_scalar(value)}" for key, value in mapping.items()) + "}"
|
||||
|
||||
|
||||
def _describe_data(data: Any) -> str:
|
||||
"""只描述 `data` 的类型与规模,不打印内容。"""
|
||||
shape = getattr(data, "shape", None)
|
||||
if shape is not None: # numpy 数组 / pandas DataFrame、Series
|
||||
return f"{type(data).__name__}({', '.join(str(d) for d in shape)})"
|
||||
if isinstance(data, str | bytes | list | tuple | set | frozenset | dict):
|
||||
return f"{type(data).__name__}({len(data)})"
|
||||
return type(data).__name__
|
||||
|
||||
|
||||
def _truncate(text: str, limit: int = _MAX_TEXT) -> str:
|
||||
return text if len(text) <= limit else text[: limit - 1] + "…"
|
||||
Reference in new issue
Block a user