字符与数
Fundamentals I: Characters and Numbers
计算机里只有 0 和 1。这一讲讲这些 0 和 1 怎么表示出字母、整数和小数,以及小数的计算结果为什么常常出乎意料。
本讲结束后你应当能
- 读懂一个完整的小程序,说出每一行在做什么
- 说出 str、int、float、bool 四种类型的区别,并用 type() 查一个值的类型
- 在二进制、八进制、十进制、十六进制之间换算
- 用转义字符和三引号控制输出的样子
- 用 ord() 和 chr() 在字符和编号之间转换
- 说明浮点数为什么不精确,并知道什么时候该避开它
- 正确预测 / // % ** 各自返回什么类型
本页目录
先看一个完整的程序
上一讲只写了一行 print("Hello world")。现在看一个真正做事的程序。先看整体,再拆零件:
# 体重指数(BMI)计算器
name = "张三"
height = 1.75 # 米
weight = 68.5 # 公斤
bmi = weight / (height ** 2)
print(name, "的 BMI 是", bmi)
print("保留一位小数:", round(bmi, 1))
print("是否偏胖:", bmi > 24)
点运行看看结果。然后逐行读:
| 行 | 在做什么 | 这一讲会讲 |
|---|---|---|
# 体重指数… | 注释,给人看的,Python 会跳过 | ✓ |
name = "张三" | 把一个字符串存进名叫 name 的变量 | ✓ 字符 |
height = 1.75 | 把一个浮点数存进 height | ✓ 数 |
bmi = weight / (height ** 2) | 算术运算,结果存起来 | ✓ 运算 |
print(name, "的 BMI 是", bmi) | 输出,多个值用逗号隔开 | ✓ 输出 |
bmi > 24 | 比较,结果是 True 或 False | ✓ 逻辑 |
一个程序无非就是这几件事:准备数据、计算、输出结果。 这一讲讲清楚”数据”到底是什么,下一讲讲怎么把数据变成字符串输出,第四讲讲怎么让程序做判断和重复。
先认一下这些符号
上面那行 bmi = weight / (height ** 2) 里有四个符号还没解释:
| 符号 | 意思 |
|---|---|
= | 赋值:把右边算出来的值,存进左边这个名字。数学里的「相等」在 Python 里要写两个等号 |
/ | 除 |
** | 幂。height ** 2 就是身高的平方 |
() | 先算括号里的,和数学里一样 |
这一讲后面还会用到别的运算符。这里先一次列出来,有个印象就行,不用记。本讲的《运算》和《逻辑》两节会逐个讲透。
算术运算符
| 运算符 | 意思 | 例子 | 结果 |
|---|---|---|---|
+ | 加 | 7 + 2 | 9 |
- | 减 | 7 - 2 | 5 |
* | 乘 | 7 * 2 | 14 |
/ | 除 | 7 / 2 | 3.5 |
// | 整除(向下取整) | 7 // 2 | 3 |
% | 取余 | 7 % 2 | 1 |
** | 幂 | 7 ** 2 | 49 |
注意 / 这一行:除得尽也是小数,4 / 2 得到的是 2.0 而不是 2。原因在《运算》那一节讲。
比较运算符(结果只有 True 和 False 两种)
| 运算符 | 意思 | 例子 | 结果 |
|---|---|---|---|
> | 大于 | 7 > 2 | True |
< | 小于 | 7 < 2 | False |
>= | 大于等于 | 7 >= 7 | True |
<= | 小于等于 | 2 <= 7 | True |
== | 相等 | 7 == 2 | False |
!= | 不等 | 7 != 2 | True |
逻辑运算符(把几个判断接起来)
| 运算符 | 意思 | 例子 | 结果 |
|---|---|---|---|
and | 且,两边都成立才成立 | True and False | False |
or | 或,有一个成立就成立 | True or False | True |
not | 非,把真假反过来 | not True | False |
计算机怎么知道哪块是字符、哪块是数
上一讲的结论是:计算机里只有 0 和 1,一串 bit 本身没有意义。那么同样是 01000001 这八位,凭什么有时候它是字母 A、有时候它是整数 65?
答案不在这串 bit 里面,而在谁来读它、按哪种约定读。程序里承担这件事的,就是类型。
数据类型
上面那个程序里已经出现了三种不同性质的值:"张三" 是一段文字,1.75 是一个小数,bmi > 24 的结果是真或假。它们的区别不只是”值不一样”,而是分属不同的类型。
这门课前期最常用的是四种:
| 类型 | 名字 | 例子 | 本讲对应小节 |
|---|---|---|---|
| 字符串 | str | "张三"、"a"、"" | 字符 |
| 整数 | int | 2026、-3、0 | 数 |
| 浮点数 | float | 1.75、-0.5、3.0 | 数 |
| 布尔值 | bool | True、False | 逻辑 |
type() 可以查一个值属于哪一类:
print(type("张三"))
print(type(1.75))
print(type(2026))
print(type(1.75 > 1))
输出里的 class 说明类型本身在 Python 里也是一种对象,这一点第四部分讲面向对象时再展开,现在可以先忽略。
类型不只是个标签。它决定了这个值能参与哪些运算,也决定了同一个运算符作用在它身上是什么含义:
print(3 * 2)
print("ab" * 2)
print(1 + 2)
print("1" + "2")
* 对数是乘法,对字符串是重复;+ 对数是加法,对字符串是拼接。所以”这行代码是什么意思”这个问题,不知道操作数的类型就无法回答。本讲后面的”类型规则”一节会把算术运算符的返回类型列全,下一讲的”类型转换”一节讲怎么在类型之间转换。
类型存在哪里
说「类型跟着值走」不是比喻。写下 n = 2026,内存里发生的事是这样的:
n只是一个名字,它自己不装东西,只是指向某个位置- 被指向的那个对象一共 28 个字节,分四段:
| 字节 | 存的是什么 |
|---|---|
| 0–7 | 引用计数:还有几个名字指着我 |
| 8–15 | 类型:指向 int 这个类型 |
| 16–23 | 位数与符号 |
| 24–27 | 值本身,2026 就在这里 |
type(n) 读的就是第 8–15 这八个字节。上一小节问的「计算机怎么知道哪块是字符哪块是数」,答案就在这里:它不用猜,值自己带着答案。
C 语言是另一条路:int n = 2026; 里类型写在名字前面,编译完之后内存里只剩下值,「它是个 int」只写在编译器生成的指令里。省下的就是上面那 20 个字节,代价是类型不能变。
一切都是 bit
一位,一个字节
计算机的存储单元只有两种状态:高电压和低电压,记作 1 和 0。
- 一个 0 或 1 叫一个 bit(位)
- 八个 bit 叫一个 byte(字节)
一个 bit 能表示 2 种情况,两个 bit 能表示 4 种,n 个 bit 能表示 2ⁿ 种。所以一个字节能表示 2⁸ = 256 种不同的取值。
这个数字很重要,后面会反复出现。
二进制怎么数
十进制里,2026 的意思是 2×10³ + 0×10² + 2×10¹ + 6×10⁰。每往左一位,权重乘以 10。
二进制一模一样,只是权重乘以 2。1101 表示:
1 1 0 1
×2³ ×2² ×2¹ ×2⁰
8 + 4 + 0 + 1 = 13
# Python 帮你换算
print(0b1101) # 二进制字面量,前缀 0b
print(bin(13)) # 反过来
print(int("1101", 2)) # 从字符串按 2 进制解析
四种进制的写法
Python 里可以直接写四种进制的整数字面量:
| 进制 | 前缀 | 例子 | 值 |
|---|---|---|---|
| 十进制 | 无 | 96 | 96 |
| 二进制 | 0b | 0b1100000 | 96 |
| 八进制 | 0o | 0o140 | 96 |
| 十六进制 | 0x | 0x60 | 96 |
print(96, 0b1100000, 0o140, 0x60) # 四个写法,同一个数
print(96 == 0x60) # 确认一下
它们只是写法不同,存进内存之后完全一样。 就像”十二”、“12”、“XII”指的是同一个数量。
十六进制之所以常用,是因为一位十六进制正好对应四位二进制,写起来短,又能一眼看出二进制结构。
字符
ASCII:字符和数字的约定
计算机只能存数字。那字母怎么办?
答案是约定一张表,规定每个字符对应哪个数字。最早得到普遍接受的一张叫 ASCII(American Standard Code for Information Interchange),用 7 个 bit 表示 128 个字符:大小写字母、数字、标点、以及一些控制字符。
整张表可以按用途分成几段,同一段内部的编号是连续的:
| 编号 | 这一段是什么 |
|---|---|
| 0–31 | 控制字符,没有可见形状(Tab 是 9,换行是 10,回车是 13) |
| 32 | 空格 |
| 33–47、58–64、91–96、123–126 | 标点,散在中间,没有规律 |
| 48–57 | 数字 0–9 |
| 65–90 | 大写 A–Z |
| 97–122 | 小写 a–z |
| 127 | DEL |
连续这一点很有用:判断一个字符是不是数字,只要看它的编号在不在 48 到 57 之间,不用挨个比较。
几个要记住的:
| 字符 | 编号 | 备注 |
|---|---|---|
'0' | 48 | 数字字符 0,不是整数 0 |
'A' | 65 | 大写字母从这里开始 |
'a' | 97 | 小写 = 大写 + 32 |
' ' | 32 | 空格也是字符 |
print(ord('A'), ord('a'), ord('0'), ord(' '))
print(chr(65), chr(97), chr(48))
# 大小写差 32,所以可以这样转换
print(chr(ord('h') - 32))
ord() 把字符变成编号,chr() 把编号变回字符。这两个函数负责字符和编号之间的来回转换。
ASCII 只有 128 个位置,装不下中文。现代的方案是 Unicode,给世界上几乎所有文字都分配了编号。ord() 同样适用:
print(ord('中'), ord('文'))
print(chr(20013), chr(25991))
Unicode 的完整机制(以及为什么会出现乱码)留到 L12。
转义字符
有些字符没法直接写在引号里,比如换行、制表符,或者引号本身。解决办法是用反斜杠 \ 开头的转义序列:
| 写法 | 表示 |
|---|---|
\n | 换行 |
\t | 制表符(Tab) |
\\ | 一个反斜杠 |
\' | 单引号 |
\" | 双引号 |
print("第一行\n第二行")
print("姓名\t学号")
print("他说:\"你好\"")
print("路径 C:\\Users\\Documents")
多行字符串
一段文字里换行多了,满篇 \n 就读不下去了。用三个引号括起来,里面的换行原样保留:
poem = """江南好,风景旧曾谙。
日出江花红胜火,
春来江水绿如蓝。"""
print(poem)
三引号还有一个好处:单引号和双引号都可以直接写,不用转义。
print("""他说:"你好",我说:'再见'。""")
print() 的行为模型
把 print() 想象成一支笔在纸上写字:笔从左往右移动,每次 print() 结束会自动换行(把笔挪到下一行开头)。
多个参数用逗号隔开,Python 会自动在相邻两个之间加一个空格:
print("a", "b", "c") # 中间有空格
print("a" + "b" + "c") # 直接拼接,没有空格
print(1, 2, 3)
不想换行就用 end 参数:
print("不换行", end="")
print(" ← 接在同一行")
print("用横线分隔", end=" --- ")
print("下一段")
数
整数:Python 的整数没有上限
大多数语言里整数有固定长度,比如 64 位,超出范围就溢出,数字会突然变成负数,或者从头绕回去。
Python 没有这个问题。 整数要多大有多大,只受内存限制:
big = 2 ** 200
print(big)
print("有", len(str(big)), "位数字")
# 阶乘增长得更吓人
import math
print(math.factorial(50))
这是 Python 对初学者友好的一点:不用操心溢出。代价是大整数运算会变慢,L11 会讲这个代价具体是多少。
浮点数:为什么结果常常出乎意料
小数在 Python 里叫浮点数(float)。现在做一个实验,先猜结果再运行:
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
如果猜的是 0.3 和 True,那就撞上了每个程序员都会遇到的那个坑。
为什么?
计算机用二进制存小数。十进制里 1/3 = 0.3333… 写不完,二进制里 0.1 也写不完:
0.1(十进制) = 0.000110011001100110011…(二进制,0011 无限循环)
存储空间有限(通常 64 bit,遵循 IEEE 754 标准),只能截断。于是存进去的并不是 0.1,而是一个和 0.1 非常接近的数。几个这样的近似值加在一起,误差就露出来了。
# 把真实存储的值多打几位看看
print(f"{0.1:.20f}")
print(f"{0.2:.20f}")
print(f"{0.1 + 0.2:.20f}")
可以看到,0.1 存进去实际是 0.10000000000000000555…。
有时候又恰好相等
自己试别的组合,很快会撞上这样的情况:
print(0.1 + 0.2 == 0.3)
print(0.1 + 0.3 == 0.4)
print(0.2 + 0.4 == 0.6)
print(0.1 + 0.7 == 0.8)
第二个是 True。四个式子在数学上是同一类,结果却不一样。
这不是随机的。截断产生的误差有方向,可能偏大也可能偏小,取决于那个数在二进制里被截断的位置:
print(f"{0.1:.20f}") # 偏大
print(f"{0.3:.20f}") # 偏小
print(f"{0.1 + 0.3:.20f}")
print(f"{0.4:.20f}") # 和上一行完全相同
0.1 存进去偏大,0.3 存进去偏小,两个误差方向相反、正好抵消,和落回到 0.4 存进去的那个数上,于是 == 成立。而 0.1 和 0.2 存进去都偏大,误差同向叠加,和是 0.30000000000000004441,0.3 本身存的是 0.29999999999999998890,两个数不同,== 就是 False。
换任何一台机器、任何一种主流语言,这些结果都一样。
碰巧相等不能当作使用 == 的依据。 恰好相等确实存在,可是要在写代码之前算出两个误差会不会抵消,代价远大于直接改用容差比较。更麻烦的是:只在部分输入上成立的判断,比从不成立的判断更危险。它会通过你的测试,然后在别的数据上出错。
什么时候该避开浮点数
一条实用原则:能用整数的地方就用整数。
典型场景是钱。0.1 元 + 0.2 元 会出问题,但如果单位换成”分”,10 + 20 = 30 永远精确:
# 有问题的算法
total = 0.0
for _ in range(10):
total += 0.1
print("用浮点数累加 10 次 0.1:", total)
print("等于 1.0 吗:", total == 1.0)
# 换成整数(单位:分)
total_cents = 0
for _ in range(10):
total_cents += 10
print("用整数(分):", total_cents / 100)
第一个循环得到的是 0.9999999999999999,十次微小误差累积的结果。
运算
算术运算符
a, b = 7, 2
print(a + b) # 加
print(a - b) # 减
print(a * b) # 乘
print(a / b) # 除
print(a // b) # 整除(向下取整)
print(a % b) # 取余
print(a ** b) # 幂
类型规则:这一段务必记住
哪些运算返回整数、哪些返回浮点数,是有明确规则的:
| 运算 | 规则 | 例子 |
|---|---|---|
/ | 永远返回 float,哪怕除得尽 | 4 / 2 → 2.0 |
// | 两个都是 int 就返回 int;有一个 float 就返回 float | 7 // 2 → 3,7.0 // 2 → 3.0 |
% | 同 // | 7 % 2 → 1 |
+ - * | 两个都是 int 就返回 int;有一个 float 就返回 float | 2 * 3 → 6,2 * 3.0 → 6.0 |
** | 同上,但负指数返回 float | 2 ** 3 → 8,2 ** -1 → 0.5 |
print(4 / 2, type(4 / 2)) # 注意是 2.0
print(7 // 2, type(7 // 2))
print(7.0 // 2, type(7.0 // 2))
print(2 ** -1, type(2 ** -1))
负数的整除和取余
// 是向下取整(往更小的方向),不是”截断小数部分”。对负数来说这两者不同:
print(7 // 2) # 3
print(-7 // 2) # 猜猜是 -3 还是 -4?
print(7 % 2)
print(-7 % 2) # 这个也猜一下
Python 保证 a == (a // b) * b + a % b 恒成立,所以 -7 // 2 是 -4,-7 % 2 是 1。
优先级
和数学里一样:** 最高,然后 * / // %,最后 + -。同级从左往右算,** 例外(从右往左)。
print(2 + 3 * 4) # 14,不是 20
print(2 ** 3 ** 2) # 2^(3^2) = 512,不是 (2^3)^2 = 64
print((2 + 3) * 4) # 20
逻辑:True 和 False
先说要它干什么
算术回答的是「多少」,比较回答的是「是还是不是」。程序能做判断,靠的就是后者。
到这一讲为止,我们写的程序都是从第一行走到最后一行,换什么输入都走同一条路。从第 4 讲开始,程序会分支、会重复,而每一个 if 后面的条件、每一个 while 后面的条件,求值的结果都必须是 True 或 False:
程序走到这一行
│
┌─────┴─────┐
│ score >= 60│ ← 先算出一个 True 或 False
└─────┬─────┘
True │ False
┌─────┴─────┐
▼ ▼
print("及格") print("不及格")
这里是两步,不是一步:先把 score >= 60 求值成一个布尔值,再按这个值选路。很多人会想成「程序看了一眼分数就知道走哪边」,中间那个值被跳过去了;等条件写成 score >= 60 and attendance >= 0.8,看不出中间那个值是什么就查不出问题。
常见的用处还有:while guess != answer 决定循环停不停,if name in roster 判断在不在里面,以及把一串判断加起来数出「满足条件的有几个」(这一节最后会见到)。
比较运算
比较运算的结果是布尔值(bool),只有两个:True 和 False。注意首字母大写。
print(3 > 2)
print(3 == 2) # 两个等号是"判断相等"
print(3 != 2) # 不等于
print(3 >= 3)
布尔值可以用 and、or、not 组合:
age = 19
has_id = True
print(age >= 18 and has_id)
print(age < 18 or has_id)
print(not has_id)
Python 里 True 和 False 本质上就是 1 和 0,可以直接参与运算:
print(True + True) # 2
print(sum([True, False, True, True])) # 统计有几个 True
最后这个写法很有用,统计”满足条件的有几个”时经常见到。
小结
- 每个值都有类型:
str、int、float、bool是最常用的四种。类型决定这个值能做什么,也决定+*这些运算符在它身上是什么含义,type()可以查 - 计算机里只有 bit。一切意义都来自约定:同一串 0 和 1,可以是数、是字符、是指令
- 四种进制只是写法不同:
0b、0o、0x、无前缀 ord()/chr()在字符和编号之间转换。'0'(字符)和0(整数)不是一回事- Python 的整数没有上限,不用担心溢出
- 浮点数不精确,因为二进制存不下大多数十进制小数。永远不要用
==比较浮点数。有些式子(0.1 + 0.3 == 0.4)碰巧成立,但写代码时预判不了是哪些 - 能用整数就用整数
/永远返回 float;//是向下取整,负数上要当心=是赋值,==是比较
练习
- 不查表,先猜下面每个表达式的结果和类型,再运行验证:
9 / 3、9 // 3、9 % 3、9 ** 0.5、-9 // 3、-9 % 3。猜错的记下来。 - 写一个程序,输入一个大写字母,输出它对应的小写字母(用
ord()和chr(),不要用.lower())。 - 把你的学号分别用二进制、八进制、十六进制打印出来,再用
int()从这三种表示还原回十进制,验证一致。 - 验证浮点数的不精确:找出三个例子,使
a + b的运行结果和数学上的结果不同(比如0.1 + 0.2),把它们的 20 位表示打印出来。 - 用
print()的end参数,在一行里输出1 2 3 4 5,数字之间用->分隔,结尾不要有多余的箭头。 - 运行
2 ** 2000,再运行2.0 ** 2000。前者给出一个 603 位的整数,后者报OverflowError。解释为什么。 然后找出最大的能让2.0 ** n不报错的整数n,并说明这个数字为什么是它。
本讲的配套上机题在 Lab 2。