Blog

Python中assert的错误用法

Python中assert的错误用法 #

以极客时间为例,我们假设下面这样的场景:后台有时候需要删除一些上线时间较长的专栏,于是,相关的开发人员便设计出了下面这个专栏删除函数。

def delete_course(user, course_id):
    assert user_is_admin(user), 'user must be admin'
    assert course_exist(course_id), 'course id must exist'
    delete(course_id)

极客时间规定,必须是 admin 才能删除专栏,并且这个专栏课程必须存在。有的同学一看,很熟悉的需求啊,所以在前面加了相应的 assert 检查。那么我想让你思考一下,这样写到底对不对呢?

Answer #

assert 的检查是可以被关闭的,比如在运行 Python 程序时,加入-O这个选项就会让 assert 失效。因此,一旦 assert 的检查被关闭,user_is_admin() 和 course_exist() 这两个函数便不会被执行。这就会导致:

  1. 任何用户都有权限删除专栏课程;并且,不管这个课程是否存在,他们都可以强行执行删除操作。

  2. 这显然会给程序带来巨大的安全漏洞。

所以,正确的做法,是使用条件语句进行相应的检查,并合理抛出异常:

def delete_course(user, course_id):
    if not user_is_admin(user):
        raise Exception('user must be admin')
    if not course_exist(course_id):
        raise Exception('coursde id must exist')
    delete(course_id)

Viewpoint #

From #

28 | 如何合理利用assert?

Python中assert的用法

Python中assert的用法

Python中assert的用法 #

Python 的 assert 语句,可以说是一个 debug 的好工具,主要用于测试一个条件是否满足。如果测试的条件满足,则什么也不做,相当于执行了 pass 语句;如果测试条件不满足,便会抛出异常 AssertionError,并返回具体的错误信息(optional)。

它的具体语法是下面这样的:

assert_stmt ::=  "assert" expression ["," expression]

来看assert expression1, expression2的形式,比如下面这个例子:

assert 1 == 2,  'assertion is wrong'

它就相当于下面这两行代码:

if __debug__:
    if not expression1: raise AssertionError(expression2)

这里的__debug__是一个常数。如果 Python 程序执行时附带了-O这个选项,比如Python test.py -O,那么程序中所有的 assert 语句都会失效,常数__debug__便为 False;反之__debug__则为 True。

不过,需要注意的是,直接对常数__debug__赋值是非法的,因为它的值在解释器开始运行时就已经决定了,中途无法改变。

assert 通常用来对代码进行必要的 self check,表明你很确定这种情况一定发生,或者一定不会发生。

Viewpoint #

From #

28 | 如何合理利用assert?

跨临界区的双重检查

Question #

什么是(跨临界区的)“双重检查”?

Answer #

先在临界区之外,判断一次关键条件,若条件不满足则立即返回。这通常被称为“快路径”,或者叫做“快速失败路径”。如果条件满足,那么到了临界区中还要再对关键条件进行一次判断,这主要是为了更加严谨。这两次条件判断常被统称为(跨临界区的)“双重检查”。由于进入临界区之前,肯定要锁定保护它的互斥锁m,显然会降低代码的执行速度,所以其中的第二次条件判断,以及后续的操作就被称为“慢路径”或者“常规路径”。

Viewpoint #

From #

音调与调式

音调与调式 #

音调 #

基频和音调的英文都为 pitch,音乐信号中音调其实也是和乐器或者人声中的基频的频率一一对应的。比如,我们给乐器调音中常说的中央 C 就是基频频率约为 261.6Hz,唱名为 do,并且它位于乐音体系的最中央的位置,因而得名。中央 C 在国际标准中为 C4,在德国标准中为 c1,为了避免混淆,我们这里统一用国际标准来做解读。

那么我们平时说的 C 大调、D 大调和我们小时候音乐课的 do\re\mi\fa\so\la\ti 以及基频频率的关系是什么呢?

do\re\mi\fa\so\la\ti 是唱名,我们平时唱谱就是用这些音来把谱子唱出来的。他们和音名,也就是在音乐中包含的七个基本音调 CDEFGAB 一一对应。我们以钢琴中的中央音为例,唱名、音名和基频频率之间的对应如表 1 所示:

我们可以看到 C5 的基频频率正好是 C4 的两倍,这个其实就是我们说的度的概念,即 C5 比 C4 高八度,且一个八度其实就是基频频率相差一倍。高一个八度我们就把国际标准音名里后面的数字加 1。所以,比如 A 的音调从低到高可写为 A0、A1、A2 一直到 A9。

调式 #

我们知道了音调是什么,音乐中 C 大调的意思就是基础音调为 C,调式为大调。那么什么是大调、什么是小调呢?

我们刚才说的两个八度音之间基频频率是 2^1 的关系,而一个八度里有 12 个半音的音程,这 12 个半音是按照比值为 21/12​ 的等比数列排布的。音程是指两个音之间的间隔。音程的单位是半音或者全音,2 个半音我们就叫 1 个全音,由此推理可得,高一个全音就需要把基频频率乘以 22/12​。

我们常说的大、小调又叫自然大、小调,都属于 7 律调式。7 律调式说的就是这 12 个半音里我们只使用其中的 7 个。大调中,每两个音之间的音程大小,依序为全 - 全 - 半 - 全 - 全 - 全 - 半,这就是大调的组成规则,而小调中每两个音之间的音程大小,依序为全 - 半 - 全 - 全 - 半 - 全 - 全。

...

分层编译

分层编译 #

Java 7 引入了分层编译(对应参数 -XX:+TieredCompilation)的概念,综合了 C1 的启动性能优势和 C2 的峰值性能优势。

分层编译将 Java 虚拟机的执行状态分为了五个层次。为了方便阐述,我用“C1 代码”来指代由 C1 生成的机器码,“C2 代码”来指代由 C2 生成的机器码。五个层级分别是:

  1. 解释执行;
  2. 执行不带 profiling 的 C1 代码;
  3. 执行仅带方法调用次数以及循环回边执行次数 profiling 的 C1 代码;
  4. 执行带所有 profiling 的 C1 代码;
  5. 执行 C2 代码。

通常情况下,C2 代码的执行效率要比 C1 代码的高出 30% 以上。然而,对于 C1 代码的三种状态,按执行效率从高至低则是 1 层 > 2 层 > 3 层。

其中 1 层的性能比 2 层的稍微高一些,而 2 层的性能又比 3 层高出 30%。这是因为 profiling 越多,其额外的性能开销越大。

这里解释一下,profiling 是指在程序执行过程中,收集能够反映程序执行状态的数据。这里所收集的数据我们称之为程序的 profile。

...

Google编程规范的几点做法

Google编程规范的几点做法 #

在 Google,对于编程规范的信仰,可能超出很多人的想象,我给你简单介绍几点。

  1. 每一个语言都有专门的委员会(Style Committee)制定全公司强制的编程规范,和负责在编程风格争议时的仲裁人(Style Arbiters)。
  2. 在每个语言相应的编程规范群里,每天都有大量的讨论和辩论。新达成的共识会被写出“大字报”张贴在厕所里,以至于每个人甚至来访者都能用坐着的时候那零碎的 5 分钟阅读。
  3. 每一个代码提交,类似于 Git 里 diff 的概念,都需要至少两次代码评审(code review),一次针对业务逻辑,一次针对可读性(readability review)。所谓的可读性评审,着重在代码风格规范上。只有通过考核的人,才能够成为可读性评审人(readability reviewer)。
  4. 有大量的开发自动化工具,确保以上的准则得到强制实施。例如,代码提交前会有 linter 做静态规则检查,不通过是无法提交代码的。

Viewpoint #

From #

26 | 活都来不及干了,还有空注意代码风格?!

避免遍历时生成临时列表

Question #

我们看下面的代码有什么问题?

# 错误示例
adict = {i: i * 2 for i in xrange(10000000)}
for key in adict.keys():
   print("{0} = {1}".format(key, adict[key]))

Answer #

keys() 方法会在遍历前生成一个临时的列表,导致上面的代码消耗大量内存并且运行缓慢。正确的方式,是使用默认的 iterator。默认的 iterator 不会分配新内存,也就不会造成上面的性能问题:

# 正确示例
for key in adict:

这也就是为什么 Google Style 2.8 对于遍历方式的选择作出了限制。

Viewpoint #

From #

26 | 活都来不及干了,还有空注意代码风格?!

和None比较时候永远使用is

和None比较时候永远使用is #

你可以自己先判断一下运行结果。

# 错误示例
x = MyObject()
print(x == None)

打印结果是 False 吗?不一定。因为对于类来说,== 的结果,取决于它的 eq() 方法的具体实现。MyObject 的作者完全可能这样实现:

class MyObject(object):
 def __eq__(self, other):
   if other:
     return self.field == other.field
   return True

正确的是在代码风格中,当你和 None 比较时候永远使用 is:

# 正确示例
x = MyObject()
print(x is None)

上面两个例子,我简单介绍了通过编程风格的限制,让 is 和 == 的使用更安全。

Viewpoint #

From #

26 | 活都来不及干了,还有空注意代码风格?!

无力的沟通

Content #

什么是无力的沟通(powerless communication)?与之对应的沟通方式是什么?无力的沟通有何优势?

与获取者强有力的沟通方式相反的,就是所谓的无力的沟通(powerless communication)。无力的沟通者倾向于用不那么肯定的方式讲话,表达出更多的疑惑,并非常依赖别人给出的建议。他们使用示弱的方式交谈,显示出自己的弱点,并会做出免责声明 、避免正面答复和表示迟疑。苏珊·凯恩在《安静》(Quiet)一书中写道,在西方社会里,人们期望我们用强有力的方式交流。我们被告知,伟大的领导者会使用“强势演讲”和“强势话语”来有力地传递出他们的信息。如果使用无力的沟通,人们在获得影响力方面肯定会处于劣势。

传统的观念认为,果断和自信对于获取影响力至关重要。实际上,这种风格并不一定总能给我们带来好处,付出者会本能地采取一种无力的沟通风格。令人惊讶的是,这种风格被证明在建立名声方面是非常有效的。我想要追溯付出者是如何通过建立声望,在展示、销售、说服和谈判4个领域获取影响力。因为付出者更看重别人的观点和利益,他们更愿意提出问题,而非给出回答;更愿意谨慎而非大胆地讲话;更愿意承认自己的弱点,而非显示自己的长处;更愿意寻求别人的建议,而非将自己的观点强加给别人。

Viewpoint #

From #

失态效应(pratfall_effect)

Content #

什么是“失态效应”(pratfall effect)?它说明了什么现象?

只有当听众能够接受其他的信息,证实讲话者的能力时,示弱才是有效的做法。

在心理学家埃利奥特·阿伦森(Elliot Aronson)开展的一项经典实验中,大学生听了4盘磁带中的一盘,磁带的内容关于一位参加急智问答面试的候选人。一半内容涉及的候选人是一名专家,他答对了92%的问题;另一半内容涉及的候选人具有一般的知识水平,只答对了30%。正如预期的那样,听众更偏爱专家。但是,当磁带里加入了候选人的一个笨拙行为时,有趣的情况出现了。盘子摔碎了,候选人说,“哦,天哪——我把咖啡洒到新买的正装上了。”当平庸的候选人表现出笨拙时,听众更不喜欢他了。当专家表现出笨拙时,听众却更喜欢他了。

心理学家将这种现象称为“失态效应”(pratfall effect)。洒了一杯咖啡会损害平庸候选人的形象:这只是听众不喜欢他的另外一个原因。但是,同样的事故让专家显得更加人性化、更容易接近——而非高高在上、遥不可及。

Viewpoint #

太强势的领域,要适当地失态一下。不擅长的领域,则要小心不出丑。

From #

社会性动物