深入Python3(九) 重构

0.摘要

  本章继续深入前一章节的例子,通过更改需求—修改代码—重构这一流程让你逐步意识到单元测试的意义,以及它对重构的重要性。

1.新的错误

  就算是竭尽了全力编写全面的单元测试,还是会遇到错误。我所说的“错误”是什么意思?错误是尚未写到的测试实例。
在这里插入图片描述
在这里插入图片描述

   这段代码非常简单。通过传入一个空字符串调用 f r o m _ r o m a n ( ) from\_roman() from_roman() ,并确保其引发一个 I n v a l i d R o m a n N u m e r a l E r r o r InvalidRomanNumeralError InvalidRomanNumeralError 例外。难的是发现错误;找到了该错误之后对它进行测试是件轻松的工作。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  用此方式编写代码将使得错误修正变得更困难。简单的错误(像这个)需要简单的测试实例;复杂的错误将会需要复杂的测试实例。在以测试为中心的环境中,由于必须在代码中精确地描述错误(编写测试实例),然后修正错误本身,看起来好像修正错误需要更多的时间。而如果测试实例无法正确地通过,则又需要找出到底是修正方案有错误,还数测试实例本身就有错误。然而从长远看,这种在测试代码和经测试代码之间的来回折腾是值得的,因为这样才更有可能在第一时间修正错误。同时,由于可以对新代码轻松地重新运行 所有 测试实例,在修正新代码时破坏旧代码的机会更低。今天的单元测试就是明天的回归测试。

2.控制需求变化

  为了获取准确的需求,尽管已经竭力将客户“钉”在原地,并经历了反复剪切、粘贴的痛苦,但需求仍然会变化。大多数客户在看到产品之前不知道自己想要什么,而且就算知道,他们也不擅长清晰地表述自己的想法。而即便擅长表述,他们在下一个版本中也会提出更多要求。因此,必须随时准备好更新测试实例以应对需求变化。
  举个例子来说,假定我们要扩展罗马数字转换函数的能力范围。正常情况下,罗马数字中的任何一个字符在同一行中不得重复出现三次以上。但罗马人却愿意该规则有个例外:通过一行中的 4 个 M 字符来代表 4000 。进行该修改后,将会把可转换数字的范围从 1…3999 拓展为 1…4999。但首先必须对测试实例进行一些修改。
在这里插入图片描述
在这里插入图片描述

  现有的已知数值不会变(它们依然是合理的测试数值),但必须在 4000 范围之内(外)增加一些。在此,我已经添加了 4000 (最短)、 4500 (第二短)、 4888 (最长) 和 4999 (最大)。
  “过大值输入” 的定义已经发生了变化。该测试用于通过传入 4000 调用 to_roman() 并期望引发一个错误;目前 4000-4999 是有效的值,必须将该值调整为 5000 。
  “太多重复数字”的定义也发生了变化。该测试通过传入 ‘MMMM’ 调用 from_roman() 并预期发生一个错误;目前 MMMM 被认定为有效的罗马数字,必须将该条件修改为 ‘MMMMM’ 。
  对范围内的每个数字进行完整循环测试,从 1 到 3999。由于范围已经进行了拓展,该 for 循环同样需要修改为以 4999 为上限。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  现在,我们有了一些由新需求导致失败的测试实例,可以考虑修正代码让它与新测试实例一致起来(刚开始编写单元测试的时候,被测试代码绝不会在测试实例“之前”出现确实让人感觉有点怪)。尽管编码工作被置后安排,但还是不少要做的事情,一旦与测试实例相符,编码工作就可以结束了。一旦习惯单元测试后,您可能会对自己曾在编程时不进行测试感到很奇怪。
在这里插入图片描述
在这里插入图片描述

3.重构

  关于全面单元测试,最美妙的事情不是在所有的测试实例通过后的那份心情,也不是别人抱怨你破坏了代码,而你通过实践证明自己没有时的快感。单元测试最美妙之处在于它给了你大刀阔斧进行重构的自由。
  重构是修改可运作代码,使其表现更佳的过程。通常,“更佳”指的是“更快”,但它也可能指的是“占用更少内存“、”占用更少磁盘空间“或者”更加简洁”。对于你的环境、你的项目来说,无论重构意味着什么,它对程序的长期健康都至关重要。
  本例中,“更佳”的意思既包括“更快”也包括“更易于维护”。具体而言,因为用于验证罗马数字的正则表达式生涩冗长,该 f r o m _ r o m a n ( ) from\_roman() from_roman() 函数比我所希望的更慢,也更加复杂。现在,你可能会想,“当然,正则表达式就又臭又长的,难道我有其它办法验证任意字符串是否为罗马数字吗?”
  答案是:只针对 5000 个数进行转换;为什么不知建立一个查询表呢?意识到 根本不需要使用正则表达式 之后,这个主意甚至变得更加理想了。在建立将整数转换为罗马数字的查询表的同时,还可以建立将罗马数字转换为整数的逆向查询表。在需要检查任意字符串是否是有效罗马数字的时候,你将收集到所有有效的罗马数字。“验证”工作简化为一个简单的字典查询。
  最棒的是,你已经有了一整套单元测试。可以修改模块中一半以上的代码,而单元测试将会保持不变。这意味着可以向你和其他人证明:新代码运作和最初的一样好。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  可以注意到这是一次函数调用,但没有 i f if if 语句包裹住它。这不是 i f   _ _ n a m e _ _   = = ′ _ _ m a i n _ _ ′ if\ \_\_name\_\_\ == '\_\_main\_\_' if __name__ ==__main__ 语块;模块被导入时它将会被调用(重要的是必须明白:模块将只被导入一次,随后被缓存了。如果导入一个已导入模块,将不会导致任何事情发生。因此这段代码将只在第一次导入时运行)。
在这里插入图片描述

  这是一段聪明的程序代码……也许过于聪明了。上面定义了 t o _ r o m a n ( ) to\_roman() to_roman() 函数;它在查询表中查找值并返回结果。而 b u i l d _ l o o k u p _ t a b l e s ( ) build\_lookup\_tables() build_lookup_tables() 函数重定义 t o _ r o m a n ( ) to\_roman() to_roman() 函数用于实际操作(像添加查询表之前的例子一样)。在 b u i l d _ l o o k u p _ t a b l e s ( ) build\_lookup\_tables() build_lookup_tables() 函数内部,对 t o _ r o m a n ( ) to\_roman() to_roman() 的调用将会针对该重定义的版本。一旦 b u i l d _ l o o k u p _ t a b l e s ( ) build\_lookup\_tables() build_lookup_tables() 函数退出,重定义的版本将会消失 — 它的定义只在 b u i l d _ l o o k u p _ t a b l e s ( ) build\_lookup\_tables() build_lookup_tables() 函数的作用域内生效。
在这里插入图片描述

  像前面那样进行同样的边界检查之后, t o _ r o m a n ( ) to\_roman() to_roman() 函数只需在查询表中查找并返回适当的值。
  同样, f r o m _ r o m a n ( ) from\_roman() from_roman() 函数也缩水为一些边界检查和一行代码。不再有正则表达式。不再有循环。
在这里插入图片描述

  它不仅能够回答你的问题,还运行得非常快!好象速度提升了 10 倍。当然,这种比较并不公平,因为此版本在导入时耗时更长(在建造查询表时)。但由于只进行一次导入,启动的成本可以由对 t o _ r o m a n ( ) to\_roman() to_roman() f r o m _ r o m a n ( ) from\_roman() from_roman() 函数的所有调用摊薄。由于该测试进行几千次函数调用(来回单独测试上万次),节省出来的效率成本得以迅速提升!
在这里插入图片描述

  在学完这一章后我也稍微认识到了单元测试的重要性……确实如果没有足够的测试用例的话,对代码重构是一件很头疼的事情,我会非常纠结新代码和旧代码的功能是否一致……这甚至会影响重构代码的效率。然而在拥有完备的单元测试的情况下,你可以放心的去重构代码,因为可以迅速的检验新旧代码功能的一致性。

4.总结

  单元测试是一个威力强大的概念,如果正确实施,不但可以降低维护成本,还可以提高长期项目的灵活性。但同时还必须明白:单元测试既不是灵丹妙药,也不是解决问题的魔术,更不是银弹。编写良好的测试实例非常艰难,确保它们时刻保持最新必须成为一项纪律(特别在客户要求关键错误修正时)。单元测试不是功能测试、集成测试或用户承受能力测试等其它测试的替代品。但它是可行的、行之有效的,见识过其功用后,你将对之前曾没有用它而感到奇怪。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
深入 Python 3 的内容涵盖了 Python 3 及其与 Python 2 的区别。Python 3 提供了一个脚本叫做 2to3。学习它。喜欢它。使用它。用 2to3 移植代码到 Python 3 是一个有关 2to3 工具能够自动整理的所有东西的参考手册。很多这些东西都是语法的变更,因此了解 Python 3 里面许多的语法变更是一个好的起点。(print 现在是一个函数,`x` 不能使用,等等。) 在 HTTP Web 服务这章,httplib2 模块通过 HTTP 获取头信息和数据。HTTP头信息返回的是字符串,而 HTTP 正文则返回的是字节。 在序列化 Python 对象这章,你将了解到为什么 Python 3 里面的 pickle 模块定义了一个和 Python 2 向后不兼容的新的数据类型。(提示:这就是因为字节和字符串的原因。) 同样 JSON也根本不支持字节类型。我将向你展示如何解决这个问题。 在案例分析:移植 chardet 到 Python 3这章,到处都是一大堆一大堆关于字节和字符串的东西。 即使你不关心 Unicode (但实际上你会的),你也会想阅读一下 Python 3 里面的字符串格式,这和 Python 2 里面的完全不一样。 Python 的专家们聪明的把 ElementTree 变成了标准库的一部分 ,然后现在它构成了新的 XML 章节的基础。解析 XML 的那些老的方式仍然可用,但是你应该避免使用它们,因为他们很糟糕! 除此之外,还有个关于 Python 的新东西 — 不是语言上的,而是社区中的 — 像 Python 包装索引(PyPI) 的出现。Python 提供了实 用工具类用来将你的代码打包成标准格式,并分发那些包到PyPI 中。阅读 打包 Python 库了解详细信息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值