读懂报错与调试

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

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

教学 01 / 10

前言

实战 2 写出了记账本 v1,前面又学了函数,咱们写的代码是越来越长了。

代码一长,碰到报错的机会就多了。不知道各位童鞋是不是这样:程序一报错,屏幕上冒出一大段英文,心里先慌了一半。然后要么把整段复制去搜,要么干脆从头再敲一遍,祈祷这次别出错。

其实,报错信息一点都不可怕。它就像医生开的诊断书,出了什么毛病、毛病在哪一行,都写得清清楚楚,就差直接告诉你怎么改了。可惜很多人从来没有认真读过它。

这一章,咱们不学新语法,专门练一件事:读懂报错,自己把 bug 找出来。

这件事没有捷径,只能多看、多练。一开始读得慢没关系,读多了你会发现,常见的报错翻来覆去就那么几种,看一眼最后一行,心里就有数了。

学这一章之前,需要先学完 实战 2 的记账本 v1,以及 第 6 章 的函数:怎么定义函数、怎么传参数、怎么用 return 返回结果。

目录

这一章会学到:

  • 一、记账本崩了——输错一个金额,程序就停了
  • 二、报错信息从下往上读——错在哪一行,又是怎么走到这一行的
  • 三、最常见的几种报错——看最后一行,就知道是什么毛病
  • 四、输错了,别崩——先检查,再转换
  • 五、不报错的 bug,怎么找?——把变量打印出来看
  • 六、常见坑
  • 七、自己查一次文档——遇到没见过的报错怎么办
  • 八、记账本 v1.1
  • 九、小结与回看

学完这一章,你能读懂报错信息,自己找到 bug 在哪儿。记账本也会升级到 v1.1,金额输错了不会再崩,而是客客气气地请你重新输入。

教学 02 / 10

一、记账本崩了

实战 2 的结尾说过,记账本有个大毛病:金额一输错,程序就崩。先来看看,它崩的时候是什么样子:

Python· 预期报错示例
item = input('买了什么:')
cost = float(input('花了多少钱:'))
print(f'记下了:{item},{cost:.2f} 元')
示例输入(每行一个值)
奶茶
十二块

金额那里,输入的是「十二块」。在网页里运行的话,程序不会打印「记下了」,而是给出这样一段:

文本
买了什么:奶茶
花了多少钱:十二块
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    cost = float(input('花了多少钱:'))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: could not convert string to float: '十二块'

是不是有点头大?一堆英文,又是 File 又是 line 的。

先别慌。这一段其实只说了三件事:

  • 出了什么错:看最后一行。ValueError ,没法把字符串 '十二块' 转成浮点数。
  • 错在哪儿:看 File "main.py", line 2 这一行和它下面贴出来的代码。main.py 是网页给你的代码起的文件名,出错的是第 2 行,也就是 cost = float(input('花了多少钱:')) 。下面那一串 ^ ,指着出错的那一段。
  • 是怎么走到这儿的:Traceback 下面那几行,记录了程序是一层一层怎么调用过来的。这个例子很简单,只有一层。

剩下的,都是细节。不过,要是程序里用了函数,报错信息还会再长一截。下一节,咱们就换一个带函数的例子,一行一行地把它拆开。

教学 03 / 10

二、报错信息从下往上读

动手拆之前,先认识一下它的名字。报错信息有个正式的名字,叫 traceback,直译过来是「回溯」,意思是把程序出错之前走过的路,一步一步倒着列出来。

下面就拿一个带函数的例子来看。把记一笔账的步骤包成函数 add_entry() ,第二次调用的时候,金额写成了中文的「二十五」:

Python· 预期报错示例
def add_entry(entries, item, text):
    cost = float(text)
    entries.append({'item': item, 'cost': cost})


entries = []
add_entry(entries, '奶茶', '12.5')
add_entry(entries, '午饭', '二十五')
print(entries)

把它存成 ledger.py ,在自己电脑上运行,看到的是这样的(文件路径每台电脑都不一样,这里用 ... 代替):

文本
Traceback (most recent call last):
  File ".../ledger.py", line 8, in <module>
    add_entry(entries, '午饭', '二十五')
  File ".../ledger.py", line 2, in add_entry
    cost = float(text)
           ^^^^^^^^^^^
ValueError: could not convert string to float: '二十五'

第一行 Traceback (most recent call last) 的意思是「最近的一次调用在最后」。所以读的时候,要从下往上读:

  1. 最后一行:错误类型和说明。ValueError 是「值不对」,后面说没法把 '二十五' 转成浮点数。
  2. 往上一层:line 2, in add_entry ,错误发生在 add_entry 函数里,第 2 行。下面还贴出了这一行代码 cost = float(text) 。Python 3.11 及以上的版本,还会在出错的地方下面画一串 ^ 。
  3. 再往上一层:line 8, in <module> ,是第 8 行调用了 add_entry() ,才走到那里的。<module> 指的是最外层的代码,不在任何函数里面。

连起来读就是:第 8 行调用 add_entry() → 进到函数里的第 2 行 → float('二十五') 出错了。

这么一读,问题也就清楚了:函数本身没毛病,是第 8 行传进去的金额不对。

那在网页里运行同一段代码,看到的又是什么样子呢?

文本
Traceback (most recent call last):
  File "main.py", line 8, in <module>
    add_entry(entries, '午饭', '二十五')
  File "main.py", line 2, in add_entry
    cost = float(text)
           ^^^^^^^^^^^
ValueError: could not convert string to float: '二十五'

对比一下就会发现,它和自己电脑上的几乎一模一样,只有文件名不同。因为网页里的代码不是存在你电脑上的文件,所以统一叫 main.py ,前面也没有文件夹路径。看到 line 8 ,就去编辑器里数到第 8 行。

不管在哪儿运行,读法都一样:先看最后一行是什么错,再往上找你自己代码里的行号。

这一章里会报错的示例代码,页面上没有放运行按钮。想亲眼看看报错长什么样,可以把代码复制到练习题的编辑器里运行,也可以存成文件,在自己电脑上运行。下面的第一道题,就是一段一运行就报错的代码。

先去做这道题,照着报错把 bug 修好。做完了,下一节再来认一认初学时最常碰到的几种报错。

练习 1 / 4照着报错修 bug

编辑器里的代码有两个 bug,一运行就报错。

先运行,读报错信息:最后一行是什么错?往上找,是你代码里的第几行、在哪个函数里?一次只改一处,改完马上再运行。

两处都改好后,运行的输出应该是:

文本
一共 2 笔,合计 37.50 元
💡 提示(3 条)
  1. 第一次运行,最后一行是 KeyError: 'cots',往上一层是 summarize 函数里的第 4 行。字典里存金额的键叫什么?

  2. 改好第一处再运行,会换成一个新的报错:NameError,后面的 Did you mean 已经告诉你该叫什么了

  3. 完整代码:

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

三、最常见的几种报错

上一节说,读报错要先看最后一行。那最后一行会出现哪些错误类型呢?种类看着很多,可初学阶段常碰到的,翻来覆去就这么几种。认得了它们,看一眼最后一行的错误类型,心里就有个大概了:

错误类型大概意思记账本里的例子
SyntaxError语法错误,代码写得不合规矩漏了冒号、括号没配对、用了中文标点
IndentationError缩进不对for 下面那一行忘了缩进
NameError名字没定义变量名打错了,total 写成了 totle
TypeError类型不对数字和字符串相加
ValueError类型对了,值不对float('十二块')
KeyError字典里没有这个键第一次遇到某个类别,就去取它的钱
IndexError列表的位置超出了范围只有 2 笔账,却去取 entries[2]
ZeroDivisionError除数是 0一笔都没记,就去算平均每笔多少钱

后面几种,前面几章或多或少都碰到过。这里重点说说最特别的一种:SyntaxError 。它特别在哪儿呢?看个例子就知道了。

1、语法错误:一行都不会运行

先猜一猜:下面这段代码运行以后,第一行的 print('开始记账') 能不能打印出来?

Python· 预期报错示例
print('开始记账')
total = 0
for cost in [12.5, 4]
    total = total + cost
print(total)

报错信息是这样的:

文本
  File "main.py", line 3
    for cost in [12.5, 4]
                         ^
SyntaxError: expected ':'

答案是不能。注意看,第一行明明是 print('开始记账') ,却什么都没打印出来。而且连 Traceback 那一行也没有,因为程序还没开始调用任何东西。

为什么呢?因为 Python 运行代码之前,会先把整段代码从头到尾检查一遍语法。只要有一处不合规矩,就一行都不运行,直接报 SyntaxError 。而 NameError 、ValueError 这些,都是运行到那一行才发现的,所以出错之前的代码照样会执行。

好在语法错误的说明一般都很直白。expected ':' 就是「这里应该有个冒号」,下面的 ^ 指着它觉得该加冒号的位置,也就是第 3 行 for 那一行的末尾。

2、中文标点:看着一样,其实不一样

漏了冒号,还算好发现。还有一种语法错误,代码乍一看一点毛病都没有:

Python· 预期报错示例
print('你好')

报错信息是这样的:

文本
  File "main.py", line 1
    print('你好')
         ^
SyntaxError: invalid character '(' (U+FF08)

invalid character 是「无效的字符」。原来,括号用成了中文的全角括号 ( ,Python 不认识它。

用中文输入法敲代码,括号、引号、冒号、逗号都很容易打成全角的。看着差不多,Python 可分得清清楚楚。所以敲代码的时候,记得先把输入法切到英文。

3、报错的行号,有时要往上看

再看一个和括号有关的例子:

Python· 预期报错示例
cost = float(input('花了多少钱:')
print(cost)

报错信息的最后一行是:

文本
SyntaxError: '(' was never closed

意思是「这个左括号一直没关上」。数一数就知道了:float( 和 input( 两个左括号,只配了一个右括号。

这回 Python 指得挺准。不过,这种「少了东西」的错误,有时要读到后面才能发现,报出来的行号会比真正出错的地方靠后。所以,如果报错的那一行怎么看都没毛病,就往上看一两行。

到这里,报错能读懂了,常见的几种也认得了。下一节,咱们回到第一节那个崩掉的记账本,想办法让它不再崩。

教学 05 / 10

四、输错了,别崩

报错会读了,接下来就回到第一节,解决记账本最常见的那个崩溃:金额输错了。

思路很简单:先检查,再转换。确定输入的是数字,才交给 float() 。

那怎么检查一个字符串是不是全是数字呢?字符串有个方法,叫 isdigit() 。拿几个字符串试一试:

Python· 可在网页运行
print('12'.isdigit())
print('十二'.isdigit())
print(''.isdigit())
print('12.5'.isdigit())

输出的结果:

文本
True
False
False
False

前三个都符合预期,连空字符串都不算数字。可 '12.5' 怎么也是 False ?

遇到拿不准的地方,就用实战 1 学过的 help() ,看看它的说明书怎么说:

Python· 可在网页运行
help(str.isdigit)

说明书的主要内容是这样的:

文本
isdigit(self, /) unbound builtins.str method
    Return True if the string is a digit string, False otherwise.

    A string is a digit string if all characters in the string are digits and there
    is at least one character in the string.

关键在第二句:字符串里的所有字符都是数字,而且至少有一个字符,才返回 True 。小数点 . 不是数字,所以 '12.5' 不合格。

可记账总会碰上带小数的金额,怎么办呢?

有个小技巧:先去掉一个小数点,再检查剩下的是不是全是数字。

那怎么去掉一个字符呢?用字符串的 replace() 方法。replace('.', '', 1) 的意思是:把 . 换成空字符串,最多换 1 个。试一试:

Python· 可在网页运行
print('12.5'.replace('.', '', 1))
print('12.5'.replace('.', '', 1).isdigit())
print('1.2.3'.replace('.', '', 1).isdigit())

输出的结果:

文本
125
True
False

三行输出,咱们一行一行地看:

  • 第一行:'12.5' 去掉小数点,变成了 125 。
  • 第二行:125 全是数字,所以检查的结果是 True 。
  • 第三行:'1.2.3' 去掉一个小数点以后,还剩一个,照样不合格,这正是我们想要的。

另外,负数前面的 - 也不是数字,所以负数也会被挡在外面。记花销正好用不上负数,这样反倒合适。

检查的办法有了,再把检查和转换包成一个函数 read_cost() 。它要做的事是:输入不对,就提醒一句,再问一遍,直到输对为止:

Python· 可在网页运行
def read_cost():
    while True:
        text = input('花了多少钱:').strip()
        if text.replace('.', '', 1).isdigit():
            return float(text)
        print('请输入数字,比如 12 或 12.5')


cost = read_cost()
print(f'记下了 {cost:.2f} 元')
示例输入(每行一个值)
十二块
12.5

输出的结果:

文本
花了多少钱:十二块
请输入数字,比如 12 或 12.5
花了多少钱:12.5
记下了 12.50 元

这个函数里,有两个地方要说一下。

一个是 return ,它是第 6 章学的。return 一执行,函数就结束了,while True 也就跟着停了。所以这个循环的出口,就是输对数字的那一刻。

另一个是 .strip() ,它是实战 1 讲过的。先把前后的空格去掉,免得 ' 12' 这样的输入被误判。

其实,Python 还有一种更通用的办法:先大胆地转换,出错了再把错误接住。那要用到 try / except ,第 19 章会专门讲。现在用 if 先检查,对记账本来说已经够用了。

先做一道练习,自己把 read_cost() 写一遍。下一节,咱们再回过头来,对付一种更难缠的 bug:不报错的那种。

练习 2 / 4输错了就再问一遍

补全函数 read_cost() :一直问「花了多少钱:」,先用 .strip() 去掉前后的空格;如果是数字(可以带一个小数点),就返回转换好的浮点数;如果不是,打印 请输入数字,比如 12 或 12.5 ,再问一遍。

「程序输入」框里预填了 12、十二块、12.5,运行后完整的输出应该是:

文本
花了多少钱:12
花了多少钱:十二块
请输入数字,比如 12 或 12.5
花了多少钱:12.5
合计:24.50 元

检查答案时还会用空行、1.2.3 、前后带空格的数字这些输入。

💡 提示(3 条)
  1. 函数里用 while True 一直问;输对了就 return,return 会让函数直接结束,循环也就停了

  2. 检查是不是数字:text.replace('.', '', 1).isdigit(),先去掉一个小数点再检查

  3. 完整代码:

    Python
    def read_cost():
        while True:
            text = input('花了多少钱:').strip()
            if text.replace('.', '', 1).isdigit():
                return float(text)
            print('请输入数字,比如 12 或 12.5')
    
    
    a = read_cost()
    b = read_cost()
    print(f'合计:{a + b:.2f} 元')
在编辑器里写这题,跑判分、看参考答案去闯关页做这题 →
教学 06 / 10

五、不报错的 bug,怎么找?

报错的 bug 其实好找,因为 Python 会告诉你错在哪儿。最麻烦的,是不报错、结果却不对的 bug。实战 1 就说过,这种 bug 可能很久都没人发现。

那这种 bug 该怎么找呢?来看一个例子。记了三笔账,想算算餐饮一共花了多少:

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

输出的结果:

文本
餐饮一共 12.50 元

奶茶 12.5 加午饭 25,应该是 37.50 才对。午饭那笔去哪儿了?

代码看了好几遍,好像都没毛病。这时候,别光盯着代码看了,把变量打印出来。

找这种 bug,可以分四步走:

  1. 说出你期望的结果:餐饮的那两笔,都应该加进 food 。
  2. 在关键的地方打印:在 if 前面,把每一圈的类别打印出来。
  3. 找到第一个和期望不一样的地方。
  4. 改掉它,再运行一次。

下面就照着这四步来。期望的结果,上面已经说清楚了。接下来在循环里加一行,用实战 1 用过的 f"{x=}" ,把每一圈的类别打印出来:

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

输出的结果:

文本
entry['category']='餐饮'
entry['category']='交通'
entry['category']='餐饮 '
餐饮一共 12.50 元

然后,找第一个和期望不一样的地方。看输出的第三行:'餐饮 ' 的引号里,「餐饮」后面多了一个空格。'餐饮 ' 和 '餐饮' 是两个不同的字符串,if 判断不成立,午饭那笔就被漏掉了。

从这里也能看出 f"{x=}" 好用在哪儿。如果只是 print(entry['category']) ,屏幕上显示的就是「餐饮」,后面的空格根本看不出来。打印成 x='...' 的样子,引号就把空格露出来了。

最后一步,改掉它。这种多出来的空格,多半是输入的时候不小心敲进去的。所以最好的改法,是在读输入的时候就把它去掉:

Python· 结合前文阅读
category = input('类别:').strip()

找到 bug、改好以后,记得把调试用的那行 print() 删掉。

先做一道练习,亲手把这个看不见的空格找出来。下一节,再把读报错、找 bug 时最容易犯的几个错误,集中说一说。

练习 3 / 4找出看不见的空格

这段代码用来算餐饮一共花了多少钱。可是有时候算出来的数不对,程序也不报错。

「程序输入」框里预填的三笔中,第二笔的类别是「餐饮 」,后面多敲了一个空格。运行一下,餐饮只算了 12.50 元,应该是 37.50 元才对。

先用 print(f'{category=}') 把类别打印出来,看看问题出在哪儿,再改好它。改好后,完整的输出应该是:

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

检查答案时还会用两组输入:类别前面多了空格,以及结束时输入的「完」后面也多了空格。别忘了把调试用的 print() 删掉。

💡 提示(3 条)
  1. 打印出来是 category='餐饮 ',引号里多了一个空格,所以和 '餐饮' 不相等

  2. 在读输入的时候就去掉前后空格:input('类别:').strip(),这样「完 」也能正常结束

  3. 完整代码:

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

六、常见坑

读报错、找 bug 的办法都学了。可真用起来,下面这几个坑还是很容易踩。

1、只看第一行,不看最后一行

traceback 是从上往下打印的,很多童鞋习惯从第一行开始读。可调用的层数一多,前面全是 File 、line ,读着读着就晕了,最要紧的那一行反倒没看见。

记住顺序:先看最后一行,知道是什么错;再往上找你自己代码里的行号。

2、方法忘了加括号

下面这段代码想判断输入的是不是数字。运行一下,看看输入「十二块」会怎么样:

Python· 可在网页运行
text = input('花了多少钱:')
if text.isdigit:
    print('是数字')
else:
    print('不是数字')
示例输入(每行一个值)
十二块

输出的结果:

文本
花了多少钱:十二块
是数字

「十二块」怎么成了数字?

因为 text.isdigit 后面少了一对括号。没有括号,就不是在调用这个方法,拿到的只是方法本身。if 遇到它,一律当成成立,所以不管输入什么,都打印「是数字」。

而且这个坑不报错,又是一个安安静静的 bug。改成 text.isdigit() 就好了。

3、一次改好几处

一看报错,觉得这里也不对、那里也可疑,一口气改了五六处。结果报错是变了,却不知道是哪一处改出来的,说不定还改出了新问题。

改 bug 最稳的办法是:一次只改一处,改完马上运行。报错消失了,说明改对了;换了一个新的报错,也别灰心,说明前一个已经解决了,接着读新的就行。

4、不读报错,整段复制去搜

遇到报错,很多童鞋的第一反应是整段复制去搜。搜索当然有用,可整段 traceback 里有文件路径、行号,每个人的都不一样,直接搜往往搜不到。

真要搜,就搜最后一行:错误类型加说明,比如 ValueError: could not convert string to float 。把引号里你自己的值去掉,搜到的结果会更多。

不过,比起去网上搜,还有一个更靠得住的地方:官方文档。下一节,咱们就去查一次。

教学 08 / 10

七、自己查一次文档

这回咱们碰上一个新的报错,看看怎么自己去查。先给记账本加个小功能:算算平均每笔花了多少钱。要是一笔账都没记,也就是 entries 是空的,会怎么样呢?

Python· 预期报错示例
entries = []
total = 0
print(f'平均每笔 {total / len(entries):.2f} 元')

报错信息的最后一行是:

文本
ZeroDivisionError: division by zero

ZeroDivisionError ,这个错误前面还没见过。遇到没见过的报错,怎么办?

Python 官方文档里,有一页专门列出了所有内置的错误类型,叫「内置异常」。这回咱们直接去查:内置异常 ZeroDivisionError 。

找到 ZeroDivisionError 那一条,文档是这么说的:当除法或取余运算的第二个参数为零时将被引发。

这句话里的「第二个参数」,指的就是除数。再对照一下代码:total / len(entries) 里,除数是 len(entries) 。一笔账都没记的时候,len(entries) 是 0,除数也就是 0 了。

知道了原因,改法也就有了:先判断有没有记账,再算平均:

Python· 可在网页运行
entries = []
total = 0
if len(entries) == 0:
    print('今天还没有记账')
else:
    print(f'平均每笔 {total / len(entries):.2f} 元')

输出的结果:

文本
今天还没有记账

一个报错查明白了,现在轮到你自己动手:在同一页文档里,再找到 IndexError 和 KeyError ,读读它们的说明。然后想一想,记账本里哪些地方可能会碰到它们?

想好了再往下看。

  • IndexError :用位置去取 entries 里的某一笔时,位置超出了列表的长度,就会碰到它。
  • KeyError :按类别汇总时,第一次遇到某个类别,就直接用 totals[category] 去取,也会碰到它。实战 2 就踩过这个坑。

以后遇到没见过的报错,就这么办:先看最后一行的错误类型,再去「内置异常」这一页查它的意思,最后回到代码里,找出是哪个值让它出了错。

这一章的本事都学全了,下一节把它们用到记账本上。

教学 09 / 10

八、记账本 v1.1

现在,把这一章学到的本事都用到记账本上。在实战 2 的记账本 v1 的基础上,主要有这三处改进:

  • 金额用 read_cost() 来读,输错了会请你重新输入,不会再崩。
  • 名称和类别都用 .strip() 去掉前后的空格。
  • 一笔都没记的时候,不算平均,免得除以 0。
Python· 可在网页运行
def read_cost():
    while True:
        text = input('花了多少钱:').strip()
        if text.replace('.', '', 1).isdigit():
            return float(text)
        print('请输入数字,比如 12 或 12.5')


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} 元')
示例输入(每行一个值)
奶茶
餐饮
十二块
12.5
午饭
餐饮 
25
完

输出的结果:

文本
输入「完」结束记账
买了什么:奶茶
类别:餐饮
花了多少钱:十二块
请输入数字,比如 12 或 12.5
花了多少钱:12.5
买了什么:午饭
类别:餐饮 
花了多少钱:25
买了什么:完
餐饮:37.50 元
一共 2 笔,合计 37.50 元,平均每笔 18.75 元

对照输出看一看,这几处改进都起作用了。「十二块」没让程序崩掉,而是请你重新输入了一次。午饭那笔的类别后面多敲了一个空格,也被 .strip() 去掉了,所以两笔都算进了餐饮。

细心的童鞋可能发现了,和 v1 比,每一笔的明细不见了。这是为了让代码短一点,v1.1 先去掉了明细,只留汇总。

记账本 v1.1 比 v1 稳当多了。可你有没有发现,代码越来越长,读起来有点费劲了?读输入、记账、汇总,全挤在一起。这个问题,等学完第 7、8 章,实战 4 会来解决。

先来做本章最后一道题:给记账本 v1.1 算一算,平均每笔花了多少钱。

练习 4 / 4记账本 v1.1:平均每笔多少钱

编辑器里已经写好了 read_cost() 和记账的循环。请接着写最后的汇总:

  • 一笔都没记的时候,打印 今天还没有记账 ,不要去算平均,免得除以 0。
  • 记了账的时候,打印 一共 3 笔,合计 41.50 元,平均每笔 13.83 元 这样的一行,金额都保留两位小数。

「程序输入」框里预填的输入中,第二笔金额先输错成了「四块」。运行后完整的输出应该是:

文本
买了什么:奶茶
花了多少钱:12.5
买了什么:地铁
花了多少钱:四块
请输入数字,比如 12 或 12.5
花了多少钱:4
买了什么:午饭
花了多少钱:25
买了什么:完
一共 3 笔,合计 41.50 元,平均每笔 13.83 元

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

💡 提示(3 条)
  1. 平均每笔是 total / count;count 是 0 的时候会报 ZeroDivisionError,所以要先判断

  2. 用 if count == 0: 打印「今天还没有记账」,else: 里再算平均

  3. 完整代码:

    Python
    def read_cost():
        while True:
            text = input('花了多少钱:').strip()
            if text.replace('.', '', 1).isdigit():
                return float(text)
            print('请输入数字,比如 12 或 12.5')
    
    
    count = 0
    total = 0
    while True:
        item = input('买了什么:').strip()
        if item == '完':
            break
        total = total + read_cost()
        count = count + 1
    
    if count == 0:
        print('今天还没有记账')
    else:
        print(f'一共 {count} 笔,合计 {total:.2f} 元,平均每笔 {total / count:.2f} 元')
在编辑器里写这题,跑判分、看参考答案去闯关页做这题 →
教学 10 / 10

九、小结与回看

这一章没学新语法,练的是写程序天天都要用的本事:读懂报错,自己把 bug 找出来。回头梳理一下,学到了这几件事:

  • 报错信息从下往上读:最后一行是错误类型和说明,再往上找你自己代码的行号。在网页里,你写的代码叫 main.py 。
  • 函数里出了错,就顺着 traceback 往上看是谁调用了它,问题常常出在传进去的值。
  • SyntaxError 在运行之前就被发现了,一行都不会执行。最常见的原因是中文标点、漏了冒号、括号没配对。
  • 用户的输入不能全信:先检查,再转换。isdigit() 加上 replace('.', '', 1) ,能挡住大部分不是数字的输入。
  • 不报错的 bug,就把变量打印出来,和期望的结果一项一项对。f"{x=}" 能把看不见的空格也露出来。
  • 遇到没见过的报错,去官方文档的「内置异常」那一页查。
  • 一次只改一处,改完马上运行。

哪里卡住了,就回去看一看:

  • 函数的定义、参数和 return :第 6 章
  • strip() 和 f"{x=}" :实战 1 的「五、常见坑」和「四、不报错,结果却是错的」
  • KeyError 和 get() :实战 2 的「五、钱都花哪儿了?」

最后说一句:报错是写程序的家常便饭,不是你学得不好的证据。写了很多年代码的人,也天天在看 traceback,区别只是他们读得快一点、找得准一点。这一章练的,就是这个「准」。

接下来的第 7、8 章讲迭代和面向对象。学完之后,实战 4 咱们把记账本从头再写两遍:先拆成几个函数,做成 v2;再写成一个类,做成 v3。

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

去闯关模式练习 →

学习目录