首页 / 资讯中心 / 文章详情

Python数字类型全解析:从int/float到精度与进制转换

Python数字类型全解析:从int/float到精度与进制转换 ★ FEATURED ARTICLE
如果让我在Python入门里挑一个“看起来简单、实际最容易翻车”的主题数字类型一定排前三。你可能会想数字有什么好讲的不就是整数和小数吗但真正写起代码2/3和2//3到底有什么区别0.1 0.2为什么不是0.3int(3.5)为什么直接报错这些问题几乎每个新人都撞过。这篇是Python基础系列的第五讲咱们把数字这个主题一次聊透从int、float、complex三种类型的基本规则到除法、取余、类型转换这些天天都要用的运算再到浮点精度、进制转换、格式化输出以及math、random、decimal这些标准库工具最后结合我在实际项目里遇到过的高频场景给你一套可以直接抄作业的处理方案。适合刚入门Python、想系统性补基础的同学也适合中间踩过坑、想回来弄明白原理的读者。1. 先把数字家族认全int、float与complex提起Python的数字类型很多教程都是拿一行type(3)带过去但我建议你花五分钟把三种类型各自的底细搞清楚。因为后面的运算规则、格式化、精度问题全都建立在“它们底层到底是什么”之上。这一节不追求面面俱到只讲真正会影响你写代码的那些点。1.1 int没有上限的整数Python 3 里的int是“任意精度整数”意思是你想写多大就写多大只要内存撑得住。它不像 C/C 里的int那样有个2147483647的上限也不会因为数字太大就溢出变成负数。比如2 ** 1000这种天文数字Python 能原样算出来print(2 ** 100) # 1267650600228229401496703205376这个特性在算法题里极其好用。做整数运算时你可以完全不用考虑“会不会溢出”思路能更集中在业务逻辑上。另一个容易忽略的点是bool其实是int的子类。True在数值运算中等价于1False等价于0所以True 1的结果是2。这不只是冷知识——当你用isinstance(x, int)去判断类型时True会被判成int但如果用type(x) is intTrue又不会被判成int。这个差异在写类型校验时最容易踩坑。此外整数字面量可以直接写二进制、八进制和十六进制0b101、0o17、0x1f。这三种写法在标志位处理、权限码解析等场景里很常见后面讲进制转换时我们再细说。1.2 float精度有限的双精度浮点数float在 Python 里对应底层 C 语言的double也就是 IEEE 754 标准下的双精度浮点数占用 64 位。它比单精度float精度高但依然是“有限精度”有效数字大约只有 15~17 位。很多人第一次被震到是在交互式环境里敲出0.1 0.2结果返回的是0.30000000000000004。这其实不是 Python 的问题而是所有采用 IEEE 754 的语言都有的现象。原因很简单计算机用二进制存储小数而二进制没法像十进制一样精确表示0.1。你用十进制写1/3也只能写成0.333...永远写不完。二进制表示0.1也是这样它是一个无限循环小数存储时只能截断于是就有了误差。我见过太多人一开始不知道这个背景到处改代码“修复”这个“Bug”最后越改越乱。正确的态度是承认它、理解它、在需要精确计算的场景绕开它。怎么绕开我会在第 4 节讲decimal模块时详细展开。另外float还有两个特殊值inf无穷大和nan非数字。1e308 * 10会得到inf0.0 / 0.0会得到nan。判断时不要直接比较要用math.isinf()、math.isnan()。1.3 complex被很多教程忽略的复数Python 可以直接支持复数写法是a bj注意虚数单位是j而不是数学书上的i。比如z 3 4j print(z.real) # 3.0 print(z.imag) # 4.0 print(z.conjugate()) # (3-4j) print(abs(z)) # 5.0复数的模复数的加减乘除、求模、共轭都直接支持底层还会自动做类型提升。有人觉得入门阶段学复数没什么用但在信号处理、FFT 变换、音频分析等领域Python 的复数支持是默认选项提前认识一下不亏。为了让你一眼看清三种类型的差异我做了一个简单对比类型典型写法底层实现精度/范围适用场景int42、0b1010、-7任意精度整数无固定上限计数、索引、编号、算法题float3.14、1.5e3IEEE 754 双精度约 15~17 位有效数字普通科学计算、图形坐标complex12j两个float组合与float相同信号处理、复平面运算2. 运算规则里藏着的新手坑除法、地板除与类型提升数字的加减乘除看起来毫无技术含量但 Python 的除法家族和取整规则跟很多人的直觉不一致而且这些规则直接影响着你写分页、写周期计算、写轮播逻辑时的结果。2.1 除法家族/、//、% 与 divmodPython 里除法相关的运算符一共有四个运算符含义示例结果/普通除法5 / 22.5//地板除向下取整5 // 22%取余5 % 21divmod(a, b)同时返回商和余数divmod(5, 2)(2, 1)第一个容易踩的坑是Python 3 里/永远是浮点除法即使两个数都是整数5 / 2也返回2.5。如果你是从 Python 2 或 C 语言转过来的可能已经习惯了整数除法直接截断到这里一定要改掉这个条件反射。第二个坑是//的取整方向。Python 的//是“向下取整”也就是往负无穷方向取整而不是向零取整。看这两个例子print(-5 // 2) # -3而不是 -2 print(5 // -2) # -3而不是 -2这跟 C 语言里的整数除法行为不同很多笔记里写“//就是整除”其实不够准确。//的准确描述是结果不大于实际商的整数。同理%取余的符号跟除数一致print(-5 % 2) # 1因为 -5 (-3) * 2 1 print(5 % -2) # -1因为 5 (-3) * (-2) (-1)divmod(a, b)在处理分页时特别好用page, offset divmod(index, page_size)一步就能得到页码和页内偏移省去两次除法计算。它内部只做一次除法性能上比a // b加a % b各算一次更好。2.2 类型提升与混合运算Python 在做数字运算时会自动把不同类型提升为更“宽”的类型规则是int提升到floatint和float提升到complex。这可以理解为“精度低的向精度高的看齐”print(1 2.0) # 3.0结果是 float print(1 2j) # (12j)结果是 complex print(2 * 3.0) # 6.0这个规则本身不复杂但有一个细节值得注意算完以后结果的类型往往不是你想的int。如果你需要整数结果特别是做金额换算、前端展示时记得显式做一次int(...)转换不要默认结果一定是整数。另一个非常隐蔽的坑是幂运算符的优先级。-2 ** 2的结果是-4不是4。因为**的优先级比左侧的负号高所以它其实是-(2 ** 2)。如果你想表达“负二的平方”必须写(-2) ** 2。这个错误在科学计算、数学公式转写里出现的频率相当高。还有一点负数开平方要小心(-1) ** 0.5返回的是复数(6.123233995736766e-171j)而math.sqrt(-1)会直接抛ValueError。原因就是math模块只处理实数**运算符则会根据数学规则自动进入复数域。如果你需要复数开方建议用cmath.sqrt()语义更清晰。2.3 常用内置函数abs、pow、round、sum、min、max这些内置函数看起来简单但细节值得过一遍abs(x)取绝对值。对复数返回模对整数返回整数对浮点数返回浮点数。pow(x, y)等价于x ** y。但它还有一个三参数版本pow(x, y, m)直接计算x ** y % m底层用快速幂性能远高于先算x ** y再取模。round(x, n)保留 n 位小数。注意它采用“银行家舍入”规则具体见第 3 节。sum(iterable, start0)求和。start是可以自定义的例如sum(nums, start1.0)可以把初始值设成浮点数避免纯整数列表求和时意外丢掉精度。min和max如果列表是空的不传default参数会抛ValueError。写健壮代码时可以给defaultNone。这些内置函数非常高频建议全都熟到不用翻文档。3. 数字的进制、格式化与显示问题数字不只能拿来算还经常要拿来“显示”。用户界面上的补零、千分位、百分比、科学计数法背后都是一套格式化逻辑。这一节我会把 Python 里的进制转换和格式化输出彻底讲清楚。3.1 进制转换bin、oct、hex 与 intPython 内置了bin、oct、hex三个函数分别把整数转成二进制、八进制、十六进制字符串n 255 print(bin(n)) # 0b11111111 print(oct(n)) # 0o377 print(hex(n)) # 0xff注意输出结果自带0b、0o、0x前缀。如果你只想要数字部分可以用format(n, b)、format(n, x)或者切片去掉前两位。反向转换用int(s, base)第二个参数指定进制print(int(ff, 16)) # 255 print(int(1010, 2)) # 10 print(int(377, 8)) # 255这个操作在解析配置文件、颜色值、网络协议标志位时特别常用。比如前端传过来的 CSS 颜色是#FFAA00你只需要int(color[1:], 16)就能拿到整数。还有一个冷知识format(255, b)返回的是不带0b的二进制字符串11111111如果配合zfill(8)就能统一补足到 8 位方便做位运算展示print(format(255, b).zfill(8)) # 111111113.2 格式化输出从 f-string 到 formatPython 3.6 之后f-string 已经是最推荐的字符串格式化方式用它处理数字尤其顺手。下面几个是我几乎每天都会用到的格式符需求写法示例输出整数补零到 3 位f{x:03d}007保留两位小数f{x:.2f}3.14千分位分隔f{x:,}1,234,567百分比f{x:.1%}66.7%科学计数法f{x:.2e}1.23e06右对齐宽度 8f{x:8}42十六进制小写f{x:x}ff组合使用也没问题比如f{price:10,.2f}表示右对齐 10 位、千分位、保留两位小数这在生成报表对齐时非常实用。我处理日志、导出 CSV、生成数字矩阵时都会用这一套可以省掉大量拼接代码。3.3 保留小数位round、format 与 Decimal“保留两位小数”这句话不同方法给出的结果可能不一样这里必须重点讲。round(x, n)的舍入规则是“银行家舍入”round-half-even当数字正好处于中间值比如2.5它会舍入到最近的偶数所以round(2.5)是2round(3.5)是4。这对统计数据处理是有意的设计可以避免系统性偏差但跟我们日常理解的“四舍五入”不一致。更麻烦的是浮点误差也会影响round。比如print(round(2.675, 2)) # 2.67而不是 2.68原因是2.675在二进制里实际存储的值略小于2.675保留两位时自然就向下走了。这跟舍入规则无关纯粹是浮点数表示问题。如果你真的需要“四舍五入”用format(x, .2f)在某些情况下可以规避部分问题但更可靠的做法是用decimal.Decimal第 4 节会展开讲。我的建议是展示层用format尽量满足需求业务层涉及到金额、费率这种要求确定性的场景直接上Decimal别跟浮点搏斗。4. 数值计算标配工具箱math、random、decimal 与 fractionsPython 标准库里和数字强相关的模块有四个math、random、decimal、fractions。它们解决的问题各不相同但组合起来几乎覆盖了日常所有数值计算需求。这一节我把每个模块的常用函数和适用场景整理清楚。4.1 math 与 cmath数学函数全家桶math模块处理实数运算常用的有基础sqrt、pow、fabs、gcd、lcm3.9 以上对数指数log、log2、log10、exp三角函数sin、cos、tan、asin、acos、atan弧度制取整floor、ceil、trunc判断isfinite、isinf、isnan、isclose常量math.pi、math.e、math.inf、math.nanmath.isclose(a, b)是判断两个浮点数是否近似相等的最正统方法它替你处理了绝对误差和相对误差。写代码时不要把两个浮点数直接比较这是一个好习惯。cmath是复数版本函数名跟math基本一一对应比如cmath.sqrt(-1)返回1j。如果你在写信号处理、复平面相关代码直接用cmath就好。这里要特别提一个不太起眼但非常实用的函数math.fsum。它对一个浮点序列求和时会使用更高精度的算法来减少累积误差。当你需要对上千个浮点数求和时sum和math.fsum的结果可能会有细微差别后者更接近真实值。统计场景里我一般直接用它。4.2 random模拟与抽样的随机数random模块的常用函数我列一下random.random()生成[0.0, 1.0)之间的浮点数random.uniform(a, b)生成[a, b]之间的浮点数random.randint(a, b)生成[a, b]之间的整数含两端random.choice(seq)从序列中随机选一个random.choices(seq, kn)有放回地随机选 n 个random.sample(seq, kn)无放回地随机选 n 个不会重复random.shuffle(seq)原地打乱列表一个经常被忽略的函数是random.seed(n)。设置固定种子后随机序列会完全可复现。写自动化测试、跑蒙特卡洛模拟、做演示数据时固定种子能让你在复现 bug 时省去大量时间。一个典型的例子是生成一个 4 行 5 列、元素都在 0~9 的数字矩阵import random random.seed(42) matrix [[random.randint(0, 9) for _ in range(5)] for _ in range(4)] for row in matrix: print(row)如果你需要的是密码、令牌、安全验证码这类随机数据记住别用random它属于伪随机序列可预测。安全场景要用secrets模块secrets.randbelow(n)、secrets.choice(seq)才是为加密设计的接口。这个区分在涉及账号系统的项目里非常关键。4.3 decimal 与 fractions要精确就别用 floatdecimal.Decimal是十进制浮点数它不采用二进制存储而是用十进制位串表示所以能精确表示0.1、2.675这样的数字。你能设置全局精度比如from decimal import Decimal, getcontext, ROUND_HALF_UP getcontext().prec 28 # 全局有效数字位数 d1 Decimal(0.1) d2 Decimal(0.2) print(d1 d2) # 0.3精确注意创建Decimal时最好传字符串而不是浮点数Decimal(0.1)是精确的而Decimal(0.1)会先把浮点 0.1 的二进制近似值转进来又带回了误差。金额场景最常用的操作是quantize它指定保留小数位数和舍入方式price Decimal(19.995) print(price.quantize(Decimal(0.01), roundingROUND_HALF_UP)) # 20.00电商、财务、报表这些对金额敏感的场景我一律建议用Decimal。虽然它比float慢一点但换来的是可预期的结果这个代价完全值得。fractions.Fraction则是精确的有理数适合需要完全避免近似的场景from fractions import Fraction a Fraction(1, 3) b Fraction(2, 5) print(a b) # 11/15它不会产生任何浮点误差但如果数值非常大计算开销也大所以一般只在科学计算的小范围数学推导里用。四个模块的定位我总结成一张表模块面向问题精度策略典型场景math/cmath数学函数与常数常规浮点科学计算、几何、信号处理random随机数与模拟伪随机抽样、测试、游戏decimal十进制精确运算十进制位串金额、税率、统计报表fractions有理数精确运算分数对象数学推导、精确比例计算5. 字符串与数字互转的实战细节严格说“字符串与数字互转”不属于数字类型本身但我在实际项目里见到的报错至少有一半来自这个环节。用户输入、文件读取、前端传参拿到的都是字符串怎么安全地变成数字这里面的门道比想象中多。5.1 int 和 float 转换的报错与安全写法先看一组最基础的行为int(123) # 123 int( 123 ) # 123会自动去空白 int(3.5) # ValueError因为整数不能直接从小数字符串转 int(0x1f) # ValueError不会自动识别进制 float(3.5) # 3.5 float(1e3) # 1000.0科学计数法会被识别int(3.5)报错是新手问得最多的一个问题。解决方案是先转float再做整数转换int(float(3.5))结果是3。注意这个过程是截断而不是四舍五入想要四舍五入需要先roundint(round(float(3.5)))。更健壮的做法是写一个统一的解析函数遇到非法输入不直接崩而是返回一个默认值或抛出明确错误def safe_float(text, default0.0): try: return float(text) except (TypeError, ValueError): return default print(safe_float(abc)) # 0.0 print(safe_float(3.14)) # 3.14我在写数据处理脚本时接口入参一律都走这种安全转换否则线上日志里到处是ValueError的堆栈。5.2 判断字符串是否是数字isdigit、isnumeric 与正则判断“一段字符串是不是数字”Python 里有三个长得像的方法但行为差别很大方法12312.3一二三①²str.isdigit()是否否是是str.isdecimal()是否否是否str.isnumeric()是否是是否看到了吧没有一个方法能直接判断中文小数点形式的数字字符串。如果你希望支持12.3这种字符串不能用这些方法应该直接用float(text)加try捕获异常或者用正则做更灵活的匹配。日常文本解析我用最多的正则模式是import re text 订单号: A1024金额: 99.8数量: 3 prices re.findall(r-?\d(?:\.\d)?, text) print(prices) # [1024, 99.8, 3]模式-?\d(?:\.\d)?能匹配整数、负数、带小数点的数字。如果想匹配“1 到 99 之间”的整数比如表单里限制el-input只能输入数字 1-99正则要写得更严格pattern r^(?:[1-9]|[1-9][0-9])$ print(bool(re.match(pattern, 1))) # True print(bool(re.match(pattern, 99))) # True print(bool(re.match(pattern, 100))) # False这里先排除0再用[1-9]匹配一位数、[1-9][0-9]匹配两位数。这种需求在跟前端表单联动时很常见值得记下来。5.3 混合字符串解析的完整案例我把上面的方法串成一个实际场景解析一条用户留言里的所有价格并求和。import re def extract_and_sum(text): numbers re.findall(r-?\d(?:\.\d)?, text) return sum(float(x) for x in numbers) comment 这个套餐 29.9 元另一个 50 元加上运费 6.5 print(extract_and_sum(comment)) # 86.4关键点有两个一是正则要匹配准确二是sum的可迭代对象里直接放生成器表达式省去中间列表。如果你担心浮点累加误差可以把float(x)换成Decimal(x)。对于展示型数据float足够对于财务统计接口建议走Decimal。6. 项目实战里数字处理的几个高频场景数字处理的知识点分散在标准库、运算符、内置函数里单独看都不难难的是在具体项目里知道怎么组合。这一节我选了四个我在真实项目里反复遇到的场景每个都有代码、有思路你可以直接拿来参考。6.1 输出数字矩阵对齐是第一优先级很多算法练习里有“输出 m 行 n 列的数字矩阵”这种题目但不少人在输出格式上栽跟头。数字矩阵如果不做对齐显示出来歪歪扭扭如果行尾多了空格在线评测系统又可能报格式错误。推荐写法是嵌套遍历 f-string 宽度控制m, n 4, 5 for i in range(1, m * n 1): print(f{i:3}, end) if i % n 0: print()输出结果1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20这里的:3意味着每个数字右对齐、占满 3 个字符宽度列跟列之间天然形成间隔。行尾空格需要额外注意如果题目对输出很严格可以在每一行用 .join(...)之后再去掉末尾空格。另外一个常见的需求是矩阵元素来自随机数或计算结果而且数字的位数差异很大。这时候可以先确定最大位数再动态设置宽度避免出现一列左一列右的视觉混乱matrix [[123, 4], [56, 7890]] width max(len(str(x)) for row in matrix for x in row) for row in matrix: print( .join(f{x:{width}} for x in row))6.2 画图时横坐标太密集数字刻度的降采样用 Matplotlib 画时间序列时“横坐标太密集”几乎是必踩的坑。数据点上百个默认刻度会把所有点都标出来文字互相重叠图完全没法看。这个问题的本质是“刻度数量”和“图片宽度”不匹配。解决办法有几种第一种用MaxNLocator限制刻度数量import matplotlib.pyplot as plt from matplotlib.ticker import MaxNLocator x range(100) y [v * v for v in x] plt.plot(x, y) plt.gca().xaxis.set_major_locator(MaxNLocator(nbins10)) plt.show()第二种直接手动指定要显示的索引位置step 10 ticks list(range(0, len(x), step)) plt.xticks(ticksticks, labels[ft{i} for i in ticks])这里核心思路就是把坐标轴刻度当成一个数字数组用步长做下采样。对于更复杂的时间日期轴可以先转成日期对象再用mdates的定位器思路是相通的。第三种如果你的横坐标是字符串标签最常见的办法是旋转标签plt.xticks(rotation45)。但旋转只是治标真正太密时还是得降采样。6.3 量化或仿真模拟中的数字处理别让浮点误差吃掉收益量化交易、蒙特卡洛模拟这类场景特别有意思逻辑本身不复杂但数字的表示方式会直接影响结论。比如计算收益率的复合累计很多人会写returns [0.01, 0.02, -0.015, 0.03] total 1.0 for r in returns: total * (1 r) print(total)这样写在小数据量时没问题但当你做上千期的回测浮点误差会不断累加。更稳的做法是先把收益率取对数累加后再还原import math log_total sum(math.log1p(r) for r in returns) total math.exp(log_total) print(total)math.log1p(r)计算的是log(1 r)它在r很小时比math.log(1 r)更精确。这个是金融计算里的常见技巧很多人第一次接触会觉得“多此一举”但用数值较大的组合测试一下差异立刻就能看出来。如果是涉及金额的撮合、结算我会直接上Decimal不是因为它更快而是因为金额的舍入规则必须完全可控浮点的二进制误差在这种场景里不可接受。6.4 数字滚轮与 UI 展示背后的整数运算有人问“Unity 里 UI 数字滚轮怎么做”或者“CSS 里元素可见时数字慢慢累加到目标值怎么做”这类需求的算法核心其实很简单目标值、当前值、步长和取模。比如一个显示当前秒数的滚轮逻辑可以写成current 0 limit 60 # 模拟每帧更新 for _ in range(70): current (current 1) % limit display f{current:02d} # 两位补零 # 在这里把 display 刷到 UI 上关键点是把“数值计算”和“显示格式”分开数值永远用整数维护只有在渲染时才做格式化补零。Unity 或前端里所谓的“数字滚动动画”本质就是每隔一小段时间更新一次数值再做插值和格式化。理解了这一点无论换什么框架都能很快落地。7. 我遇到的五个高频翻车点以及现在的处理习惯最后这部分是真正让我想写这篇教程的原因。以下五个问题我在不同项目里反复见过自己也踩过不止一次。把它们列出来希望你直接绕过去。7.1 用 直接比较浮点数这是最常见的坑。0.1 0.2 0.3返回False这是浮点表示方式决定的不是逻辑错误。处理办法是判断差值是否足够小import math if math.isclose(0.1 0.2, 0.3, rel_tol1e-9): print(相等)如果业务场景要求完全精确就不要用float改用Decimal。对于新手我的建议是先理解isclose再在需要时切换到Decimal不要指望round能解决所有问题。7.2 判断整除时误用 / 而不是 %“这个数是不是能被 3 整除”最自然的写法是x / 3然后判断结果是不是整数但更直接、更不容易出错的是x % 3 0。取模运算在判断周期性、奇偶性、循环边界时都是首选不要绕道去算除法再判断。同理判断一个数在不在某个区间可以直接链式比较1 x 99Python 支持这种写法很多其他语言反而不行。7.3 忽略 0 和负数的边界条件写数字处理逻辑时我习惯在完成基本功能后专门测三个边界0、负数、极大值。比如格式化字符串f{x:.2f}对负数没问题但如果你在代码里写了余额: str(abs(x))负数就直接变成正数显示业务上就是严重事故。再比如random.randint(0, 0)不会报错但random.randint(1, 0)会抛异常因为区间不合法。边界值测试是最容易被省略但最值得做的测试。7.4 把布尔值直接当数字累加因为True是int的子类sum([True, False, True])会返回2这在某些场景下是特性但也会掩盖问题。比如你想统计列表里有多少个“通过”的值如果列表里混入True而不是1最后的结果可能对但代码的可读性很差。更清晰的写法是显式转换或使用列表推导count sum(1 for item in results if item pass)不要依赖隐式的布尔到整数转换除非你有意利用它。7.5 大数字转字符串的性能与显示截断int没有上限但把超大整数转成字符串是成本很高的操作。如果你要把一个 10 万位的整数转成字符串这会占用大量内存和时间。日常不太会遇到这种情况但做算法题或处理大数据集时要知道可以用 Python 内置的sys.set_int_max_str_digits()调节最大位数限制或者在设计算法时就避免把大整数转成字符串。另外超大整数在显示时要注意截断策略。比如前端展示一个 20 位的订单号你可能希望显示成12345...67890这里就涉及字符串切片而不是数字运算逻辑上不要混淆。最后说说我自己的处理习惯吧。凡是涉及金额、分数、编号这类对精度和格式敏感的数字我都不直接甩给float而是先明确四件事输入是什么类型、要不要保留小数、显示成什么格式、边界值会不会出问题。这四件事想清楚数字相关的坑基本能避掉一大半。这一讲从类型讲到了运算规则又讲了标准库和实战场景内容确实不少但只要你把每节的示例代码在本地跑一遍再回到项目里用起来这些规则就会变成你自己的东西不用刻意背。
阅读完成 · 觉得有帮助?
咨询建站