记账本 v1

完整讲解 · 10 段教学·配 4 道练习题·预计 60 分钟

本页是本章的通读版,可直接读完全部讲解。想动手写代码、跑判分,去 闯关模式。

教学 01 / 10

前言

前面三章,我们学了列表、元组、字典、集合,还学了条件判断和循环。

学到这里,各位童鞋是不是有种感觉:脑子里装了一堆零件,每个零件都认识,可真要拼成一个能用的程序,又不知道从哪儿下手?

其实,这时候缺的往往不是语法,而是拆问题的习惯。

什么叫拆问题呢?就是写程序之前,先用大白话把要做的事一条一条写下来,再看看每一条该用学过的哪个零件。拆清楚了,写代码基本上就是照着翻译。

光说不练可不行。这一章,咱们还是拿实战 1 的记账本来练手。它现在只能记一笔,而且程序一结束,记过的账也就忘了。我们要让它一口气记下一整天的账,最后还能告诉你:钱都花到哪儿去了。

代码会比实战 1 长不少,第一遍写不对很正常。别急着看答案,先对照流程,把程序一步一步走一遍,看看是哪一步出了岔子。自己找出来的问题,比直接看答案记得牢多了。

学这一章之前,需要先学完 实战 1 的记账本 v0,以及第 3~5 章的列表、字典、条件判断和循环。哪里卡住了,就回到对应的章节看一看:第 3 章 、第 4 章 、第 5 章 。

目录

这一章会学到:

  • 一、一天要记好几笔,怎么办?——看看记账本 v0 差在哪儿
  • 二、先别写代码,把需求拆开——用大白话写出步骤,再画成流程
  • 三、一笔账怎么存?——两个列表为什么容易对不上,换成「列表里装字典」
  • 四、让程序一直问下去——用 while True 加 break ,输入「完」就结束
  • 五、钱都花哪儿了?——用字典按类别汇总
  • 六、常见坑
  • 七、自己查一次文档——dict.items() 到底返回了什么
  • 八、记账本 v1
  • 九、小结与回看

学完这一章,你的记账本可以一口气记下一整天的账,列出每一笔明细,再按类别算出各花了多少钱。

教学 02 / 10

一、一天要记好几笔,怎么办?

咱们先回顾一下,实战 1 写出来的记账本 v0 是这样的:

Python· 可在网页运行
item = input('买了什么:')
cost = float(input('花了多少钱:'))
print(f'记下了:{item},{cost:.2f} 元')
示例输入(每行一个值)
奶茶
12.5

输出的结果:

文本
买了什么:奶茶
花了多少钱:12.5
记下了:奶茶,12.50 元

问题很明显:它只能记一笔。可一天下来,早饭、地铁、午饭、奶茶……少说也有五六笔。

那么,照着样子多写几遍 input() ,行不行呢?

Python· 语法示意
item1 = input('买了什么:')
cost1 = float(input('花了多少钱:'))
item2 = input('买了什么:')
cost2 = float(input('花了多少钱:'))
item3 = input('买了什么:')
cost3 = float(input('花了多少钱:'))
# ……今天要是记了 8 笔呢?

写是能写,可问题马上就来了:

  • 每天花几笔是不固定的,今天 3 笔,明天可能 8 笔。代码里到底写几个 input() ?
  • 变量 item1 、item2 、item3……越写越多,最后算合计时,还得一个一个加起来。

是不是发现,这两个问题,前面几章其实都给过办法?

  • 数量不固定、要装很多个东西,用列表。
  • 同样的事要做很多遍,用循环。

所以这一章要做的,就是这件事:用列表把账装起来,用循环一笔一笔地问,记完了再汇总。

不过,先别急着写代码。动手之前,还有一件更要紧的事要做,下一节就来说。

教学 03 / 10

二、先别写代码,把需求拆开

上一节说,先别急着写代码。为什么呢?因为很多童鞋一拿到题目,就直接打开编辑器开始敲。敲着敲着就乱了:这个变量放循环里面还是外面?什么时候停下来?合计又在哪儿算?

其实,写代码之前,先用大白话把要做的事写下来,很多问题自己就清楚了。

不信咱们试一试。假设没有电脑,你拿着一个小本子帮朋友记账,会怎么做?

  1. 准备一个空本子。
  2. 问朋友:买了什么?
  3. 朋友说「完了」,就不再问了,直接跳到第 6 步。
  4. 再问:花了多少钱?
  5. 把这一笔写到本子上,回到第 2 步接着问。
  6. 把本子上的每一笔念一遍,再加起来算个总数。

这几步,就是程序要做的事。把它们画成流程,中间那一圈「问了又问」就看得更清楚了:

文本
① 准备一个空账本
② 问:买了什么?
     ├─ 回答「完」:跳到 ⑤
     └─ 其他回答:往下走
③ 问:花了多少钱?
④ 把这一笔记进账本,回到 ②
⑤ 列出每一笔,算出合计

画成流程的时候,清单里的第 3 步并进了第 ② 步,变成了它下面的两个分叉,所以流程比清单少了一步。

流程有了,接下来,给每一步找一个学过的零件,也就是看看每一步该用什么来写:

要做的事用什么
准备一个空账本空列表 entries = []
一遍一遍地问循环
回答「完」就不问了if 判断,再用 break 跳出循环
记一笔账列表的 append()
算合计再用一次循环,把金额一笔一笔加起来

你会发现,表格右边那一栏,全是前面几章学过的东西,没有一样是新的。

不过,还有一个问题表里没回答:「一笔账」本身该怎么存?它有名称、有金额,后面还要加上类别。这几样东西怎么放在一起,就是下一节要解决的问题。

教学 04 / 10

三、一笔账怎么存?

上一节留下了一个问题:一笔账该怎么存?先想想,一笔账至少有两样东西:买了什么、花了多少钱。

最先想到的办法,可能是用两个列表,一个存名称,一个存金额:

Python· 可在网页运行
items = ['奶茶', '地铁', '午饭']
costs = [12.5, 4, 25]
print(items[0], costs[0])
print(items[2], costs[2])

输出的结果:

文本
奶茶 12.5
午饭 25

同一个位置上的名称和金额,正好凑成一笔账。看起来没毛病。

可是,要是删掉其中一笔呢?比如地铁那笔记错了,要删掉:

Python· 可在网页运行
items = ['奶茶', '地铁', '午饭']
costs = [12.5, 4, 25]
del items[1]
print(items[1], costs[1])

输出的结果:

文本
午饭 4

午饭怎么变成 4 块钱了?

因为只删了 items 里的「地铁」,忘了删 costs 里的 4 ,两个列表一下子就对不上了。而且程序没有报错,又是一个「不报错的 bug」。

说到底,两个列表要时刻保持一致,每次增删都得记着同时改两处,一不留神就会出错。那么,能不能把一笔账的名称和金额绑在一起,放进同一个东西里呢?

能。第 4 章学的字典,干的正是这个活儿:

Python· 可在网页运行
entry = {'item': '奶茶', 'cost': 12.5}
print(entry['item'])
print(entry['cost'])

输出的结果:

文本
奶茶
12.5

一个字典就是一笔账,用 'item' 、'cost' 这两个键,就能取出名称和金额。

一笔账是一个字典,那很多笔账呢?就是很多个字典。把它们装进一个列表里,再用 for 一笔一笔地打印出来:

Python· 可在网页运行
entries = [
    {'item': '奶茶', 'cost': 12.5},
    {'item': '地铁', 'cost': 4},
    {'item': '午饭', 'cost': 25},
]
for entry in entries:
    print(f"{entry['item']} {entry['cost']:.2f} 元")

输出的结果:

文本
奶茶 12.50 元
地铁 4.00 元
午饭 25.00 元

这段代码里,有个小地方要留意一下:f-string 外面的引号,换成了双引号。为什么要换呢?因为花括号里取键用的是单引号,外面要是也用单引号,Python 3.12 之前的版本会直接报语法错误。新版本虽然不报错,可读起来也很容易看混。

再回到删除的问题。这回要删掉地铁那笔,del entries[1] 一下就行。名称和金额是一起删掉的,不可能再对不上了。

算合计也不难,同样是用循环,一笔一笔地加:

Python· 可在网页运行
entries = [
    {'item': '奶茶', 'cost': 12.5},
    {'item': '地铁', 'cost': 4},
    {'item': '午饭', 'cost': 25},
]
total = 0
for entry in entries:
    total = total + entry['cost']
print(f'合计:{total:.2f} 元')

输出的结果:

文本
合计:41.50 元

total 一开始是 0,循环每转一圈,就把这一笔的金额加上去。是不是和第 5 章算 1 到 100 的和,是一个路子?

账本的样子定下来了。先做一道练习,用 for 循环打印明细、算出合计。下一节,再让程序自己一笔一笔地问。

练习 1 / 4列表里装字典:打印明细和合计

编辑器里的 entries 存了今天的四笔账,每一笔都是一个字典。请用 for 循环把每一笔打印成一行,金额保留两位小数,同时算出合计,最后打印合计。

运行后的输出应该是:

文本
早饭 8.00 元
地铁 4.00 元
午饭 25.00 元
奶茶 12.50 元
合计:49.50 元
💡 提示(3 条)
  1. 合计 total = 0 要写在循环前面,只准备一次

  2. 循环里用 entry['item'] 和 entry['cost'] 取名称和金额;f-string 外面用双引号,里面取键用单引号

  3. 完整代码:

    Python
    entries = [
        {'item': '早饭', 'cost': 8},
        {'item': '地铁', 'cost': 4},
        {'item': '午饭', 'cost': 25},
        {'item': '奶茶', 'cost': 12.5},
    ]
    total = 0
    for entry in entries:
        print(f"{entry['item']} {entry['cost']:.2f} 元")
        total = total + entry['cost']
    print(f'合计:{total:.2f} 元')
在编辑器里写这题,跑判分、看参考答案去闯关页做这题 →
教学 05 / 10

四、让程序一直问下去

账本的样子有了,接下来就该让程序一笔一笔地问了。

那要问几次呢?事先并不知道,得等用户说「完」才停。

第 5 章讲 for 循环和 while 循环的区别时说过,while 循环主要用在「满足条件就反复执行」的情况,还顺口提了一句「死循环 + break 退出」。现在用的就是这一招:

Python· 可在网页运行
entries = []
print('输入「完」结束记账')
while True:
    item = input('买了什么:')
    if item == '完':
        break
    cost = float(input('花了多少钱:'))
    entries.append({'item': item, 'cost': cost})
print(f'一共记了 {len(entries)} 笔')
示例输入(每行一个值)
奶茶
12.5
午饭
25
完

输出的结果:

文本
输入「完」结束记账
买了什么:奶茶
花了多少钱:12.5
买了什么:午饭
花了多少钱:25
买了什么:完
一共记了 2 笔

这段代码里,有几个地方值得说一说。

先看 while True 。它的条件永远成立,所以这个循环会一直转下去,一遍一遍地问「买了什么」。

那它什么时候停呢?靠的是循环里的 break 。用户一输入「完」,break 就直接跳出整个循环,去执行循环后面的 print() 。

再看看循环里的顺序:先判断是不是「完」,再问金额。这个顺序可不是随便排的,第六节「常见坑」会讲到,反过来会出什么事。

最后是 append() 。每记一笔,就用它把这笔账的字典加到 entries 的末尾。这样等循环结束的时候,entries 里装的就是今天的全部账了。

在网页里运行时,「程序输入」框里要一行填一个值,顺序是名称、金额、名称、金额……最后一行写「完」。试着多加两笔再运行,看看「一共记了几笔」会不会跟着变。

程序能一直问下去了。先做一道练习,用 while True 把金额一笔一笔地记下来。下一节,再来回答记账本最想回答的问题:钱都花哪儿了?

练习 2 / 4一直问,直到输入「完」

用 while True 一直问「花了多少钱:」,输入「完」就停下来。最后打印一共记了几笔、合计多少钱,金额保留两位小数。

「程序输入」框里预填了 12.5、4、25 和「完」,运行后完整的输出应该是:

文本
花了多少钱:12.5
花了多少钱:4
花了多少钱:25
花了多少钱:完
一共记了 3 笔,合计 41.50 元

检查答案时还会换别的输入,包括一上来就输入「完」的情况,这时应该打印 一共记了 0 笔,合计 0.00 元 。

💡 提示(3 条)
  1. 先用 text = input('花了多少钱:') 拿到原样的字符串,判断是不是「完」,确定不是再用 float() 转换

  2. count 和 total 已经在循环前面准备好了,循环里只管往上加:total = total + float(text)、count = count + 1

  3. 完整代码:

    Python
    count = 0
    total = 0
    while True:
        text = input('花了多少钱:')
        if text == '完':
            break
        total = total + float(text)
        count = count + 1
    print(f'一共记了 {count} 笔,合计 {total:.2f} 元')
在编辑器里写这题,跑判分、看参考答案去闯关页做这题 →
教学 06 / 10

五、钱都花哪儿了?

账是记下来了,可光看一串明细,还是不知道钱都花到哪儿去了。怎么办呢?给每一笔加上类别,比如「餐饮」「交通」,再按类别加起来,就一目了然了。

具体要做两件事。第一件,在每笔账的字典里加一个 'category' 键,记下这笔账的类别。第二件,按类别汇总。汇总要的结果是「餐饮花了多少、交通花了多少」,一个类别对应一个金额,这不就是一个字典吗?所以再准备一个空字典 totals ,用来装汇总的结果:

Python· 预期报错示例
entries = [
    {'item': '奶茶', 'category': '餐饮', 'cost': 12.5},
    {'item': '地铁', 'category': '交通', 'cost': 4},
    {'item': '午饭', 'category': '餐饮', 'cost': 25},
]
totals = {}
for entry in entries:
    category = entry['category']
    totals[category] = totals[category] + entry['cost']
print(totals)

想法很直接:把这一类原来的钱取出来,加上这一笔,再存回去。第 4 章「同一个科目再学一次,记录怎么更新」就是这么更新的。

可是一运行,报错信息的最后一行是:

文本
KeyError: '餐饮'

为什么呢?

第一次遇到「餐饮」的时候,totals 还是空的,里面根本没有 '餐饮' 这个键,totals['餐饮'] 自然取不出来。

那怎么办呢?第 4 章那一节也给过办法:get() 。totals.get(category, 0) 的意思是:有这个类别,就取出它的钱;没有,就当作 0。把出错的那一行改一下试试:

Python· 可在网页运行
entries = [
    {'item': '奶茶', 'category': '餐饮', 'cost': 12.5},
    {'item': '地铁', 'category': '交通', 'cost': 4},
    {'item': '午饭', 'category': '餐饮', 'cost': 25},
]
totals = {}
for entry in entries:
    category = entry['category']
    totals[category] = totals.get(category, 0) + entry['cost']
print(totals)

输出的结果:

文本
{'餐饮': 37.5, '交通': 4}

这回结果对了。这个循环一共转了三圈,咱们一圈一圈地看:

  • 第一圈遇到「餐饮」,这时 totals 里还没有它,get() 给出 0,加上 12.5,存进去。
  • 第二圈遇到「交通」,也是第一次见,同样从 0 开始,加上 4。
  • 第三圈又遇到「餐饮」,这回取出的是 12.5,再加上 25,就变成了 37.5。

汇总是算对了,可直接打印整个字典,看着不太像账本。最后再用一个 for 循环,把每个类别一行一行地打印出来:

Python· 可在网页运行
entries = [
    {'item': '奶茶', 'category': '餐饮', 'cost': 12.5},
    {'item': '地铁', 'category': '交通', 'cost': 4},
    {'item': '午饭', 'category': '餐饮', 'cost': 25},
]
totals = {}
for entry in entries:
    category = entry['category']
    totals[category] = totals.get(category, 0) + entry['cost']
for category in totals:
    print(f'{category}:{totals[category]:.2f} 元')

输出的结果:

文本
餐饮:37.50 元
交通:4.00 元

这里有两点要说明一下。

第一,用 for 遍历字典,拿到的是一个一个的键,所以还要用 totals[category] 把金额取出来。

第二,类别的顺序,和它们第一次出现的顺序一样:先记了餐饮,餐饮就排在前面。

按类别汇总也会了。先做一道练习,自己把汇总写一遍。练完以后,下一节把这一章最容易犯的几个错误,集中看一看。

练习 3 / 4按类别汇总

编辑器里已经写好了记账的循环:先问「类别:」,输入「完」就结束,否则再问「花了多少钱:」。请把每一笔的金额加到字典 totals 对应的类别上,循环结束后,按类别第一次出现的顺序,一行一个类别打印出来,金额保留两位小数。

「程序输入」框里预填了三笔,运行后完整的输出应该是:

文本
类别:餐饮
花了多少钱:12.5
类别:交通
花了多少钱:4
类别:餐饮
花了多少钱:25
类别:完
餐饮:37.50 元
交通:4.00 元

检查答案时还会换两组别的输入。

💡 提示(3 条)
  1. 直接写 totals[category] + cost 会在第一次遇到某个类别时报 KeyError,用 totals.get(category, 0) 取原来的钱

  2. 累加:totals[category] = totals.get(category, 0) + cost;打印时可以用 for category, money in totals.items():

  3. 完整代码:

    Python
    totals = {}
    while True:
        category = input('类别:')
        if category == '完':
            break
        cost = float(input('花了多少钱:'))
        totals[category] = totals.get(category, 0) + cost
    for category, money in totals.items():
        print(f'{category}:{money:.2f} 元')
在编辑器里写这题,跑判分、看参考答案去闯关页做这题 →
教学 07 / 10

六、常见坑

记账本要用的零件,前面几节都学过了。可真把它们拼到一起时,有几个错误特别常见,而且有的根本不报错。下面一个一个来看。

1、先转换了,才判断「完」

先看一个顺序上的坑。只记金额的时候,有的童鞋会这样写:

Python· 预期报错示例
total = 0
while True:
    cost = float(input('花了多少钱:'))
    if cost == '完':
        break
    total = total + cost
print(f'合计:{total:.2f} 元')
示例输入(每行一个值)
12.5
完

第一笔 12.5 没问题,一输入「完」,报错信息的最后一行是:

文本
ValueError: could not convert string to float: '完'

为什么呢?因为 float() 写在了 if 前面。「完」还没来得及判断,就先被拿去转成数字了,当然转不了。

所以顺序要反过来:先拿到原样的字符串,判断是不是「完」。确定不是「完」,再把它转成数字:

Python· 可在网页运行
total = 0
while True:
    text = input('花了多少钱:')
    if text == '完':
        break
    total = total + float(text)
print(f'合计:{total:.2f} 元')
示例输入(每行一个值)
12.5
完

输出的结果:

文本
花了多少钱:12.5
花了多少钱:完
合计:12.50 元

2、合计放进了循环里

下面这段代码,和上面改好的那段几乎一模一样,只是挪动了一行。先猜一猜:记了三笔以后,合计会是多少?

Python· 可在网页运行
while True:
    total = 0
    text = input('花了多少钱:')
    if text == '完':
        break
    total = total + float(text)
print(f'合计:{total:.2f} 元')
示例输入(每行一个值)
12.5
4
25
完

输出的结果:

文本
花了多少钱:12.5
花了多少钱:4
花了多少钱:25
花了多少钱:完
合计:0.00 元

明明记了三笔,合计怎么是 0?

因为 total = 0 写在了循环里面,每转一圈都被清零一次。最后一圈输入「完」,刚清完零就跳出来了,所以是 0。

记住一句话:只需要做一次的准备工作,放在循环前面。空账本 entries = [] 、合计 total = 0 都是这样。

3、忘了写 break

再看一段只记名称的代码。运行之前,先想一想:它会在哪儿停下来?

Python· 预期报错示例
entries = []
while True:
    item = input('买了什么:')
    entries.append(item)
print(entries)
示例输入(每行一个值)
奶茶
午饭
完

结果它根本停不下来。程序把三行输入都读完了,接着还在问「买了什么」,最后报错了,报错信息的最后一行是:

文本
EOFError: 程序还想读取输入,但「程序输入」里的内容已经用完了。请在输入框里每行填一个值,再运行一次。

为什么会这样?因为循环里没有 break ,while True 就永远停不下来,连「完」都被当成一笔普通的账记了进去。在网页里,是输入用完了,程序没东西可读,才报了错。要是在自己电脑上运行,它会一直问下去,要按 Ctrl + C 才能强行停下来。

所以写 while True 的时候,先想好出口:什么情况下 break ?

4、所有账都变成了同一笔

最后这个坑最隐蔽。下面的代码只在循环前面建了一个字典 entry ,每一圈往里面填名称和金额,再追加到 entries 里:

Python· 可在网页运行
entries = []
entry = {}
while True:
    item = input('买了什么:')
    if item == '完':
        break
    entry['item'] = item
    entry['cost'] = float(input('花了多少钱:'))
    entries.append(entry)
print(entries)
示例输入(每行一个值)
奶茶
12.5
午饭
25
完

输出的结果:

文本
买了什么:奶茶
花了多少钱:12.5
买了什么:午饭
花了多少钱:25
买了什么:完
[{'item': '午饭', 'cost': 25.0}, {'item': '午饭', 'cost': 25.0}]

奶茶那一笔怎么没了,变成了两笔午饭?

问题出在 entry = {} 上。它写在循环前面,只执行了一次,所以从头到尾只有这一个字典。每一圈都在改这同一个字典,append() 进去的也一直是它。

第 3 章「名单复制以后,怎么还会一起变」讲过:append() 和赋值都不会复制出新的一份,只是多了一个地方指向同一份数据。所以列表里的两项,其实是同一个字典,显示的自然都是最后一次改成的午饭。

改法是每一圈都新建一个字典,就像第四节那样直接写在 append() 里:

Python· 结合前文阅读
entries.append({'item': item, 'cost': cost})

这个坑和第 2 个正好反过来:合计要在循环前面准备,只做一次;而每一笔账的字典,要在循环里面新建,每圈一个。

坑看完了,下一节换一件事:自己查一次文档,把第五节打印类别的那段代码,写得更顺手一点。

教学 08 / 10

七、自己查一次文档

先回头看看,第五节打印每个类别的时候,我们是这么写的:

Python· 结合前文阅读
for category in totals:
    print(f'{category}:{totals[category]:.2f} 元')

这样写能用,就是有点绕:遍历拿到的只是键,还得再用 totals[category] 把钱取一遍。那么,有没有办法一次就把键和值都拿到呢?

第 4 章字典的函数和方法表里,有一行 dict.items() ,也许用得上。它到底返回的是什么呢?这回咱们自己来查。

先问问 Python 自己:

Python· 可在网页运行
help(dict.items)

说明书的主要内容是这样的(不同版本的 Python,显示得略有差别):

文本
items(...) unbound builtins.dict method
    D.items() -> a set-like object providing a view on D's items

说明书就一句,还挺抽象:返回一个「像集合一样的对象」,提供字典里各项的「视图」。

看不太懂?没关系,再去官方文档查一查中文版:dict.items() 。文档里说,它返回由字典项,也就是「(键,值) 对」组成的一个新视图。

还是不够直观?那就用最笨、也最可靠的办法:打印出来看看。

Python· 可在网页运行
totals = {'餐饮': 37.5, '交通': 4}
print(totals.items())

输出的结果:

文本
dict_items([('餐饮', 37.5), ('交通', 4)])

这下清楚了:里面是一个一个的元组,每个元组都是 (键, 值) 。外面包着的 dict_items(...) ,就是文档说的「视图」。它不是真正的列表,但可以像列表一样,用 for 一个一个地取出来。

每次取出来的都是一个元组。那怎么把元组里的键和值分开呢?其实,元组可以一次拆给两个变量:

Python· 可在网页运行
category, money = ('餐饮', 37.5)
print(category)
print(money)

输出的结果:

文本
餐饮
37.5

第 5 章讲 match / case 时见过的「解包」,就是这个意思。

items() 能一对一对地取,元组又能一次拆开。把这两样合起来,遍历的时候就能同时拿到类别和金额:

Python· 可在网页运行
totals = {'餐饮': 37.5, '交通': 4}
for category, money in totals.items():
    print(f'{category}:{money:.2f} 元')

输出的结果:

文本
餐饮:37.50 元
交通:4.00 元

和原来的写法比一比,少取了一次 totals[category] ,读起来也更直白。

回头看看这次查文档,用的还是这三步:

  • 先 help() 一下。
  • 看不懂,就去查官方的中文文档。
  • 还拿不准,就打印出来看看。

这个更顺手的写法,下一节的记账本 v1 马上就会用上。

教学 09 / 10

八、记账本 v1

零件都备齐了,现在把它们拼起来。和前面几节比,这回每一笔多问了一项「类别」。记账本 v1 是这样的:

Python· 可在网页运行
entries = []
print('输入「完」结束记账')
while True:
    item = input('买了什么:')
    if item == '完':
        break
    category = input('类别:')
    cost = float(input('花了多少钱:'))
    entries.append({'item': item, 'category': category, 'cost': cost})

print('----- 明细 -----')
number = 0
total = 0
totals = {}
for entry in entries:
    number = number + 1
    print(f"{number}. {entry['item']}({entry['category']}){entry['cost']:.2f} 元")
    total = total + entry['cost']
    totals[entry['category']] = totals.get(entry['category'], 0) + entry['cost']

print('----- 汇总 -----')
for category, money in totals.items():
    print(f'{category}:{money:.2f} 元')
print(f'一共 {total:.2f} 元')
示例输入(每行一个值)
奶茶
餐饮
12.5
地铁
交通
4
午饭
餐饮
25
完

输出的结果:

文本
输入「完」结束记账
买了什么:奶茶
类别:餐饮
花了多少钱:12.5
买了什么:地铁
类别:交通
花了多少钱:4
买了什么:午饭
类别:餐饮
花了多少钱:25
买了什么:完
----- 明细 -----
1. 奶茶(餐饮)12.50 元
2. 地铁(交通)4.00 元
3. 午饭(餐饮)25.00 元
----- 汇总 -----
餐饮:37.50 元
交通:4.00 元
一共 41.50 元

代码看着长,可别被吓到。它其实就两大块,和第二节画的流程一一对得上:

  • 上半部分负责「记」:while True 一直问,输入「完」就 break ,每一笔都存成字典,追加到 entries 里。
  • 下半部分负责「算」:一个 for 循环同时干三件事。它给每一笔编号并打印出来,把金额累加到合计里,再按类别累加。循环结束以后,用上一节查到的 items() 打印每个类别,最后打印合计。

这里面,编号用的 number 是个新面孔。不过它和合计 total 是一个路子:都在循环前面从 0 开始,每转一圈加一次。

记账本 v1 已经能用了,不过还有不少毛病。比如金额要是输成了「十二块」,程序马上就崩了。再比如,程序一关,记的账也全没了。这些问题,后面的实战章会一个一个解决。

先来做本章最后一道题:给记账本加一个预算提醒。

练习 4 / 4记账本 v1:加上预算提醒

今天的预算是 50 元。编辑器里已经写好了记账的部分,请接着写汇总:

  1. 按类别打印每一类花了多少钱,一行一类。
  2. 最后一行打印合计,再看有没有超出预算:没超出(包括刚好花完)打印「还剩 xx.xx 元」,超出了打印「超支 xx.xx 元」。

金额都保留两位小数。「程序输入」框里预填了三笔,运行后完整的输出应该是:

文本
买了什么:奶茶
类别:餐饮
花了多少钱:12.5
买了什么:地铁
类别:交通
花了多少钱:4
买了什么:午饭
类别:餐饮
花了多少钱:25
买了什么:完
餐饮:37.50 元
交通:4.00 元
一共 41.50 元,还剩 8.50 元

检查答案时还会用一组超支的输入,以及一上来就输入「完」的情况。

💡 提示(3 条)
  1. 先用一个 for 循环遍历 entries,同时累加合计 total 和按类别的 totals(用 totals.get(类别, 0))

  2. 汇总打印完以后,用 if total > budget: 判断:超出就算 total - budget,否则算 budget - total

  3. 完整代码:

    Python
    budget = 50
    entries = []
    while True:
        item = input('买了什么:')
        if item == '完':
            break
        category = input('类别:')
        cost = float(input('花了多少钱:'))
        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 total > budget:
        print(f'一共 {total:.2f} 元,超支 {total - budget:.2f} 元')
    else:
        print(f'一共 {total:.2f} 元,还剩 {budget - total:.2f} 元')
在编辑器里写这题,跑判分、看参考答案去闯关页做这题 →
教学 10 / 10

九、小结与回看

这一章,记账本从只能记一笔,变成了能记下一整天的账。回头梳理一下,学到了这几件事:

  • 写代码之前,先用大白话把步骤写下来,再画成流程。每一步对上一个学过的零件,写代码就是照着翻译。
  • 一笔账有好几样东西,就用字典把它们绑在一起,很多笔账再用列表装起来。要是拆成两个列表分开存,增删的时候很容易对不上。
  • 不知道要问几次,就用 while True 一直问,满足条件时用 break 跳出。读进来的输入,要先判断,再转换。
  • 只做一次的准备工作,比如 entries = [] 、total = 0 ,放在循环前面。而每一笔账的字典,要在循环里面新建。
  • 按类别汇总用字典,totals.get(category, 0) 解决第一次遇到某个类别时还没有这个键的问题。
  • 遍历字典时想同时拿到键和值,用 items() 。

哪里卡住了,就回到对应的章节看一看:

  • 列表的增删和 append() :第 3 章「一、List(列表)」
  • 为什么改一处、另一处跟着变:第 3 章「名单复制以后,怎么还会一起变」
  • 字典的取值、更新和 get() :第 4 章「一、字典(Dictionary)」
  • while 循环和 break :第 5 章「二、循环语句」

回头看看,记账本 v1 已经有二十多行了,比实战 1 长了好几倍。可你会发现,里面没有一行是新语法,全是前面几章学过的东西。这就是拆问题的好处:大问题拆小了,每一小块都是你会的。

当然,它还有毛病。金额要是输成了「十二块」,程序直接就崩了,屏幕上冒出一大段报错。接下来的第 6 章讲函数。学完之后,在实战 3 里,咱们专门来对付报错:看看那一大段报错到底在说什么,又该怎么让程序别那么容易崩。

读完了?动手练一遍才算真会。

去闯关模式练习 →

学习目录