澳门新浦京手机版Python中何时使用断言 assert

本文由码农网 –
邱康原创翻译,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划!

 

练习27:创造性和防御性编程

原文:Exercise 27: Creative And Defensive
Programming

译者:飞龙

你已经学到了大多数C语言的基础,并且准备好开始成为一个更严谨的程序员了。这里就是从初学者走向专家的地方,不仅仅对于C,更对于核心的计算机科学概念。我将会教给你一些核心的数据结构和算法,它们是每个程序员都要懂的,还有一些我在真实程序中所使用的一些非常有趣的东西。

在我开始之前,我需要教给你一些基本的技巧和观念,它们能帮助你编写更好的软件。练习27到31会教给你高级的概念和特性,而不是谈论编程,但是这些之后你将会应用它们来编写核心库或有用的数据结构。

编写更好的C代码(实际上是所有语言)的第一步是,学习一种新的观念叫做“防御性编程”。防御性编程假设你可能会制造出很多错误,之后尝试在每一步尽可能预防它们。这个练习中我打算教给你如何以防御性的思维来思考编程。

菲纳格动态逆定律:

 

创造性编程思维

在这个简单的练习中要告诉你如何做到创造性是不可能的,但是我会告诉你一些涉及到任务风险和开放思维的创造力。恐惧会快速地扼杀创造力,所以我采用,并且许多程序员也采用的这种思维方式使我不会惧怕风险,并且看上去像个傻瓜。

  • 我不会犯错误。
  • 人们所想的并不重要。
  • 我脑子里面诞生的想法才是最好的。

我只是暂时接受了这种思维,并且在应用中用了一些小技巧。为了这样做我会提出一些想法,寻找创造性的解决方案,开一些奇奇怪怪的脑洞,并且不会害怕发明一些古怪的东西。在这种思维方式下,我通常会编写出第一个版本的糟糕代码,用于将想法描述出来。

然而,当我完成我的创造性原型时,我会将它扔掉,并且将它变得严谨和可考。其它人在这里常犯的一个错误就是将创造性思维引入它们的实现阶段。这样会产生一种非常不同的破坏性思维,它是创造性思维的阴暗面:

  • 编写完美的软件是可行的。
  • 我的大脑告诉我了真相,它不会发现任何错误,所以我写了完美的软件。
  • 我的代码就是我自己,批判它的人也在批判我。

这些都是错误的。你经常会碰到一些程序员,它们对自己创造的软件具有强烈的荣誉感。这很正常,但是这种荣誉感会成为客观上改进作品的阻力。由于这种荣誉感和它们对作品的依恋,它们会一直相信它们编写的东西是完美的。只要它们忽视其它人的对这些代码的观点,它们就可以保护它们的玻璃心,并且永远不会改进。

同时具有创造性思维和编写可靠软件的技巧是,采用防御性编程的思维。

会出错的,终将会出错 —-  在最糟糕的时刻。

 

防御性编程思维

在你做出创造性原型,并且对你的想法感觉良好之后,就应该切换到防御性思维了。防御性思维的程序员大致上会否定你的代码,并且相信下面这些事情:

  • 软件中存在错误。
  • 澳门新浦京手机版 ,你并不是你的软件,但你需要为错误负责。
  • 你永远不可能消除所有错误,只能降低它们的可能性。

这种思维方式让你诚实地对待你的代码,并且为改进批判地分析它。注意上面并没有说充满了错误,只是说你的代码充满错误。这是一个需要理解的关键,因为它给了你编写下一个实现的客观力量。

就像创造性思维,防御性编程思维也有阴暗面。防御性程序员是一个惧怕任何事情的偏执狂,这种恐惧使他们远离可能的错误或避免犯错误。当你尝试做到严格一致或正确时这很好,但是它是创造力和专注的杀手。

防御性编程是什么意思

防御性编程,简单的说,就是在编程的时候有目的地预测可能的故障点。目的是在那些可能发生的问题发生前解决它们。你看见了问题,对吧?预测意料之外的事情本来就有内在的难度,当你想要预测意料之外的事情并且解决它就更是难上了好几倍。

下面我们看几个实际的例子。

澳门新浦京手机版 1

使用断言的最佳时机偶尔会被提起,通常是因为有人误用,因此我觉得有必要写一篇文章来阐述一下什么时候应该用断言,为什么应该用,什么时候不该用。

八个防御性编程策略

一旦你接受了这一思维,你可以重新编写你的原型,并且遵循下面的八个策略,它们被我用于尽可能把代码变得可靠。当我编写代码的“实际”版本,我会严格按照下面的策略,并且尝试消除尽可能多的错误,以一些会破坏我软件的人的方式思考。

永远不要信任输入

永远不要提供的输入,并总是校验它。

避免错误

如果错误可能发生,不管可能性多低都要避免它。

过早暴露错误

过早暴露错误,并且评估发生了什么、在哪里发生以及如何修复。

记录假设

清楚地记录所有先决条件,后置条件以及不变量。

防止过多的文档

不要在实现阶段就编写文档,它们可以在代码完成时编写。

使一切自动化

使一切自动化,尤其是测试。

简单化和清晰化

永远简化你的代码,在没有牺牲安全性的同时变得最小和最整洁。

质疑权威

不要盲目遵循或拒绝规则。

这些并不是全部,仅仅是一些核心的东西,我认为程序员应该在编程可靠的代码时专注于它们。要注意我并没有真正说明如何具体做到这些,我接下来会更细致地讲解每一条,并且会布置一些覆盖它们的练习。

条件语句

这是最容易进行防御性编程的地方之一,也是最容易满足的地方。在用PHP编程的许多情况下你不会需要“else”。

假设,你在写一个函数并且需要一个条件语句。在这里,你只需要为你特定的变量使用三个条件语句如下:

if($var == a){ }
else if($var == b){ }
else if($var == c){ }

没有其他可能性了,你说,并且继续码代码。但是,让我们在这里停一下。我知道你知道这里没有其他可能性了。并且我相信你。但有时候(不可预测的)情况会发生。我们忘掉了一些情况。我们检查错误。我们最终重用了一些代码,超出了原本的预定范围。突然我们有了泄露错误或者有时候是静默的错误状态,因为我们没有使用catch。使用else代码块。在使用switch时要使用default。用它们来返回或者记录错误,这样你才知道发生了什么(如果发生了的话)。虽然会多用两行代码,但当一些你无法预测的事情发生时,这是值得的。

对那些没有意识到用断言的最佳时机的人来说,Python的断言就是检测一个条件,如果条件为真,它什么都不做;反之它触发一个带可选错误信息的AssertionError。如下例所示:

应用这八条策略

这些观点都是一些流行心理学的陈词滥调,但是你如何把它们应用到实际编程中呢?我现在打算向你展示这本书中的一些代码所做的事情,这些代码用具体的例子展示每一条策略。这八条策略并不止于这些例子,你应该使用它们作为指导,使你的代码更可靠。

绝不相信用户输入

你以前有没有听说过这个说法?大多数程序员听过。这有一点含糊,通俗点讲,理所当然。但它是真理。你绝不应该相信用户输入。这不是说你假设所有用户是疯狂的黑客,他们使用一些精心设计的命令来摧毁你的应用。没有必要妄想。但是,你应该假设用户不知道你的代码,他们不知道你需要填写什么参数,或者参数应该多长。他们不知道什么文件类型或者什么大小能上传(即使应用告诉了他们)。偶尔他们会是机器或者黑客并且他们希望在他们的输入中运行脚本,有时候甚至是在登陆后的输入中。你怎么知道你能相信认证或者验证码能在用户输入之前提供一个安全的堡垒?

答案:绝不。

你绝不相信用户输入。如果你信任的用户输入,那么你永远不会有一个突破。明白了吗?所以总是要评估你的输入,一定要保证你在处理数据尤其是要存入数据库或者要把它展示出来时使用了合适的技术。因此 – 绝不相信输入,即使来自不是用户的输入的地方 – 输入验证永远是你的朋友。看看Survive the Deep End: PHP Security 并且使用 validation library.吧。

澳门新浦京手机版 2

永远不要信任输入

让我们来看一个坏设计和“更好”的设计的例子。我并不想称之为好设计,因为它可以做得更好。看一看这两个函数,它们都复制字符串,main函数用于测试哪个更好。

undef NDEBUG
#include "dbg.h"
#include <stdio.h>
#include <assert.h>

/*
 * Naive copy that assumes all inputs are always valid
 * taken from K&R C and cleaned up a bit.
 */
void copy(char to[], char from[])
{
    int i = 0;

    // while loop will not end if from isn't '' terminated
    while((to[i] = from[i]) != '') {
        ++i;
    }
}

/*
 * A safer version that checks for many common errors using the
 * length of each string to control the loops and termination.
 */
int safercopy(int from_len, char *from, int to_len, char *to)
{
    assert(from != NULL && to != NULL && "from and to can't be NULL");
    int i = 0;
    int max = from_len > to_len - 1 ? to_len - 1 : from_len;

    // to_len must have at least 1 byte
    if(from_len < 0 || to_len <= 0) return -1;

    for(i = 0; i < max; i++) {
        to[i] = from[i];
    }

    to[to_len - 1] = '';

    return i;
}


int main(int argc, char *argv[])
{
    // careful to understand why we can get these sizes
    char from[] = "0123456789";
    int from_len = sizeof(from);

    // notice that it's 7 chars + 
    char to[] = "0123456";
    int to_len = sizeof(to);

    debug("Copying '%s':%d to '%s':%d", from, from_len, to, to_len);

    int rc = safercopy(from_len, from, to_len, to);
    check(rc > 0, "Failed to safercopy.");
    check(to[to_len - 1] == '', "String not terminated.");

    debug("Result is: '%s':%d", to, to_len);

    // now try to break it
    rc = safercopy(from_len * -1, from, to_len, to);
    check(rc == -1, "safercopy should fail #1");
    check(to[to_len - 1] == '', "String not terminated.");

    rc = safercopy(from_len, from, 0, to);
    check(rc == -1, "safercopy should fail #2");
    check(to[to_len - 1] == '', "String not terminated.");

    return 0;

error:
    return 1;
}

copy函数是典型的C代码,而且它是大量缓冲区溢出的来源。它有缺陷,因为它总是假设接受到的是合法的C字符串(带有''),并且只是用一个while循环来处理。问题是,确保这些是十分困难的,并且如果没有处理好,它会使while循环无限执行。编写可靠代码的一个要点就是,不要编写可能不会终止的循环。

safecopy函数尝试通过要求调用者提供两个字符串的长度来解决问题。它可以执行有关这些字符串的、copy函数不具备的特定检查。他可以保证长度正确,to字符串具有足够的容量,以及它总是可终止。这个函数不像copy函数那样可能会永远执行下去。

这个就是永远不信任输入的实例。如果你假设你的函数要接受一个没有终止标识的字符串(通常是这样),你需要设计你的函数,不要依赖字符串本身。如果你想让参数不为NULL,你应该对此做检查。如果大小应该在正常范围内,也要对它做检查。你只需要简单假设调用你代码的人会把它弄错,并且使他们更难破坏你的函数。

这个可以扩展到从外部环境获取输入的的软件。程序员著名的临终遗言是,“没人会这样做。”我看到他们说了这句话后,第二天有人就这样做,黑掉或崩溃它们的应用。如果你说没有人会这样做,那就加固代码来保证他们不会简单地黑掉你的应用。你会因所做的事情而感到高兴。

这种行为会出现收益递减。下面是一个清单,我会尝试对我用C写的每个函数做如下工作:

  • 对于每一个参数定义它的先决条件,以及这个条件是否导致失效或返回错误值。如果你在编写一个库,比起失效要更倾向于错误。
  • 对于每个先决条件,使用assert(test && "message");在最开始添加assert检查。这句代码会执行检查,失败时OS通常会打印断言行,通常它包括信息。当你尝试弄清assert为什么在这里时,这会非常有用。
  • 对于其它先决条件,返回错误代码或者使用我的check宏来执行它并且提供错误信息。我在这个例子中没有使用check,因为它会混淆比较。
  • 记录为什么存在这些先决条件,当一个程序员碰到错误时,他可以弄清楚这些是否是真正必要的。
  • 如果你修改了输入,确保当函数退出或中止时它们也会正确产生。
  • 总是要检查所使用的函数的错误代码。例如,人们有时会忘记检查fopenfread的返回代码,这会导致他们在错误下仍然使用这个资源。这会导致你的程序崩溃或者易受攻击。
  • 你也需要返回一致的错误代码,以便对你的每个函数添加相同的机制。一旦你熟悉了这一习惯,你就会明白为什么我的check宏这样工作。

只是这些微小的事情就会改进你的资源处理方式,并且避免一大堆错误。

对你的代码的假设

不要假设任何事情。如果前两个主题教会我们一些事情的话,那就是我们不应该做任何假设。作为程序员,尤其是致力于一个项目太久后,我们开始做很多假设。我们假设用户知道一些我们知道的事情。不一定是技术细节,也可以是程序的功能性细节。我们假设用户知道文件能有多大因为。。。我们已经知道。或者他们知道为了让邮件脚本。。。但事实不是,他们不知道以上任何东西。这好像更多的是前端的工作,但明显的是你在后端仍然要处理用户行为和用户输入,所以值得好好想想。

另一个许多程序员都会做的惊人的假设是我们在任何时候对于我们的函数,类或者其它代码段的明显的功能属性。一个具有防御性的程序员会仔细考虑的不仅仅是用一般的文档来描述函数是干什么的——他们也将写下他们对输入,参数,用例,或任何其他类似的东西做出的任何假设。因为我们都是人,我们过一段时间会忘掉一些事。我们最后也很可能会面临其他人维护,扩展或者替换我们的代码。如果没有别的,回想一下,编程是发生在一个充满技术变革的世界里。如果你的应用仍然能使用几年,可能会升级PHP版本并且失去一些功能,或者一些你自己代码里面具有交互的组件之间需要改变。预测这些是很困难的,所以好的注释和文档是非常重要的。

很多人将断言作为当传递了错误的参数值时的一种快速而简便的触发异常的方式。但实际上这是错误的,而且是非常危险的错误,原因有两点。首先,AssertionError通常是在测试函数参数时给出的错误。你不会像下面这样编码:

避免错误

上一个例子中你可能会听到别人说,“程序员不会经常错误地使用copy。”尽管大量攻击都针对这类函数,他们仍旧相信这种错误的概率非常低。概率是个很有趣的事情,因为人们不擅长猜测所有事情的概率,这非常难以置信。然而人们对于判断一个事情是否可能,是很擅长的。他们可能会说copy中的错误不常见,但是无法否认它可能发生。

关键的原因是对于一些常见的事情,它首先是可能的。判断可能性非常简单,因为我们都知道事情如何发生。但是随后判断出概率就不是那么容易了。人们错误使用copy的情况会占到20%、10%,或1%?没有人知道。为了弄清楚你需要收集证据,统计许多软件包中的错误率,并且可能需要调查真实的程序员如何使用这个函数。

这意味着,如果你打算避免错误,你不需要尝试避免可能发生的事情,而是要首先集中解决概率最大的事情。解决软件所有可能崩溃的方式并不可行,但是你可以尝试一下。同时,如果你不以最少的努力解决最可能发生的事件,你就是在不相关的风险上浪费时间。

下面是一个决定避免什么的处理过程:

  • 列出所有可能发生的错误,无论概率大小,并带着它们的原因。不要列出外星人可能会监听内存来偷走密码这样的事情。
  • 评估每个的概率,使用危险行为的百分比来表示。如果你处理来自互联网的情况,那么则为可能出现错误的请求的百分比。如果是函数调用,那么它是出现错误的函数调用百分比。
  • 评估每个的工作量,使用避免它所需的代码量或工作时长来表示。你也可以简单给它一个“容易”或者“难”的度量。当需要修复的简单错误仍在列表上时,任何这种度量都可以让你避免做无谓的工作。
  • 按照工作量(低到高)和概率(高到低)排序,这就是你的任务列表。
  • 之后避免你在列表中列出的任何错误,如果你不能消除它的可能性,要降低它的概率。
  • 如果存在你不能修复的错误,记录下来并提供给可以修复的人。

这一微小的过程会产生一份不错的待办列表。更重要的是,当有其它重要的事情需要解决时,它让你远离劳而无功。你也可以更正式或更不正式地处理这一过程。如果你要完成整个安全审计,你最好和团队一起做,并且有个更详细的电子表格。如果你只是编写一个函数,简单地复查代码之后划掉它们就够了。最重要的是你要停止假设错误不会发生,并且着力于消除它们,这样就不会浪费时间。

视野狭窄

另一件可以使我们忘记好的评论实践以及标准的东西是视野狭窄。许多程序员都具有视野狭窄的毛病。你知道这种感觉 – 你解决问题,你处于最佳状态。你觉得与你的音乐(或没有)独立于自己的小世界中,并且你就在编码,突然两小时过了,你意识到你已经写了无数行没有注释的代码。我们所有人偶尔都会遇到这种事情,但重要的是在某处发现这个情况并且补上应有的注释。

澳门新浦京手机版 3

过早暴露错误

如果你遇到C中的错误,你有两个选择:

  • 返回错误代码。
  • 中止进程。

这就是处理方法,你需要执行它来确保错误尽快发生,记录清楚,提供错误信息,并且易于程序员来避免它。这就是我提供的check宏这样工作的原因。对于每一个错误,你都要让它你打印信息、文件名和行号,并且强制返回错误代码。如果你使用了我的宏,你会以正确的方式做任何事情。

我倾向于返回错误代码而不是终止程序。如果出现了大错误我会中止程序,但是实际上我很少碰到大错误。一个需要中止程序的很好例子是,我获取到了一个无效的指针,就像safecopy中那样。我没有让程序在某个地方产生“段错误”,而是立即捕获并中止。但是,如果传入NULL十分普遍,我可能会改变方式而使用check来检查,以保证调用者可以继续运行。

然而在库中,我尽我最大努力永不中止。使用我的库的软件可以决定是否应该中止。如果这个库使用非常不当,我才会中止程序。

最后,关于“暴露”的一大部分内容是,不要对多于一个错误使用相同的信息或错误代码。你通常会在外部资源的错误中见到这种情况。比如一个库捕获了套接字上的错误,之后简单报告“套接字错误”。它应该做的是返回具体的信息,比如套接字上发生了什么错误,使它可以被合理地调试和修复。当你设计错误报告时,确保对于不同的错误你提供了不同的错误消息。

语法和命名的一致性

一致性是一个灰色地带 – 它更多的是关于编码标准之类的,但它和防御性编程也有联系。在PHP中,有标准规范你的代码格式以便别人查看,或者你以后使用。但常常没人让你的代码标准化。但是无论你是否按照标准编码,你至少要保持一致性 – 这能让你少犯错误。这对于需要大量时间返回并且修复的小的语法错误尤其适用。如果你总是使用相同的间隔,格式和语法,命名规格等等你就能更好的避免犯错以至于误读你自己的代码。你更可能快速浏览代码并且找到你需要的东西。

你应该用TypeError来替代,“断言”解决了错误的异常类型。

记录假设

如果你遵循并执行了这个建议,你就构建了一份“契约”,关于函数期望这个世界是什么样子。你已经为每个参数预设了条件,处理潜在的错误,并且优雅地产生失败。下一步是完善这一契约,并且添加“不变量”和“后置条件”。

不变量就是在函数运行时,一些场合下必须恒为真的条件。这对于简单的函数并不常见,但是当你处理复杂的结构时,它会变得很必要。一个关于不变量的很好的例子是,结构体在使用时都会合理地初始化。另一个是有序的数据结构在处理时总是排好序的。

后置条件就是退出值或者函数运行结果的保证。这可以和不变了混在一起,但是也可以是一些很简单的事情,比如“函数应总是返回0,或者错误时返回-1”。通常这些都有文档记录,但是如果你的函数返回一个分配的资源,你应该添加一个后置条件,做检查来确保它返回了一个不为NULL的东西。或者,你可以使用NULL来表示错误,这种情况下,你的后置条件就是资源在任何错误时都会被释放。

在C编程中,不变量和后置条件都通常比实际的代码和断言更加文档化。处理它们的最好当时就是尽可能添加assert调用,之后记录剩下的部分。如果你这么做了,当其它人碰到错误时,他们可以看到你在编写函数时做了什么假设。

总结

总的来说,除去用户行为和动作,不要对你的程序做任何假设。假设是具有防御性编程习惯的程序员最大的敌人。不要假设你不需要 default 语句或者 else 代码块。尽量使用正确的用户错误信息,警告,日志或者任何其它你假设不会用到的代码。你的假设通常是正确的 – 但我们不在乎。我们在乎的是它们出错的时候。一定要计划得好,准备着你可能需要在几小时,几周,几个月甚至几年后回顾你的代码,或者其他人需要 – 相应的就要好好写文档。别假设它永远不需要升级,扩展或者维护。那是无知的,在更多的情况下是疏忽。有时候保持一颗防御性编程的心能帮你更有效更安全地估计,计划和编程。

但是对断言来说更危险也更纠结的是:如果你执行Python时使用了-O或-OO优化标识,这能够通过编译却从来不会被执行,实际上就是说并不能保证断言会被执行。当恰当地使用了断言,这非常好的,但当不恰当地使用了断言,在使用-O标识执行时它将导致代码被彻底中断。

避免过多文档

程序员编写代码时的一个普遍问题,就是他们会记录一个普遍的bug,而不是简单地修复它。我最喜欢的方式是,Ruby
on
Rails系统只是简单地假设所有月份都有30天。日历太麻烦了,所以与其修复它,不如在一些地方放置一个小的注释,说这是故意的,并且几年内都不会改正。每次一些人试图抱怨它时,他们都会说,“文档里面都有!”

如果你能够实际修复问题,文档并不重要,并且,如果函数具有严重的缺陷,你在修复它之前可以不记录它。在Ruby
on
Rails的例子中,不包含日期函数会更好一些,而不是包含一个没人会用的错误的函数。

当你为防御性编程执行清理时,尽可能尝试修复任何事情。如果你发现你记录了越来越多的,你不能修复的事情,需要考虑重新设计特性,或简单地移除它。如果你真的需要保留这一可怕的错误的特性,那么我建议你编写它、记录它,并且在你受责备之前找一份新的工作。

那么我们什么时候应该使用断言呢?如果没有特别的目的,断言应该用于如下情况:

使一切自动化

你是个程序员,这意味着你的工作是通过自动化消灭其它人的工作。它的终极目标是使用自动化来使你自己也失业。很显然你不应该完全消除你做的东西,但如果你花了一整天在终端上重复运行手动测试,你的工作就不是编程。你只是在做QA,并且你应该使自己自动化,消除这个你可能并不是真的想干的QA工作。

实现它的最简单方式就是编写自动化测试,或者单元测试。这本书里我打算讲解如何使它更简单,并且我会避免多数编写测试的信条。我只会专注于如何编写它们,测试什么,以及如何使测试更高效。

下面是程序员没有但是应该自动化的一些事情:

  • 测试和校验。
  • 构建过程。
  • 软件部署。
  • 系统管理。
  • 错误报告。

尝试花一些时间在自动化上面,你会有更多的时间用来处理一些有趣的事情。或者,如果这对你来说很有趣,也许你应该编写自动化完成这些事情的软件。

  • 防御性的编程
  • 运行时对程序逻辑的检测
  • 合约性检查(比如前置条件,后置条件)
  • 程序中的常量
  • 检查文档

简单化和清晰化

“简单性”的概念对许多人来说比较微妙,尤其是一些聪明人。它们通常将“内涵”与“简单性”混淆起来。如果他们很好地理解了它,很显然非常简单。简单性的测试是通过将一个东西与比它更简单的东西比较。但是,你会看到编写代码的人会使用最复杂的、匪夷所思的数据结构,因为它们认为做同样事情的简单版本非常“恶心”。对复杂性的爱好是程序员的弱点。

你可以首先通过告诉自己,“简单和清晰并不恶心,无论谁在干什么事情”来战胜这一弱点。如果其它人编写了愚蠢的观察者模式涉及到19个类,12个接口,而你只用了两个字符串操作就可以实现它,那么你赢了。他们就是错了,无论他们认为自己的复杂设计有多么高大上。

对于要使用哪个函数的最简单测试是:

  • 确保所有函数都没有问题。如果它有错误,它有多快或多简单就不重要了。
  • 如果你不能修复问题,就选择另外一个。
  • 它们会产生相同结果嘛?如果不是就挑选具有所需结果的函数。
  • 如果它们会产生相同结果,挑选包含更少特性,更少分支的那个,或者挑选你认为最简单的那个。
  • 确保你没有只是挑选最具有表现力的那个。无论怎么样,简单和清晰,都会战胜复杂和恶心。

你会注意到,最后我一般会放弃并告诉你根据你的判断。简单性非常讽刺地是一件复杂的事情,所以使用你的品位作为指引是最好的方式。只需要确保在你获取更多经验之后,你会调整你对于什么是“好”的看法。

(断言也可以用于代码测试,用作一个做事毛手毛脚的开发人员的单元测试,只要能你接受当使用-O标志时这个测试什么都不做。我有时也会在代码中用”assert
Fasle”来对还没有实现的分支作标记,当然我希望他们失败。如果稍微更细节一些,或许触发NotImplementedError是更好的选择)

质疑权威

最后一个策略是最重要的,因为它让你突破防御性编程思维,并且让你转换为创造性思维。防御性编程是权威性的,并且比较无情。这一思维方式的任务是让你遵循规则,因为否则你会错失一些东西或心烦意乱。

这一权威性的观点的坏处是扼杀了独立的创造性思维。规则对于完成事情是必要的,但是做它们的奴隶会扼杀你的创造力。

这条最后的策略的意思是你应该周期性地质疑你遵循的规则,并且假设它们都是错误的,就像你之前复查的软件那样。在一段防御性编程的时间之后,我通常会这样做,我会拥有一个不编程的休息并让这些规则消失。之后我会准备好去做一些创造性的工作,或按需做更多的防御型编程。

因为程序员是对于代码正确性表现出的信心不同,因此对于什么时候使用断言的意见各不相同。如果你确信代码是正确的,那么断言没有任何意义,因为它们从不会失败,因此你可以放心地移除它们。如果你确信它们会失败(例如对用户输入的数据的检测),你不敢用断言,这样编译就能通过,但你跳过了你的检查。

顺序并不重要

在这一哲学上我想说的最后一件事,就是我并不是告诉你要按照一个严格的规则,比如“创造!防御!创造!防御!”去做这件事。最开始你可能想这样做,但是我实际上会做不等量的这些事情,取决于我想做什么,并且我可能会将二者融合到一起,没有明确的边界。

我也不认为其中一种思维会优于另一种,或者它们之间有严格的界限。你需要在编程上既有创造力也要严格,所以如果想要提升的话,需要同时做到它们。

在以上两种情况之间的情况就显得特别有趣了,那就是当你相信代码是正确的,但又不是特别确定的时候。或许你忘记了一些奇怪的边角情况(因为我们都是人),在这种情况下,额外的运行时检查将帮助你尽可能早地捕获错误,而不是写了一大堆代码之后。

附加题

  • 到现在为止(以及以后)书中的代码都可能违反这些规则。回退并挑选一个练习,将你学到的应用在它上面,来看看你能不能改进它或发现bug。
  • 寻找一个开源项目,对其中一些文件进行类似的代码复查。如果你发现了bug,提交一个补丁来修复它。

(这就是为什么使用断言的时机会不同。因为我们对代码正确性的信息不同,对于一个人有用的断言,对于另一个人来说却是无用的运行时测试。)

另一个断言用得好的地方就是检查程序中的不变量。一个不变量是一些你能相信为真的条件,除非一个缺陷导致它变成假。如果有一个缺陷,越早发现越好,因此我们需要对其进行测试,但我们不想因为这些测试而影响代码执行速度。因此采用断言,它能在开发时生效而在产品中失效。

一个关于不变量的例子可能是这样的情况。如果你的函数在开始的时候期望一个打开的数据库连接,并且在函数返回后该数据库连接依然是打开的,这是一个函数的不变量:

澳门新浦京手机版 4

断言也是一个很好的检查点注释。为了替代如下注释:

#当我们执行到这里,我们知道n>2

你可以确保在运行时用以下断言:

澳门新浦京手机版 5

断言也是一种防御性的编程形式。你不是在防范当前代码发生错误,而防范由于以后的代码变更发生错误。理想情况下,单元测试应该直到这个作用,但是让我们面对这样一个现实:即使存在单元测试,他们在通常情况下也不是很完备。内建的机器人可能没有工作,但数周以来也没有人注意到它,或者人们在提交代码之前忘记了执行测试。内部检查将是防止错误渗入的另一道防线,尤其对于那些悄悄地失败,但会引起代码功能错误并返回错误结果的情况有效。

假设你有一系列的if…elif代码块,你预先知道变量期望的值:

澳门新浦京手机版 6

假设这段代码现在完全正确。但它会一直正确吗?需求变更,代码变更。如果需求变为允许target

w,并关联到run_w_code,那将会发生什么情况?如果我们变更了设置target的代码,但是忘记了改变这个代码块,它就会错误地调用run_z_code(),错误就会发生。对于这段代码最好的方法就是编写一些防御性的检查,这样它的执行,即使在变更以后,要么正确,要么马上失败。

在代码开始添加注释是个好的开端,但是人们都不太喜欢读和更新这些注释,这些注释会很快变得过时。但对于断言,我们可以同时对这块代码编写文档,如果这些断言被违反了,会直接引起一个简单而又直接的失败。

澳门新浦京手机版 7

这里的断言同时用于防御性编程和检查文档。我认为这是最优的解决方案:

澳门新浦京手机版 8

这诱使开发者去不理代码,移除像value
==c这类不必要的测试,以及RuntimeError的“死代码”。另外,当”unexpected
error”错误发生时这个消息将非常窘迫,确实会发生。

合约式设计是断言另一个用得好的地方。在合约式设计中,我们认为函数与其他调用者遵循合约,例如像这样的情况:

“如果你传给我一个非空字符串,我保证返回转换成大写的首字母。”

如果合约被破坏了,不管是被函数本身还是调用者,这都会产生缺陷。我们说这个函数需要有前置条件(对期望的参数的限制)和后置条件(对返回结果的约束)。因此这个函数可能是这样的:

澳门新浦京手机版 9

合约式设计的目的是,在一个正确的程序里,所有的前置条件和后置条件都将得到处理。这是断言的经典应用,自(这个想法持续)我们发布无缺陷的程序并且将其放入产品,程序将是正确的并且我们可以放心地移除检查。

这里是我建议不使用断言的情况:

*不要用于测试用户提供的数据,或者那些需要在所有情况下需要改变检查的地方

*不要用于检查你认为在通常使用中可能失败的地方。断言用于非常特别的失败条件。你的用户绝不看到一个AssertionError,如果看到了,那就是个必须修复的缺陷。

*特别地不要因为断言只是比一个明确的测试加一个触发异常矮小而使用它。断言不是懒惰的代码编写者的捷径。

*不要将断言用于公共函数库输入参数的检查,因为你不能控制调用者,并且不能保证它不破坏函数的合约。

*不要将断言用于你期望修改的任何错误。换句话,你没有任何理由在产品代码捕获一个AssertionError异常。

*不要太多使用断言,它们使代码变得晦涩难懂。

 

 

 

 

 

 

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注