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

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

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

#妙笔生花创作挑战#

初学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 选择题,竟然费了这么大的篇幅来写……

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

#头条创作挑战赛#

相关推荐

python创建文件夹,轻松搞定,喝咖啡去了

最近经常在录视频课程,一个课程下面往往有许多小课,需要分多个文件夹来放视频、PPT和案例,这下可好了,一个一个手工创建,手酸了都做不完。别急,来段PYTHON代码,轻松搞定,喝咖啡去了!import...

如何编写第一个Python程序_pycharm写第一个python程序

一、第一个python程序[掌握]python:python解释器,将python代码解释成计算机认识的语言pycharm:IDE(集成开发环境),写代码的一个软件,集成了写代码,...

Python文件怎么打包为exe程序?_python3.8打包成exe文件

PyInstaller是一个Python应用程序打包工具,它可以将Python程序打包为单个独立可执行文件。要使用PyInstaller打包Python程序,需要在命令行中使用py...

官方的Python环境_python环境版本

Python是一种解释型编程开发语言,根据Python语法编写出来的程序,需要经过Python解释器来进行执行。打开Python官网(https://www.python.org),找到下载页面,选择...

[编程基础] Python配置文件读取库ConfigParser总结

PythonConfigParser教程显示了如何使用ConfigParser在Python中使用配置文件。文章目录1介绍1.1PythonConfigParser读取文件1.2Python...

Python打包exe软件,用这个库真的很容易

初学Python的人会觉得开发一个exe软件非常复杂,其实不然,从.py到.exe文件的过程很简单。你甚至可以在一天之内用Python开发一个能正常运行的exe软件,因为Python有专门exe打包库...

2025 PyInstaller 打包说明(中文指南),python 打包成exe 都在这里

点赞标记,明天就能用上这几个技巧!linux运维、shell、python、网络爬虫、数据采集等定定做,请私信。。。PyInstaller打包说明(中文指南)下面按准备→基本使用→常用...

Python自动化办公应用学习笔记40—文件路径2

4.特殊路径操作用户主目录·获取当前用户的主目录路径非常常用:frompathlibimportPathhome_dir=Path.home()#返回当前用户主目录的Path对象...

Python内置tempfile模块: 生成临时文件和目录详解

1.引言在Python开发中,临时文件和目录的创建和管理是一个常见的需求。Python提供了内置模块tempfile,用于生成临时文件和目录。本文将详细介绍tempfile模块的使用方法、原理及相关...

python代码实现读取文件并生成韦恩图

00、背景今天战略解码,有同学用韦恩图展示各个产品线的占比,效果不错。韦恩图(Venndiagram),是在集合论数学分支中,在不太严格的意义下用以表示集合的一种图解。它们用于展示在不同的事物群组之...

Python技术解放双手,一键搞定海量文件重命名,一周工作量秒搞定

摘要:想象一下,周五傍晚,办公室的同事们纷纷准备享受周末,而你,面对着堆积如山的文件,需要将它们的文件名从美国日期格式改为欧洲日期格式,这似乎注定了你将与加班为伍。但别担心,Python自动化办公来...

Python路径操作的一些基础方法_python路径文件

带你走进@机器人时代Discover点击上面蓝色文字,关注我们Python自动化操作文件避开不了路径操作方法,今天我们来学习一下路径操作的一些基础。Pathlib库模块提供的路径操作包括路径的...

Python爬取下载m3u8加密视频,原来这么简单

1.前言爬取视频的时候发现,现在的视频都是经过加密(m3u8),不再是mp4或者avi链接直接在网页显示,都是经过加密形成ts文件分段进行播放。今天就教大家如果通过python爬取下载m3u8加密视频...

探秘 shutil:Python 高级文件操作的得力助手

在Python的标准库中,shutil模块犹如一位技艺精湛的工匠,为我们处理文件和目录提供了一系列高级操作功能。无论是文件的复制、移动、删除,还是归档与解压缩,shutil都能以简洁高效的方式完成...

怎么把 Python + Flet 开发的程序,打包为 exe ?这个方法很简单!

前面用Python+Flet开发的“我的计算器v3”,怎么打包为exe文件呢?这样才能分发给他人,直接“双击”运行使用啊!今天我给大家分享一个简单的、可用的,把Flet开发的程序打包为...