【问题标题】:Python 3.x rounding half upPython 3.x 四舍五入
【发布时间】:2020-03-05 01:23:05
【问题描述】:

我知道关于在 python 中舍入的问题已经被问过多次,但答案对我没有帮助。我正在寻找一种将浮点数四舍五入并返回浮点数的方法。该方法还应该接受一个参数,该参数定义要舍入到的小数位。我写了一个实现这种舍入的方法。但是,我认为它看起来一点也不优雅。

def round_half_up(number, dec_places):
    s = str(number)

    d = decimal.Decimal(s).quantize(
        decimal.Decimal(10) ** -dec_places,
        rounding=decimal.ROUND_HALF_UP)

    return float(d)

我不喜欢它,我必须将 float 转换为字符串(以避免浮点不准确),然后使用 decimal 模块。 您有更好的解决方案吗?

编辑:正如下面的答案所指出的,我的问题的解决方案并不那么明显,因为正确的舍入首先需要正确表示数字,而浮点数不是这种情况。所以我希望下面的代码

def round_half_up(number, dec_places):

    d = decimal.Decimal(number).quantize(
        decimal.Decimal(10) ** -dec_places,
        rounding=decimal.ROUND_HALF_UP)

    return float(d)

(与上面的代码不同,只是浮点数直接转换为十进制数,而不是先转换为字符串)在这样使用时返回 2.18:round_half_up(2.175, 2) 但不是因为@ 987654324@ 将返回Decimal('2.17499999999999982236431605997495353221893310546875'),浮点数由计算机表示的方式。 令人惊讶的是,第一个代码返回 2.18,因为浮点数首先转换为字符串。似乎 str() 函数对最初要舍入的数字进行了隐式舍入。所以有两个舍入发生。尽管这是我所期望的结果,但在技术上是错误的。

【问题讨论】:

  • @IcesHay 我不确定我是否通过将数字四舍五入来理解你的意思。你能给我们举一些例子吗?
  • @Kenivia 我的意思是,你从 0-4 向下和从 5-9 向上四舍五入相关的小数位:如果四舍五入到 0 位小数:2.4 = 2; 2.5 = 3; 3.5 = 4 等

标签: python python-3.x rounding


【解决方案1】:

舍入出奇地难以做到正确,因为您必须非常小心地处理浮点计算。如果您正在寻找一个优雅 的解决方案(简短、易于理解),那么您所拥有的就是一个很好的起点。正确地说,您应该将 decimal.Decimal(str(number)) 替换为从数字本身创建小数,这将为您提供其精确表示的十进制版本:

d = Decimal(number).quantize(...)...

Decimal(str(number)) 有效地舍入两次,因为将浮点数格式化为字符串表示会执行其自己的舍入。这是因为str(float value) 不会尝试打印浮点数的完整十进制表示,它只会打印足够的数字以确保如果您将这些确切的数字传递给float 构造函数,您会得到相同的浮点数。

如果你想保持正确的舍入,但避免依赖于大而复杂的 decimal 模块,你当然可以这样做,但你仍然需要一些方法来实现精确的算术需要正确舍入。例如,您可以使用fractions

import fractions, math

def round_half_up(number, dec_places=0):
    sign = math.copysign(1, number)
    number_exact = abs(fractions.Fraction(number))
    shifted = number_exact * 10**dec_places
    shifted_trunc = int(shifted)
    if shifted - shifted_trunc >= fractions.Fraction(1, 2):
        result = (shifted_trunc + 1) / 10**dec_places
    else:
        result = shifted_trunc / 10**dec_places
    return sign * float(result)

assert round_half_up(1.49) == 1
assert round_half_up(1.5) == 2
assert round_half_up(1.51) == 2
assert round_half_up(2.49) == 2
assert round_half_up(2.5) == 3
assert round_half_up(2.51) == 3

请注意,上面代码中唯一棘手的部分是浮点到分数的精确转换,可以卸载到as_integer_ratio() float 方法,这是小数和分数在内部所做的.所以如果真的想去掉对fractions的依赖,可以将小数运算简化为纯整数运算;以牺牲一些易读性为代价保持在相同的行数内:

def round_half_up(number, dec_places=0):
    sign = math.copysign(1, number)
    exact = abs(number).as_integer_ratio()
    shifted = (exact[0] * 10**dec_places), exact[1]
    shifted_trunc = shifted[0] // shifted[1]
    difference = (shifted[0] - shifted_trunc * shifted[1]), shifted[1]
    if difference[0] * 2 >= difference[1]:  # difference >= 1/2
        shifted_trunc += 1
    return sign * (shifted_trunc / 10**dec_places)

请注意,测试这些函数会突出在创建浮点数时执行的近似值。例如,print(round_half_up(2.175, 2)) 打印出2.17,因为十进制数2.175 不能用二进制精确表示,所以它被一个恰好比十进制 2.175 略小的近似值代替。该函数接收该值,发现它小于对应于小数点 2.175 的实际分数,并决定将其 向下舍入。这不是实现的怪癖;该行为源自浮点数的属性,也存在于 Python 的 round 内置函数 32

【讨论】:

  • d = Decimal(number).quantize(...) 的问题是,浮点数没有精确的表示。所以Decimal(2.175) 会给你 Decimal('2.17499999...') (因此四舍五入会不正确)而Decimal('2.175') 是 Decimal('2.175')。这就是我先转换为字符串的原因。
  • @IcesHay 确实没有像 0.1 这样的数字在二进制中的精确表示(即分数 1/10),因为它的分母不是 2 的幂。但是一旦你有一个实际的Python float,近似值已经发生,并且您在内存中的数字确实具有精确表示,它由as_integer_ratio()提供(如果是0.1,则为3602879701896397/36028797018963968)。 decimal.Decimal(float) 构造函数使用该表示,随后的舍入将是正确的并准确执行。
  • 你确定吗?因为我测试了函数round_half_up(2.175, 2),它返回了不正确的 2.17。也许我做错了什么?
  • @IcesHay 就是这样,当读取为浮点数时,2.175 近似为一个可精确表示为分数 2448832297382707/1125899906842624 的数字,它略小于 2.175。 (您可以使用'%.20f' % 2.175 进行测试,其计算结果为'2.17499999999999982236'。)例如,在使用半舍入的Python 2.7 中,round(2.175) 返回2.17,这种结果以documented 作为限制浮点数。
  • @IcesHay 这不是浮点的工作方式。一旦将字符串读入浮点数,原始数字(将“2.175”与“2.174999999999...”区分开来)将不可挽回地丢失。您的问题是关于实现数字的四舍五入,这就是这个答案所提供的。看来您的实际问题完全是另外一回事,因为现在事实证明,即使是 Python 2 round(它确实实现了round-half-up)也不够好。请编辑问题以指定实际用例 - 也许可以通过避免浮点数和使用小数来满足。
【解决方案2】:

我不喜欢它,我必须将浮点数转换为字符串(以避免 浮点数不准确),然后使用十进制模块。做 你有更好的解决方案吗?

是的;如果您需要精确表示诸如 2.675 之类的数字并将它们舍入为 2.68 而不是 2.67,请使用 Decimal 在整个程序中表示您的数字。

没有别的办法。屏幕上显示为 2.675 的浮点数 不是 实数 2.675;事实上,它比 2.675 略小,这就是为什么它被四舍五入到 2.67:

>>> 2.675 - 2
0.6749999999999998

它仅以字符串形式显示为'2.675',因为这恰好是最短的字符串,例如float(s) == 2.6749999999999998。请注意,这种较长的表示(有很多 9)也不准确。

无论您如何编写舍入函数,my_round(2.675, 2) 都无法向上舍入为 2.68my_round(2 + 0.6749999999999998, 2) 也无法向下舍入为 2.67;因为输入实际上是相同的浮点数。

因此,如果您的数字 2.675 曾经被转换为浮点数并再次返回,那么您已经丢失了关于它应该向上还是向下取整的信息。解决办法是一开始就不要让它浮起来。

【讨论】:

    【解决方案3】:

    在尝试了很长时间来产生一个优雅的单行函数之后,我最终得到了一个大小堪比字典的东西。

    我想说最简单的方法就是

    def round_half_up(inp,dec_places):
        return round(inp+0.0000001,dec_places)
    

    我承认这并非在所有情况下都是准确的,但如果您只是想要一个简单的快速解决方法,它应该可以工作。

    【讨论】:

    • 感谢您的努力。您的解决方案可能很简单,但它是一种简单的方法,而且不是很实用。再次澄清一下:我正在寻找一种解决方案,即将浮点值正确“一半”四舍五入到任何小数位。我很好奇是否有其他人遇到过类似的问题并且有一个优雅而有效的解决方案。
    • 这个解决方法不正确;例如,round_half_up(1.499999999, 0) 返回 2.0 而不是 1.0。
    • @user4815162342 它不像通常那样向下舍入 1.5,这就是为什么 op 首先问这个问题
    • @user4815162342 但是是的,在这些情况下它是不准确的。但如果您只想将 1.5 向下舍入,我认为这是一个不错的快速解决方法。顺便说一句,你的回答很好:)
    • 谢谢。如果您昨天问我,我会说将 1.499999999 舍入到 2.0 通常是不正确的,尤其是对于 OP。但是在与 cmets 中的 OP 对我的回答进行了最新一轮讨论之后,我不再确定,并且开始认为这个问题以目前的形式无法回答。鉴于此,我取消了我的反对票。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-01-12
    • 2018-09-14
    • 1970-01-01
    • 2012-08-04
    • 2023-03-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多