Skip to content

测试驱动的开发

23 / 44

写一个函数或者写一个程序换一种说法其实就是实现一个算法” —— 而所谓的算法”,Wikipedia 上的定义是这样的

In mathematics and computer science, an algorithm is an unambiguous specification of how to solve a class of problems. Algorithms can perform calculation, data processing, and automated reasoning tasks.

算法”,其实没多神秘就是解决问题的步骤而已

在第二部分的第一章里我们看过一个判断是否为闰年的函数

让我们写个判断闰年年份的函数取名为 is_leap(),它接收一个年份为参数若是闰年则返回 True,否则返回 False。

根据闰年的定义

  • 年份应该是 4 的倍数
  • 年份能被 100 整除但不能被 400 整除的不是闰年
  • 所以相当于要在能被 4 整除的年份中排除那些能被 100 整除却不能被 400 整除的年份

不要往回翻现在自己动手尝试着写出这个函数你会发现其实并不容易的……

python
def is_leap(year):
    pass

第一步跟很多人想象得不一样第一步不是上来就开始写……

第一步是先假定这个函数写完了我们需要验证它返回的结果对不对……

这种通过先想办法验证结果而后从结果倒推的开发方式是一种很有效的方法论叫做 “Test Driven Development”,以测试为驱动的开发

如果我写的 is_leap(year) 是正确的那么

  • is_leap(4) 的返回值应该是 True
  • is_leap(200) 的返回值应该是 False
  • is_leap(220) 的返回值应该是 True
  • is_leap(400) 的返回值应该是 True

能够罗列出以上四种情况其实只不过是根据算法考虑全面之后的结果 —— 但你自己试试就知道了无论多简单的事想要考虑全面好像并不容易……

所以在写 def is_leap(year) 中的内容之前我只是用 pass 先把位置占上而后在后面添加了四个用来测试结果的语句 —— 它们的值现在当然都是 False…… 等我把整个函数写完了写正确了那么它们的值就都应该变成 True

python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

def is_leap(year):
    pass

is_leap(4) is True
is_leap(200) is False
is_leap(220) is True
is_leap(400) is True
plaintext
False

False

False

False

考虑到更多的年份不是闰年所以排除顺序大抵上应该是这样

  • 先假定都不是闰年
  • 再看看是否能被 4 整除
  • 再剔除那些能被 100 整除但不能被 400 整除的年份……

于是先实现第一句:“先假定都不是闰年”:

python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

def is_leap(year):
    r = False
    return r

is_leap(4) is True
is_leap(200) is False
is_leap(220) is True
is_leap(400) is True
plaintext
False
True
False
False

然后再实现这部分:“年份应该是 4 的倍数”:

python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

def is_leap(year):
    r = False
    if year % 4 == 0:
        r = True
    return r

is_leap(4) is True
is_leap(200) is False
is_leap(220) is True
is_leap(400) is True
plaintext
True
False
True
True

现在剩下最后一条了:“剔除那些能被 100 整除但不能被 400 整除的年份”…… 拿一个参数值比如200 为例

  • 因为它能被 4 整除所以使 r = True
  • 然后再看它是否能被 100 整除 —— —— 既然如此再看它能不能被 400 整除
    • 如果不能那就让 r = False
    • 如果能就保留 r 的值…… 如此这般200 肯定使得 r = False
python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

def is_leap(year):
    r = False
    if year % 4 == 0:
        r = True
        if year % 100 == 0:
            if year % 400 !=0:
                r = False
    return r

is_leap(4) is True
is_leap(200) is False
is_leap(220) is True
is_leap(400) is True
plaintext
True
True
True
True

尽管整个过程读起来很直观但真的要自己从头到尾操作就可能四处出错不信你就试试 —— 这一页最下面添加一个单元格自己动手从头写到尾试试……

当然,Python 内建库中的 datetime.py 模块里的代码更简洁之前给你看过

python
# cpython/Lib/datetime.py
def _is_leap(year):
    return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
_is_leap(300)
plaintext
False

你自己动手从写测试开始逐步把它实现出来试试?—— 肯定不能允许你拷贝粘贴哈哈

Python 语言中有专门用来试错的流程控制 —— 今天的绝大多数编程语言都有这种试错语句”。

当一个程序开始执行的时候有两种错误可能会导致程序执行失败

  • 语法错误(Syntax Errors)
  • 意外(Exceptions)

比如 Python3 你写 print i而没有写 print(i)那么你犯的是语法错误于是解析器会直接提醒你你在第几行犯了什么样的语法错误语法错误存在的时候程序无法启动执行

但是有时会出现这种情况语法上完全正确但出现了意外这种错误都是程序已经执行之后才发生的(Runtime Errors)—— 因为只要没有语法错误程序就可以启动比如你写的是 print(11/0)

python
print(11/0)















plaintext
ZeroDivisionError                         Traceback (most recent call last)

<ipython-input-2-5544d98276be> in <module>
----> 1 print(11/0)

ZeroDivisionError: division by zero

虽然这个语句本身没有语法错误但这个表达式是不能被处理的于是它触发了 ZeroDivisionError这个意外使得程序不可能继续执行下去

Python 定义了大量的常见意外”,并且按层级分类

在第三部分阅读完毕之后可以回来重新查看以下官方文档
https://docs.python.org/3/library/exceptions.html

bash
BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StopAsyncIteration
      +-- ArithmeticError
      |    +-- FloatingPointError
      |    +-- OverflowError
      |    +-- ZeroDivisionError
      +-- AssertionError
      +-- AttributeError
      +-- BufferError
      +-- EOFError
      +-- ImportError
      |    +-- ModuleNotFoundError
      +-- LookupError
      |    +-- IndexError
      |    +-- KeyError
      +-- MemoryError
      +-- NameError
      |    +-- UnboundLocalError
      +-- OSError
      |    +-- BlockingIOError
      |    +-- ChildProcessError
      |    +-- ConnectionError
      |    |    +-- BrokenPipeError
      |    |    +-- ConnectionAbortedError
      |    |    +-- ConnectionRefusedError
      |    |    +-- ConnectionResetError
      |    +-- FileExistsError
      |    +-- FileNotFoundError
      |    +-- InterruptedError
      |    +-- IsADirectoryError
      |    +-- NotADirectoryError
      |    +-- PermissionError
      |    +-- ProcessLookupError
      |    +-- TimeoutError
      +-- ReferenceError
      +-- RuntimeError
      |    +-- NotImplementedError
      |    +-- RecursionError
      +-- SyntaxError
      |    +-- IndentationError
      |         +-- TabError
      +-- SystemError
      +-- TypeError
      +-- ValueError
      |    +-- UnicodeError
      |         +-- UnicodeDecodeError
      |         +-- UnicodeEncodeError
      |         +-- UnicodeTranslateError
      +-- Warning
           +-- DeprecationWarning
           +-- PendingDeprecationWarning
           +-- RuntimeWarning
           +-- SyntaxWarning
           +-- UserWarning
           +-- FutureWarning
           +-- ImportWarning
           +-- UnicodeWarning
           +-- BytesWarning
           +-- ResourceWarning

FileNotFoundError 为例 —— 当我们想要打开一个文件之前其实应该有个办法提前验证一下那个文件是否存在如果那个文件并不存在就会引发意外”。

python
f = open('test_file.txt', 'r')















plaintext
FileNotFoundError                         Traceback (most recent call last)

<ipython-input-3-5fac19176fe6> in <module>
----> 1 f = open('test_file.txt', 'r')

FileNotFoundError: [Errno 2] No such file or directory: 'test_file.txt'

Python 我们可以用 try 语句块去执行那些可能出现意外的语句try 也可以配合 exceptelsefinally 使用从另外一个角度看try 语句块也是一种特殊的流程控制专注于当意外发生时应该怎么办?”

python
try:
    f = open('test_file.txt', 'r')
except FileNotFoundError as fnf_error:
    print(fnf_error)
plaintext
[Errno 2] No such file or directory: 'test_file.txt'

如此这般的结果是

当程序中的语句 f = open('test_file.txt', 'r') 因为 test_file.txt 不存在而引发意外之时except 语句块会接管流程而后又因为在 except 语句块中我们指定了 FileNotFoundError所以若是 FileNotFoundError 真的发生了那么except 语句块中的代码print(fnf_error) 会被执行……

你可以用的试错流程还有以下变种

python
try:
    do_something()
except built_in_error as name_of_error:
    do_something()
else:
    do_something()

或者

python
try:
    do_something()
except built_in_error as name_of_error:
    do_something()
else:
    do_something()
finally:
    do_something()

甚至可以嵌套

python
try:
    do_something()
except built_in_error as name_of_error:
    do_something()
else:
    try:
        do_something()
    except built_in_error as name_of_error:
        do_something()
...

更多关于错误处理的内容请在阅读完第三部分中与 Class 相关的内容之后再去详细阅读以下官方文档

理论上这一章不应该套上这么大的标题:《测试驱动开发》,因为在实际开发过程中所谓测试驱动开发要使用更为强大更为复杂的模块框架和工具比如起码使用 Python 内建库中的 unittest 模块

在写程序的过程中为别人和将来的自己写注释 Docstring;在写程序的过程中为了保障程序的结果全面正确而写测试或者干脆在最初写的时候就考虑到各种意外所以使用试错语句块 —— 这些明明是天经地义的事情却是绝大多数人不做的…… 因为感觉有点麻烦

这里是聪明反被聪明误的最好示例长期堆积的地方很多人真的是因为自己很聪明所以才觉得没必要麻烦” —— 这就好像当年苏格拉底仗着自己记忆力无比强大甚至干脆过目不忘于是鄙视一切记笔记的人一样

但是随着时间的推移随着工程量的放大到最后那些聪明人都被自己坑死了 —— 聪明本身搞不定工程能搞定工程的是智慧苏格拉底自己并没完成任何工程是他的学生柏拉图不顾他的嘲笑用纸笔记录了一切而后柏拉图的学生亚里士多德才有机会受到苏格拉底的启发写了前分析篇》,提出对人类影响至今的三段论”……

千万不要因为这第二部分中所举的例子太容易而把自己迷惑了刻意选择简单的例子放在这里是为了让读者更容易集中精力去理解关于自己动手写函数的方方面面 —— 可将来你自己真的动手去做哪怕真的去阅读真实的工程代码你就会发现难度还是很高的现在的轻敌会造成以后的溃败

现在还不是时候等你把整本书都完成之后记得回来再看这个链接