结算开发中遇到的坑

坑1:浮点数不精确性

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

解决办法:

1
2
3
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使用问题

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

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

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

1
2
3
4
5
6
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的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/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位,也不能保证说方法没有问题。
后来询问了在银行做开发的朋友,他们对于数据的计算都是在数据库的存储过程中运算的,并对上面坑中的数值放到数据库中做四舍五入发现确实没有问题。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/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中替换原来的四舍五入函数,长期策略是逐步将计算过程挪到数据库通过存储过程来实现。