Skip to content

Commit 1ce15da

Browse files
committed
新增「浮点数对象」章节(IEEE 754、缓冲池、比较与哈希)
1 parent 106065e commit 1ce15da

8 files changed

Lines changed: 326 additions & 0 deletions

File tree

.vitepress/config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export default defineConfig({
4949
items: [
5050
{ text: 'Python 对象初探', link: '/objects/object/' },
5151
{ text: 'Python 整数对象', link: '/objects/long-object/' },
52+
{ text: 'Python 浮点数对象', link: '/objects/float-object/' },
5253
{ text: 'Python 字符串对象', link: '/objects/str-object/' },
5354
{ text: 'Python 列表对象', link: '/objects/list-object/' },
5455
{ text: 'Python 元组对象', link: '/objects/tuple-object/' },

SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
- [Python 对象初探](objects/object/index.md)
1414
- [Python 整数对象](objects/long-object/index.md)
15+
- [Python 浮点数对象](objects/float-object/index.md)
1516
- [Python 字符串 对象](objects/string-object/index.md)
1617
- [Python List 对象](objects/list-object/index.md)
1718
- [Python 元组对象](objects/tuple-object/index.md)

index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- [ ] Python 内建对象
1919
- [x] Python 对象初探
2020
- [x] Python 整数对象
21+
- [x] Python 浮点数对象
2122
- [x] Python 字符串对象
2223
- [x] Python 列表对象
2324
- [x] Python 元组对象
Lines changed: 32 additions & 0 deletions
Loading
Lines changed: 37 additions & 0 deletions
Loading
Lines changed: 36 additions & 0 deletions
Loading
Lines changed: 33 additions & 0 deletions
Loading

objects/float-object/index.md

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Python 浮点数对象
2+
3+
浮点数大概是最容易让人「踩坑」的类型——下面这一幕几乎每个 Python 工程师都见过:
4+
5+
```python
6+
>>> 0.1 + 0.2
7+
0.30000000000000004
8+
>>> 0.1 + 0.2 == 0.3
9+
False
10+
```
11+
12+
这不是 Python 的 bug,而是 IEEE 754 浮点数的固有特性。这一章我们就来看 `PyFloatObject` 是怎么实现的,并把「为什么不精确」讲清楚。
13+
14+
## 数据结构
15+
16+
浮点数的结构体简单到了极点:
17+
18+
`源文件:`[Include/floatobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/floatobject.h#L15)
19+
20+
```c
21+
// Include/floatobject.h
22+
typedef struct {
23+
PyObject_HEAD
24+
double ob_fval; // 一个 C 的 double
25+
} PyFloatObject;
26+
```
27+
28+
对象头之后只跟一个 C 的 `double``ob_fval`)。注意它用的是 `PyObject_HEAD` 而非 `PyObject_VAR_HEAD`——所以浮点数是**定长对象**:无论值是 `0.0` 还是 `1e308`,占用的内存都一样大。
29+
30+
![PyFloatObject 定长结构](float-struct.svg)
31+
32+
这一点可以直接验证,也正好和整数(变长、按位段增长)形成对照:
33+
34+
```python
35+
>>> import sys
36+
>>> sys.getsizeof(0.0) == sys.getsizeof(1e308)
37+
True
38+
```
39+
40+
## IEEE 754:浮点数为什么「不精确」
41+
42+
那个 `double` 遵循 IEEE 754 双精度标准:64 个二进制位,分成 **1 位符号 + 11 位指数 + 52 位尾数**。它能表示极大、极小的数,但只有 **52 位尾数**来记录有效数字。
43+
44+
问题就出在这里:很多十进制小数,换成二进制是**无限循环小数**,52 位根本装不下。`0.1` 就是典型——它的二进制是 `0.0001100110011…` 无限循环,就像十进制写不尽 `1/3` 一样。于是计算机只能存一个**最接近的可表示值**
45+
46+
![IEEE 754 与 0.1 的不精确](float-ieee754.svg)
47+
48+
`0.1` 在内存里实际存的并不是 0.1,而是一个极接近它的值。把它的「真身」打印出来看看:
49+
50+
```python
51+
>>> from decimal import Decimal
52+
>>> Decimal(0.1)
53+
Decimal('0.1000000000000000055511151231257827021181583404541015625')
54+
```
55+
56+
既然 `0.1``0.2` 存进去就已经有微小误差,它们相加自然不会正好等于同样有误差的 `0.3`,于是 `0.1 + 0.2` 得到 `0.30000000000000004`
57+
58+
> 那为什么 `print(0.1)` 显示的是干净的 `0.1`?因为 Python 的 `repr` 会输出「能唯一还原出这个 double 的最短十进制字符串」——`0.1` 足以还原,就不必把后面那串 `…0055` 都显示出来。显示是「最短表示」,存储仍是那个带误差的二进制值。
59+
60+
所以涉及金额等需要精确小数的场景,应改用 `decimal.Decimal` 或整数(以「分」为单位)。
61+
62+
## 浮点数的创建与缓冲池
63+
64+
和整数、元组类似,浮点数也有**缓冲池**复用对象,避免频繁申请释放。它用一条单链表 `free_list`,最多缓存 100 个:
65+
66+
`源文件:`[Objects/floatobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/floatobject.c#L24)
67+
68+
```c
69+
// Objects/floatobject.c
70+
#define PyFloat_MAXFREELIST 100 // 最多缓存 100 个空闲浮点对象
71+
static PyFloatObject *free_list = NULL; // 空闲对象单链表
72+
```
73+
74+
创建浮点数走 `PyFloat_FromDouble`:链表非空就取链表头复用,否则才向系统申请:
75+
76+
`源文件:`[Objects/floatobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/floatobject.c#L115)
77+
78+
```c
79+
// Objects/floatobject.c
80+
PyObject *
81+
PyFloat_FromDouble(double fval)
82+
{
83+
PyFloatObject *op = free_list;
84+
if (op != NULL) {
85+
free_list = (PyFloatObject *) Py_TYPE(op); // 取链表头复用(链表穿过 ob_type 字段)
86+
numfree--;
87+
} else {
88+
op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject)); // 池空才新建
89+
......
90+
}
91+
(void)PyObject_INIT(op, &PyFloat_Type);
92+
op->ob_fval = fval; // 填入数值
93+
return (PyObject *) op;
94+
}
95+
```
96+
97+
![浮点数缓冲池 free_list](float-freelist.svg)
98+
99+
浮点数销毁时则挂回链表头;只有当池里已满 100 个,才真正 `free` 掉。
100+
101+
## 浮点数的比较
102+
103+
浮点数之间的比较是直接比 `double`。有意思的是**浮点数和整数比较**——CPython 在这里特别小心,避免精度损失。看 `float_richcompare`:
104+
105+
`源文件:`[Objects/floatobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/floatobject.c#L348)
106+
107+
```c
108+
// Objects/floatobject.c —— float_richcompare(与 int 比较的分支)
109+
else if (PyLong_Check(w)) {
110+
int vsign = i == 0.0 ? 0 : i < 0.0 ? -1 : 1;
111+
int wsign = _PyLong_Sign(w);
112+
if (vsign != wsign) {
113+
/* 符号不同,光看符号就能定胜负,无需比较大小 */
114+
......
115+
}
116+
/* 符号相同:按位数等信息精确比较,而不是粗暴地把 int 转成 double */
117+
nbits = _PyLong_NumBits(w);
118+
......
119+
}
120+
```
121+
122+
它没有简单地「把 int 转成 double 再比」——因为一个很大的整数转成 `double` 会丢失精度,比较结果就可能出错。CPython 先比符号,再按整数的位数等信息**精确**比较。这带来一个微妙但正确的结果:
123+
124+
```python
125+
>>> 2.0 ** 53 == 2 ** 53
126+
True
127+
>>> 2.0 ** 53 == 2 ** 53 + 1 # float 表示不了 2^53+1,但比较仍然精确
128+
False
129+
>>> float(2 ** 53 + 1) == 2 ** 53 + 1 # 一旦把 int 转成 float,就丢了精度
130+
False
131+
```
132+
133+
`2.0 ** 53` 这个 `double` 和整数 `2**53+1` 比较,得到正确的 `False`;而如果先 `float(2**53+1)`,它会被舍入成 `2^53`,精度就丢了。
134+
135+
## 浮点数的哈希
136+
137+
为了让「数值相等的对象哈希也相等」,CPython 精心设计了数值的哈希:**值相等的 `int``float` 哈希一致**
138+
139+
```python
140+
>>> 2.0 == 2
141+
True
142+
>>> hash(2.0) == hash(2)
143+
True
144+
```
145+
146+
这条规则很重要:它保证了数值相等的 `int``float` 在字典、集合里是**同一个键**
147+
148+
![int 与 float 哈希一致](float-hash.svg)
149+
150+
```python
151+
>>> d = {1: 'a'}
152+
>>> d[1.0] # 1 == 1.0 且哈希相同 → 命中同一项
153+
'a'
154+
```
155+
156+
(浮点数的哈希由 `_Py_HashDouble` 计算,整数与浮点数共用一套规则,使等值者哈希一致。)
157+
158+
## 特殊值:inf 与 nan
159+
160+
IEEE 754 还定义了几个特殊值:正负**无穷大** `inf`**非数** `nan`(Not a Number,如 `0.0/0.0` 的结果)。
161+
162+
```python
163+
>>> float('inf'), float('-inf'), float('nan')
164+
(inf, -inf, nan)
165+
```
166+
167+
`nan` 有一个反直觉但符合标准的性质:**它不等于任何值,包括它自己**
168+
169+
```python
170+
>>> n = float('nan')
171+
>>> n == n
172+
False
173+
```
174+
175+
所以判断一个浮点数是不是 `nan`,不能用 `x == x`(对 `nan` 恒为 `False`),要用 `math.isnan(x)`。这也意味着含 `nan` 的容器在做成员判断时要小心。
176+
177+
---
178+
179+
小结一下浮点数对象的要点:
180+
181+
- `PyFloatObject` 只在对象头后放一个 C 的 `double`,是**定长对象**(对照整数的变长),任何浮点数大小恒定;
182+
- 它遵循 **IEEE 754 双精度**:52 位尾数装不下像 `0.1` 这样的二进制无限循环小数,只能存最接近的值,这就是 `0.1 + 0.2 != 0.3` 的根源;需要精确小数请用 `decimal`
183+
- 创建时有 **free list 缓冲池**(单链表,≤ 100)复用对象;
184+
- 与整数比较时**精确处理**避免精度损失;数值相等的 `int``float` **哈希一致**,是同一个字典键;
185+
- 特殊值 `inf``nan` 中,`nan` 不等于包括自身在内的任何值,判断用 `math.isnan`

0 commit comments

Comments
 (0)