结算开发中遇到的坑

坑1:浮点数不精确性

In [1]: 0.1+0.1+0.1-0.3
Out[1]: 5.551115123125783e-17

解决办法:

In [2]: from decimal import Decimal
In [3]: Decimal('0.1') + Decimal('0.1') + Decimal('0.1') - Decimal('0.3')
Out[3]: Decimal('0.0')

坑2:Decimal使用问题

In [5]: Decimal(0.1) + Decimal(0.1) + Decimal(0.1) - Decimal(0.3)
Out[5]: Decimal('2.775557561565156540423631668E-17')

解决办法:
参照坑1的解决办法,Decimal传入值需要str类型
更多用法查看:docs.python.org/2/libra

坑3:四舍五入不准确问题

In [3]: '{:.2f}'.format(Decimal(str(1001.8250)))
Out[3]: '1001.82'
In [2]: Decimal('1001.8250').quantize(Decimal('0.01'))
Out[2]: Decimal('1001.82')
In [4]: round(2.55, 1)
Out[4]: 2.5

解决办法:
发现问题原因为在不能正确四舍五入的float数值中都是因为数据存储末位的.5被存储为.4999999…的形式,解决办法是在.5上加.1的值。

def exact_round(num, exp=2):
    """
    准确的四舍五入方法
    :param num: 数值
    :param exp: 保留精度
    :return:
    """
    str_num = str(num)
    dec_num = Decimal(str_num)
    exp_unit = Decimal('0.1') ** exp
    mini_unit = Decimal('0.1') ** (exp + 1)
    if dec_num % exp_unit == (5 * mini_unit):
        dec_num += mini_unit
    return Decimal(dec_num).quantize(exp_unit, rounding=ROUND_HALF_EVEN)

为了验证这个方法写了个测试脚本:

#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
摘    要: exact_round.py
创 建 者: abc
创建日期: 2017-01-05
"""
__author__ = "abc"
from decimal import Decimal, ROUND_HALF_EVEN
def exact_round(num, exp=2):
    """
    准确的四舍五入方法
    :param num: 数值
    :param exp: 保留精度
    :return:
    """
    str_num = str(num)
    dec_num = Decimal(str_num)
    exp_unit = Decimal('0.1') ** exp
    mini_unit = Decimal('0.1') ** (exp + 1)
    raw_num = dec_num
    if dec_num % exp_unit == (5 * mini_unit):
        dec_num += mini_unit
    raw_result = Decimal(raw_num).quantize(exp_unit, rounding=ROUND_HALF_EVEN)
    result = Decimal(dec_num).quantize(exp_unit, rounding=ROUND_HALF_EVEN)
    if result != raw_result:
        print "raw:round({}, {}) = > {}; fixed: round({}, {}) => {}".format(
            raw_num, exp, raw_result,
            dec_num, exp, result
        )
    return result
 
if __name__ == "__main__":
    val = 900.0000
    while val < 1001.8600:
        for exp in range(0, 6):
            exact_round(val, exp=exp)
        val += 0.0005

脚本中我们将被修正过的数据打印出来,发现被打印出来的都是四舍五入不正确的数值,经过方法处理可以保证准确的输出。

因为我们的测试只是覆盖了部分的数值,精度深度也只到到了6位,也不能保证说方法没有问题。
后来询问了在银行做开发的朋友,他们对于数据的计算都是在数据库的存储过程中运算的,并对上面坑中的数值放到数据库中做四舍五入发现确实没有问题。

于是我将这个方法做的运算与数据库的运算结果做对比写了测试脚本。

#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
摘    要: test_db_round.py
创 建 者: abc
创建日期: 2017-01-06
"""
__author__ = "abc"
import os
import sys
sys.path.append(os.path.dirname(os.path.split(os.path.realpath(__file__))[0]))
from lib.utils import exact_round
from model import Model
 
def test_db_round(val, exp):
    """
    test_db_round
    :return:
    """
    sql = "SELECT round({}, {}) as val".format(str(val), exp)
    db_round = Model().raw(sql)[0]["val"]
    exa_round = exact_round(val, exp)
    if str(db_round) != str(exa_round):
        print "db:{}; ex:{}".format(str(db_round), str(exa_round))
 
if __name__ == "__main__":
    val = 900.0000
    while val < 1001.8600:
        for exp in range(0, 6):
            test_db_round(val, exp=exp)
        val += 0.0005



经过测试后发现没有数据被打印出,证明在测试范围内Python方法和数据库的运算结果没有差异。

关于浮点数不精确性的事情相信学过计算机组成原理这门课程的都明白,这里不再赘述,放个链接:
从如何判断浮点数是否等于0说起——浮点数的机器级表示

话说为什么要在Python中做财务相关运算呢,可能最初开发这个系统的人缺乏这方面的经验,然后通过扩展精度保留位数来解决这个问题的,但终究在做四舍五入时可能产生1分的差异。
既然发现这个问题,本着眼里不揉沙子的态度,快速的解决方案是在Python中替换原来的四舍五入函数,长期策略是逐步将计算过程挪到数据库通过存储过程来实现。

个人博客地址: noogel.xyz
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值