计算思维与编程世界观 / Computational Thinking and the Programming Mindset
第 1 章 计算思维与编程世界观 / Computational Thinking and the Programming Mindset
1.1 重新提出那个被回答过太多次的问题
"我的孩子为什么要学编程?"——这个问题在每一场家长会、每一次书店巡礼、每一段短视频里都被反复咀嚼。流行的回答往往是:编程是未来、编程高薪、编程培养逻辑。这些答案没有错,但它们回避了一个更值得追究的母题:孩子真正需要从编程中获得的能力,究竟是写代码本身,还是写代码背后的那套思维?
把视线从就业市场移开,回到认知科学的基础结论上。2006 年,Jeannette Wing 在《Communications of the ACM》发表的那篇被引用无数次的文章《Computational Thinking》提出了一个石破天惊的判断:计算思维(Computational Thinking)是每个人的基本技能,而不仅仅是计算机科学家的基本技能。她把计算思维定义为"将问题及其解决方案以某种形式表达出来,从而能够由信息处理代理(agent)有效执行"的过程。这里的"信息处理代理"既可以是人,也可以是机器。换句话说,计算思维的本质,是把一个模糊的现实问题,翻译成可以被执行的步骤。
这个定义与"写代码"的关系,就好比数学思维与"做数学题"的关系。学数学不是为了在生活中反复解方程,而是为了训练一种抽象、推理与建模的能力。计算思维也一样——它不是关于 Python 还是 JavaScript 的语法之争,而是关于如何把混沌世界拆成可以逐步解决的形式。
对于中级学习者而言,认清这一点尤其关键。这一阶段的读者往往已经掌握了若干语法、写过几个项目,开始感受到"会写代码"与"会解决问题"之间的鸿沟。这一章的目的,就是在这道鸿沟上架起一座桥,让"学编程"重新回到"学思维"的轨道上。
1.2 计算思维的四大支柱
如果我们同意计算思维是一项可被结构化训练的能力,那么下一步就是追问:它由哪些可训练的子能力构成?Wing 之后的十几年里,无论是 CSTA(Computer Science Teachers Association)的 K-12 标准,还是国际信息学奥林匹克与中国信息技术课程标准,都逐渐收敛到同一组核心支柱——分解(Decomposition)、模式识别(Pattern Recognition)、抽象(Abstraction)、算法设计(Algorithm Design)。这四个支柱不是孤立的技巧,而是一个层层递进的认知流水线:从混沌中拆解,到相似中归纳,到差异中抽取,再到确定性中求解。
```mermaid flowchart TD A[Computational Thinking] --> B[Decomposition] A --> C[Pattern Recognition] A --> D[Abstraction] A --> E[Algorithm Design] B --> B1[Break problem into sub-problems] C --> C1[Identify similarities and trends] D --> D1[Extract essential features, ignore details] E --> E1[Design step-by-step solution] B1 --> F[Programming Practice] C1 --> F D1 --> F E1 --> F F --> F1[Scratch: visual composition] F --> F2[Python: textual expression] ```
图 1-1 计算思维四大支柱与编程实践的映射关系 / Mapping of four pillars to programming practice
这张图揭示了一个常被忽略的事实:四大支柱并非各行其是,而是汇聚到一个共同的落点——可被执行的解决方案。而这个落点在学习者的成长路径上,会有不同的物质载体:Scratch 是积木式的可视化拼装,Python 是文本化的精确表达。本书将沿着这两条平行的轨道,逐步铺开对四大支柱的训练。
1.2.1 分解(Decomposition):把大问题切成可咀嚼的小问题
分解是所有计算的起点。一个看起来无从下手的问题,往往只是因为它的粒度太大。试想一个简单的场景:整理书包。如果让一个从未被训练过分解能力的孩子面对"整理书包"这个指令,他会卡住——从哪里开始?装什么?按什么顺序?而一个经过分解训练的孩子,会迅速把这道题切成:列出今天要用的物品 → 决定物品的优先级 → 在书包里划分隔层 → 逐格摆放 → 检查遗漏。这五个子问题,每一个都是几分钟内可以解决的小任务。
把这一思维平移到编程中,就得到了一种叫自顶向下设计(Top-Down Design)的方法学。它的核心思想是:先写一个主函数(或主积木),它本身不解决具体问题,而是把任务委派给若干子函数或子模块;每个子模块再继续委派,直到每个子任务都简单到可以直接用几行代码完成。这种方法在 Scratch 中体现为"广播 + 角色协作",在 Python 中体现为函数嵌套与模块导入。
让我们用一个具体的 Python 例子感受这种分解的力度。假设你要写一个程序,判断用户输入的年份是否为闰年(leap year)。初学者的第一反应可能是直接写一堆 `if`,但分解思维要求你先问:这个判断由哪些子判断组成?
```python def is_divisible(year, divisor): """判断 year 是否能被 divisor 整除 / Check divisibility.""" return year % divisor == 0
def is_leap_year(year): """判断闰年 / Determine leap year using decomposition."""
子任务 1:能被 4 整除吗?
Sub-task 1: divisible by 4?
div_by_4 = is_divisible(year, 4)
子任务 2:能被 100 整除吗?
Sub-task 2: divisible by 100?
div_by_100 = is_divisible(year, 100)
子任务 3:能被 400 整除吗?
Sub-task 3: divisible by 400?
div_by_400 = is_divisible(year, 400)
合并子结论:能被 4 整除但不能被 100 整除,或者能被 400 整除
return (div_by_4 and not div_by_100) or div_by_400 ```
注意,这段代码的可读性几乎与自然语言无异。这就是分解带来的红利:每个函数只承担一个最小决策点,主函数负责把这些决策点组合起来。一旦任务发生变化——比如规则从"格里高利历"换成"儒略历"——你只需要修改 `is_divisible` 的传入参数或组合逻辑,而不必重写整个程序。
Scratch 中实现同样的分解,靠的是自定义积木(My Blocks)与广播机制。你可以在"自制积木"区创建一个名为 "判断闰年" 的积木,它接受一个输入"年份",返回"是"或"否"。其他积木只需要调用它即可。这种"积木即函数"的隐喻,对初学者极其友好——他们能在不接触语法的情况下,体会到"调用—返回"的力量。
1.2.2 模式识别(Pattern Recognition):在重复中看见结构
如果说分解是"切",那么模式识别就是"合"——它要求我们从若干相似的案例中,归纳出共同的规律。这一能力在数学课上以"找规律"的形式出现,在编程课中则体现为对循环结构(Loop)、递归结构(Recursion)、代码复用(Reuse)的把握。
举一个最朴素的例子:让程序打印 1 到 100 的所有整数。完全不会模式识别的写法,是写一百次 `print`。稍有意识的写法,会发现"每次打印的数字比上一次多 1"这个规律,于是用循环解决:
```python
反例:手动重复 / Anti-pattern: manual repetition
print(1) print(2) print(3)
... 重复到 100
正例:用循环捕捉规律 / Use loop to capture the pattern
for i in range(1, 101): print(i) ```
这不仅仅是从"长代码"到"短代码"的进步,更是从"动作描述"到"规律描述"的飞跃。前者告诉计算机"做什么",后者告诉计算机"做几次以及按什么规则做"。这是编程思维从"指令式"进入"声明式"的关键拐点。
但模式识别的训练不止于循环。我们再看一个稍复杂的例子:识别用户输入密码是否符合"至少 8 位、含大小写、含数字"的规则。看似这是一道规则判断题,但通过模式识别,我们能把它分解为三个独立的"子检查"——每一种字符类型的出现,都是一种模式。我们甚至可以把这种模式封装成可复用的检查器:
```python def check_length(password): return len(password) >= 8
def check_uppercase(password): return any(c.isupper() for c in password)
def check_digit(password): return any(c.isdigit() for c in password)
def is_strong_password(password): """组合模式检查 / Combine pattern checks.""" return all([ check_length(password), check_uppercase(password), check_digit(password), ]) ```
这里,每一个 `check_*` 函数都是一个可被复用的模式探测器。一旦你学会了这种"先识别模式,再封装为函数"的节奏,你面对任何校验类需求(手机号、邮箱、身份证)都能举一反三。在 Scratch 中,等价的做法是创建若干个布尔型自定义积木,并在主积木中用"且"积木把它们组合起来。
1.2.3 抽象(Abstraction):抽离细节,抓住本质
抽象是四大支柱中最容易"被误解"的一环。许多人把它等同于"忽略细节",但这其实只是抽象的副作用,而非本质。抽象的本质,是在多个具体的事物中,提炼出它们共享的"形",并把这个"形"作为一个可被独立操作的实体。
我们用一个生活化例子来体会。设想你是一个小学老师,要带全班同学去春游。你脑子里出现的第一个抽象概念,不是"小明、小红、小李……"而是"学生"这个集合;不是"学校门口、十字路口、公园门口"而是"地点"这个坐标;不是"走路、坐车、换乘"而是"交通方式"。这些名词——学生、地点、交通方式——都是从具体事物中抽离出来的"抽象"。它们让你能在不关心"哪个学生""哪条路"的情况下,先把整个流程设计出来。
在编程中,抽象的最强武器是函数(Function)与类(Class)。函数把一段重复出现的逻辑"打包",让你能像使用动词一样使用它;类把一组相关的状态与行为"打包",让你能像使用名词一样使用它。两者都是把"具体实现"藏起来,只暴露"做什么"的接口。
例如在 Python 中,列表(list)就是一种抽象:你不需要关心列表底层是用数组还是链表实现的,你只需要知道"可以按下标取元素、可以追加、可以切片"。这种接口与实现的分离,正是抽象能让你应对复杂问题的原因。
```python
不抽象:每种图形都画一遍
Without abstraction: draw each shape from scratch
def draw_square(size): for _ in range(4): t.forward(size) t.right(90)
def draw_triangle(size): for _ in range(3): t.forward(size) t.right(120)
抽象后:抽出"正多边形"这一共同概念
With abstraction: extract "regular polygon" concept
def draw_regular_polygon(sides, size): angle = 360 / sides for _ in range(sides): t.forward(size) t.right(angle)
复用抽象
Reuse the abstraction
draw_regular_polygon(4, 100) # 正方形 / Square draw_regular_polygon(3, 100) # 三角形 / Triangle draw_regular_polygon(6, 100) # 六边形 / Hexagon ```
抽象的力量还体现在命名上。一个好的函数名(如 `is_strong_password`)本身就是一种抽象——它让你在阅读主流程时不必跳进函数体,节省了宝贵的认知带宽。Scratch 中的"自制积木命名"训练,在本质上是同一种能力的视觉化版本。
1.2.4 算法设计(Algorithm Design):让解决方案可被信赖
四大支柱的最后一环,是把前面的分解、模式、抽象整合起来,形成一个可在有限步骤内解决特定问题的明确流程——这就是算法(Algorithm)。算法不是"程序"的同义词,程序可以是任意代码,而算法必须满足三个性质:有穷性(Finiteness)、确定性(Definiteness)、可执行性(Effectiveness)。
算法的训练,在 Scratch 阶段往往以"路径规划""迷宫求解""游戏通关策略"的形式出现;在 Python 阶段,则进入更系统的领域——排序、搜索、动态规划、图算法。但无论载体如何变化,算法设计的核心训练目标是一致的:在多种可能的方案中,选出时间与空间代价最优的那一个。
来看一个典型的对比:在有序数组中查找特定元素。直觉算法是"从头到尾遍历一遍",这叫线性查找(Linear Search),时间复杂度为 $O(n)$。但如果我们知道数组是有序的,就可以采用二分查找(Binary Search),每次把搜索区间减半,时间复杂度为 $O(\log n)$。
```python def linear_search(arr, target): """线性查找 / Linear Search - O(n)""" for i, value in enumerate(arr): if value == target: return i return -1
def binary_search(arr, target): """二分查找 / Binary Search - O(log n) 要求数组已排序 / Requires sorted array. """ left, right = 0, len(arr) - 1 while left <= right: mid = (left + right) // 2 if arr[mid] == target: return mid elif arr[mid] < target: left = mid + 1 else: right = mid - 1 return -1 ```
当 $n = 1{,}000{,}000$ 时,线性查找在最坏情况下需要 1,000,000 次比较,而二分查找最多只需约 20 次——这是同一道题、同一台机器、同一门语言下的两种算法,效率差距却高达 5 万倍。这就是算法设计思维的价值:它让你看到代码背后的"代价",并据此做出更好的决策。
在 Scratch 中,算法设计的训练多以"项目式任务"出现。例如设计一个自动走迷宫的程序:你需要规划"遇到墙就左转""走到死路就回溯"这样的策略,并把它翻译成积木序列。Scratch 的优势在于可视化——你可以让小猫在迷宫里"演一遍"给你看,从而直观地检验算法是否真的有效。
1.3 Scratch 与 Python:双轨教学法的差异化价值
许多家长和学习者都会问:先学 Scratch 还是直接学 Python?这一问题背后隐含的是对"教学路径"的关切。本书的立场是双轨并进:在不同的学习阶段,用不同的语言承载相应的思维训练。这不是折中,而是一种基于认知科学的设计选择。
1.3.1 Scratch:思维训练的低摩擦场
Scratch 由 MIT 媒体实验室于 2007 年发布,它把代码变成一块块可以拖拽的彩色积木。写程序从"打字"变成了"拼图",语法错误(Syntax Error)这种最常见的初学挫折被彻底消除。学习者可以把全部认知资源投入到"想清楚逻辑"上,而不是"写对括号"上。
更关键的是,Scratch 的设计哲学里藏着两条思维训练的暗线:
1. 即时反馈(Immediate Feedback):点击绿旗即可运行,所见即所得。 2. 事件驱动(Event-Driven):通过"当绿旗被点击""当接收到消息"等积木,让学习者天然理解"触发—响应"这一计算模型。
对于 8 至 12 岁的学习者,或者编程零基础的成年人,Scratch 是把"思维训练"门槛降到最低的载体。MIT 在 2023 年的一项追踪研究显示,使用 Scratch 起步的学习者,在后续转向 Python 时,其抽象能力与调试能力显著优于直接学习文本语言的学习者。
但 Scratch 也有