【问题标题】:Pythonic way to ROUND_HALF_UP float with an arbitrary precision以任意精度实现 ROUND_HALF_UP 浮点的 Pythonic 方式
【发布时间】:2017-07-13 14:19:20
【问题描述】:

首先我想提一下,这个问题不是重复的:

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

但我的考虑是 这不是 Pythonic...根据docs 我应该使用类似的东西:

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.65 不符合“四舍五入”的条件,因为它真的是1.649999999999999911182158029987...
  • 我知道 - 这就是我写的原因:“因为浮点数和小数之间转换的性质” - 但你是对的,我应该更精确。另一方面,我现在想知道我可能应该省略浮点数并对十进制数执行所有计算......
  • 不,这不是问题所在。问题是您根本没有floats。无法恢复创建浮点数的字符串。在整个代码中将它们保留为字符串或小数,以避免“一个”陷阱
  • 抱歉 - 在你提出你的回复之前,我已经编辑了我的回复......是的 - 你是对的......你的回答结束了这个问题。

标签: python floating-point decimal rounding


【解决方案1】:

正如你所说,如果你尝试使用大于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')

如 cmets 中所述,可以使用科学记数法小数作为“工作”量化参数,从而简化函数:

def round_my(num, precision):
    quant_level = Decimal('1e{}'.format(precision))
    return Decimal(num).quantize(quant_level, ROUND_HALF_UP) 

这也通过了上面提到的测试用例。

【讨论】:

  • 使用 Decimal('1e1') 而不是 Decimal('10') 进行量化应该可以按预期工作。
  • @MarkDickinson 这似乎有效。我有点惊讶,你知道为什么会这样吗?
  • quantize 方法使用第二个参数的指数来确定如何量化。这是一个有点奇怪的设计,但它是 decimal 模块所基于的规范中的内容。 (因此使用Decimal('2e1')Decimal('13e1') 进行量化也可以。)
  • 你的方法总是返回一个Decimal,而它们可能应该返回输入类型。
  • @Boris 您可以随时再次将其转换为另一种类型。但是,转换小数 -> 浮点数可能会导致精度损失,因此您应该考虑是否真的需要。
【解决方案2】:

这是一个与内置 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))

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-15
    • 2017-05-20
    • 1970-01-01
    相关资源
    最近更新 更多