Python 环境加载中…

同一个记账本写三遍

4 道练习题·预计 60 分钟·做对一题解锁下一段

完整通读本章 →

教学 01 / 10· 已读

前言

前面两章,咱们学了迭代和面向对象。学了新东西,先别急着往下赶,回头看看一直在写的记账本。

记账本写到 v1.1,已经能用了,输错了也不怕。可回头看看代码,各位童鞋是不是觉得它越来越长,读起来也越来越费劲?想加个新功能,得在一大段代码里来回找地方,生怕改了这里,又弄坏了那里。

有这种感觉很正常。因为程序能跑,只是第一步;写得好读、好改,才是下一步。

所以这一章不加什么大功能,咱们把同一个记账本,换两种写法再写一遍:先拆成几个函数,做成 v2;再写成一个类,做成 v3。功能一模一样,代码的样子却会大不相同。

这种「功能不变,只改写法」的活儿,有个专门的名字,叫重构。别小看它,写程序的人花在重构上的时间,一点都不比写新功能少。

一开始你可能会觉得:程序能跑不就行了,何必这么折腾?没关系,先跟着写一遍。等写完这一章,再回头看看 v1.1,你大概就不这么想了。

学这一章之前,需要先学完 实战 3 的记账本 v1.1,以及 第 7 章 的列表生成式、第 8 章 的类、__init__ 、实例属性和类属性。

目录

这一章会学到:

  • 一、v1.1 越来越难改了——一大段代码挤在一起
  • 二、v2:拆成几个函数——每个函数只做一件事
  • 三、加个新功能试试——只加一个函数,别的都不用动
  • 四、v3:写成一个类——数据和操作放在一起
  • 五、三种写法比一比——什么时候用哪一种
  • 六、常见坑
  • 七、自己查一次文档——给自己的代码写说明书
  • 八、记账本 v3
  • 九、小结与回看

学完这一章,你能把一段长代码拆成函数、再写成类,也能说清楚三种写法各适合什么时候用。

教学 02 / 10· 已读

一、v1.1 越来越难改了

要说清楚为什么要重构,先得看看现在的代码到底哪里不好。咱们回头看看实战 3 的记账本 v1.1。read_cost() 这个函数先放在一边,剩下的主程序是这样的:

Python· 结合前文阅读
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 这些变量,上面在用,下面也在用。改的时候,心里难免直打鼓:这一处改了,别的地方会不会跟着出问题?

那么,问题到底出在哪儿呢?仔细看看,这一大段其实干了三件事:读输入、记账、汇总。三件事搅在一起,所以既难读,也难改。

既然是三件事,能不能把它们分开,各写各的呢?下一节就来试试。

教学 03 / 10

二、v2:拆成几个函数

要把几件事分开,用什么工具呢?答案是早就学过的函数。

第 6 章学函数的时候,说的是:函数把一段代码包起来、起个名字,就可以反复用。其实函数还有一个同样重要的用处:把一件事和别的事隔开。

那就按上一节说的三件事,拆成三个函数。每个函数做什么、要传进去什么、交回来什么,先在这张表里列清楚:

函数做什么要传进去什么交回来什么
read_entries()一直问,直到输入「完」什么都不用记好的账,一个列表
totals_by_category(entries)按类别汇总账的列表类别对应金额的字典
print_report(entries)打印汇总账的列表什么都不交,只管打印

三个函数里,咱们先写汇总。它只管算,不管账是怎么来的:

Python· 可在网页运行
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 章学的列表生成式,一行就能算出来:

Python· 可在网页运行
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。在那之前,先做一道练习:仿照上面的汇总函数,按类别数一数,每一类各记了几笔。

练习 1 / 4按类别数一数笔数题目有问题?

仿照 totals_by_category() ,写一个函数 count_by_category(entries) :返回一个字典,类别对应这一类记了几笔。函数里只算、不打印,用 return 交出结果。

编辑器下面已经写好了三次调用,运行后的输出应该是:

文本
{'餐饮': 2, '交通': 1}
{'娱乐': 1}
{}
main.py
可编辑
做对当前题解锁下一段 ·04

推荐一个,全网最全最友好的中文 AI 编程指南: coding.stormzhang.ai

学习目录