CS1602计算导论
第 2 讲Part 1 计算基础与 Python 入门AI Level 0

字符与数

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 + 29
-减7 - 25
*乘7 * 214
/除7 / 23.5
//整除(向下取整)7 // 23
%取余7 % 21
**幂7 ** 249

注意 / 这一行:除得尽也是小数,4 / 2 得到的是 2.0 而不是 2。原因在《运算》那一节讲。

比较运算符(结果只有 True 和 False 两种)

运算符意思例子结果
>大于7 > 2True
<小于7 < 2False
>=大于等于7 >= 7True
<=小于等于2 <= 7True
==相等7 == 2False
!=不等7 != 2True

逻辑运算符(把几个判断接起来)

运算符意思例子结果
and且,两边都成立才成立True and FalseFalse
or或,有一个成立就成立True or FalseTrue
not非,把真假反过来not TrueFalse

计算机怎么知道哪块是字符、哪块是数

上一讲的结论是:计算机里只有 0 和 1,一串 bit 本身没有意义。那么同样是 01000001 这八位,凭什么有时候它是字母 A、有时候它是整数 65?

答案不在这串 bit 里面,而在谁来读它、按哪种约定读。程序里承担这件事的,就是类型。

数据类型

上面那个程序里已经出现了三种不同性质的值:"张三" 是一段文字,1.75 是一个小数,bmi > 24 的结果是真或假。它们的区别不只是”值不一样”,而是分属不同的类型。

这门课前期最常用的是四种:

类型名字例子本讲对应小节
字符串str"张三"、"a"、""字符
整数int2026、-3、0数
浮点数float1.75、-0.5、3.0数
布尔值boolTrue、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 里可以直接写四种进制的整数字面量:

进制前缀例子值
十进制无9696
二进制0b0b110000096
八进制0o0o14096
十六进制0x0x6096
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
127DEL

连续这一点很有用:判断一个字符是不是数字,只要看它的编号在不在 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() 结束会自动换行(把笔挪到下一行开头)。

多个参数用逗号隔开,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 就返回 float7 // 2 → 3,7.0 // 2 → 3.0
%同 //7 % 2 → 1
+ - *两个都是 int 就返回 int;有一个 float 就返回 float2 * 3 → 6,2 * 3.0 → 6.0
**同上,但负指数返回 float2 ** 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;// 是向下取整,负数上要当心
  • = 是赋值,== 是比较

练习

  1. 不查表,先猜下面每个表达式的结果和类型,再运行验证:9 / 3、9 // 3、9 % 3、9 ** 0.5、-9 // 3、-9 % 3。猜错的记下来。
  2. 写一个程序,输入一个大写字母,输出它对应的小写字母(用 ord() 和 chr(),不要用 .lower())。
  3. 把你的学号分别用二进制、八进制、十六进制打印出来,再用 int() 从这三种表示还原回十进制,验证一致。
  4. 验证浮点数的不精确:找出三个例子,使 a + b 的运行结果和数学上的结果不同(比如 0.1 + 0.2),把它们的 20 位表示打印出来。
  5. 用 print() 的 end 参数,在一行里输出 1 2 3 4 5,数字之间用 -> 分隔,结尾不要有多余的箭头。
  6. 运行 2 ** 2000,再运行 2.0 ** 2000。前者给出一个 603 位的整数,后者报 OverflowError。解释为什么。 然后找出最大的能让 2.0 ** n 不报错的整数 n,并说明这个数字为什么是它。

本讲的配套上机题在 Lab 2。