保留小数点后 x 位,如何对浮点数进行四舍五入?

111

有没有一种方法可以将Python中的浮点数保留x位小数?例如:

>>> x = roundfloat(66.66666666666, 4)
66.6667
>>> x = roundfloat(1.29578293, 6)
1.295783

我已经找到了缩短/截断它们的方法(66.666666666 --> 66.6666),但无法四舍五入(66.666666666 --> 66.6667)。

6个回答

174

我觉得有必要对Ashwini Chaudhary的答案提出一些反驳。尽管看起来,round函数的两个参数形式可以将Python浮点数舍入到指定的小数位数,但实际上这并不是你想要的解决方案,即使你认为它是。让我解释一下...

将(Python)浮点数舍入到某个小数位数的能力是经常被要求的,但事实证明它很少是实际所需的。看似简单的答案round(x, number_of_places)有点像一个诱人的陷阱:它看起来好像做了你想做的事情,但由于Python浮点数在内部以二进制形式存储,它正在做一些相当微妙的事情。考虑以下例子:

>>> round(52.15, 1)
52.1

对于一个对round的天真理解,这看起来是错误的:它应该向上取整到52.2而不是向下取整到52.1。要理解为什么不能依赖这样的行为,需要明白尽管这看起来是一个简单的十进制到十进制操作,但它远非简单。

那么这就是上面例子中真正发生的事情(深呼吸)。我们显示了离最近的n位小数点后的十进制数字最接近的二进制浮点数的十进制表示,这个二进制浮点数是用十进制编写的数值的二进制浮点数近似值。因此,为了从原始数字文字到显示输出,底层机制已经进行了四次在二进制和十进制格式之间的转换,每个方向上进行了两次转换。将其分解(并假设 IEEE 754 binary64 格式、舍入至偶数规则和 IEEE 754 规则):

  1. 首先,将数字字面量52.15解析并转换为Python浮点数。实际存储的数字是7339460017730355 * 2**-47,或者52.14999999999999857891452847979962825775146484375

  2. 作为round操作的第一步,在内部计算最接近存储数字的小数点后1位的十进制字符串。由于存储的数字稍低于原始值52.15,我们最终将其向下舍入并得到一个字符串52.1。这就解释了为什么我们得到的最终输出是52.1而不是52.2

  3. 然后,在round操作的第二步中,Python将该字符串转换回浮点数,得到最接近52.1的二进制浮点数,现在是7332423143312589 * 2**-47,或者52.10000000000000142108547152020037174224853515625

  4. 最后,在Python的交互式环境中,浮点值被显示(用十进制表示)。这涉及将二进制值转换回十进制字符串,得到52.1作为最终输出。

在Python 2.7及更高版本中,我们有一个愉快的情况,即步骤3和4中的两次转换互相抵消了。这是由于Python选择了repr实现,它产生的是保证正确舍入到实际浮点数的最短十进制值。选择的一个后果是,如果你从具有15个或更少有效数字的任何(不太大、不太小的)十进制字面量开始,那么相应的浮点数将显示出这些完全相同的数字:

>>> x = 15.34509809234
>>> x
15.34509809234

遗憾的是,这进一步加深了Python存储十进制值的假象。但在Python 2.6中并非如此!以下是在Python 2.6中执行的原始示例:

>>> round(52.15, 1)
52.200000000000003
我们不仅会按相反的方向四舍五入,得到的是52.2而不是52.1,而且显示的值甚至不打印为52.2!这种行为导致了许多类似“round is broken!”的Python错误跟踪器报告。但是出问题的不是round函数,而是用户的期望。(好吧,好吧,在Python 2.6中,round函数有点问题,因为它没有使用正确的舍入方法。)
简短版:如果你正在使用双参数round函数,并且期望从一个十进制中的二进制近似值的舍入结果获得可预测的行为,那么你就会遇到麻烦。
所以够了,“双参数round函数很糟糕”的争论足够了。相反,你应该使用什么?根据你想要做什么,有几个可能性。
- 如果你是为了显示目的而进行四舍五入,那么你根本不需要一个浮点数结果;你需要一个字符串。在这种情况下,答案是使用字符串格式化:
>>> format(66.66666666666, '.4f')
'66.6667'
>>> format(1.29578293, '.6f')
'1.295783'

即使如此,人们仍然必须了解内部二进制表示,以免被表面上的十进制中间情况的行为所惊讶。

>>> format(52.15, '.1f')
'52.1'
如果你在一个需要对十进制四舍五入方向有所关注的场景中操作(例如在某些金融环境中),你可能想要使用Decimal 类型来表示你的数字。在Decimal类型上进行十进制四舍五入比在二进制类型上进行四舍五入更合理(同样,在二进制类型上固定位数地进行四舍五入是完全合理的)。此外,decimal 模块可以更好地控制四舍五入模式。在Python 3中,round可以直接完成这项工作。在Python 2中,您需要使用quantize方法。
>>> Decimal('66.66666666666').quantize(Decimal('1e-4'))
Decimal('66.6667')
>>> Decimal('1.29578293').quantize(Decimal('1e-6'))
Decimal('1.295783')
  • 在极少数情况下,round 的双参数版本确实是你想要的:也许你正在将浮点数分成大小为 0.01 的区间,而且你不太关心边界情况如何处理。然而,这些情况很少出现,并且很难仅凭这些情况来证明 round 内置函数的双参数版本的存在。


  • 另一种方法。除了float(),这种表示法叫什么?float('%.2f' % 1.29578293)会产生1.3,它会舍去零。或者float('%.3f' % 1.29578293)是1.296。 - gseattle
    @gseattle:自 Python 3 开始,使用固定精度格式化然后转换为“float”应该完全等同于使用“round”。这个格式化操作再次将二进制浮点数转换为十进制字符串,并且在内部使用完全相同的机制来执行“round”。(对于 Python 2,有点不同,因为半路行为是不同的。) - Mark Dickinson
    关于MicroPython,可以在这里讨论。 - Serge Stroobandt
    嗨,马克。由于您似乎在浮点运算方面非常有知识,您是否愿意在此帖子上分享您的专业知识? - Géry Ogam

    112

    使用内置函数 round()

    In [23]: round(66.66666666666,4)
    Out[23]: 66.6667
    
    In [24]: round(1.29578293,6)
    Out[24]: 1.295783
    

    round() 的帮助文档:

    round(number[, ndigits]) -> 浮点数

    将一个数四舍五入到指定的小数位数(默认为0位小数)。这总是返回一个浮点数。小数位数可以是负数。


    我不喜欢这个想法,尽管我知道@mark-dickinson提供的解释是正确的,但我喜欢这个简单的答案。 - CodingMatters

    1

    Python和Numpy中的默认舍入:

    In: [round(i) for i in np.arange(10) + .5]
    Out: [0, 2, 2, 4, 4, 6, 6, 8, 8, 10]
    

    我使用以下代码将整数四舍五入应用于 pandas series:
    ``` import decimal decimal.getcontext().rounding = decimal.ROUND_HALF_UP ```
    最后,我创建了此函数以将其应用于 pandas series 对象。
    def roundint(value):
        return value.apply(lambda x: int(decimal.Decimal(x).to_integral_value()))
    

    现在您可以使用roundint(df.columnname)来进行操作。

    对于数字:

    In: [int(decimal.Decimal(i).to_integral_value()) for i in np.arange(10) + .5]
    Out: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    

    信用:kares

    0

    我编写了一个函数(用于 Django 项目的 DecimalField),但它也可以用于 Python 项目:

    这段代码:

    • 管理整数位数以避免数字太高
    • 管理小数位数以避免数字太低
    • 管理有符号和无符号数字

    带有测试的代码:

    def convert_decimal_to_right(value, max_digits, decimal_places, signed=True):
    
        integer_digits = max_digits - decimal_places
        max_value = float((10**integer_digits)-float(float(1)/float((10**decimal_places))))
    
        if signed:
            min_value = max_value*-1
        else:
            min_value = 0
    
        if value > max_value:
            value = max_value
    
        if value < min_value:
            value = min_value
    
        return round(value, decimal_places)
    
    
    value = 12.12345
    nb = convert_decimal_to_right(value, 4, 2)
    # nb : 12.12
    
    value = 12.126
    nb = convert_decimal_to_right(value, 4, 2)
    # nb : 12.13
    
    value = 1234.123
    nb = convert_decimal_to_right(value, 4, 2)
    # nb : 99.99
    
    value = -1234.123
    nb = convert_decimal_to_right(value, 4, 2)
    # nb : -99.99
    
    value = -1234.123
    nb = convert_decimal_to_right(value, 4, 2, signed = False)
    # nb : 0
    
    value = 12.123
    nb = convert_decimal_to_right(value, 8, 4)
    # nb : 12.123
    

    0
    def trim_to_a_point(num, dec_point):
       
        factor = 10**dec_point # number of points to trim
        num = num*factor # multiple
        num = int(num) # use the trimming of int 
        num = num/factor #divide by the same factor of 10s you multiplied 
        
        return num
    
    
    #test
    a = 14.1234567
    trim_to_a_point(a, 5)
    
    output
    ========
    14.12345
    
    1. 将数字乘以10的指定次幂

    2. 使用int()方法截断小数部分

    3. 除以之前乘以的相同数字

    4. 完成!

    我只是出于教育目的发布了这个,我认为它是正确的 :)


    0

    马克·迪金森的回答虽然完整,但在float(52.15)情况下无法工作。经过一些测试,这是我正在使用的解决方案:

    import decimal
            
    def value_to_decimal(value, decimal_places):
        decimal.getcontext().rounding = decimal.ROUND_HALF_UP  # define rounding method
        return decimal.Decimal(str(float(value))).quantize(decimal.Decimal('1e-{}'.format(decimal_places)))
    

    将'value'转换为浮点数,然后再转换为字符串非常重要,这样,'value'可以是浮点数、十进制数、整数或字符串类型!

    希望这能帮到大家。


    1
    使用上述函数并输入2.5和0,输出结果为2,这与默认行为相同。这是否符合预期? - Canute S
    谢谢Canute S,我主要用这种方法处理两位小数(货币),但似乎对于0位小数不起作用。 - NMC
    @CanuteS 如果你找到一个完全可行的解决方案,请与我们分享! - NMC
    2
    该代码无法适当地格式化,因此在下面作为答案发布。 - Canute S
    @CanuteS 谢谢!"decimal.getcontext().rounding = decimal.ROUND_HALF_UP" 这个方法很有用!我会更新我的回答。 - NMC

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