# 11编程中易犯错误汇总:一个综合案例
在上一篇文章中,我们学习了如何区分好的代码与坏的代码,如何写好代码。所谓光说不练假把式,在这篇文章中,我们就做一件事——一起来写代码。首先,我会先列出问题,然后要求读者自己写一份答案;然后,我会给出我写的代码;最后,我们还会以这个问题为例,讨论编程中常见的错误。
## 1 问题描述
在[这个](http://wiki.openhatch.org/index.php?title=Scrabble_challenge)页面中,有一道Python相关的练习题,可以作为Python相关的进阶题目。题目的描述如下:
> Write a Python script that takes a Scrabble rack as a command-line argument and prints all valid Scrabble words that can be constructed from that rack, along with their Scrabble scores, sorted by score. An example invocation and output:
    $ python scrabble.py ZAEFIEE
    17 feeze
    17 feaze
    16 faze
    15 fiz
    15 fez
    12 zee
    12 zea
    11 za
    6 fie
    6 fee
    6 fae
    5 if
    5 fe
    5 fa
    5 ef
    2 ee
    2 ea
    2 ai
    2 ae
简单来说,就是给你一个文件,里面包含了很多的单词。接下来,从命令行中输入一些字母,需要找出文件中,哪些单词可以由命令行中的字母组成。随后,还需要根据评分规则,对找到的单词进行评分,并按照分数从高到低输出。
## 2 作者的代码
这是一道很好的Python练习题,强烈建议读者尝试独立解决这个问题。相信有不少读者会觉得这个练习比较简单,但是,我们这一篇文章,更多的还是接着上一篇文章,继续讨论如何写出好的代码。因此,**读者可以尝试打磨自己的代码**,尽可能给出一个自认为完美的、可读性强的、具有美感的答案。
下面是笔者提供的答案:
    #!/usr/bin/python
    #-*- coding: UTF-8 -*-
    from __future__ import print_function
    import sys
    from collections import Counter
   
    scores = {"a": 1, "c": 3, "b": 3, "e": 1, "d": 2, "g": 2,
             "f": 4, "i": 1, "h": 4, "k": 5, "j": 8, "m": 3,
             "l": 1, "o": 1, "n": 1, "q": 10, "p": 3, "s": 1,
             "r": 1, "u": 1, "t": 1, "w": 4, "v": 4, "y": 4,
             "x": 8, "z": 10}
   
    def get_word_list(file_name):
        word_list = []
        with open(file_name) as f:
            for line in f:
                word_list.append(line.strip())
        return word_list
   
    def get_valid_words(word_list, rack):
        c = Counter(rack)
        return [ word for word in word_list if not (Counter(word) - c) ]
   
    def lower_valid_words(words):
        return [ word.lower() for word in words ]
   
    def get_scores(words):
        d = {}
        for word in words:
            d[word] = sum(scores[c] for c in word)
        return d
   
    def main():
        if len(sys.argv) != 2:
            raise SystemExit('Usage: scrabble_change.py RACK')
   
        rack = sys.argv[1]
   
        word_list = get_word_list('sowpods.txt')
        valid_words = get_valid_words(word_list, rack.upper())
   
        valid_words = lower_valid_words(valid_words)
   
        d = get_scores(valid_words)
        for val, key in sorted(zip(d.values(), d.keys()), reverse=True):
            print(val, key)
   
    if __name__ == '__main__':
        main()
## 3 编程中常见的错误
在讲解习题之前,我们先简单介绍一下测试用例的概念,读者可以尝试编写几个测试用例,进行简单的测试,以保证自己代码的正确性。
### 3.1 测试用例
测试用例又称之为Case,就是构造不同的输入,判断*输出*结果是否符合预期。这么说可能比较抽象,我们打个比方,想象一个登陆页面,可以输入用户名和密码,则它至少有以下几个测试用例:
* 用户没有输入用户名和密码,直接点击登陆,预期结果是提示输入用户名
* 用户输入的用户名太短,预期结果是提示用户用户名不能小于6个字符
* 用户输入了用户名没有输入密码,尝试进行登陆,预期结果是提示用户输入密码
* 用户输入了错误的密码,预期结果是提示用户密码错误
* 用户输入了正确的用户名和密码,预期结果是登陆成功,并跳转到首页
我们刚刚科普了一下“测试用例”的概念,接下来,可以为上面的程序构造一些测试用例,然后来进行简单的测试,以保证代码的正确性:
* RACK为空
* RACK的字符比单词多
* RACK的字符比单词少
* RACK的字符与单词一样多
* 单词里面有多个相同的字符
### 3.2 常见的错误
在笔者曾经见过的代码中,大家容易犯的错误有:
1. 代码不符合规范,例如,等号两边没有空格,逗号后面没有空格,函数之间没有空行等。这是最简单也最容易犯的错误,希望大家都不要犯这种错误,这类错误给人的感觉很初级,说明读者还处于初学者的水平。代码规范问题其实非常好解决,建议大家使用PyCharm来编写Python代码。PyCharm会对所有不符合PEP 8规范的代码,在右侧的侧边栏以黄色显示。将鼠标移动到黄色标记处,会提示代码哪里不符合规范。我们只需要根据PyCharm的提示修改代码即可。
2. 没有充分利用“函数”。没有充分利用“函数”也是大家容易犯的错误,我们在上一篇文章中强调过函数的好处。函数是代码修改的最小单元,与此同时,函数的名字又可以作为对函数的解释,这样别人阅读我们代码时,就有一种像读短文的愉悦感。此外,函数越短小,则说明它功能越独立,也越容易被复用。下面的代码就是一个没有充分利用“函数”的反面例子,如果读者是初学者,只要能够解决问题就已经很棒了。如果大家希望使用Python作为养家糊口的技能,则需要将代码写得简单优美、好维护。
        def main():
            with open('/root/iopus/20161119/sowpods.txt','r') as f:
                word_list = [i.strip() for i in f.readlines()]
            valid_words = {}
            try:
                rack = sys.argv[1].upper()
                for word in word_list:
                    temp_rack = list(rack)
                    for letter in word:
                        try:
                            temp_rack.remove(letter)
                        except:
                            break
                    if len(rack) - len(temp_rack) == len(word):
                        valid_words[word] = calculate_score(word)
                valid_words = sorted(valid_words.iteritems(),key=lambda d:d[1],reverse=True)
                for word in valid_words:
                    print('{},{}'.format(word[1],word[0]))
            except:
                print "Usage: scrabble.py [RACK]"
                exit(1)
3. 函数命名不准确。刚刚说到没有充分利用“函数”是不好的,与此同时,如果函数的命名不规范,也容易给大家造成误会。我们反提到,函数的名称可以作为对函数这段代码的解释,既然是解释,那就要求准确无误、简单明了。显然,我们很多工程师刚开始是没有意识到这个问题的。例如,如果函数名称是`get_xxx`、`find_xxx`、`check_xxx`,则函数不应该对输入的参数有任何的“修改”。有“修改”就再创建一个新的函数来修改,不要将两件不同的事情,放在同一个函数之中。例如,在下面这个函数之中,最后一行对结果进行了修改:
        #Step 3: find valid words
        def find_valid_words(all_words, rack):
            valid_words = []
            for word in all_words:
                rack_characters = list(rack)
                word_characters = list(word)
                valid = True
                for char in word_characters:
                    if char in rack_characters:
                        rack_characters.remove(char)
                    else:
                        valid = False
                        break
                if valid:
                    valid_words.append(word)
            return [ word.lower() for word in valid_words ]
    下面也是一个命名不准确的例子。函数的名称是`open_file`,则我们的预期是返回一个文件对象,但是其实函数的作用是得到一个单词列表。如果将函数的名称,修改为`get_word_list`将会更加具有可读性。
        def open_file(file_name):
            word_list = []
            with open(file_name) as file_dict:
                for word in file_dict:
                     word_list.append(word.strip())
            return word_list
4. 异常考虑不充分。有人在知乎上提问,“[编程到底难在哪里?](https://www.zhihu.com/question/22508677/answer/141334678)”。其中一个高赞答案得到了4.2万个赞。作者以买苹果为例,非常形象地介绍了编程难在哪里。简单来说,就是异常情况的考虑。对于编程来说,就是有if就有else。例如,在我们这个习题中,用户在命令行输入构造单词的字符集合。那么,如果用户没有输入呢?在我们的标准答案中,首先就会判断用户是否按照预期进行了输入,如果用户没有输入,则我们会返回错误,并提示用户进行输入。
5. 多余的计算导致性能不佳。我们前面讨论了代码规范和异常情况,接下来简单地讨论一下性能。在这个习题中,用户的输入与文件中的单词,都是大写字母。但是,在最后结果评分与结果输出时,是小写字母。那么,这就涉及到一个问题,应该在什么时候将输入中的大写转换为小写?有两种方法:1)在找到有效的单词以后,再来进行相应的转换;2)在读入单词列表时,顺便将文件中所有的单词都转换为小写。如果读者之前没有意识到这个问题,那么,现在可以想一下,哪一种方法更好。显然第一种方法更好的。考虑这样一种情况,如果文件中有100万个单词,我们最终只找到了100个有效的单词。则第一种方法只需要将100个单词从大写转换为小写,第二种方法需要将100万个单词从大写转换为小写。显然,第二种方法存在大量的无用计算,从而导致程序运行时间变长。
## 4 总结
在这一篇文章中,我们使用了一种全新的教学方法——边做边学,希望可以给读者带来不一样的收获。这篇文章中的练习还是比较小的练习,但是也存在这么多可以讨论的地方,对于一个大型项目来说,更加需要注意这些细节,时刻牢记上一篇文章中的方法论与代码品味,努力成为编写高质量代码的优秀工程师。

编程中易犯错误汇总:一个综合案例.md的更多相关文章

  1. javascript中易犯的错误有哪些

    javascript中易犯的错误有哪些 一.总结 一句话总结: 比如循环中函数的使用 函数中this的指向谁(函数中this的使用) 变量的作用域 1.this.timer = setTimeout( ...

  2. CSS网页布局中易犯的30个小错误

    即使是CSS高手,也难免在书写CSS代码的时候出一些小错误,或者说,任何一种代码都是如此.小错误却往往造成大问题,浪费很多无辜的时间来调试和排错.查看下面这份CSS网页布局中易犯的10个小错误,努力的 ...

  3. [golang 易犯错误] golang 局部变量初始化:=的陷阱

    我们知道,golang中局部变量初始化方法(使用“:=”创建并赋值),让我们在使用变量时很方便.但是,这也是易犯错误的地方之一.特别是这个初始化符还支持多个变量同时初始化,更特别的是它还支持原有变量赋 ...

  4. R语言编程中的常见错误

    R语言编程中的常见错误有一些错误是R的初学者和经验丰富的R程序员都可能常犯的.如果程序出错了,请检查以下几方面. 使用了错误的大小写.help().Help()和HELP()是三个不同的函数(只有第 ...

  5. 【C++】常见易犯错误之数值类型取值溢出与截断(3)

    0.  前言 本节是“[C++]常见易犯错误之数值类型取值溢出与截断(1)” 的补充,主要探讨浮点型的取值溢出. 1. 相关知识 (1) 浮点型数据取值范围如下: 单精度型 float 3.4 * 1 ...

  6. 【C++】常见易犯错误之数值类型取值溢出与截断(1)

    1. 数据类型数值范围溢出 如标题所述,该错误出现的原因是由于变量的值超出该数据类型取值范围而导致的错误. 例题如下: (IDE环境:C-Free,编译器为mingw5,如下图) # include ...

  7. java代码书写易犯错误

    java代码书写易犯错误: 常见报错: 控制台报错: 找不到或无法加载主类 HelloWorld 原因: java.lang.NoClassDefFoundError: cn/itcast/day01 ...

  8. 【转】十个JavaScript中易犯的小错误,你中了几枪?

    目录 常见错误一:对于this关键词的不正确引用 常见错误二:传统编程语言的生命周期误区 常见错误三:内存泄露 常见错误四:比较运算符 常见错误五:低效的DOM操作 常见错误6:在for循环中的不正确 ...

  9. 十个JavaScript中易犯的小错误,你中了几枪?

    序言 在今天,JavaScript已经成为了网页编辑的核心.尤其是过去的几年,互联网见证了在SPA开发.图形处理.交互等方面大量JS库的出现. 如果初次打交道,很多人会觉得js很简单.确实,对于很多有 ...

随机推荐

  1. GIP画图

    世界坐标:相对于winform窗体来说的, 页面坐标:相对于控件的 设置坐标:相对于显示器 获得Graphics对象一般有两种方式: 1.控件.CreateGraphics();//通过该方式创建后要 ...

  2. ARC099E. Independence

    考虑一个子问题.给定无向图 $G$,如何判断能否将 $G$ 的点集分成两部分 $S$.$T$ 使得 $S$ 和 $T$ 导出的子图都是完全图? 这个问题把我难住了.解法是考虑 $G$ 的补图 $G'$ ...

  3. 有关java5以后的线程

    创建线程的方式 方式一 继承于Thread类 /** * 多线程的创建,方式一:继承于Thread类 * 1. 创建一个继承于Thread类的子类 * 2. 重写Thread类的run() --> ...

  4. ajax同源策略,jsonP跨域访问

    浏览器处于安全性的考虑,要求ajax请求,必须满足同源策略 规定:访问的协议://域名:端口号都相同时满足同源策略,浏览器可以正确解析数据,否则如果有一项不满足要求,则属于跨域访问,浏览器可以正常获取 ...

  5. Tensorflow安装错误Cannot uninstall wrapt

    解决办法:安装之前先执行:pip install wrapt --ignore-installed

  6. git回退

    以前,如果是要去除某一块功能,我都是选择性删除,选择性注释,然后前后逻辑各种查看,各种比较.每一次,改完这些我总感觉心好累啊!!!然后,我就发现了 Git 一个非常强大的功能:回滚.当然我还是喜欢叫它 ...

  7. kettle处理未发现hadoop插件问题

    背景:将测试环境的中kettle转换传输到生产环境上出现hadoop插件无法获取的错误 Hadoop File Output 2.0 - ERROR (version 7.1.0.0-12, buil ...

  8. 使用python操作zookeeper

    kazoo 介绍 zookeeper的开发接口以前主要以java和c为主,随着python项目越来越多的使用zookeeper作为分布式集群实现,python的zookeeper接口也出现了很多,现在 ...

  9. 在Win10上运行ESXI-Comstomer

    在Win10上运行ESXI-Comstomer 来源 https://www.v-front.de/p/esxi-community-packaging-tools.html ESXi-Customi ...

  10. .NET监视程序运行时间

    使用Stopwatch类(命名空间:System.Diagnostics;) 示例: using System; using System.Collections.Generic; using Sys ...