V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
knowckx
V2EX  ›  Python

请教一个 Python 浮点数的小问题

  •  
  •   knowckx · 2022-02-06 01:42:08 +08:00 · 5031 次点击
    这是一个创建于 1027 天前的主题,其中的信息可能已经有所发展或是发生改变。

    v1 = 2.2 * 3 # 6.6
    v2 = 3.3 * 2 # 6.6
    print(v1, v2, v1==v2, v1<=v2, v1>=v2)

    输出结果是:
    6.6000000000000005 6.6 False False True

    这个结果惊讶到我了,没想到这里也会有坑。
    所以浮点数比较的正确方式是?

    60 条回复    2022-02-10 01:47:32 +08:00
    0ZXYDDu796nVCFxq
        1
    0ZXYDDu796nVCFxq  
       2022-02-06 01:44:56 +08:00   ❤️ 1
    chevalier
        2
    chevalier  
       2022-02-06 01:46:54 +08:00
    跟 Python 没关系,了解一下浮点数的原理,所有的语言都这样

    正确的比较,使用语言自带的浮点库,或者 v1-v2 < 0.0000……01 这样
    knowckx
        3
    knowckx  
    OP
       2022-02-06 01:47:46 +08:00
    @gstqc
    谢谢引用,所以
    v1>=v2 要改写成
    math.isclose(v1, v2) or v1 > v2

    这样有点繁琐了吧……
    knowckx
        4
    knowckx  
    OP
       2022-02-06 01:51:19 +08:00
    @chevalier
    啊,我理解部分小数无法精确存储
    只是其他语言没这么麻烦的
    secondwtq
        5
    secondwtq  
       2022-02-06 02:01:33 +08:00
    这东西能简单能复杂,看你想要简单的还是要麻烦的
    randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition Comparing Floating Point Numbers, 2012 Edition | Random ASCII – tech blog of Bruce Dawson
    0ZXYDDu796nVCFxq
        6
    0ZXYDDu796nVCFxq  
       2022-02-06 02:04:19 +08:00
    @knowckx 哪个语言的浮点数可以直接 ==?
    yaojin
        7
    yaojin  
       2022-02-06 02:10:39 +08:00 via Android
    该睡了,还是说你是海外的,太卷了吧
    lunaticus7
        8
    lunaticus7  
       2022-02-06 02:14:52 +08:00   ❤️ 1
    https://docs.python.org/3/library/decimal.html
    想要精确小数的话可以用 decimal
    knowckx
        9
    knowckx  
    OP
       2022-02-06 02:18:24 +08:00
    @gstqc 我试了下 go 可以的

    func Test_FloatEqual(t *testing.T) {
    v1 := 2.2 * 3
    v2 := 3.3 * 2
    fmt.Println(v1, v2, v1 == v2, v1 <= v2, v1 >= v2)
    }

    输出结果:
    === RUN Test_FloatEqual
    6.6 6.6 true true true
    --- PASS: Test_FloatEqual (0.00s)
    PASS
    knowckx
        10
    knowckx  
    OP
       2022-02-06 02:19:44 +08:00
    @secondwtq
    我回头看下 谢谢
    knowckx
        11
    knowckx  
    OP
       2022-02-06 02:19:59 +08:00
    @yaojin 习惯了熬夜……
    knowckx
        12
    knowckx  
    OP
       2022-02-06 02:21:14 +08:00
    @lunaticus7 这个方式不错,就是麻烦点,要把所有可能要比较的浮点数都用 decimal 转一遍
    iBugOne
        13
    iBugOne  
       2022-02-06 02:48:35 +08:00 via Android   ❤️ 8
    @knowckx Go 语言里 2.2*3 这种写法不涉及浮点运算,因为它是一个常量。大部分 C 语言编译器也会做这样的优化,而 Python 是“写啥跑啥”的,所以只有 Python 是真的创建了两个浮点数和两个整数并且做浮点乘法的。

    Go 换成这种写法你就发现区别了:

    c1 := 2.2
    c2 := 3.3
    v1 := c1 * 3
    v2 := c2 * 2
    LeeReamond
        14
    LeeReamond  
       2022-02-06 09:03:54 +08:00   ❤️ 1
    没有什么简单的办法,首先是类 C 语言的通用问题,其次是 py 里想简写的话,运算符可以重载,但只能发生在对象上,所以还要全局钩子设置对象,很繁琐,不如接受全球程序员都接受的事实。
    ipwx
        15
    ipwx  
       2022-02-06 10:04:48 +08:00   ❤️ 5
    唉,又疯了一个。。。不得不感慨科班还是硬道理。
    ----

    “我理解部分小数无法精确存储” —— 所有不能写成 Sum[2^i] ( i 可为负数)的浮点数都不能精确存储。

    “只是其他语言没这么麻烦的” “我试了下 go 可以的” —— 这大概就是 Go 语言被很多人喜欢的原因吧,隐藏了非常多的实践细节。但是在我看来你甚至不知道 Go 语言哪些时候已经帮你包办了,哪些时候需要自己处理,这种不一致性会让人发狂。比如 Go 的协程为了实现真时间片而在代码里面真的插入了一堆别的语言要手动写的 sleep(0),知道这个我是震惊的,这如果是写算法妥妥的浪费了一堆时间啊!

    浮点数比较的正确方法:

    a == b 应该是 abs(a - b) < epsilon
    a <= b 应该是 a < b + epsilon
    a < b 这个倒可以直接 a < b

    ----

    @iBugOne

    附:其他语言的常量比较结果 2.2 * 3 == 3.3 * 2

    JS false https://ideone.com/o297hf
    PHP false https://ideone.com/HMz6Nl

    C++ false https://ideone.com/E9YMqN
    C# false https://ideone.com/tylfdw
    Java false https://ideone.com/yz7Beu

    你看无论编译型还是非编译型,就算是常量它也不应该有这个等号啊。。。。Go 会输出等于我是震惊的。
    ipwx
        16
    ipwx  
       2022-02-06 10:06:28 +08:00   ❤️ 1
    哦对 epsilon 是一个自己可以控制的小数常量,根据业务需求定。比如一般我会取 epsilon = 1e-7 (对 float 也一般有效了)。但是对 double 而言你也许可以使用 epsilon = 1e-12 。但无论如何自己能控制比语言帮你处理(却不知道到底怎么做的)要安全多了。
    rcocco
        17
    rcocco  
       2022-02-06 10:33:51 +08:00   ❤️ 2
    坑在于你输入的值(屏幕上显示的值)和实际值并不一样,当你输入 2.2 的时候,程序使用的实际值是 2.20000000000000017763568394002504646778106689453125 。Python 和很多语言认为这个精确值太长了不方便人类阅读,所以会自作主张在输出时显示为 2.2 。
    而你输入 3.3 的时候实际值是 3.29999999999999982236431605997495353221893310546875 。
    所以 2.20000000000000017763568394002504646778106689453125 * 3 >= 3.29999999999999982236431605997495353221893310546875 * 2 为 True 没有任何问题,不信你拿计算器敲一遍

    假设 Round 表示对你输入的数字取最接近的浮点数,
    你输入 2.2*3 == 3.3*2 ,实际进行的是:Round(Round(2.2) * 3) == Round(Round(3.3) * 2)
    Round 后的结果可能比原来大,也可能小,还可能等。
    所以浮点数比较只能作差取绝对值,差小于某个很小的数就认为是相等。
    knowckx
        18
    knowckx  
    OP
       2022-02-06 11:30:23 +08:00
    @iBugOne 感谢回复,我跑了下确实有区别,但是你提到的
    2.2*3 是一个常量 我没有理解,google 了下也没搜到什么内容,似乎是 go 对常量表达式有优化
    agagega
        19
    agagega  
       2022-02-06 12:44:46 +08:00   ❤️ 1
    void foo(double);

    int main(void) {
    foo(2.2 * 3);
    foo(3.3 * 2);
    return 0;
    }

    // clang -Ofast -S -emit-llvm -o -

    define i32 @main() local_unnamed_addr #0 {
    tail call void @foo(double 0x401A666666666667) #2
    tail call void @foo(double 6.600000e+00) #2
    ret i32 0
    }

    这俩是不一样的
    joApioVVx4M4X6Rf
        20
    joApioVVx4M4X6Rf  
       2022-02-06 13:02:53 +08:00
    从业务角度来说的话,比较的时候,需要先统一精度。比如业务上能接受小数点后 5 位,那么就可以 round(2.2*3, 5) == round(3.3*2, 5)。 (最近一直在做财务方面的系统。。。
    knowckx
        21
    knowckx  
    OP
       2022-02-06 13:04:01 +08:00
    @ipwx 首先感谢回复,其实我也算科班,考的 408 ,985 硕毕业,计组刷过没有 5 遍也有 3 遍,IEEE 754 的题更是做到烂
    但是我不认为您这样上来就用"科班"来打标签并 diss 别人是什么好行为。
    计科的学生学完 IEEE 754 上机后就不会踩浮点数==的坑啦?真不见得。

    下面回到就事论事,我再次搜集了些资料,补充一下自己的看法
    ```
    浮点数比较的正确方法:

    a == b 应该是 abs(a - b) < epsilon # 这个没问题,或者使用 math.isclose
    a <= b 应该是 a < b + epsilon # 这个也没问题,我想了下,这个应该算是“最佳实践”,感谢分享
    a < b 这个倒可以直接 a < b # 理论上没问题,但是我感觉会很多人会踩坑,下面示例
    ```

    def CompareTwoFloat(v1 :float, v2 :float):
    if v1 < v2:
    pass # 小于的情况,啪啦啪啦写代码
    elif v1 > v2:
    pass # 大于的情况,啪啦啪啦写代码
    else:
    pass # 等于的情况,啪啦啪啦写代码

    v1 = 2.2 * 3 # 6.6
    v2 = 3.3 * 2 # 6.6
    CompareTwoFloat(v1, v2)

    一个不小心就会踩坑,实际上走分支 1 ,
    实际上必须先判断 isclose 去掉 equal 的情况再比较大于小于,真是坑死了
    knowckx
        22
    knowckx  
    OP
       2022-02-06 13:07:29 +08:00
    @v2exblog 这情况直接用 decimal 包会好一点吧……
    joApioVVx4M4X6Rf
        23
    joApioVVx4M4X6Rf  
       2022-02-06 13:08:49 +08:00
    @knowckx 是的,技术上可以选择更高精度的运算类型。业务上简单的直接 round 也行
    knowckx
        24
    knowckx  
    OP
       2022-02-06 13:09:53 +08:00
    @v2exblog 嗯……反正别用自带的浮点数,就是个白板
    knowckx
        25
    knowckx  
    OP
       2022-02-06 13:32:39 +08:00
    我刚思考了下,得出一个看上去奇怪的结论:
    a <= b 应该是 a < b + epsilon
    a < b 应该也是 a < b + epsilon
    GeruzoniAnsasu
        26
    GeruzoniAnsasu  
       2022-02-06 13:55:42 +08:00
    ipwx
        27
    ipwx  
       2022-02-06 15:06:44 +08:00
    @knowckx 其实主要问题是你那个例子里面是要条件完全闭合。。。

    a < b 肯定是 a < b 没错,+epsilon 放进你那个例子还是会出问题。

    所以只不过是涉及到 == 判断,你需要额外特殊第一优先处理罢了。。。
    ipwx
        28
    ipwx  
       2022-02-06 15:07:43 +08:00
    "a < b 肯定是 a < b 没错,+epsilon 放进你那个例子还是会出问题。"

    不信你代入一下

    if a < b + epsilon:
    ...
    elif a > b - epsilon:
    ...
    else:
    ...

    你会发现比起你的例子还是有可能运行到 else ,你在这个例子完全不可能运行 else 。gg
    akazure
        29
    akazure  
       2022-02-06 15:25:25 +08:00
    Python 有 Fraction 标准库,可以用分子分母算
    https://docs.python.org/zh-cn/3/library/fractions.html
    leimao
        30
    leimao  
       2022-02-06 15:48:39 +08:00
    哈哈,多踩几次坑就记住了 。
    wangnimabenma
        31
    wangnimabenma  
       2022-02-06 15:57:00 +08:00
    电脑没法用有限的空间存储无限的数据

    1. 用高精度库 (还是会有精度丢失只是小到无法察觉或者是说可有可无)
    2. 用分数表示
    Jooooooooo
        32
    Jooooooooo  
       2022-02-06 16:20:45 +08:00
    因为计算机只有二进制, 十进制下看起来有限的小数, 二进制下表达不了, 十进制 1/3 也搞不定.
    des
        33
    des  
       2022-02-06 16:26:28 +08:00 via iPhone
    楼上基本都说清楚了,我再补充一点
    精度损失基本都发生在“进制转换”的过程中
    举个栗子:“1 小时 20 分钟”转换成“多少小时”的数字,应该写成多少? 1.33333333 是不是?
    反向的话也是同理,计算机是二进制,钟表分和秒是 60 进制

    另一个问题来了,为什么不写成分数形式?
    两个原因,1 、很多时候不需要那么高精度; 2 、计算起来性能太差,也费存储空间

    顺带再科普一个知识,浮点数做加减乘除是不会损失精度的,提前是使用得当
    des
        34
    des  
       2022-02-06 16:29:42 +08:00 via iPhone
    @des 说错,乘除不算
    niqy1988
        35
    niqy1988  
       2022-02-06 16:34:22 +08:00 via Android
    @knowckx
    应该是 a < b 写作 a < b - epsilon
    whusnoopy
        36
    whusnoopy  
       2022-02-06 17:11:11 +08:00
    @knowckx #21

    歪个楼讨论下「科班」的问题,非针对楼主,请不用在意,只是有感而发

    是不是正儿八经的相关专业就叫科班?

    如果我不学无术啥也没学挂科挂到退学,这种大家应该都不会认可是科班

    那如果我是相关专业且是做题家,考试分数都挺高,但动手实践各种坑,这种怎么说?不确定现在学校里都是怎样,反正我上学那会(也是 985 本硕计算机专业),班上总有几个纯考试分不错,但真正代码都不一定会写,这种算科班么?

    如果我不是相关专业,但系统性学习和理解并能实践相关知识,这种虽然没有对应文凭,是不是也可以算科班?
    12101111
        37
    12101111  
       2022-02-06 17:50:51 +08:00
    我发现 rust 压根不让这么乘

    fn main() {
    assert_eq!(2.2*3, 3.3*2);
    }

    error[E0277]: cannot multiply `{float}` by `{integer}`
    --> src/main.rs:2:19
    |
    2 | assert_eq!(2.2*3, 3.3*2);
    | ^ no implementation for `{float} * {integer}`
    |
    = help: the trait `Mul<{integer}>` is not implemented for `{float}`

    error[E0277]: cannot multiply `{float}` by `{integer}`
    --> src/main.rs:2:26
    |
    2 | assert_eq!(2.2*3, 3.3*2);
    | ^ no implementation for `{float} * {integer}`
    |
    = help: the trait `Mul<{integer}>` is not implemented for `{float}`

    For more information about this error, try `rustc --explain E0277`.

    如果改成这样
    fn main() {
    assert_eq!(2.2*3.0, 3.3*2.0);
    }

    thread 'main' panicked at 'assertion failed: `(left == right)`
    left: `6.6000000000000005`,
    right: `6.6`', src/main.rs:2:5
    0attocs
        38
    0attocs  
       2022-02-06 18:18:16 +08:00
    @des #22 浮点加减运算怎么可能不会损失精度,运气好的话 relative error 是 bounded by epsilon/2 。运气不好的话很多加减运算本身可以是 ill-condition 的,遇上浮点加减运算的 rounding error 之后 relative error 会高得离谱。比如考虑实数:

    x = 1 + 2^-52 + 2^-53 + 2^-54
    y = 1 + 2^-54

    实数减法 x - y 的结果是 2^-52 + 2^-53 = 3 * 2^-53 ,但 64 位浮点数减法 fp(x) fp(-) fp(y) 的结果是 2^-51 = 4 * 2^-53 ,relative error 是夸张的 |(4^-53 - 3 * 2^-53) / 3 * 2^-53| = 1/3 。
    des
        39
    des  
       2022-02-06 18:27:12 +08:00
    @0attocs 所以才说“前提得正确使用”啊
    0attocs
        40
    0attocs  
       2022-02-06 18:42:13 +08:00
    @knowckx #19 需要区分<和<=时的一种做法是考虑用 a <= b + delta 代替 a < b ,但一般只有在为某个涉及浮点数比较的分析而定义语义时需要考虑吧。一般情况下更应该考虑的是问题本身是否是 well-conditioned 的以及算法+实现本身是否 stable 。
    0attocs
        41
    0attocs  
       2022-02-06 18:49:01 +08:00
    @des #28 在你这套荒谬逻辑下“进制转换也是不会损失精度的,提前是使用得当”,而你举的例子只是使用不当。
    maojun
        42
    maojun  
       2022-02-06 18:51:06 +08:00 via iPhone
    很好奇业界的最佳实践是怎么样的,蹲一个大佬。另外,如果用分数结构来表示浮点数的话,貌似就可以回避很多计算过程中的精度损失了吧🤨不过不知道为什么很少看到有这么做的。
    0attocs
        43
    0attocs  
       2022-02-06 19:10:46 +08:00
    @des #22 而且你提的“精度损失基本都发生在“进制转换”的过程中”说法本身也很奇怪。且不说多少涉及实数 /浮点数的问题 /程序里有你所谓的“进制转换”,你这里的 error 完全就是 rounding error ,跟什么进制转换没必然关系,只要需要浮点数 /有限精度表示就有可能引入 error 。都是二进制就没事了吗,2^-53 的 64 位浮点数表示会带来精度损失吗?
    0attocs
        44
    0attocs  
       2022-02-06 19:14:49 +08:00
    @0attocs #32 *1+2^-53 的 64 位浮点数表示
    UN2758
        45
    UN2758  
       2022-02-06 19:15:05 +08:00
    @maojun #42 分数表示理解起来也很麻烦,一般是对数化之后做运算
    akazure
        46
    akazure  
       2022-02-06 19:31:51 +08:00
    @maojun 我记得好像有个游戏公司(还是 AMD 推土机来着)就是梭哈整数运算,几乎放弃浮点数运算,然后寄了的
    0attocs
        47
    0attocs  
       2022-02-06 19:54:45 +08:00 via iPhone
    @ipwx 加一个常量 tolerance epsilon 肯定比比没有要好,但还是有点蒙住眼睛骗自己问题解决了的味道,无法被称为“正确”。要知道浮点数的分布不是均匀的,ulp 不是一个常量,而 machine epsilon 只是定义为 1 右边 ulp 的一半。

    一个比只加常量 tolerance epsilon 更好一点点的方案是根据比较的两数来 scale 这个 tolerance epsilon 。此时 x < y 会被定义为 | x - y | / | x | <= epsilon or | x - y | / | y | <= epsilon 。
    littlewing
        48
    littlewing  
       2022-02-06 20:00:02 +08:00
    @whusnoopy 哪里都有杠精
    sutra
        49
    sutra  
       2022-02-06 20:40:04 +08:00
    需要注意的是 golang 那个是 float64 ,别的另外几个语言是 32 bit 的。
    disk
        50
    disk  
       2022-02-06 21:00:05 +08:00
    看需求吧,有些情况可以考虑符号计算
    ipwx
        51
    ipwx  
       2022-02-06 21:54:10 +08:00
    @0attocs | x - y | / | x | <= epsilon or | x - y | / | y | <= epsilon 也并不那么合理。

    比如我 delta = x - y 是某一步运算。算到很后面我需要比较 delta 是不是和 0 相等。

    那很自然应该 abs(delta) < epsilon 而不是 abs(delta) / abs(delta) < epsilon
    qdcanyun
        52
    qdcanyun  
       2022-02-06 23:45:09 +08:00
    jinliming2
        53
    jinliming2  
       2022-02-06 23:45:42 +08:00
    @knowckx #18: https://go.dev/blog/constants#numbers 所有纯数字表达式(包括四则运算的结果、复数等)都是常量,会在编译时给你替换好。
    icyalala
        54
    icyalala  
       2022-02-07 02:04:33 +08:00   ❤️ 1
    Python 和其他语言是正确的,编译时和运行时表现相同。

    Go 才是特例:Go 会在编译时把 constant expressions 在编译时求值,用的还是高精度计算:
    https://go.dev/ref/spec#Constants 这样就会造成编译时和运行时表现不同。

    这个问题 10 年前就有人提到了: https://github.com/golang/go/issues/2789
    Go 的作者之一回复 "It is too late to change the language spec at this point."
    Hanggi
        55
    Hanggi  
       2022-02-07 03:19:47 +08:00
    计算机计算整数是精确的,所以如果想要达到精确计算通常可以使用类似 Decimal 的库,或者用 BigInt 转换使用。

    浮点数不管哪个语言都有这个问题,打开 chrome 控制台,在里面输入 0.1 + 0.2 回车试试。
    whusnoopy
        56
    whusnoopy  
       2022-02-07 08:55:53 +08:00
    whusnoopy
        57
    whusnoopy  
       2022-02-07 09:04:40 +08:00
    @littlewing #48

    不好意思前面按错快捷键直接发出去了

    如果你觉得我是杠精,那不应该浪费时间再回我一句

    如果是我的表述让其他人不适,请告知我不妥的地方,这样我才能意识到问题并去改正

    如果是我想讨论的大家意识偏差有问题,那么我们回到问题本身,看是我的逻辑有问题还是定义有问题

    在 #15 @ipwx 的回复里提了一句「科班才是硬道理」并有点赞,不管赞的是这句话,还是后面更详尽的解释,联想本站很多讨论和社会舆论,是有很多对科班有要求的地方

    在 #21 @knowckx 的回复里,通过自己的专业和学习过程,说明自己是正儿八经的科班,并不认为科班就能避开主楼里提出和衍生出来的问题

    那我在 #36 里想讨论的,如果大家有假定「科班可以避开相关问题」,楼主也列举了他的科班经历并表示同经历的很多人也还是会有疑惑,那到底是大家对「科班」的定义不一致,还是「科班可以避开相关问题」这个假设就不应该成立?
    joApioVVx4M4X6Rf
        58
    joApioVVx4M4X6Rf  
       2022-02-07 10:00:23 +08:00
    @qdcanyun 这个帖子总结的真棒!!学到了
    pcell
        59
    pcell  
       2022-02-07 19:01:42 +08:00
    其实我更好奇 excel 是怎么处理小数点,财务都用 excel 记数应该是没有这种问题。
    jinliming2
        60
    jinliming2  
       2022-02-10 01:47:32 +08:00
    @pcell excel 也存在问题,但是现在比较新的版本都做了近似补偿: https://docs.microsoft.com/en-us/office/troubleshoot/excel/floating-point-arithmetic-inaccurate-result
    比如输入公式:=1.333+1.225-1.333-1.225 可以得到 0 ,但是输入 =(1.333+1.225-1.333-1.225) 就得到了 -2.22045E-16
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1340 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 37ms · UTC 23:38 · PVG 07:38 · LAX 15:38 · JFK 18:38
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.