Scheme 解释器、尾递归与声明式 SQL
前七篇都在 Python 里打转。最后一篇,CS61A 故意把你拽进另一种语言——Scheme(Lisp 方言),再让你亲手写一个"能运行 Scheme 的解释器"。为什么?因为跳出一种语言,你才真正看清"编程"这件事的边界:原来函数可以没有名字、原来递归可以不用占栈、原来"我要什么"和"我怎么做"可以彻底分开。这一篇是 CS61A 的高潮,也是把全课程串起来的那根线。 一、Scheme 基础:括号即一切是什么:Scheme 里一切都是 S-表达式(S-expression)——要么是一个原子(数字、符号),要么是括号包裹的组合 (操作符 操作数 ...)。所有语法都是这一种形式的变体。 1234(define (square x) (* x x)) ; 定义过程 square(square 5) ; => 25(define x 10)(if (> x 0) "正" "负") ...
效率、迭代器与生成器
回到篇三那个优雅的 fib:代码和数学定义一字不差,美不美?美。快不快?一点都不快。当 n 大到 40,它能卡到让你怀疑人生。为什么"正确的代码"会这么慢?又该怎么救?本篇先给你一把尺子(大 O)量效率,再用"迭代"和"记忆化"两把刀修掉 fib,最后引出 Python 里最被低估的利器——生成器,它让"无限序列"成为可能。 一、增长阶(大 O):规模翻倍时耗时怎么变是什么:衡量算法效率,不看"跑了 3 毫秒还是 5 毫秒"(那取决于机器和常数),而看输入规模 n 增长时,步数怎么随 n 变化。忽略常数倍,只看"增长趋势",就是大 O 记号。 阶 含义 例子 O(1) 常数,与 n 无关 列表按下标访问 O(n) 线性,随 n 正比增长 遍历一遍 O(n²) 平方,双层嵌套 冒泡排序 O(2ⁿ) 指数,n 每+1 量翻倍 朴素斐波那契 坑:大 O 丢掉了常数,所以"O(n) 一定比 O(n²) 快"只在 n 足...
面向对象编程:类、继承与方法分派
前面我们一直把"数据"和"操作数据的函数"分开写:先定义 make_rat,再写 add_rat。但真实世界里,一个"学生"既有一堆属性(姓名、成绩),又有一堆行为(选课、算 GPA)。把它们拆在两处,代码会越来越散、越来越难找。面向对象(OOP) 的解法很直白:把"数据"和"操作它的方法"打包进同一个类(class)。本篇从 self 这个最小齿轮讲起,一直讲到 Python 怎么决定"调的是哪个方法"(MRO)。 一、类与实例:蓝图与成品是什么: 类(class) 是对象的"蓝图",描述一类事物长什么样、能干什么; 实例(instance) 是按蓝图造出的具体对象; self 是约定俗成的第一个参数,代表"当前这个实例自己"。 12345678class Dog: def __init__(self, name): # 构造器,创建时自动调用 self.name = name ...
可变性、容器、树与链表
前面几篇,我刻意没让你碰"改掉一个已有的值"。因为**可变性(mutability)**是 CS61A 乃至整个编程里"便利"和"灾难"同一个源头。它能让代码更短更自然,也能让 bug 在最意想不到的地方炸——而且炸得毫无痕迹。本篇先把"可变 vs 不可变"这条生死线划清,再看别名、容器,最后动手实现 CS61A 两尊经典递归结构:Link 与 Tree。 一、可变 vs 不可变:一条分界生死线是什么: 不可变(immutable):创建后内容不能改。int、float、str、tuple、frozenset 都在此列。你做的任何"修改"其实是生成新对象。 可变(mutable):创建后内部可改。list、dict、set、以及你自己写的类实例都在此列。改的是"同一个对象"。 12345s = "abc"# s[0] = "x" # 报错:str 不可变lst = [1, 2, 3]lst[0] = ...
数据抽象与序列
前几篇我们玩的"值"都是数字、字符串这种原子。可真实程序里,数据从来不是孤立的——一个学生有姓名、年龄、成绩;一个有理数有分子、分母。当数据变复杂,"怎么存"和"怎么用"就会纠缠在一起,改一处崩一片。CS61A 给出的解药叫数据抽象(data abstraction):用一层"接口"把两者隔开。本篇就用"有理数"这个小例子,把这个影响你一辈子的思想钉死。 一、数据抽象:构造函数 + 选择器是什么:CS61A 对"数据"有一个极简也极深刻的定义——数据 = 构造函数(constructor) + 选择器(selector)。 构造函数:把零散的部件"打包"成一个整体(如 make_rat(n, d)); 选择器:从整体里"取出"某个部件(如 numer(r) 取分子、denom(r) 取分母)。 只要这两者行为一致,内部到底用元组、字典还是两个独立变量存,根本不重要。这层隔离叫抽象壁垒(abstractio...
递归:从线性到树形
第一次写递归的人,脑子里通常有个挥之不去的念头:"我得把每一次调用都想象清楚,才算写对。"结果越想越乱,最后放弃、改写成循环。但递归真正的法门恰恰相反——你不必追踪每一次调用的细节,你只需要相信:子问题已经被正确地解决了。这种"信仰之跃(leap of faith)"是 CS61A 教给你最重要的思维转换之一。本篇从最干净的线性递归,一直讲到会"指数爆炸"的树形递归。 一、递归是什么:函数自己调用自己是什么:递归(recursion) = 一个函数在自己的定义里调用自己。一个"正确且能停"的递归必须满足三条,CS61A 叫它"递归三定律": 有一个或多个基例(base case):不再自我调用,直接返回;这是递归的"刹车"。 每次递归都在向基例化简:问题规模严格变小,绝不允许越调越大。 递归调用解决子问题,再把子问题的结果组合成总结果。 缺了第 1 条 → 无限递归,栈炸;缺了第 2 条 → 永远到不了基例,同样栈炸。两条是递归的命门。 二、线...
高阶函数与 lambda
学到这你大概已经能写"命令式"程序了:定义几个函数、调来调去、用 if/for 控制流程。但 CS61A 真正的第一个分水岭在这一篇——当你意识到"函数"可以像数字一样被传来传去、被装进盒子、被工厂批量生产,"编程"这件事的维度就升了一级。这种能"吃函数、拉函数"的函数,叫高阶函数(higher-order function)。它是后面装饰器、回调、甚至整个函数式编程的地基。 一、什么是"一等公民":函数不是语法糖,是值是什么:在 Python 里,函数和数字、字符串一样,是货真价实的对象(object)。这意味着函数可以:被赋值给变量、当参数传给别的函、作为返回值、存进容器。具备这种待遇的,叫一等公民(first-class citizen)。 1234567def square(x): return x * xf = square # 函数赋值给变量(没加括号,不是调用!)print(f(4)) # 16print(type(f)...
函数、调用表达式与环境模型
很多人学编程是从"记语法"开始的:先背 if 怎么写、for 怎么写,再背一堆库函数。CS61A 偏不这么干。它开篇就抛出一个看似哲学的问题:计算机到底是怎么把"名字"和"值"对应起来的?把这个想明白,后面高阶函数、递归、面向对象都不是新东西,只是同一套"名字—值"规则的变体。这一篇,我们先把最底层的"调用"和"环境"拆开看透。 一、调用表达式:你以为的"先算哪个"其实是铁律写 square(3 + 4) 时,大脑下意识就知道要先算 3+4 再平方。但"下意识"在编程里不够——它是一条求值规则,而且顺序不能乱。 是什么:Python 里 操作符(操作数, 操作数, ...) 这种结构叫调用表达式(call expression)。求值分两步,顺序很关键: 先按从左到右的顺序,把每个操作数(operand)求值成"值"; 再把操作符(也就是那个函数对象)作用在已经求好值的操作数上。 12345de...

