一、老板的新需求:三处一模一样的折扣代码
第二天一早,书店老板王姐把老周和小陈叫到前台:"周哥,咱们现在线上也接了个小程序,线下收银机也得算折扣,月底盘账还要算一遍折扣金额。三处都要打折逻辑,你给我弄利索点。"
小陈打开代码,愣住了。折扣逻辑在三个文件里各出现了一遍:
# 文件A:线下收银机
if total > 100:
total = total * 0.9
# 文件B:线上小程序
if total > 100:
total = total * 0.9
# 文件C:月底盘账
if total > 100:
total = total * 0.9
"师傅,这……"小陈弱弱地说,"万一哪天王姐说'改成满 200 打 85 折',我得改三个地方,改漏一个就完了。"
老周笑了:"这就是我们说的代码复制粘贴的麻烦。记住:复制粘贴一时爽,改需求时火葬场。 那怎么办?把这段逻辑抽出来,只写一遍,三处调用——这就是函数。"
二、函数 / 过程 / 子程序
老周写下:
def apply_discount(total, threshold=100, rate=0.9):
"""如果总额超过门槛,就按折扣率打折;返回最终金额。"""
if total > threshold:
return total * rate
return total
# 三个地方都改成调用它
line_price = apply_discount(120.0) # 收银机
mini_price = apply_discount(120.0) # 小程序
report_price = apply_discount(120.0) # 盘账
"这就是函数(function),有的语言叫过程(procedure)或子程序(subroutine),一回事。它把'一段逻辑'打包成一个有名字的积木,随时调用。"
"函数引出了三个新概念,你注意看:"
- 参数(parameter):
total, threshold, rate——调用时由外面传进来的'输入'。apply_discount(120.0)里那个120.0就叫实参。 - 返回值(return value):
return后面的结果——函数算完吐出来的'输出'。 - 作用域(scope):函数里的
total是函数的"私人空间",外面再有一个total,互不干扰。参数、返回值、作用域,是函数最核心的三个衍生品。
小陈试着改完三个调用点,长舒一口气:"现在改需求只改一处了。可师傅,我还有一个问题——"
"问。"
"书店有 5 万本书,每本书有书名、作者、价格、库存。我总不能写 5 万个变量吧? 什么 book1_price、book2_price……"
老周眼里闪光:"问到点子上了。这就是第三章'数据组织'要解决的问题——把相关数据捆起来。但在那之前,我还有个更刺激的东西要给你看。"
三、递归:问题自己长得像自己
老周打开一张图,那是云间书店的图书分类:
图书
├── 文学
│ ├── 小说
│ │ ├── 科幻小说
│ │ └── 历史小说
│ └── 诗歌
├── 教材
│ ├── 小学
│ └── 大学
└── 漫画
"我要统计'图书'这个根分类下总共有多少个分类节点。你写一个函数试试。"
小陈写了半天,写了三层嵌套循环,把自己绕晕了:"这也太恶心了……分类要是十层深,我得写十层循环?"
老周递给他一张纸:"试试这个思路——统计一个分类下的节点数,等于:这个分类自己(1 个)+ 它每个子分类各自的节点数。而'统计子分类的节点数',跟'统计根分类'是同一件事。"
def count_nodes(category):
total = 1 # 自己算一个
for child in category.children: # 遍历子分类
total += count_nodes(child) # 子分类,交给"同一个函数"去数
return total
"看,count_nodes 里面调用了 count_nodes 自己。"老周说,"这叫递归(recursion)。适用场景就一句话:问题本身是自相似的——树的分支长得像树,阶乘可以拆成更小的阶乘,文件夹里套文件夹。递归就是让函数自己解决自己的'缩小版'。"
"注意两个要点:一是要有'基线条件'(base case)——上面代码里,子分类为空时,total=1 直接返回,不再递归;二是每次递归都要往基线条件靠近,否则就是无限套娃,把栈撑爆。"
"递归不是炫技,是当问题的结构本身就是'自己套自己'时,最诚实的写法。循环能干的它大多能干,但有时候递归写出来像把问题照镜子,一眼就懂。"
四、第三层:数据组织
4.1 数组 / 列表:同类型多个值
"现在回到你的问题:5 万本书怎么办?"老周说,"先看最简单的——同类型、多个值。今天卖出的一串价格,还记得吗?"
sales = [29.9, 45.0, 12.5, 88.0, 23.0] # 列表:一串同类型的数
print(sales[0]) # 29.9 用下标(索引)访问,从 0 开始
print(sales[3]) # 88.0
sales.append(66.0) # 追加一个
"数组(array) 或 列表(list),把'同类型的一堆值'排成一排,用下标取。这是数据组织最朴素的一招。"
4.2 结构体 / 记录:不同类型但相关的数据捆一起
"但书有书名、有作者、有价格、有库存——类型都不一样,数组装不了。"老周敲了敲桌子,"于是有了结构体(struct),有的语言叫记录(record),Python 里最简单的是用类或者 namedtuple。你看:"
# Python 中用 namedtuple 做"结构体"
from collections import namedtuple
Book = namedtuple("Book", ["title", "author", "price", "stock"])
b1 = Book("三体", "刘慈欣", 29.9, 120)
b2 = Book("活着", "余华", 25.0, 80)
print(b1.title) # 三体
print(b2.stock) # 80
"结构体的意义:把'不同类型但相关'的数据捆成一个整体。b1 是一个'书',它有四个字段。从此你操作的是一个完整的'书',而不是四个孤零零的变量。"
"这引出了一堆衍生概念:字段(field)、对齐和内存布局(C 语言里 struct 怎么在内存里排列,决定了性能和兼容性)——这些等讲到内存管理再展开。"
4.3 指针 / 引用:不搬数据,只给"门牌号"
"最后一个,也是最容易劝退新人的:指针(pointer)/ 引用(reference)。"老周说,"我先讲一个场景——为什么需要它。"
"假设你有一个'畅销书排行榜',榜上有 100 本书。现在排行第 1 名的书,库存从 120 变成 5,你要更新数据。两个方案:"
- 方案 A(复制):把整本书的数据(书名、作者、价格……全复制一遍)改完再送回去。书小还行,要是书里还有 1MB 的封面图?复制一次就卡一次。
- 方案 B(指针):排行榜里不存书本身,只存书的内存地址——"书在内存第几号房间"。要改库存?按门牌号找过去,直接改原房间里的数据。
// C 语言:指针
struct Book {
char title[50];
int stock;
};
void restock(struct Book *p, int amount) { // p 是"指向 Book 的指针"
p->stock += amount; // 通过 p 找到原来的书,直接改它的库存
}
int main() {
struct Book b = {"三体", 120};
restock(&b, 10); // &b 是"b 的门牌号(地址)"
printf("%d", b.stock); // 130
}
"& 是'取地址',* 是'按地址找过去'。指针就是门牌号——不搬数据,只指路。它带来了巨大的灵活性,也带来了巨大的危险:门牌号写错了,就访问了不该访问的内存。"
"链表、树、图这些数据结构,全建立在指针之上——每个节点不光存数据,还存'下一个节点在哪':"
struct Node {
int data;
struct Node *next; // 指向下一个节点的指针
};
"这就是链表:一串节点手拉手,靠指针串起来。"
五、章末:老周的"第二、三层"总结
老周在白板上补上两笔:
第二层:复用(不想把所有代码写在一起)
纯文本
代码块
├── 函数 / 过程 / 子程序
│ ├── 场景:一段逻辑要在多处用
│ └── 衍生:参数、返回值、作用域
└── 递归
└── 场景:问题本身是自相似的(树、阶乘)
第三层:数据组织(把相关数据捆起来)
纯文本
数据
├── 数组 / 列表
│ └── 场景:同类型多个值
├── 结构体 / 记录(struct)
│ ├── 场景:不同类型但相关的数据放一起
│ └── 衍生:字段、对齐、内存布局
└── 指针 / 引用
├── 场景:直接操作内存、间接访问
└── 衍生:链表、树、图
"小陈,给这层起个主题名。"
小陈想了想:"复用和数据组织——函数解决'逻辑重复',数组/结构体/指针解决'数据散乱'。"
"对。但你有没有发现一个问题?"老周把代码翻出来,"现在 Book 是一堆字段,折扣函数是一堆函数,它们各管各的,谁也不认识谁。可真实世界不是这样的——书'知道'自己会不会打折吗?库存该不该由书自己管?"
"这……好像是该把数据和操作捆在一起?"
"没错,那就是第四章——类。从'结构体'到'类',就差一步,却跨了一个时代。"