百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Python中为什么round(1.5)和round(2.5)都等于2?round()函数详解

itomcoil 2025-01-17 11:59 23 浏览

#妙笔生花创作挑战#

初学Python的同学会碰到一个很奇怪的问题 :Python中,为什么 round(1.5) 等于2,而 round(2.5) 也等于2?!

round() 函数是 Python 中的一个内置函数,用于将一个数字修约到最接近的整数或指定的小数位数。

该函数遵循特定的规则来确定修约后的值。跟 C/C++/Java 语言中的不一样,它不是简单的“四舍五入”,初学者很容易被它搞蒙,希望下面这篇文章,能让你彻底搞明白这些规则。

1、一道二级Python真题

先来看一道全国计算机等级考试(NCRE)二级 Python 中的真题,这题比较简单,没有坑:

1.1 题目:x=2.6,表达式 round(x) 的结果是______。

  • A. 2.6
  • B. 2
  • C. 3
  • D. 2.0

参考答案: C. 3

1.2 解析:

对于二级考试来说,关于 round() 函数,大家只要记住 “四舍六入” 就够了,注意!是“”入,而不是中学所学的“五”入。

这题中,round(2.6)只有一个参数,没有表示精度的第二个参数,那就是对第一个参数 取整数,即:小数点后保留0位。因为小数点后是"6",按照我们中学时就学过的 “四舍五入” 的原则,0.6大于等于0.5,则直接进一位,就成了:2+1 ,结果为 3 。

而对于指定精度位数后面跟的是 “五” 的情况下,到底是应该 “入”,还是 “舍”,相对复杂,我把详细解析放在下面,感兴趣的可以进一步深入学习、思考。不想深究的,二级考试如果 “中大奖” 碰到 “五” 的情况,又不明白的话,就随便蒙一个吧。

2、“四舍六入五取偶”的中庸之道

Python中的 round() 函数,在做修约(round)时,采用的策略叫做“四舍六入五取偶”,真有点合乎中华文化中的 “中庸之道”、“不偏不倚”,为啥这么说呢?请继续往下看。

先看下截止今天最新的 Python 稳定版 Python 3.12.4 的官方文档:

round(number, ndigits=None)

返回 number 舍入到小数点后 ndigits 位精度的值。如果 ndigits 被省略或为 None,则返回最接近输入值的整数。

对于支持round() 方法的内置类型,结果值会舍入至最接近的 10 的负 ndigits 次幂的倍数;如果与两个倍数同样接近,则选用偶数。因此,round(0.5) 和 round(-0.5) 均得出 0 而 round(1.5) 则为 2。ndigits 可为任意整数值(正数、零或负数)。如果省略了 ndigits 或为 None ,则返回值将为整数。否则返回值与 number 的类型相同。

对于一般的 Python 对象 number, round 将委托给 number.__ round __。

备注: 对浮点数执行round() 的行为可能会令人惊讶:例如,round(2.675, 2) 将给出 2.67 而不是期望的 2.68。这不算是程序错误:这一结果是由于大多数十进制小数实际上都不能以浮点数精确地表示。请参阅 tut-fp-issues 了解更多信息。

官方文档解释得比较形式化,不太容易理解,我们来举例说明,先以小数点后只有1位小数的情况为例。

2.1 修约规则

2.1.1 当被修约的数字小于5时,该数字被舍去。

当被修约的数字(1位的小数部分)小于 0.5 时,小数部分为 0.1、0.2、0.3、0.4中的一个,round() 函数直接舍弃小数部分,取前面的整数部分。

举例:

  • round(8.3) → 8(3小于5,舍去0.3,得8)
  • round(-4.2)→ 4(2小于5,舍去0.2,得-4)

用数形结合的思维看,修约后的值,都往原点方向靠近

2.1.2 当被修约的数字大于5时,则考虑进位。

当被修约的数字(1位的小数部分)大于 0.5 时,小数部分为 0.6、0.7、0.8、0.9中的一个,需要进位,最终修约后的数值将是“更靠近的整数”。

举例:

  • round(1.7) → 2(7大于5,距离1.7最近的整数分别是1和2,而1.7距离2比距离1更近,因此得2)
  • round(-3.9)→ -4(9大于5,距离-3.9最近的整数分别是-4和-3,而-3.9距离-4比距离-3更近,因此得-4)

用数形结合的思维看,修约后的值,都往原点方向远离

2.1.3 当被修约的数字等于5时,则需根据5前面的数字来决定

  1. 如果5前面的数字是奇数,则进位,修约后末尾数字都成为偶数;
  2. 如果5前面的数字是偶数,则将5舍掉,修约后末尾数字都成为偶数;
  3. 如果5的后面还有不为“0”的任何数,则此时无论5的前面是奇数还是偶数,均应进位。

举例:

  • round(41.5) → 42(保留小数点后0位,即取整,5前面的数字是奇数1,则进位,修约后得到42,其最后一个数字是2,为偶数)
  • round(3.25, 1) → 3.2(保留小数点后1位,5前面的数字是偶数2,则将5舍掉,修约后得到3.2,其最后一个数字是2,为偶数)
  • round(3.25001, 1) → 3.3(保留小数点后1位,则看2后面是啥,因为2后面是5,且5后面还有数“001”,此时,不管5前面的数字是奇数还是偶数,都要进位,因此变成3.2+0.1,得3.3)

上面3条规则,前两条都很好理解:从数轴看,修约后的结果都是 -- 转成距离最近的整数

而第3条规则是:当需要修约的数值恰好位于两个数中间时(即被修约的数字是5开头),Python的 round() 函数采用的这种策略叫做 银行家舍入法 (Banker's Rounding),也称为 “四舍六入五取偶” 法(也称为“偶数舍入法”),即在这种情况下,会舍入到最近的偶数。

2.2 为什么是“四舍六入五取偶”这个规则?

这是‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‮‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‮‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‮‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬为了符合计算机中的 IEEE 754 浮点运算标准,现在大多数较新的编程语言的 round() 函数,都默认使用这种方法,如:Go、Rust、Python 等。

这种舍入法的优点是,与通常的四舍五入相比,它在平均数方面更能保持原有数据的特性。因此,银行家舍入法在很多需要更小误差的科学和计算机系统中得到了广泛应用。它也被称为 统计学家舍入(statistician's rounding)或无偏舍入(unbiased rounding)。

为什么说这种舍入法在平均数方面更能保持原有数据的特性呢?还是举例说明。

假设有两个客户去银行提款,一个账上剩1.5分,一个账上剩2.5分,那对银行家来说,他总共要付给两位客户:1.5+2.5=4分钱,但如果都按照“四舍五入”的方法来给客户付钱,银行家要付:2+3=5分,跟4分比亏了1分;而按照“四舍六入五取偶”的方法,银行家要付:2+2=4分,不亏不赚刚刚好! 银行家么,就是要精明!

好吧,我承认:上面都是我一本正经瞎扯的。

银行家舍入法之所以被认为在平均数方面更能保持原有数据的特性,主要是因为它减少了一般四舍五入方法中可能引入的系统性偏差。

在传统的四舍五入中,每当数字达到5时,总是进行进位。这意味着,对于一系列随机分布的数值(0、1、2、3、4、5、6、7、8、9的几率都相同),其中,1、2、3、4这4种情况会舍去,6、7、8、9这4种情况会进位。然而,因为5总是导致进位,所以实际上进位的次数会稍微多于舍去的次数。这种偏差在处理大量数据时可能会导致结果的总体较大的偏差。

而银行家舍入法则采用了一种“取偶”的策略:当数字正好是5,且5前面是偶数时,它选择舍去5,而不是进位。这种方法确保了在数值恰好是5时,舍去和进位的次数大致相等。这样,长期来看,进位和舍去的操作在数量上更加平衡,从而减少了总体上的偏差。

因此,银行家舍入法在处理大量数据时,能够更好地保持原有数据的统计特性,特别是平均数。这在需要高精度计算的金融、科学和工程领域尤为重要。

现在,你理解为什么说它跟“中庸之道”、“不偏不倚”有联系了吧?

3、特殊情况说明

需要注意的是,由于浮点数的表示方式,某些看似简单的舍入操作可能会产生意想不到的结果。例如:

round(2.675, 2)

对数字 2.675 取小数点后保留 2 位的近似值,如果按照我们前面 “四舍六入五取偶” 的规则, 5 的前面是 7 , 7 是奇数,那么要进位把 7 变成偶数 8,最后的结果应该是 2.68 。

而实际运行结果确是 2.67

这又是为啥呢?不得不说,Python 中 round() 的坑真是多啊!

原来:目前计算机中的数据,都是采用二进制的方式进行存储和运算的;而程序中的 2.675 ,则是写给人看的,则是十进制的。

每一个十进制的整数,都可以表示成一个有限的二进制数。如十进制的 13 ,可以表示成二进制的 1101 ;但不是每一个十进制的小数,都能转换成一个有限的二进制小数;只有当十进制小数能够表示为2的负幂次之和时,它才能被表示为有限的二进制小数。否则,它将是一个无限循环的二进制小数。

以十进制的0.1为例,它在二进制中表示为0.0 0011 0011 0011 0011…(其中“0011”这个序列无限循环)。因为没有一个有限的二进制数能够精确地等于十进制的0.1,所以它无法用有限的二进制数表示。

而目前的计算机都是有精度限制的(32位、64位之类),因此,这些数字在计算机中存储的就不是刚刚好,而是一个近似的值,或者比实际想表达的十进制数略小,或者略大。

比如, 2.675 在计算机内部的实际表示,是略小于 2.675 的:

import decimal
print(decimal.Decimal(2.675))

# `输出 2.67499999999999982236431605997495353221893310546875`

所以, round(2.675, 2) 实际上相当于 round(2.67499999999999982236431605997495353221893310546875, 2) ,对于 2.674999... 这个数,取小数点后保留2位的数值,按照 “四舍六入五取偶” 的原则,小数点后第3位实际存储在计算机中的是 4 ,“四舍”后,当然得到的就是 2.67 啦!

4、高级篇

最后,对于还想进一步深入挖掘、研究的同学,再给大家一些底层的线索,感兴趣的(喜欢刨根问底的),可以进一步去探索。

下载 CPython 的源码包,解压缩后,在 Python\bltinmodule.c 中,builtin_round_impl() 是内置函数 round() 的具体实现;其中,又涉及到了更底层的调用,想要搞明白的,可以继续看下去,写得太累,我是暂时不想再写下去了

/*[clinic input]
round as builtin_round

    number: object
    ndigits: object = None

Round a number to a given precision in decimal digits.

The return value is an integer if ndigits is omitted or None.  Otherwise
the return value has the same type as the number.  ndigits may be negative.
[clinic start generated code]*/

static PyObject *
builtin_round_impl(PyObject *module, PyObject *number, PyObject *ndigits)
/*[clinic end generated code: output=ff0d9dd176c02ede input=275678471d7aca15]*/
{
    PyObject *round, *result;

    if (!_PyType_IsReady(Py_TYPE(number))) {
        if (PyType_Ready(Py_TYPE(number)) < 0)
            return NULL;
    }
    
    round = _PyObject_LookupSpecial(number, &_Py_ID(__round__));
    if (round == NULL) {
        if (!PyErr_Occurred())
            PyErr_Format(PyExc_TypeError,
                         "type %.100s doesn't define __round__ method",
                         Py_TYPE(number)->tp_name);
        return NULL;
    }
    
    if (ndigits == Py_None)
        result = _PyObject_CallNoArgs(round);
    else
        result = PyObject_CallOneArg(round, ndigits);
    Py_DECREF(round);
    return result;

}

没想到这个简单的知识点、二级Python中最多只有1分的 Python 选择题,竟然费了这么大的篇幅来写……

能耐着性子,把这篇长文看完的,一定是有着坚韧不拔的毅力,独立思考的习惯,和深入挖掘的精神,干啥啥成,给你点赞!

#头条创作挑战赛#

相关推荐

C|经典实例理解算法之顺推、逆推、迭代、递归思想

递推算法可以不断利用已有的信息推导(迭代)出新的信息,在日常应用中有如下两种递推算法。①顺推法:从已知条件出发,逐步推算出要解决问题的方法。例如斐波那契数列就可以通过顺推法不断递推算出新的数据。②...

[西门子PLC] 博途编程之递归算法

首先跟大伙讲一讲哈,递归算法瞅着是挺优雅挺不错的,可实际上没啥大用,在真正的项目里能不用就别用递归,为啥呢?因为用了递归可能会惹出大麻烦,后面会给大伙举例讲讲原因。那啥叫递归呢?从名字上就能看出来,就...

SQL 也能递归?一文搞懂 Recursive CTE的魔力

很多人以为递归(Recursive)只属于编程语言,和SQL没什么关系。但其实SQL中也能实现递归操作,特别是在处理树结构、路径查找时,WITHRECURSIVE展现出强大威力。本文将带你...

10张动图学会python循环与递归

  一图胜千言!  循环难学?十张动图GIFS有助于认识循环、递归、二分检索等概念的具体运行情况。  本文代码实例以Python语言编写。  一、循环  GIF1:最简单的while循环  GIF...

C语言学习之-----(十三) 函数递归

(十三)函数递归一、栈在说函数递归的时候,顺便说一下栈的概念。栈是一个后进先出的压入(push)和弹出(pop)式数据结构。在程序运行时,系统每次向栈中压入一个对象,然后栈指针向下移动一个位置。当系...

Python自动化办公应用学习笔记19—— 循环控制:break 和 continue

在Python的循环结构中,break和continue是两个特殊的保留字,主要用于改变循环的执行流程。1.定义与核心作用break:立即终止当前循环,跳出整个循环体(仅限最内层循环)conti...

循环与递归的那些事
循环与递归的那些事

大家好,我是贠学文,点击右上方“关注”,每天为您分享java程序员需要掌握的知识点干货。在任何的编程语言中,循环和递归永远都是一个避不开的话题,因为在某些特定的场景下,用递归确实要比循环简单得多,比如说遍历文件夹目录等等,但是,递归也有下面...

2025-08-02 18:49 itomcoil

漫谈递归、迭代、循环——人理解迭代,神理解递归

后续计划好几天没有更新了,没有偷懒。随着源码的阅读,学习到了字典和集合的底层实现。字典这种数据结构的搜索效率很高,底层结构采用了效率优于红黑树的哈希表。红黑树是一种平衡二叉树,C++中的map和lin...

Excel递归与循环——货物分箱问题

递归指通过函数自身调用实现复杂计算,在Excel中多通过支持递归的函数(如LAMBDA)实现。第一,简化复杂逻辑表达:对于有明确递推关系的问题,递归能将多层嵌套的逻辑转化为简洁的自我调用形式,比手...

MongoDB入门之索引

索引就像书的目录,如果查找某内容在没有目录的帮助下,只能全篇查找翻阅,这导致效率非常的低下;如果在借助目录情况下,就能很快的定位具体内容所在区域,效率会直线提高。索引简介首先打开命令行,输入mongo...

MongoDB之集合管理一

最近的几篇博客都是关于MongoDB的,虽然个人感觉也没多少知识点,但没想到竟然有转载我的博客的,不管有经过我同意还是没经过我同意,说明写的应该还是有价值的,这也是我写博客的一个动力之一吧。上一博客学...

SpringBoot集成扩展-访问NoSQL数据库之Redis和MongoDB!

与关系型数据库一样,SpringBoot也提供了对NoSQL数据库的集成扩展,如对Redis和MongoDB等数据库的操作。通过默认配置即可使用RedisTemplate和MongoTemplate...

揭秘你不会画“信息结构图”的本质

编辑导语:产品信息结构图有助于清晰地展示产品信息,一定程度上可以为后台上传数据提供依据,但不少人可能觉得产品信息结构图很难,这可能是对数据库表结构不理解等因素导致的。本篇文章里,作者就产品信息结构图的...

MongoDB导入导出备份数据

要提前安装mongodb-database-tools参考:centos离线安装mongodb-database-tools导出数据常用的导出有两种:mongodump和mongoexport,两种方...

mongodb导入导出及备份

-------------------MongoDB数据导入与导出-------------------1、导出工具:mongoexport1、概念:mongoDB中的mongoexport...