同一个记账本写三遍
4 道练习题·预计 60 分钟·做对一题解锁下一段
前言
前面两章,咱们学了迭代和面向对象。学了新东西,先别急着往下赶,回头看看一直在写的记账本。
记账本写到 v1.1,已经能用了,输错了也不怕。可回头看看代码,各位童鞋是不是觉得它越来越长,读起来也越来越费劲?想加个新功能,得在一大段代码里来回找地方,生怕改了这里,又弄坏了那里。
有这种感觉很正常。因为程序能跑,只是第一步;写得好读、好改,才是下一步。
所以这一章不加什么大功能,咱们把同一个记账本,换两种写法再写一遍:先拆成几个函数,做成 v2;再写成一个类,做成 v3。功能一模一样,代码的样子却会大不相同。
这种「功能不变,只改写法」的活儿,有个专门的名字,叫重构。别小看它,写程序的人花在重构上的时间,一点都不比写新功能少。
一开始你可能会觉得:程序能跑不就行了,何必这么折腾?没关系,先跟着写一遍。等写完这一章,再回头看看 v1.1,你大概就不这么想了。
学这一章之前,需要先学完 实战 3 的记账本 v1.1,以及 第 7 章 的列表生成式、第 8 章 的类、__init__ 、实例属性和类属性。
目录
这一章会学到:
- 一、v1.1 越来越难改了——一大段代码挤在一起
- 二、v2:拆成几个函数——每个函数只做一件事
- 三、加个新功能试试——只加一个函数,别的都不用动
- 四、v3:写成一个类——数据和操作放在一起
- 五、三种写法比一比——什么时候用哪一种
- 六、常见坑
- 七、自己查一次文档——给自己的代码写说明书
- 八、记账本 v3
- 九、小结与回看
学完这一章,你能把一段长代码拆成函数、再写成类,也能说清楚三种写法各适合什么时候用。
一、v1.1 越来越难改了
要说清楚为什么要重构,先得看看现在的代码到底哪里不好。咱们回头看看实战 3 的记账本 v1.1。read_cost() 这个函数先放在一边,剩下的主程序是这样的:
entries = []
print('输入「完」结束记账')
while True:
item = input('买了什么:').strip()
if item == '完':
break
category = input('类别:').strip()
cost = read_cost()
entries.append({'item': item, 'category': category, 'cost': cost})
total = 0
totals = {}
for entry in entries:
total = total + entry['cost']
totals[entry['category']] = totals.get(entry['category'], 0) + entry['cost']
for category, money in totals.items():
print(f'{category}:{money:.2f} 元')
if len(entries) == 0:
print('今天还没有记账')
else:
print(f'一共 {len(entries)} 笔,合计 {total:.2f} 元,平均每笔 {total / len(entries):.2f} 元')二十来行代码,从上到下一口气写下来。现在只有二十来行,还算凑合。可要是再加几个功能呢?比如下面这三个:
- 汇总之前,先把每一笔的明细列出来。
- 找出今天最大的一笔开销。
- 同时管两本账:自己的一本,家里的一本。
每加一个功能,都得在这一大段里找地方插进去。而且 total 、totals 、category 这些变量,上面在用,下面也在用。改的时候,心里难免直打鼓:这一处改了,别的地方会不会跟着出问题?
那么,问题到底出在哪儿呢?仔细看看,这一大段其实干了三件事:读输入、记账、汇总。三件事搅在一起,所以既难读,也难改。
既然是三件事,能不能把它们分开,各写各的呢?下一节就来试试。
二、v2:拆成几个函数
要把几件事分开,用什么工具呢?答案是早就学过的函数。
第 6 章学函数的时候,说的是:函数把一段代码包起来、起个名字,就可以反复用。其实函数还有一个同样重要的用处:把一件事和别的事隔开。
那就按上一节说的三件事,拆成三个函数。每个函数做什么、要传进去什么、交回来什么,先在这张表里列清楚:
| 函数 | 做什么 | 要传进去什么 | 交回来什么 |
|---|---|---|---|
read_entries() | 一直问,直到输入「完」 | 什么都不用 | 记好的账,一个列表 |
totals_by_category(entries) | 按类别汇总 | 账的列表 | 类别对应金额的字典 |
print_report(entries) | 打印汇总 | 账的列表 | 什么都不交,只管打印 |
三个函数里,咱们先写汇总。它只管算,不管账是怎么来的:
def totals_by_category(entries):
totals = {}
for entry in entries:
category = entry['category']
totals[category] = totals.get(category, 0) + entry['cost']
return totals
entries = [
{'item': '奶茶', 'category': '餐饮', 'cost': 12.5},
{'item': '地铁', 'category': '交通', 'cost': 4},
{'item': '午饭', 'category': '餐饮', 'cost': 25},
]
print(totals_by_category(entries))输出的结果:
{'餐饮': 37.5, '交通': 4}注意最后一行:这回不用一笔一笔地输入,而是直接拿上面写好的列表来试。为什么能这样试呢?因为函数拆出来以后,每一块都能单独拿出来试。这是拆函数最大的好处之一。
汇总有了,还差一个合计。合计当然也能写个循环,一笔一笔地加,不过还有更省事的办法:Python 自带一个求和的函数 sum() ,配上第 7 章学的列表生成式,一行就能算出来:
entries = [
{'item': '奶茶', 'category': '餐饮', 'cost': 12.5},
{'item': '地铁', 'category': '交通', 'cost': 4},
{'item': '午饭', 'category': '餐饮', 'cost': 25},
]
print([entry['cost'] for entry in entries])
print(sum([entry['cost'] for entry in entries]))输出的结果:
[12.5, 4, 25]
41.5对照两行输出,就能看出这一句分成了两步:
- 列表生成式先把每一笔的金额取出来,组成一个新列表,也就是第一行的
[12.5, 4, 25]。 sum()再把列表里的数加起来,得到第二行的41.5。
原来要写一个 for 循环才能算出的合计,现在一行就够了。
汇总和合计都有了。剩下的读输入和打印汇总,下一节也包成函数,拼出完整的 v2。在那之前,先做一道练习:仿照上面的汇总函数,按类别数一数,每一类各记了几笔。
仿照 totals_by_category() ,写一个函数 count_by_category(entries) :返回一个字典,类别对应这一类记了几笔。函数里只算、不打印,用 return 交出结果。
编辑器下面已经写好了三次调用,运行后的输出应该是:
{'餐饮': 2, '交通': 1}
{'娱乐': 1}
{}推荐一个,全网最全最友好的中文 AI 编程指南: coding.stormzhang.ai