通过游戏编程学Python(6)— 英汉词典、背单词

通过游戏编程学Python

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



前言

在过去的几篇文章里,我们已经学习了Python基础语法的大部分内容,所以在今后的文章里,我们每节课涵盖的“知识点”可能会相对少一点,同样的知识点讲解也不会太深入,而是渐渐将我们的重心放在游戏编程上,比如如何构思、实现、展现游戏,以及人机对战的简单AI实现等等。

本节课我们一起来做个英汉小字典,使用的也是Python里的字典。听起来像个绕口令 😄,但用起来就会发现,Python里的字典就是跟我们生活中见到的字典差不多。

另外,之前问哥一直使用中文变量,意图将程序“本地化”,但相信大家现在已经有了一定的基础,阅读代码不再是太困难的事,所以借着英汉字典的小例子,问哥决定“和世界接轨”,开始使用英文字母变量。😄

本篇文章不是做游戏,所以问哥把文章里的“游戏”两个字打上了引号,因为问哥也知道学英语不太好玩啊 😄。但是研究如何用代码实现,却是一件有趣的事。而且问哥饱受20多年英语学习之苦,这里也会和大家分享一个背单词的方法。


一、知识点

  1. 代码的格式
  2. Python中的字典
  3. 单行input()

二、第五个“游戏”——英汉词典、背单词

1. 玩法简介

程序会从本地的TXT文件中读取英文单词和释义,建立一个内存中的英汉字典库。用户启动程序,可以选择是查字典,还是背单词。

查字典的时候不用特别指定英汉还是汉英,程序默认会先查找有没有用户输入的这个单词(英汉查找)。如果没有找到,则程序认为用户是希望汉英查找,然后再从释义里查找。但是因为汉英查找可能会产生很多个结果(比如查单个汉字“车”),为了不使程序难以展现,以及避免用户错误输入了单个汉字,程序只会展现最开始查到的10条结果,只有用户确认显示全部,才会继续显示。

如果用户选择背单词,则程序会从字典里随机抽取8个单词组成一个单元,按照记忆方法逐条展现(稍候会分享方法,二十年前问哥使用这个方法一个月内背完四级单词并在之后顺利通过四级),用户随时可以中断学习,回到初始目录。

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

2. 游戏流程

1
yes
no
yes
no
yes
no
其它选择
2
yes
no
游戏开始
选择1-查字典,2-背单词,其它键-结束
请输入英语或汉字
有结果吗?
显示结果
继续查吗?
结束吗?
程序结束
换一个试试
抓取单词并按记忆规律排序
显示单词
继续还是退出

3. 程序代码

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

全部代码:

import random

# 从本地英汉词典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 hello():
    # 显示欢迎界面,并获取用户选择
    print('-' * 40)
    print('='*10 + ' 欢迎来到英汉小词典 ' + '='*10)
    print('-' * 40)
    choose = input('1-查字典 | 2-背单词 | 按其它任意键退出:')
    print('-' * 40)
    return choose

def look_up(word) -> dict:
    # 把用户输入的单词在字典里查询,返回一个包含所有查询结果的新字典
    result_dict = {}
    # 默认先英汉查找,如果找得到就返回结果
    if DICT.get(word):
        result_dict[word] = DICT[word]
    else:
        # 如果用户输入的中文,或者英汉查找未找到的话,返回的汉英查找结果可能包含多条
        values = list(DICT.values())
        keys = list(DICT.keys())
        for i in range(len(DICT)):
            if word in values[i]:
                result_dict[keys[i]] = values[i]
    return result_dict

def show_result(my_dict):
    # 将查询返回的新字典打印出来
    print('-' * 40)
    # 如果新字典为空,说明字典里无论英汉还是汉英都没有查到这个词
    if len(my_dict) == 0:
        print('很抱歉,没有查到这个词,换一个试试吧')
        return
    # 如果是汉英查询,新字典里很可能会多于10条查询结果,默认只显示前10条
    elif len(my_dict) > 10:
        key_list = list(my_dict)
        for i in key_list[:10]:
            print(i + ': ' + my_dict[i])
        if input(f'还有{len(key_list)-10}条结果未显示(y-继续 | n-退出):').lower().startswith('y'):
            for i in key_list[10:]:
                print(i + ': ' + my_dict[i])
    else:
        for i in my_dict:
            print(i + ': ' + my_dict[i])
    print('-' * 40)
            

def recite_word():
    # 从字典中抽取8个单词,按照记忆曲线的顺序组成新的列表
    print('本轮会随机抽取8个单词,按照记忆曲线排列,共展现32次')
    print('单词记忆过程中,按任意键继续,q-退出')
    print('-' * 40)
    if len(DICT) >= 8:
        word_list = random.sample(list(DICT.keys()), 8)
        seq = [1, 2, 1, 2, 3, 4, 3, 4, 1, 2, 3, 4, 5, 6, 5, 6, 7, 8, 7, 8, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8]
        recite_list = [word_list[i-1] for i in seq]
    else:
        recite_list = list(DICT.keys())
    for i in recite_list:
        print(i + ': ' + DICT[i])
        if input().lower().startswith('q'):
            return
    print('您本轮一共背诵了8个单词')
    print('-' * 40)


# 正式程序从这里开始
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
    else:
        print('欢迎使用,再见!')
        break

3. 代码简析

正式的程序主体从77行的while True语句开始。绝大部分功能都重构到自定义的函数里,虽然问哥之前也说过,对于像这种很小的程序,某些功能即使放到主程序里,也没有什么区别,代码数量也不会有所变化。但是把功能重构进自定义的函数也是显然意见的:

  1. 从读代码的角度,可以让别人很容易的看到主程序的运行流程。前提是语法或注释写得清楚,让人很容易就能明白程序是怎样运行的。
  2. 从写代码的角度,可以把更多的精力放在思考调整、优化整体流程上,而不用关心细节是怎样实现的。特别是当要排错,或是修改某些参数时,使用重构函数的方式可以很容易定位到需要修改的地方。

所以问哥也是推荐大家尽量把简单的功能也放进函数里,使代码看起来更加简洁、高效。

本程序的主流程很清晰:

  1. 游戏开始时,先进入外层while循环,通过自定义函数 hello() 打印欢迎信息,以及菜单供用户选择;
  2. 使用if语句判断用户的选择,如果是1,则进入查字典的内层while循环;如果是2,则进入背单词的内层while循环,如果是其他字符,则使用break退出循环,结束程序;
  3. 进入内层while循环后,分别调用查单词的自定义函数 look_up(),并将查找结果放入一个新的临时字典result_dict 返回主程序,然后直接放进另一个自定义函数 show_result() 打印出来;内循环2里调用背单词的自定义函数 recite_word() 显示要背的单词;
  4. 两个内循环都提供一个用户输入判断:如果输入除了字母y以外的任意字符,将使用break退出内层循环。

注意:对于嵌套循环,使用break只能退出break语句所在的哪一层循环,并不是全部退出。

三、知识点

1. 写代码的默认规则

在Python里,程序员可以使用字母、数字、“_”做为变量名(不能以数字开头),甚至使用中文(Python2需要特别设置)作为变量名。但是为了程序的可读性和可移植性,程序员们都默认准守一些规则,如果大家感兴趣,可以自行搜索PEP8深入了解一下。不过每个程序员还是有自己的经验和习惯,所以也并不强求所有人一定要按照这些规则。比如问哥写的代码,也许在专业人的眼里,不够规范、不够专业,但能够和别人交流,能让别人看懂,我觉得就够了。😄

Python里的变量虽然有很多选择,但一些保留字是不能用作变量的,比如bool值True和False,None,import, in, and, or, not等,相信大家对于这些单词凭常识也会避开。此外,还有一些需要注意的地方,虽然语法上行得通,但是会让人产生阅读困难,所以一般建议大家遵守一下规则。

  1. “__”(两个下划线)开始的变量一般是内置变量,比如__init__等等,所以自定义变量最好避开;
  2. 内置变量在语法上可以用作变量名,但一旦作为变量名使用,内置函数就没法调用了,比如list, int等;
  3. 常量名一般全部大写(之前介绍过);
  4. 首字母大写的变量一般用作的名称,虽然我们还没学到对象编程,但这点最好也要避免;
  5. 常见的变量命名规则通常有“驼峰命名法”,即把变量里的每个单词(或从第二个单词开始)首字母大写,但要注意和类名作区分。比如MyList,或myList等;
  6. 问哥比较习惯的是使用“_”把单词间隔开作为变量名,比如my_list, my_string等。

以上各项不做强制要求,Python也不会报错,大家可以按照自己的习惯,勤加练习,形成自己的代码风格。

2. 什么是字典

我们说了这么久的字典,其实它一点都不神秘,只不过是和列表差不多的一个可以放进任何东西的容器类对象。但是它对保存的数据有一定的格式要求,比如一定要以键值对的形式保存在一对大括号里。所谓的键值对就是一个英文冒号(:)连接在一起的一对数据,如果有多个键值对,字典元素之间用英文半角逗号分开。比如:

{1: ‘the first one’, 2: ‘the second one’, 3:‘the third one’, 4:'the first one}

冒号左边的数据称作“键”,可以是任何数据类型(上面用的数字,本例使用的是英文单词字符串),但在同一个字典里,键不能重复。冒号右边的数据称作“值”,也可以是任何数据类型,但是可以重复。这也和我们现实中的字典是一样,比如在英汉词典里,你只能查到一个叫present的单词,但是关于“礼物”的英文翻译却可以有很多个。

定义一个空字典的方法就是一对空的大括号,然后对字典赋值最常用的方式,就是以字典名[键名] = 值的形式添加进键值对。

比如本例中,先创建了一个空的字典DICT,再使用循环遍历txt文件中读取到的数据,进行字符串分割后,把单词作为键,释义作为值,添加进字典。

DICT = {}
for i in p:
    i = i.replace('"','').split(',')
    DICT[i[0].strip().lower()] = ','.join(i[1:]).strip()

顺便给大家截图看一下txt文件里的单词样子,以帮助大家理解,字符串里的几个函数的作用(基本上我们都已学过):
在这里插入图片描述

字符串的replace()方法

这里可以看到txt文件中的单词和释义之间是用英文半角逗号“,”隔开的,而且单词和释义两边都有英文双引号"",而这个双引号是我们不需要的,所以第一步先用字符串的replace()方法把引号替换成空字符串。然后再通过链式操作调用split()方法,返回一个列表

i = i.replace('"','').split(',')

这时 i 就从一个长字符串(包含一行数据)变量变成一个去除了引号,根据逗号分割成字符串元素的列表了。

但是我们同时注意到,释义里也有逗号,而我们只想根据分隔单词和释义的第一个逗号把一行长字符串一分为二,左边作为键,右边作为值。所以我们在为字典赋值的时候,又用字符串的join()方法结合列表的切片操作,把右边的元素通过逗号连在一起。而左边的单词呢,为了查找方便,使用lower()方法将其统统转成小写。最后别放忘记使用strip()方法去掉换行符“\n”以及多于的空格。

DICT[i[0].strip().lower()] = ','.join(i[1:]).strip()

这些方法我们都学过,希望大家也都能看得懂,如果觉得理解还有些困难,可以放进编译器,自己试试看。值得注意的是,这些方法都是可以返回一个新的字符串,所以我们才可以把它们并排写在一起,链式调用。

重复赋值

另外我想大家也看到txt文件里的第一行是"word",“translation”,而这只是标题,并不是字典内容,换句话说,并不是我们需要的。所以默认程序执行第一条赋值的时候,字典里的第一个键值对会变成{‘word’: ‘translation’},但是一点都不用担心,因为在字典的后面,会再次出现word这个单词正确的释义:
在这里插入图片描述
这时就体现字典的另一个特性:同一个键的重复赋值以最后一个为准。于是在程序给字典DICT赋值,当读到后面的时候,又看到键’word’的新释义,就自然把最开始那个’translation’的值给覆盖掉了。

读取字典

创建好了字典DICT,当然是为了后面查单词使用。读取字典的值一般来说有两种,

  1. 直接像列表那样通过DICT[‘键名’]来读取对应的值;
  2. 使用字典的get()方法,括号里放入键名。

一般来讲直接调用键名会使代码更简洁,但使用get()方法时,如果字典里没有这个键,get()方法返回一个None,也就是空,而直接调用会报错,如下图所示:
在这里插入图片描述
注意:这里使用了整数作为字典的键,所以不使用双引号。如果键名是字符串’1’,双引号一定不能少。

我们之前提到过,None翻译成布尔型数值就是False,所以在本课的例子里,look_up()这个自定义函数里,可以直接放在 if 语句里进行判断:试着查找该键(word),如果查不到,get()返回结果为None,就是False,反之,如果查到结果,就是True,继续后面给临时字典result_dict 赋值。

def look_up(word) -> dict:
    # 把用户输入的单词在字典里查询,返回一个包含所有查询结果的新字典
    result_dict = {}
    if DICT.get(word):
        result_dict[word] = DICT[word]

另外捎带提一个不太重要的点,就是在自定义函数那一行语句的末尾,冒号之前,可以使用 -> 跟上该函数/方法返回的数据类型,起到一个标记的作用。本例中只有这个函数返回的数据是字典,所以我在这里写上 def look_up(word) -> dict: 也是为了提醒自己注意。

这样的注释没有什么实际用处,可写可不写,但是在阅读别人的代码时,可能会遇到这样的写法,只要能看懂就好。同样地,自定义函数里的形参,也可以加上冒号和数据类型,用来提醒使用者传入什么类型的参数。比如look_up()这个函数也可以写成:

def look_up(word:str) -> dict:

单独调用字典的键和值

我们在创建字典时,是通过键值对的形式成对输入的,但是字典的键和值也可以单独拿出来。

字典的keys()方法返回一个由字典的键组成的“字典键”容器类对象。而values()方法返回一个由字典的值组成的“字典值”容器类对象。
在这里插入图片描述
虽然它们左右两边打了一对中括号,但它们不是列表,也不能使用索引值下标去引用里面的值。但是可以通过for循环遍历。我们之前提到过,字典是没有顺序的,所以它不支持索引查询。但我们可以使用list()函数将它变成列表,进而可以使用索引去调用了。

    else:
        # 如果用户输入的中文,或者英汉查找未找到的话,返回的汉英查找结果可能包含多条
        values = list(DICT.values())
        keys = list(DICT.keys())
        for i in range(len(DICT)):
            if word in values[i]:
                result_dict[keys[i]] = values[i]

元组

其实我们还可以使用字典的另一个方法items(),返回一个包含键和值的元组组成的“字典键和值”的容器对象。同样地,它也可以使用list()将其转换成一个列表,列表里每个元素都是一个元组。
在这里插入图片描述
我们之前没有提到过元组,但它并不复杂,可以理解为列表的常量,也是一个容器类对象。它在元素左右两边使用一对小括号(与列表的中括号和字典的大括号区别开来),像列表一样可以使用索引值下标调用,但是它的元素值不允许修改,也不允许增加或删除元素。

所以上面的自定义函数look_up()里的内容也可以写成这样:

    else:
        # 如果用户输入的中文,或者英汉查找未找到的话,返回的汉英查找结果可能包含多条
        key_value = list(DICT.items())
        for i in range(len(key_value)):
            if word in key_value[i][1]:
                result_dict[key_value[i][0]] = key_value[i][1]

注意,因为转换成列表key_value后,列表里的每个元素都是一个元组,如果需要再读取元组里的单个元素,则需要两个中括号,第一个中括号是列表下标,第二个则是元组下标。比如key_value[i][1] 指的就是列表中索引值为 i 的元组元素里,索引值为1的元素,因为元组的索引值也是从0开始,所以元组[1]调用了元组的第2个元素,也就是字典的值。

3. 单行input()语句

我们之前使用过许多次input(),但大都是跟在print()后面。print()用来打印一条提示信息,提示用户在后面的input()光标处输入内容。但实际上input()自己就可以完成print()的功能。只要在input()的括号里放入需要先打印出来的内容,就可以达到提示输入的效果,比如:

choose = input('1-查字典 | 2-背单词 | 按其它任意键退出:')

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

4. 背单词模块

如果用户在开始的菜单里选择了’2’,则将会进入背单词模块。这里通过一个自定义函数 recite_word() 来实现

    elif choose == '2':
        while True:
            recite_word()

而这个函数里主要的内容如以下代码:

    if len(DICT) >= 8:
        word_list = random.sample(list(DICT.keys()), 8)
        seq = [1, 2, 1, 2, 3, 4, 3, 4, 1, 2, 3, 4, 5, 6, 5, 6, 7, 8, 7, 8, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8]
        recite_list = [word_list[i-1] for i in seq]
    else:
        recite_list = list(DICT.keys())
    for i in recite_list:
        print(i + ': ' + DICT[i])
        if input().lower().startswith('q'):
            return

如果字典里没有8个单词,就别那么复杂了,一个个显示就好。如果大于8个,则使用我们讲过的random.sample()方法,从字典的键列表里随机抽取8个单词,返回到word_list这个列表中。

然后,关键的关键,是单词展示的顺序,也就是背单词的方法。这里我使用了二十年前我考四级时,一位网名叫做“领航”的学长分享的办法:将需要背诵的单词分成16组,因为人的短期记忆,最好每组里包含3到5个单词。背诵方法就是,1->2->1和2,快速浏览,不求精解,然后3->4->3和4,再1、2、3和4放在一起浏览一遍。同样地,5->6->5和6,7->8->7和8,再5、6、7、8,然后1到8组再放在一起浏览一遍。另外8组单词也是这样背诵。再最后把16组单词放在一起浏览一遍。这样可以保证每个单词都背诵了5遍,而且是按照人的记忆规律来复现的(艾斯宾浩记忆曲线)。如果每组5个单词的话,这样一轮下来80个单词就记得差不多了。问哥使用这个方法,一个月内就背完了四级词汇,而且效果相当不错。

好了,回到我们的程序中来。这里我创建了一个名叫seq的列表,里面放了需要展示的单词的索引值。而索引值的顺序排列就是根据上面介绍的背单词的方法。当然,为了简化,这里只选了8个单词,每个单词就是一个组。

然后又使用了列表生成式生成最终要展示的、排好顺序的单词列表。正好大家也可以复习一下列表生成式:

recite_list = [word_list[i-1] for i in seq]

再最后,循环遍历要背诵的单词列表recite_list ,一条条展现,但是只要用户输入了“q”,函数自动结束,回到主程序。

    for i in recite_list:
        print(i + ': ' + DICT[i])
        if input().lower().startswith('q'):
            return

总结与思考

不知不觉这已经是问哥的第7篇博客了,每篇8000字以上(可能是问哥废话太多)。

对于知识点的学习,问哥一直觉得是相当枯燥,而且问哥也不是计算机专业出身,所以一直坚持“用到了再去学”的实用原则。然后因为兴趣坚持至今,也算是形成了自己学习编程的“野路子”。所以如果大家觉得相关的知识点问哥讲得不够准确和深入,可以参考其他老师的文章自行学习,也欢迎给问哥留言。常言道,三人行,必有我师焉。

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

  • 7
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

请叫我问哥

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

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

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

打赏作者

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

抵扣说明:

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

余额充值