Pythonic方法对任意精度的浮点数进行ROUND_HALF_UP四舍五入处理

4
首先我想提到这个问题不是重复的: Python Rounding Inconsistently Python 3.x rounding behavior 我知道IEEE 754,并且我知道:
“简单的“始终将0.5四舍五入”技术会导致略微偏向更高的数字。在大量计算时,这可能是重要的。 Python 3.0方法消除了这个问题。”
我同意ROUND_HALF_UP方法比Python默认实现的方法差。尽管如此,有些人不知道这一点,如果规范要求,就需要使用该方法。使其起作用的简单方法是:
def round_my(num, precission):
    exp  = 2*10**(-precission)
    temp = num * exp
    if temp%2 < 1:
        return int(temp - temp%2)/exp
    else:
        return int(temp - temp%2 + 2)/exp

但我认为这不符合Python风格...根据文档,我应该使用类似以下方式的代码:
def round_my(num, pricission):
    N_PLACES = Decimal(10) ** pricission       # same as Decimal('0.01')
    # Round to n places
    Decimal(num).quantize(N_PLACES)

问题是这样做不能通过所有测试案例:
class myRound(unittest.TestCase):
    def test_1(self):
        self.assertEqual(piotrSQL.round_my(1.53, -1), 1.5)
        self.assertEqual(piotrSQL.round_my(1.55, -1), 1.6)
        self.assertEqual(piotrSQL.round_my(1.63, -1), 1.6)
        self.assertEqual(piotrSQL.round_my(1.65, -1), 1.7)
        self.assertEqual(piotrSQL.round_my(1.53, -2), 1.53)
        self.assertEqual(piotrSQL.round_my(1.53, -3), 1.53)
        self.assertEqual(piotrSQL.round_my(1.53,  0), 2)
        self.assertEqual(piotrSQL.round_my(1.53,  1), 0)
        self.assertEqual(piotrSQL.round_my(15.3,  1), 20)
        self.assertEqual(piotrSQL.round_my(157.3,  2), 200)

由于浮点数和十进制数之间的转换性质,以及量化似乎不能处理指数为10或100的情况。有没有Pythonic的方法来解决这个问题?
我知道我可以添加无限小的数字,然后使用round(num+10**(precission-20),-pricission),但这是非常错误的,"小狗会死掉"……

1
你知道例如 1.65 不会被认为是“四舍五入”的,因为它实际上是 1.649999999999999911182158029987... 吗? - MSeifert
我知道 - 这就是为什么我写了:“由于浮点数和十进制数之间的转换的性质” - 但你是对的,我应该更加精确。另一方面,我现在在想,我可能应该省略浮点数,并在十进制数上执行所有计算... - Piotr Siejda
3
不,那不是问题所在。问题在于你有float类型。无法还原创建float的字符串。在整个代码中保持它们为字符串或十进制数以避免这种“陷阱”。 - MSeifert
抱歉 - 在你发出回复之前,我已经编辑了我的回复... 是的 - 你是正确的... 你的答案解决了这个问题。 - Piotr Siejda
2个回答

3

正如您所说,如果您尝试使用大于 1 的数字进行 quantize,那么这是无效的:

>>> Decimal('1.5').quantize(Decimal('10'))
Decimal('2')
>>> Decimal('1.5').quantize(Decimal('100'))
Decimal('2')

但是你可以简单地进行除法、量化和乘法:
from decimal import Decimal, ROUND_HALF_UP

def round_my(num, precision):
    N_PLACES = Decimal(10) ** precision
    # Round to n places
    return (Decimal(num) / N_PLACES).quantize(1, ROUND_HALF_UP) * N_PLACES

然而,只有在输入 Decimal 并将其与 Decimal 进行比较时,才能通过测试:

assert round_my('1.53', -1) == Decimal('1.5')
assert round_my('1.55', -1) == Decimal('1.6')
assert round_my('1.63', -1) == Decimal('1.6')
assert round_my('1.65', -1) == Decimal('1.7')
assert round_my('1.53', -2) == Decimal('1.53')
assert round_my('1.53', -3) == Decimal('1.53')
assert round_my('1.53',  0) == Decimal('2')
assert round_my('1.53',  1) == Decimal('0')
assert round_my('15.3',  1) == Decimal('20')
assert round_my('157.3',  2) == Decimal('200')

如评论中所述,可以将科学计数法小数作为“工作”量化参数来简化函数:
def round_my(num, precision):
    quant_level = Decimal('1e{}'.format(precision))
    return Decimal(num).quantize(quant_level, ROUND_HALF_UP) 

这也通过了上述的测试用例。

2
使用Decimal('1e1')而不是Decimal('10')进行量化应该能够按预期工作。 - Mark Dickinson
@MarkDickinson 看起来可以。我有点惊讶,你知道为什么会这样吗? - MSeifert
3
“quantize”方法使用第二个参数的指数来确定如何进行量化。这是一个有点奇怪的设计,但它是基于“decimal”模块规范的。因此,使用“Decimal('2e1')”或“Decimal('13e1')”进行量化也是可行的。 - Mark Dickinson
你的方法总是返回一个 Decimal,但它们应该返回输入类型。 - user3064538
@Boris 你总是可以再次将其转换为另一种类型。然而,十进制 -> 浮点数的转换可能会导致精度损失,因此你应该考虑是否真的需要这样做。 - MSeifert
与其展示使用没有指数的小数进行量化的边缘情况,然后展示如何使用指数进行量化,不如直接编辑您的答案以(客观地?)展示正确的方法。 - Zach Young

0
这里有一个版本,它与内置的round()函数的行为和API相匹配:
from decimal import Decimal, ROUND_HALF_UP

def round_half_up(number, ndigits=None):
    return_type = type(number)
    if ndigits is None:
        ndigits = 0
        return_type = int
    if not isinstance(ndigits, int):
        msg = f"'{type(ndigits).__name__}' object cannot be interpreted as an integer"
        raise TypeError(msg)
    quant_level = Decimal(f"10E{-ndigits}")
    return return_type(Decimal(number).quantize(quant_level, ROUND_HALF_UP))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接