数据抽象与序列
前几篇我们玩的"值"都是数字、字符串这种原子。可真实程序里,数据从来不是孤立的——一个学生有姓名、年龄、成绩;一个有理数有分子、分母。
当数据变复杂,"怎么存"和"怎么用"就会纠缠在一起,改一处崩一片。
CS61A 给出的解药叫数据抽象(data abstraction):用一层"接口"把两者隔开。本篇就用"有理数"这个小例子,把这个影响你一辈子的思想钉死。
一、数据抽象:构造函数 + 选择器
是什么:CS61A 对"数据"有一个极简也极深刻的定义——数据 = 构造函数(constructor) + 选择器(selector)。
- 构造函数:把零散的部件"打包"成一个整体(如
make_rat(n, d)); - 选择器:从整体里"取出"某个部件(如
numer(r)取分子、denom(r)取分母)。
只要这两者行为一致,内部到底用元组、字典还是两个独立变量存,根本不重要。这层隔离叫抽象壁垒(abstraction barrier)。
以"有理数"为例:
1 | def make_rat(n, d): |
输出:
1 | 1 2 |
是什么(再强调):add_rat 只通过选择器 numer/denom 和数据打交道,从不直接写 r[0]。这意味着——
坑(抽象壁垒被打破的代价):如果你在 add_rat 里直接写 a[0] + b[0],那就把"底层是元组"这个实现假设渗透到了上层。哪天你想把有理数改成"自动约分的结构体",上层全员报错。规矩是:只走构造函数/选择器,绝不戳内部表示。这层壁垒让你能单独换底层、上层纹丝不动。
本质一句话:数据抽象 = 用"构造函数+选择器"当接口,把"怎么存"和"怎么用"隔成两层,改底层不伤上层。
二、序列抽象:一类"可按下标访问"的对象
是什么:Python 里有一大类对象共享同一套接口——序列(sequence):能用整数下标访问、能求长度。这套统一接口就是 len(seq) 和 seq[i]。只要实现了它,list、tuple、str 就能用同一套写法操作。
最常见的四种序列:
| 类型 | 可变性 | 字面量 | 典型用途 |
|---|---|---|---|
list |
可变 | [ ] |
动态集合 |
tuple |
不可变 | ( ) |
固定记录、作字典键 |
str |
不可变 | ' ' / " " |
文本 |
range |
不可变(惰性) | range() |
整数区间(篇七细讲) |
1 | lst = [10, 20, 30] |
坑:str 不可变,所以 s[0] = 'x' 会直接报错。很多人误以为"字符串也能像列表一样改某个字符"——不行,要改只能整体重建:s = 'x' + s[1:]。
本质一句话:序列抽象 = 一套 len/下标 统一接口,让不同容器能被同一套代码操作。
三、切片与列表推导:日常主力语法
切片(slice) seq[start:stop:step] 取出子序列,规则是左闭右开,且支持负数下标(从末尾数):
1 | nums = [0, 1, 2, 3, 4, 5] |
列表推导(list comprehension) 是"生成列表"的紧凑写法,本质等价于 map + filter 的组合:
1 | nums = [1, 2, 3, 4, 5] |
输出:
1 | [1, 9, 25] |
坑:切片是浅拷贝新列表——对不可变元素(数字、字符串)没问题;但若列表里装的是可变对象(如子列表),新列表里的引用仍指向原来那些对象,改一个会影响"两边"。深浅拷贝的坑我们篇五接着挖。
本质一句话:切片 [start:stop:step] 取子序列、左闭右开;列表推导 [... for ... if ...] 是 map+filter 的语法糖。
四、为什么抽象是写大程序不崩的底线
把"数据怎么存"和"数据怎么用"分开,是程序能长大而不腐败的关键。一旦你引入了篇五要讲的可变性,抽象壁垒就更容易被打破——因为现在"存的东西"会被偷偷改掉。
所以理解清楚:
- 不可变序列(tuple / str / range) 是抽象壁垒的"好朋友"——它保证拿到的数据不会被别人暗中篡改;
- 可变序列(list / dict / set) 灵活,但用之前得想清楚"谁还能改它"。
1 | 理想的分层: |
小结
- 数据抽象 = 构造函数 + 选择器,用接口隔离"存"与"用";只走接口、不戳内部,壁垒才立得住。
- 序列统一接口:
len()与整数下标;list 可变、tuple/str/range 不可变。 - 切片
[start:stop:step]左闭右开、支持负数;列表推导是 map+filter 的语法糖(浅拷贝)。 - 不可变序列是抽象壁垒的好朋友;可变序列灵活但需警惕"谁在改它"。

