通过游戏编程学Python(番外篇)— 单词小测验

通过游戏编程学Python

通过游戏编程学Python(6)— 英汉词典、背单词
通过游戏编程学Python(番外篇)— 乱序成语、猜单词
通过游戏编程学Python(5)— 猜成语(下)
通过游戏编程学Python(4)— 猜成语(上)



前言

基本上到今天,我们已经学完了Python比较常用的基础语法,像一些其他比较少用的技巧和概念,比如集合、正则表达式、lambda表达式等等,问哥会在用到的时候再向大家介绍。咱们这个系列是通过游戏编程学Python,自然先学用得到的,这样大家也不用有太多压力。如果大家觉得着急要给游戏添加什么功能,可以给问哥留言。

上节课我们已经介绍了字典的用法,以及实际创建了一个电子版的英汉互译小字典。另外我们还开发了背单词的模块,虽然样子还比较简陋,但运行下来效果还不错。今天,我们就在此基础上,再扩展一项功能:单词小测验。

有了测验,我们自然要记录下我们答错的词——表示我们还不太熟嘛,然后创建一个生词本,在测试结束的时候保存起来。


一、知识点

  1. try…except避免程序错误
  2. 列表List在函数里的调用
  3. 集合类型的容器
  4. ASCII码
  5. txt写入操作

二、背单词的扩展——单词小测验

1. 玩法简介

测验“游戏”会从字典里随机抽取出5个单词,然后为每个单词,除了正确释义外,再配上另外4个随机的错误释义,构成5道单选题。全部答完后,显示正确率。然后我们创建一个生词本,把答错的单词放进去。这样下次开始玩的时候,如果有生词本就可以选择从生词本里提取单词来做选择题。

程序截图如下:
在这里插入图片描述

2. 游戏流程

3
yes
yes
no
no
yes
no
no
yes
yes
no
游戏开始
选择1-查字典,2-背单词,3-小测试,其它键-结束
是否测试生词表的单词?
生词表是否存在?
创建题目
用户是否回答正确?
创建生词表
是否继续?
是否保存生词表?
保存生词表到本地txt文件

3. 程序代码

由于查词典和背单词的函数功能在上篇文章已分享,所以这里略去,重点介绍新增加的单词小测验部分的函数和代码。需要完整代码的同学可以查阅上一篇文章,把代码合并在一起即可。

本次代码使用的英汉词典txt文件下载自CSDN,因为是收费下载,所以不便分享。大家可以自行搜索,或下载合适自己的词典txt文件用于练习。

新增部分代码:

import random
import time
# 从本地英汉词典TXT文件中读取英文词库
with open(r'C:\Coding\dictionary\EnWords.txt', 'r', encoding = 'utf-8') as f:
    p = f.readlines()
# 创建字典常量
DICT = {}
for i in p:
    i = i.replace('"','').split(',')
    DICT[i[0].strip().lower()] = ','.join(i[1:]).strip()
# 省略部分代码
def exam(n) -> list:
    # 进一步判断是否从生词本读取单词
    unknow_words = []
    if input('读取生词本吗?(y-是 | n-否):').lower().startswith('y'):
        try:
            with open(r'C:\Coding\dictionary\UnknowWords.txt', 'r', encoding = 'utf-8') as f:
                word_list = f.readlines()
            if word_list == []:
                print('生词本为空!将从字典中抽取测试!')
                word_list = list(DICT.keys())
            else:
                word_list = [i.strip() for i in word_list]
        except:
            print('您还没有创建生词本!')
            return unknow_words
    else:
        word_list = list(DICT.keys())
    time.sleep(2)
    #背单词开始
    while True:
        # 循环n次答题为一轮
        if len(word_list) <= n:
            n = len(word_list)
        if n == 0:
            print('所有生词已检查完!')
            return unknow_words
        unknow_words += print_exam(n, word_list) # 调取正式测试的子程序,并记录生词
        if not input('继续下一轮吗?(y-是 | n-否):').lower().startswith('y'):
            return unknow_words

def print_exam(n, word_list)->list:
    # 将题目打印在控制台
    unknow_words = []
    for i in range(n):
        # 先把词库打散,再用pop()方法取出末尾的单词,这样下次不会再选出同一个词
        random.shuffle(word_list)
        guess_word = word_list.pop()
        guess_cn = DICT[guess_word]
        # 从字典里抽出另外4个混淆选项的单词释义,并把正确释义放在一起打散
        guess_choice = random.sample(list(DICT.values()), 4)
        while guess_cn in guess_choice: # 检查正确答案是否也在随机抽取的4个混淆选项里
            guess_choice = random.sample(list(DICT.values()), 4)
        guess_choice.append(guess_cn)
        random.shuffle(guess_choice)
        print('-' * 40)
        print(guess_word)
        print()
        for i in range(len(guess_choice)):
            # 显示A,B,C,D,E的字母选项
            print(chr(i+65) + '. ' + guess_choice[i].strip())
            # 得到正确答案的选项字母
            if guess_choice[i] == guess_cn:
                item = chr(i+65)
        print()
        player_guess = input('请选择正确的选项:').upper()
        if player_guess == item:
            print('答对了,太棒了!')
        else:
            print('很遗憾,答错了。正确答案是' + item)
            unknow_words.append(guess_word)
        time.sleep(2)
    print('-' * 40)
    print(f'您一共答对了{n-len(unknow_words)}个单词,正确率{round(100*(n-len(unknow_words))/n, 2)}%')
    print('-' * 40)
    return unknow_words

def save_words(unknow_words):
    # 保存测试错误的单词到生词本TXT文件
    words = '\n'.join(unknow_words)
    with open(r'C:\Coding\dictionary\UnknowWords.txt', 'w', encoding = 'utf-8') as f:
        f.write(words)
    print(f'保存了{len(unknow_words)}个生词')
    time.sleep(1)

# 正式程序从这里开始
while True:
    choose = hello()
    if choose == '1':
        while True:
            word = input('请输入英语或汉字:').lower()
            show_result(look_up(word))
            # 询问用户是否继续查单词,否则跳回上级菜单
            if not input('继续查吗?(y-继续 | n-退出):').lower().startswith('y'):
                break
    elif choose == '2':
        while True:
            recite_word()
            if not input('继续背单词吗?(y-继续 | n-退出):').lower().startswith('y'):
                break
    elif choose == '3':
        unknow_words = []
        while True:
            unknow_words.extend(exam(5))
            if not input('继续测验吗?(y-继续 | n-退出):').lower().startswith('y'):
                if len(unknow_words) > 0 and input('保存错词表吗?(y-是 | n-否):').lower().startswith('y'):
                    save_words(set(unknow_words))
                break
    else:
        print('欢迎使用,再见!')
        break

4. 代码简析

由于我们在上一章已经搭建好了整个字典的框架,所以对于新增的功能,我们不需要再从头写程序,只要像搭积木一样把新功能加进去即可——创建一个新的菜单选项,即可进入单词小测验。

当用户选择3并回车以后,程序进入分支选项,调用单词测验的自定义函数 exam(),并告诉程序我们每轮检查最多5个单词。由于我们需要在测验结束保存错误答案(生词),所以我们需要创建一个新的列表,用来保存我们所有测验中答错的单词。当测验结束后,询问玩家是否继续测验,如果玩家回答n(只要不是y),程序会检查是否有答错的单词,并询问玩家是否保存生词本。得到肯定答复后,程序会调用另一个自定义函数save_words(),在指定目录创建一个名为UnknowWords.txt的文本文件。文件保存结束后返回上级菜单。

exam() 子程序(自定义函数)首先会检查玩家是否想测验生词本里的单词,如果生词本文件不存在,或者文件里没有单词(内容为空),则自动从字典中抽取单词进行测验。然后再调用另一个自定义函数print_exam() 进行测验题的打印和玩家的互动操作。

连续测验的单词不会重复,如果生词表里的单词测试完,将自动返回上级菜单。

相比上一章的代码,这个新功能增加了三个子程序(自定义函数),用来实现1)读取测验单词库,2)进行测验,3)保存生词 的操作。其中子程序1又调用了子程序2。大家注意多层自定义函数传参的操作。

三、知识点

1. try…except

在实现我们想要添加的新功能之前,我们往往要考虑到用户可能会对我们的程序进行一些意想不到的操作。比如有时候认为要用户输入1很简单,但往往用户的答案五花八门,one、first、中文字一,第一、第一个等等等等,如果我们无法对所有的操作都写好判断(如果else也没有的话),程序往往就会报错、奔溃中断。所以如果有一个试错的工具,把那些可能会引起程序奔溃的操作直接跳转到另外的步骤,无疑增加的程序的可靠性。

任何一款高级语言都提供类似的错误处理操作方法,Python也不例外。那就是 try…(试着执行某段代码)except…(如果报错就转到这里)。

回到我们本章的例子。

        try:
            with open(r'C:\Coding\dictionary\UnknowWords.txt', 'r', encoding = 'utf-8') as f:
                word_list = f.readlines()
            if word_list == []:
                print('生词本为空!将从字典中抽取测试!')
                word_list = list(DICT.keys())
            else:
                word_list = [i.strip() for i in word_list]
        except:
            print('您还没有创建生词本!')
            return unknow_words

我们打算在开始测试之前由玩家决定是否从生词本里抽取单词,但是我们又不想单独写个子程序去判断生词本文件UnknowWords.txt存不存在(如果玩家首次开始测试,那生词本是必然不存在的)。那么我们干脆一律去读取生词本文件。但是如果文件真的不存在,程序会报错,并自动终止。

FileNotFoundError: [Errno 2] No such file or directory

这时我们就可以使用try…except…结构,把对文件的读取操作放在try语句的代码块里,一旦发生错误,比如找不到文件,就跳转到except语句,执行该代码块里的操作。
在这里插入图片描述
当然try…except…的语句用途很多,比如可以写多条except语句,并在except语句后面跟上出现的错误类型,比如FileNotFound类型的错误等,进而实现更加精确的判断,从而提示用户出现了什么类型的错误。而在我们的小程序里,目前还用不到这样的细节,所以这里省略不写,而不管在文件读取过程中发生什么错误,都一概执行except里面的语句。

2. 列表的调用及常用操作

列表的传参

这一节其实是个蛮重要的特点,但也可以说是一个蛮好用的功能。我们在全局变量与局部变量的章节曾经说过,主程序里定义的变量和子程序(自定义函数)里定义的变量(包括形参)是不一样的,即使同名同姓,往往内容也互不相同。但这句话后面可能要打个括号,列表除外。

仔细看本章的例子里,子程序exam() 调用print_exam() 的时候传进去一个列表变量word_list,这个列表保存了我们想要测验的词库(可能源自生词本,也可能源自整个字典)。

def exam(n) -> list:
    # 代码略
    while True:
        if len(word_list) <= n:
            n = len(word_list)
        if n == 0:
            print('所有生词已检查完!')
            return unknow_words
        unknow_words += print_exam(n, word_list) # 调取正式测试的子程序,并记录生词

def print_exam(n, word_list)->list:
    # 代码略
    for i in range(n):
        # 先把词库打散,再用pop()方法取出末尾的单词,这样下次不会再选出同一个词
        random.shuffle(word_list)
        guess_word = word_list.pop()

随后我们在子程序print_exam() 里把传进来的列表赋值给同名局部变量、形参word_list,然后对其进行打乱、删除(pop())等操作。这个变量在操作完成后并没有返回给调用它的exam() 函数。然而我们却在exam() 函数里根据列表word_list的长度进行while循环判断,如果局部变量互不影响的话,exam() 里的word_list也不会发生变化,这个while循环不就成了死循环?

然而,这个列表在一个子程序(自定义函数)中发生了变化,在其他地方也照样生效。这是为什么呢?

其实,这并不违背我们之前讲的“局部变量互不相同”、“变量离开作用域即消失”等原则,因为我们在传参的过程中,对于列表这样的容器类对象,传的并不是列表本身,而是列表这个容器在内存中的地址。而这个地址在局部变量离开作用域的时候,并不会随着局部变量消失。调用子程序的父程序中列表对该地址的引用依然有效。那么子程序对该地址内的元素做出的变更,自然也随着该地址反应到了父程序。

这么说也许有点抽象,我们举个例子:比如某个小区居委会(子程序)的管辖里,有某个家庭(列表)有五口人(元素),突然有一天做核酸发现了阳性,居委会就打电话给疾控中心(另一个子程序):你好,我这里X号楼X室(列表地址)里有阳性。于是疾控中心循着门牌号,上门来把其中一个阳性病人接去了方舱(对列表进行操作)。当疾控中心走后,居委会再次检查该家庭,会发现只剩四口人了。

通过列表的这种特性,我们可以很方便地把对列表的操作交给另一个子程序(自定义函数)而不用要求返回,也不用对列表进行复制等额外操作。但要注意的是,如果我们不希望子程序对我们的列表真的进行操作的时候,我们需要传一个列表的副本,这在后面的章节会介绍到。

列表的返回

细心的小伙伴们应该能注意到,我们新增的两个自定义函数exam()print_exam() 都创建了一个函数内的局部列表变量unknow_words,然后把它作为函数的值返回。

def exam(n) -> list:
    unknow_words = []
    # 代码略
    unknow_words += print_exam(n, word_list)
    # 代码略
            return unknow_words

def print_exam(n, word_list)->list:
    unknow_words = []
    # 代码略
    return unknow_words

而在主程序中,我们同样也有这样一个unknow_words的变量。从主程序中我们也可以看出,这个列表就是用来保存我们在测验中答错的单词,以方便我们进一步保存在生词本里。

    elif choose == '3':
        unknow_words = []
        while True:
            unknow_words.extend(exam(5))
            if not input('继续测验吗?(y-继续 | n-退出):').lower().startswith('y'):
                if len(unknow_words) > 0 and input('保存错词表吗?(y-是 | n-否):').lower().startswith('y'):
                    save_words(set(unknow_words))
                break

根据我们之前说的变量作用域的不同(主程序与不同的子程序),这三个unknow_words可谓互不相干,他们都各自代表了一个列表地址。

主程序中的unknow_words记录了用户完成所有测验后的生词,子程序exam() 中的unknow_words记录了多少轮测验后的生词,而子程序print_exam() 中的unkown_words记录的是每一轮测验后的生词。所以在每轮、每次、每回开始测验时(也就是每层菜单下),这个变量都要清零。

    unknow_words = []

而在接收下级程序返回上来的列表时,我们要把它们添加在一起。

列表的extend()方法

把列表添加进另一个列表,从而形成一个更大的列表,我们一般有两种方法,在本例中都有体现。

exam() 子程序中,使用了我们之前介绍过的列表的加法:列表a加列表b返回一个新的合并列表c。所以我们把unknow_words的列表与子程序print_exam() 返回的列表相加,再把合并的新列表赋值给变量unknow_words

    unknow_words += print_exam(n, word_list)

而在主程序中,为了接收从下级程序返回的列表,我们使用了列表的extend()方法,直接把exam() 子程序返回的列表“纳入”了自己的列表里。

    unknow_words.extend(exam(5))

在本例中,两种方法的效果相同。实际上,两种方法有一些细微的差别,值得我们注意。

  1. 列表的加法只能在列表之间进行,也就是列表加列表。而extend()方法可以让列表“纳入”其他容器类对象的值,比如元组、字典(默认是键key),以及我们待会介绍的集合等。甚至可以直接extend()一个range()。比如 list_a.extend(range(5))就是把0,1,2,3,4这5个整数元素纳入到原来的列表list_a里。
  2. 列表的加法返回一个新的列表,只是我们可以通过赋值把原来的变量名指向这个新的列表,而原来的列表仍然留在内存里,并未改动。而extend()方法并没有创建新列表,而是在现有的列表基础上“纳入”新的元素。所以相对来说,extend()方法更加“节省”内存(虽然微不足道)。

列表的pop()方法

还记得我们在介绍列表的时候,有提到过一种删除列表元素的方法,列表的pop()方法。准确地说,pop()的意思是从现有的列表里把某个元素“弹”出来。如果括号里没有赋值,默认就弹出列表的最后一个元素(不是列表)。

pop()的这种特性,用在我们今天这个例子里再好不过了。比如在子程序print_exam()里:

        random.shuffle(word_list)
        guess_word = word_list.pop()

我们想要实现的目的是:每次在单词表里随机“拿出”一个单词用于测试。于是我们用介绍过的random.shuffle()的方法实现随机,再用列表的pop()方法实现“拿出”末尾一个单词。这样就可以保证每次测试的单词不会重复,而且剩下的单词越来越少,直到跳出循环。

    while True:
        # 剩下的单词不足一轮(5个)时,就全部作一轮
        if len(word_list) <= n:
            n = len(word_list)
        # 没有剩下的单词时,就跳出循环,回到主程序
        if n == 0:
            print('所有生词已检查完!')
            return unknow_words

3. 什么是集合

我们已经认识了列表、字典、元组等容器类对象,今天我们再简单说一下集合。

Python里集合的关键字是set,用一对大括号{}把集合元素包在一起。注意,要把集合的大括号与字典的大括号区分开来。字典的大括号里是键值对成对出现的元素,而集合里都是单个的独立元素。不过集合和字典也有相同的属性:不支持重复元素(键)、没有顺序所以不支持索引下标引用。

把一个容器变成集合可以用set()方法。因为集合最大的特点就是它里面的元素不允许重复,所以我们最重要、也最常用的用法就是使用set()将列表转成集合,去除重复元素。

比如本例中,我们在保存生词本(调用save_words() 自定义函数)之前,使用set()方法把生词列表unknow_words的元素去重。

    save_words(set(unknow_words))

除此之外,Python的集合还有着数学中集合的概念,可以进行集合的交集、并集、差集等运算,运算符分别是 &、|、-。由于我们在游戏编程中暂时用不到,大家可以看以下示例自行理解。
在这里插入图片描述

4. ASCII码

与计算机打交道,不可能不接触到ASCII码。虽然也许你不认得它,但在每天使用电脑的过程中,你却默默地使用着ASCII码。简单来说,因为我们的电脑只认得数字(0和1这样的二进制),所以我们除了数字以外的所有字符(包括数字这个形象的字符)都需要进行编码,才能让计算机理解、并正确打印在屏幕上。而我们使用的编码之一就是ASCII码。
在这里插入图片描述
如上图所示,ASCII码约定好,把一些常用的字母、数字及特殊符号,在电脑中用一串0和1的二进制表示(当然,为了人类能够更好的理解,我们看到的常常是十进制),当需要展现给人类时,就默认按照ASCII码把字母、数字及符号的形状打印在屏幕上。所以,在计算机看来,所有的字符串其实都是一串数字。于是,字符串之间也可以进行大小的比较,比如:
在这里插入图片描述
这里进行的其实是字符所转换的ASCII码的数字大小的比较。字符A的ASCII的码是65,字符B是66,所以A小于B,同理,小写字母a的ASCII码是97,所以a大于A。而数字字符1的ASCII码是49,特殊符号#的ASCII码是35,所以1大于#。(注意:这里的1是字符串’1’,而不是数字1)

Python给我们提供了两个内置函数,ord()和chr(),用来将字符串和ASCII码(十进制)互相转换。比如:
在这里插入图片描述
所以在本章的例子里,我们就利用了chr()这个函数,通过循环,来顺序生成A、B、C、D、E这五个选项的大写字母。

        for i in range(len(guess_choice)):
            # 显示A,B,C,D,E的字母选项
            print(chr(i+65) + '. ' + guess_choice[i].strip())
            # 得到正确答案的选项字母
            if guess_choice[i] == guess_cn:
                item = chr(i+65)

显示效果为:
在这里插入图片描述

5. 写入txt

上一章我们介绍了txt的读操作,那自然也有写操作。而且比较简单,就是把open的一个参数从’r’(readonly)换成’w’(write)。表达的意思即,程序将用“写”的方式打开某个txt文件,如果没有该文件,那就创建一个

def save_words(unknow_words):
    # 保存测试错误的单词到生词本TXT文件
    words = '\n'.join(unknow_words)
    with open(r'C:\Coding\dictionary\UnknowWords.txt', 'w', encoding = 'utf-8') as f:
        f.write(words)

当用“写”的方式打开(或创建)某文件,并把该文件对象赋值给某个变量(这里是f),就可以继续调用文件类对象的方法write(),把字符串内容写入改文件了。注意:在这里我们又使用了字符串的join()方法,用换行符(转义代码’\n’)把从主程序传进来的集合对象unknow_words(还记得我们在保存之前使用set()方法去重了吗)拼接在一起。这样每个单词都是独立一行保存在生词本里。这样做的目的,是为了下次从生词本读取单词的时候可以直接使用readlines()方法转换成列表。(当然也可以使用空格把单词拼接,不换行保存,在下次读取的时候需要再调用split()方法分割成列表,请读者自己试试看。😃)

最后保存好的生词本内容显示如下:
在这里插入图片描述


总结与思考

虽然本篇是番外篇,属于上一章内容的扩展,但却包含了不少新的知识点,所以不知不觉问哥又写了一万字。即便如此,依然没有把某个知识点讲得足够通透。不过问哥也觉得没有太大的必要。正如问哥之前说过的,我们只用学我们目前所能用到的、对我们游戏编程有用的知识,这样在我们目前这个阶段,就够了。不然贪多嚼不烂、理解不够透彻,再加上如果又用不到的话,这些知识很快就会被抛在脑后了。如果大家想对某个知识点进行深入的了解,可以在网上找到大量的文章自学,也可以私信问哥。

谢谢大家读到这里,我们下次再见!

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

请叫我问哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值