Python Decimal 信号标记:后一笔计算精确,Inexact 为什么仍为真

前天 3阅读

批量处理测量值时,程序算完每一行就检查 Inexact,给发生精度损失的记录打标。第一行除不尽,第二行明明得到精确的零点五,却也被判为不精确。问题可能出在诊断状态没有清零:Decimal 的信号标记会保留此前发生过的条件。

把一次观察的起点写进代码

上下文的 flags 像一组会保持亮起的指示灯。某次运算触发信号后,对应项变为真;后面一次正常运算不会替它熄灯。因此读取标记回答的是“从上次清除以来有没有发生过”,无法直接回答“刚才这一笔是否发生过”。

下面只观察两个信号,显式关闭对应陷阱,让运算完成后再检查标记。保存为 demo.py,执行 python demo.py。三个有效数字足以让七分之一触发信号,又能精确表示二分之一;代码没有使用最终小数位格式化来制造差异。

Python Decimal 信号标记:后一笔计算精确,Inexact 为什么仍为真

AI概念示意图:前一次计算点亮的标记持续保留,明确重置后才开始新的观察。

from decimal import Decimal, Inexact, Rounded, localcontext

with localcontext() as ctx:
    ctx.prec = 3
    ctx.traps[Inexact] = False
    ctx.traps[Rounded] = False
    ctx.clear_flags()
    first = Decimal(1) / Decimal(7)
    assert ctx.flags[Inexact] and ctx.flags[Rounded]
    print("first:", first, ctx.flags[Inexact])

    second = Decimal(1) / Decimal(2)
    assert second == Decimal("0.5") and ctx.flags[Inexact]
    print("exact but sticky:", second, ctx.flags[Inexact])

    with localcontext() as inner:
        assert inner.flags[Inexact]
        inner.clear_flags()
        assert not inner.flags[Inexact]
    assert ctx.flags[Inexact]
    print("nested clear changed outer:", False)

    ctx.clear_flags()
    second = Decimal(1) / Decimal(2)
    assert not ctx.flags[Inexact] and not ctx.flags[Rounded]
    print("after reset:", second, ctx.flags[Inexact])

    ctx.clear_flags()
    trimmed = +Decimal("1.2300")
    assert trimmed == Decimal("1.23")
    assert ctx.flags[Rounded] and not ctx.flags[Inexact]
    print("zeros only:", trimmed, ctx.flags[Rounded], ctx.flags[Inexact])

输出中 first 是 0.143 True,exact but sticky 是 0.5 True。第二个 True 属于尚未清除的历史状态。after reset 则是 0.5 False,证明先清除再计算能够把这笔精确除法与上一笔隔开,而不必改变计算精度。

局部上下文也会带入已有标记

localcontext 创建当前上下文的副本。进入嵌套块时,inner 仍能看到外层已经置真的 Inexact;它不会因为是新变量就自动成为空白记录。示例在副本里清除后,外层仍为真,所以 nested clear changed outer 输出 False。

如果函数要独立汇报一组计算,可以进入自己的局部上下文,先清除标记,再执行该组运算,最后复制需要返回的诊断值。这样既明确观察窗口,也避免为了重置诊断而直接改动调用者仍要检查的状态。

清除位置取决于问题:检查每条记录,就在每条记录前清除;检查整批是否曾出现异常条件,就在整批前清除一次。若循环中不断清除,最后只读一次,会丢掉早期记录触发过的证据。需要定位具体哪行,还应把行号和检查结果一起保存。

不要把所有置真的标记都当成数值错误

末行得到 1.23 True False:一元加号把数应用到当前精度,丢弃的只是尾零,所以 Rounded 为真而 Inexact 为假。这个反例说明“执行过舍入”与“丢掉非零信息”是不同条件。业务若关心数值是否变化,不能只看 Rounded。

clear_flags 清除全部信号标记,不调整精度,也不改变陷阱开关。不要把它写成 clear_traps;后者改变的是以后遇到条件时是否抛异常。生产代码应按要求选择观察或中断方式,本例关闭两个陷阱只是为了展示可检查的结果。

标记还不是逐步运算日志,它不保存触发次数和发生位置。一个复杂表达式中任何一步触发,都可能让最后看到的标记为真。排查时把计算分成有业务含义的小段,并在段前清除、段后读取,才有足够证据归因。

若要将诊断放入结果列表,应保存当时的布尔值或 flags 的副本。直接保存可变映射的引用,后续清除时可能连已记录的观察也一起改变,失去追溯每个窗口的意义。

最后保留“不清除时误归因”和“清除后精确计算不置位”这对样本。只测试会触发信号的输入,无法发现状态串到下一条的错误;也不要把所有标记为假推广为输入格式和业务范围都合格,那些条件仍需单独校验。

参考资料

资料核对日期:2026年10月2日(北京时间)。示例在 Linux、CPython 3.12.14 中独立运行。

Python:Decimal 信号、上下文与 clear_flags

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。