在Python中优雅地表示浮点数

13

我想将浮点数表示为一个字符串,四舍五入到一定数量的有效数字,并且绝不使用指数格式。本质上,我想显示任何浮点数并确保它“看起来很好”。该问题有几个部分:

  • 我需要能够指定有效数字的数量。
  • 有效数字的数量需要是可变的,这不能使用字符串格式化运算符完成。
  • 我需要它按人们的预期般四舍五入,而不是像1.999999999999这样。

我已经想出了一种方法来实现这一点,尽管它看起来像一个解决方案,并且不完美。(最大精度为15个有效数字)

>>> def f(number, sigfig):
    return ("%.15f" % (round(number, int(-1 * floor(log10(number)) + (sigfig - 1))))).rstrip("0").rstrip(".")

>>> print f(0.1, 1)
0.1
>>> print f(0.0000000000368568, 2)
0.000000000037
>>> print f(756867, 3)
757000

有没有更好的方法做这个?为什么Python没有内置函数做这个?


有没有任何编程语言内置此功能的函数? - Andrew Jaffe
3
以防万一,您是否知道'%.*g' % (3, 3.142596) 格式化选项?(特别是星号) - tzot
我确实错过了星号格式选项,但使用'g'返回指数,而使用'f'则远非完美:"%.*f" % (0, 34500000000000000000000) 返回 '34499999999999999000000'。 - dln385
你没有说明你正在做什么,但如果你使用浮点数进行与货币相关的计算,那么这是一个非常糟糕的想法。要么使用 decimal 模块,要么将数量存储为整数分(或本地货币等价物)。Decimal 真的是你最好的选择。 - George
4个回答

8
似乎没有内置的字符串格式化技巧可以让您(1)打印浮点数,其第一个有效数字出现在小数点后15位之后,并且(2)不以科学计数法表示。因此只能进行手动字符串操作。
下面我使用decimal模块从浮点数中提取小数位。使用float_to_decimal函数将浮点数转换为Decimal对象。明显的方法decimal.Decimal(str(f))是错误的,因为str(f)可能会丢失有效数字。 float_to_decimal是从decimal模块文档中提取的。
一旦小数位作为int元组获得,下面的代码就会做出明显的事情:截取所需数量的有效数字,必要时四舍五入,将数字连接成字符串,在适当的位置添加符号、小数点和左侧或右侧的零。
在底部,您会找到我用来测试f函数的几个案例。
import decimal

def float_to_decimal(f):
    # http://docs.python.org/library/decimal.html#decimal-faq
    "Convert a floating point number to a Decimal with no loss of information"
    n, d = f.as_integer_ratio()
    numerator, denominator = decimal.Decimal(n), decimal.Decimal(d)
    ctx = decimal.Context(prec=60)
    result = ctx.divide(numerator, denominator)
    while ctx.flags[decimal.Inexact]:
        ctx.flags[decimal.Inexact] = False
        ctx.prec *= 2
        result = ctx.divide(numerator, denominator)
    return result 

def f(number, sigfig):
    # https://dev59.com/GnE85IYBdhLWcg3wqliE#2663623
    assert(sigfig>0)
    try:
        d=decimal.Decimal(number)
    except TypeError:
        d=float_to_decimal(float(number))
    sign,digits,exponent=d.as_tuple()
    if len(digits) < sigfig:
        digits = list(digits)
        digits.extend([0] * (sigfig - len(digits)))    
    shift=d.adjusted()
    result=int(''.join(map(str,digits[:sigfig])))
    # Round the result
    if len(digits)>sigfig and digits[sigfig]>=5: result+=1
    result=list(str(result))
    # Rounding can change the length of result
    # If so, adjust shift
    shift+=len(result)-sigfig
    # reset len of result to sigfig
    result=result[:sigfig]
    if shift >= sigfig-1:
        # Tack more zeros on the end
        result+=['0']*(shift-sigfig+1)
    elif 0<=shift:
        # Place the decimal point in between digits
        result.insert(shift+1,'.')
    else:
        # Tack zeros on the front
        assert(shift<0)
        result=['0.']+['0']*(-shift-1)+result
    if sign:
        result.insert(0,'-')
    return ''.join(result)

if __name__=='__main__':
    tests=[
        (0.1, 1, '0.1'),
        (0.0000000000368568, 2,'0.000000000037'),           
        (0.00000000000000000000368568, 2,'0.0000000000000000000037'),
        (756867, 3, '757000'),
        (-756867, 3, '-757000'),
        (-756867, 1, '-800000'),
        (0.0999999999999,1,'0.1'),
        (0.00999999999999,1,'0.01'),
        (0.00999999999999,2,'0.010'),
        (0.0099,2,'0.0099'),         
        (1.999999999999,1,'2'),
        (1.999999999999,2,'2.0'),           
        (34500000000000000000000, 17, '34500000000000000000000'),
        ('34500000000000000000000', 17, '34500000000000000000000'),  
        (756867, 7, '756867.0'),
        ]

    for number,sigfig,answer in tests:
        try:
            result=f(number,sigfig)
            assert(result==answer)
            print(result)
        except AssertionError:
            print('Error',number,sigfig,result,answer)

这是一些漂亮的代码。正是我想要的。如果你将它改成使用 Decimal 而不是 float(这很容易做到),那么我们就可以完全避免浮点错误。 我唯一发现的问题是长整数。例如,尝试 (34500000000000000000000, 17, '34500000000000000000000')。虽然,这可能只是由于浮点误差造成的。 - dln385
不错。我对“d”的定义进行了轻微修改,修复了长整数的问题,并且作为副作用,还允许您传递数字字符串作为“number”参数。 - unutbu
这个修复方法有效,但我发现其他无关的错误。使用 (756867, 7, '756867.0'),返回结果是 75686.7。看起来填充结果的条件有点不对。 - dln385

6
如果你需要浮点数精度,你需要使用Python标准库中的decimal模块: Python Standard Library
>>> import decimal
>>> d = decimal.Decimal('0.0000000000368568')
>>> print '%.15f' % d
0.000000000036857

1
十进制模块似乎可能有所帮助,但这并没有回答我的问题。 - dln385
@dln385:怎么会呢?它满足你列出的所有要求。 - Billy ONeal
1
  1. 这指定了精度,而不是有效数字。两者之间有很大的区别。
  2. 它不会超过小数点四舍五入。例如,带有3个有效数字的756867是757000。请参见原始问题。
  3. 对于大数字(如长整型),该方法会失效。
- dln385
@dln385:你有看过文档吗?**1.**“十进制模块包含了有效位数的概念,因此1.30 + 1.20等于2.50。保留末尾的零以表示其重要性,这是货币应用的惯常表现方式。对于乘法,“学校”方法使用乘数中的所有数字。例如,1.3 * 1.2得到1.56,而1.30 * 1.20得到1.5600。” **2.它使用固定点而不是浮点,因此您首先不会遇到这样的问题,3.**十进制模块支持任意精度。 - Billy ONeal
1
@Billy ONeal:是的,我确实阅读了文档。在我上一条评论中,我解释了为什么 '%.15f' % d 没有回答我的问题。至于 decimal 模块,我找不到一个四舍五入到有效数字(包括小数点左侧)或显示数字而不使用指数的方法。此外,需要注意的是,即使使用小数,'%.15f' % d 仍然无法处理大整数。 - dln385

0
这里是一个片段,根据给定的误差线格式化一个值。
from math import floor, log10, round

def sigfig3(v, errplus, errmin):
    i = int(floor(-log10(max(errplus,errmin)) + 2))
    if i > 0:
        fmt = "%%.%df" % (i)
        return "{%s}^{%s}_{%s}" % (fmt % v,fmt % errplus, fmt % errmin)
    else:
        return "{%d}^{%d}_{%d}" % (round(v, i),round(errplus, i), numpy.round(i))

示例:

5268685 (+1463262,-2401422) becomes 5300000 (+1500000,-2400000)
0.84312 +- 0.173124 becomes 0.84 +- 0.17

-1

需要任意精度浮点数才能正确回答这个问题。因此使用十进制模块是必须的。没有方法可以将十进制转换为字符串,而不使用指数格式(原始问题的一部分),因此我编写了一个函数来实现这一点:

def removeExponent(decimal):
    digits = [str(n) for n in decimal.as_tuple().digits]
    length = len(digits)
    exponent = decimal.as_tuple().exponent
    if length <= -1 * exponent:
        zeros = -1 * exponent - length
        digits[0:0] = ["0."] + ["0"] * zeros
    elif 0 < -1 * exponent < length:
        digits.insert(exponent, ".")
    elif 0 <= exponent:
        digits.extend(["0"] * exponent)
    sign = []
    if decimal.as_tuple().sign == 1:
        sign = ["-"]
    print "".join(sign + digits)

问题在于尝试四舍五入到有效数字。Decimal的“quantize()”方法不会高于小数点进行四舍五入,而“round()”函数始终返回浮点数。我不知道这些是否是错误,但这意味着将无限精度浮点数四舍五入的唯一方法是将其解析为列表或字符串,并手动进行四舍五入。换句话说,对于这个问题没有明智的答案。

您不需要任意精度浮点数。请注意,他从未需要舍入后的答案作为浮点数,只需要作为字符串。顺便说一句,-1 * anything 可以写成 -anything - Ponkadoodle
我不理解你的“不会四舍五入到小数点后面更高的位数”。例如,尝试使用Decimal('123.456').quantize(Decimal('1e1')) - Mark Dickinson
顺便提一句,在Python 3.x中对Decimal实例进行四舍五入会返回另一个Decimal。 - Mark Dickinson
@Mark Dickinson:你说得对,我没有预料到“Decimal('123.456').quantize(Decimal('10'))”会有不同的行为。此外,如果Python 3.x中十进制小数位四舍五入返回一个十进制值,那么看来是时候升级了。谢谢您的指点。 - dln385

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