小编典典

如何截断浮点值?

python

我想从浮点数中删除数字,以使小数点后的位数固定不变,例如:

1.923328437452 → 1.923

我需要作为字符串输出到另一个函数,而不是打印。

我也想忽略丢失的数字,而不是四舍五入。


阅读 253

收藏
2020-12-20

共1个答案

小编典典

首先,对于那些只需要复制和粘贴代码的人来说,该函数是:

def truncate(f, n):
    '''Truncates/pads a float f to n decimal places without rounding'''
    s = '{}'.format(f)
    if 'e' in s or 'E' in s:
        return '{0:.{1}f}'.format(f, n)
    i, p, d = s.partition('.')
    return '.'.join([i, (d+'0'*n)[:n]])

这在Python
2.7和3.1+中有效。对于较旧的版本,不可能获得相同的“智能舍入”效果(至少,并非没有很多复杂的代码),但是在截断前舍入到小数点后12位将在大多数时间起作用:

def truncate(f, n):
    '''Truncates/pads a float f to n decimal places without rounding'''
    s = '%.12f' % f
    i, p, d = s.partition('.')
    return '.'.join([i, (d+'0'*n)[:n]])

说明

基本方法的核心是将值完全精确地转换为字符串,然后仅将超出所需数目的字符的所有内容都切掉。后面的步骤很容易;可以通过字符串操作来完成

i, p, d = s.partition('.')
'.'.join([i, (d+'0'*n)[:n]])

decimal模块

str(Decimal(s).quantize(Decimal((0, (1,), -n)), rounding=ROUND_DOWN))

第一步,转换为字符串非常困难,因为存在一些成对的浮点文字(即,您在源代码中编写的内容),它们均产生相同的二进制表示形式,但应以不同的方式截断。例如,考虑0.3和0.29999999999999998。如果您0.3使用Python程序编写,则编译器将使用IEEE浮点格式将其编码为位序列(假定为64位浮点数)

0011111111010011001100110011001100110011001100110011001100110011

这是最接近0.3的值,可以准确地表示为IEEE浮点数。但是,如果您0.29999999999999998使用Python程序编写代码,则编译器会将其转换为
完全相同的value
。在一种情况下,您希望将其截断为(一位)0.3,而在另一种情况下,您希望将其截断为0.2,但是Python只能给出一个答案。这是Python的根本限制,或者实际上是任何没有延迟评估的编程语言。截断功能只能访问存储在计算机内存中的二进制值,而不能访问您实际在源代码中键入的字符串。1个

如果您再次使用IEEE 64位浮点格式将位序列解码回十进制数,则会得到

0.2999999999999999888977697537484345957637...

因此0.2即使您可能并不想这样做,也会提出一个幼稚的实现。有关浮点表示错误的更多信息,请参见Python教程

使用非常接近整数但又 有意
不等于该整数的浮点值是非常罕见的。因此,在截断时,从所有可能对应于内存值的“十进制”十进制表示中选择是最有意义的。Python
2.7及更高版本(但不是3.0)提供了一种完善的算法来执行此操作,我们可以通过默认的字符串格式设置操作来访问该算法

'{}'.format(f)

唯一需要注意的是,如果数字足够大或足够小g,就使用指数表示法(1.23e+4),这就像格式规范一样。因此,该方法必须抓住这种情况并以不同的方式处理它。在某些情况下,使用f格式规范会引起问题,例如试图将3e-10精度截断为28位(它会产生0.0000000002999999999999999980),但我不确定如何最好地处理这些问题。

如果你确实 正在
与工作floats表示非常接近圆形数字,但故意不等于他们(像0.29999999999999998或99.959999999999994),这会产生一些假阳性,即它会圆数字,你不想圆润。在这种情况下,解决方案是指定固定的精度。

'{0:.{1}f}'.format(f, sys.float_info.dig + n + 2)

此处使用的精度位数并不重要,它只需要足够大即可确保在字符串转换中执行的任何舍入操作都不会将值“累加”到其漂亮的十进制表示形式。我认为sys.float_info.dig + n + 2在所有情况下都足够了,但是如果没有2增加的话,这样做就没有什么害处。

在Python的早期版本(最高2.6或3.0)中,浮点数格式更加粗糙,并且会定期生成类似

>>> 1.1
1.1000000000000001

如果这是你的情况,如果你
希望使用“好”十进制表示为截断,所有你能做的(据我所知)是挑选的数字一定数目,少于一个完整的精度表示的float,与轮截断之前,请先将其编号为那么多位数。典型的选择是12

'%.12f' % f

但是您可以对其进行调整以适合您使用的数字。


1好…我撒了谎。从技术上讲,您 可以
指示Python重新解析其自己的源代码,并提取与传递给截断函数的第一个参数相对应的部分。如果该参数是浮点文字,则可以将其在小数点后的特定位置截断并返回。但是,如果参数是变量,则此策略不起作用,这使其相当无用。以下内容仅供参考:

def trunc_introspect(f, n):
    '''Truncates/pads the float f to n decimal places by looking at the caller's source code'''
    current_frame = None
    caller_frame = None
    s = inspect.stack()
    try:
        current_frame = s[0]
        caller_frame = s[1]
        gen = tokenize.tokenize(io.BytesIO(caller_frame[4][caller_frame[5]].encode('utf-8')).readline)
        for token_type, token_string, _, _, _ in gen:
            if token_type == tokenize.NAME and token_string == current_frame[3]:
                next(gen) # left parenthesis
                token_type, token_string, _, _, _ = next(gen) # float literal
                if token_type == tokenize.NUMBER:
                    try:
                        cut_point = token_string.index('.') + n + 1
                    except ValueError: # no decimal in string
                        return token_string + '.' + '0' * n
                    else:
                        if len(token_string) < cut_point:
                            token_string += '0' * (cut_point - len(token_string))
                        return token_string[:cut_point]
                else:
                    raise ValueError('Unable to find floating-point literal (this probably means you called {} with a variable)'.format(current_frame[3]))
                break
    finally:
        del s, current_frame, caller_frame

将其通用化以处理您传入变量的情况似乎是一个失败的原因,因为您必须在程序的执行过程中向后追溯,直到找到为变量赋值的浮点文字。如果有一个。大多数变量将从用户输入或数学表达式初始化,在这种情况下,二进制表示就全部存在。

2020-12-20