文章目录

  • 写在前面
  • STEP.1 从一段代码开始
  • STEP.2 灵活的精确度控制
  • STEP.3 舍入控制
  • 结束


写在前面

如果使用Python进行数据处理,那么对浮点类型的数据进行处理是避免不了的操作,这篇文章就对Python支持的浮点操作进行研究。
在开始之前,需要先明确两个概念,一个是浮点数精确度(和另一个概念双精度、单精度没直接关系),一个是小数点后的位数。

明确一下,本文提到的Python除非特殊说明,默认是指Python3。


举例如下
浮点数精度:可以理解为有效数字最大个数,比如“10.0023”和“0.000001”有效数字个数为6,那么精确度为6的浮点数变量就能精确保存这两个数。
小数点位数:这个是需要指定的小数点后保留多少位,一般还会跟随着指定超出的位数怎么处理,Python里面提供8种处理方式。

STEP.1 从一段代码开始

试着运行下面这段代码

from decimal import Decimal
from decimal import localcontext, getcontext

a = Decimal(12.345)
b = Decimal(1.000)

print("=" * 100)

print(a * b)
with localcontext() as ctx:
    ctx.prec = 10
    print(a * b)
print(a * b)

print("=" * 100)

print(a.quantize(Decimal('0.01'), rounding="ROUND_CEILING"))
print(a.quantize(Decimal('0.01'), rounding="ROUND_DOWN"))
print(a.quantize(Decimal('0.01'), rounding="ROUND_FLOOR"))
print(a.quantize(Decimal('0.01'), rounding="ROUND_HALF_DOWN"))
print(a.quantize(Decimal('0.01'), rounding="ROUND_HALF_EVEN"))
print(a.quantize(Decimal('0.01'), rounding="ROUND_HALF_UP"))
print(a.quantize(Decimal('0.01'), rounding="ROUND_UP"))
print(a.quantize(Decimal('0.01'), rounding="ROUND_05UP"))

print("=" * 100)

g_ctx = getcontext()
g_ctx.rounding = "ROUND_CEILING"
print(g_ctx.quantize(a * b, Decimal('0.001')))
with localcontext() as ctx:
    ctx.prec = 10
    ctx.rounding = "ROUND_DOWN"
    print((a * b).quantize(Decimal('0.001')))
print(g_ctx.quantize(a * b, Decimal('0.001')))

print("=" * 100)

输出

====================================================================================================
12.34500000000000063948846218
12.34500000
12.34500000000000063948846218
====================================================================================================
12.35
12.34
12.34
12.35
12.35
12.35
12.35
12.34
====================================================================================================
12.346
12.345
12.346
====================================================================================================

STEP.2 灵活的精确度控制

上面代码块中,ctx.prec = 10就是对精确度进行制定。可以看出,默认精确度是28,也就是之前说的28个有效数字。但是使用with的上下文方式来指定精确度,就可以限定精确度的影响范围。

with localcontext() as ctx:
    ctx.prec = 10
    print(a * b)

就是这段代码。为什么能生效?可以看一下这个localcontext的源码。

def localcontext(ctx=None):
    """Return a context manager for a copy of the supplied context

    Uses a copy of the current context if no context is specified
    The returned context manager creates a local decimal context
    in a with statement:
        def sin(x):
             with localcontext() as ctx:
                 ctx.prec += 2
                 # Rest of sin calculation algorithm
                 # uses a precision 2 greater than normal
             return +s  # Convert result to normal precision

         def sin(x):
             with localcontext(ExtendedContext):
                 # Rest of sin calculation algorithm
                 # uses the Extended Context from the
                 # General Decimal Arithmetic Specification
             return +s  # Convert result to normal context

    >>> setcontext(DefaultContext)
    >>> print(getcontext().prec)
    28
    >>> with localcontext():
    ...     ctx = getcontext()
    ...     ctx.prec += 2
    ...     print(ctx.prec)
    ...
    30
    >>> with localcontext(ExtendedContext):
    ...     print(getcontext().prec)
    ...
    9
    >>> print(getcontext().prec)
    28
    """
    if ctx is None: ctx = getcontext()
    return _ContextManager(ctx)

这个方法返回了一个_ContextManager类,继续进入看看。

class _ContextManager(object):
    """Context manager class to support localcontext().

      Sets a copy of the supplied context in __enter__() and restores
      the previous decimal context in __exit__()
    """
    def __init__(self, new_context):
        self.new_context = new_context.copy()
    def __enter__(self):
        self.saved_context = getcontext()
        setcontext(self.new_context)
        return self.new_context
    def __exit__(self, t, v, tb):
        setcontext(self.saved_context)

可以看出,这里面使用了一个上下文管理器。
__enter__方法定义了进入时刻要做的事情,也就是with localcontext()时候要做的事情。__exit__定义了with代码块执行完了之后要做的事情。做的事情就是将当前上下文复制一份并且保存当前上下文,然后将当前上下文设置为新复制出来的上下文,等到退出的时候再将当前上下文进行还原。

copy这个复制是深度复制方式,可以随意修改新复制出来的对象,而不会对原对象进行修改。同时重写了__copy__方法。

def copy(self):
        """Returns a deep copy from self."""
        nc = Context(self.prec, self.rounding, self.Emin, self.Emax,
                     self.capitals, self.clamp,
                     self.flags.copy(), self.traps.copy(),
                     self._ignored_flags)
        return nc
    __copy__ = copy

这样的话就可以在全局精确度设置的同时,需要单独处理的局部指定不同的精度。不只是对精度进行控制,还可以对舍入方式等进行指定。

STEP.3 舍入控制

舍入控制分为两个部分,一个是小数点后保留位数,一个是舍入方式。

print(a.quantize(Decimal('0.01'), rounding="ROUND_CEILING"))
print(a.quantize(Decimal('0.01'), rounding="ROUND_DOWN"))
print(a.quantize(Decimal('0.01'), rounding="ROUND_FLOOR"))
print(a.quantize(Decimal('0.01'), rounding="ROUND_HALF_DOWN"))
print(a.quantize(Decimal('0.01'), rounding="ROUND_HALF_EVEN"))
print(a.quantize(Decimal('0.01'), rounding="ROUND_HALF_UP"))
print(a.quantize(Decimal('0.01'), rounding="ROUND_UP"))
print(a.quantize(Decimal('0.01'), rounding="ROUND_05UP"))

quantize就是进行舍入的设置,分别可以制定保留小数点后位数以及使用的舍入方式。舍入方式可以进行全局定义。

也可以局部定义。

with localcontext() as ctx:
    ctx.prec = 10
    ctx.rounding = "ROUND_DOWN"
    print((a * b).quantize(Decimal('0.0001')))

支持的舍入操作

# 舍入方向 Infinity。
decimal.ROUND_CEILING

# 舍入方向为零。
decimal.ROUND_DOWN

# 舍入方向为 -Infinity。
decimal.ROUND_FLOOR

# 舍入到最接近的数,同样接近则舍入方向为零。
decimal.ROUND_HALF_DOWN

# 舍入到最接近的数,同样接近则舍入到最接近的偶数。
decimal.ROUND_HALF_EVEN

# 舍入到最接近的数,同样接近则舍入到零的反方向。
decimal.ROUND_HALF_UP

# 舍入到零的反方向。
decimal.ROUND_UP

# 如果最后一位朝零的方向舍入后为 0 或 5 则舍入到零的反方向;否则舍入方向为零。
decimal.ROUND_05UP

结束

本篇文章主要演示了Python的Decimal舍入相关操作,特别是在一些对浮点精度有要求的场景,一定要使用Decimal进行数据处理。
代码位置:https://github.com/huanghyw/py-notepad/blob/master/source/DecimalOpt.py