Python結算開發中遇到的坑

Ben9996 7年前發布 | 11K 次閱讀 數據庫 Python Python開發

坑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類型

更多用法查看: https://docs.python.org/2/library/decimal.html

坑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 </code></pre>

腳本中我們將被修正過的數據打印出來,發現被打印出來的都是四舍五入不正確的數值,經過方法處理可以保證準確的輸出。

因為我們的測試只是覆蓋了部分的數值,精度深度也只到到了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 </code></pre>

經過測試后發現沒有數據被打印出,證明在測試范圍內Python方法和數據庫的運算結果沒有差異。

關于浮點數不精確性的事情相信學過計算機組成原理這門課程的都明白,這里不再贅述,放個鏈接:

從如何判斷浮點數是否等于0說起——浮點數的機器級表示

話說為什么要在Python中做財務相關運算呢,可能最初開發這個系統的人缺乏這方面的經驗,然后通過擴展精度保留位數來解決這個問題的,但終究在做四舍五入時可能產生1分的差異。

既然發現這個問題,本著眼里不揉沙子的態度,快速的解決方案是在Python中替換原來的四舍五入函數,長期策略是逐步將計算過程挪到數據庫通過存儲過程來實現。

 

來自:http://noogel.xyz/2017/03/02/1.html

 

 本文由用戶 Ben9996 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!