mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 06:03:45 +08:00
mod
This commit is contained in:
247
极客时间专栏/Python核心技术与实战/规范篇/26 | 活都来不及干了,还有空注意代码风格?!.md
Normal file
247
极客时间专栏/Python核心技术与实战/规范篇/26 | 活都来不及干了,还有空注意代码风格?!.md
Normal file
@@ -0,0 +1,247 @@
|
||||
<audio id="audio" title="26 | 活都来不及干了,还有空注意代码风格?!" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8d/77/8d6bc3a5f8c72fde21e006d85b215977.mp3"></audio>
|
||||
|
||||
你好,我是蔡元楠,是极客时间《大规模数据处理实战》的作者。今天是我第二次受邀来我们专栏分享了,很高兴再次见到你。今天我分享的主题是:活都来不及干了,还有空注意代码风格吗?!
|
||||
|
||||
许多来Google参观的人,用完洗手间后,都会惊奇而略带羞涩地问:“你们马桶前面的门上,贴着的Python编程规范,是用来搞笑的吗?”
|
||||
|
||||
这事儿还真不是搞笑,Google对编码规范的要求极其严格。今天,我们就来聊聊编程规范这件事儿。
|
||||
|
||||
对于编程规范(style guide) 的认知,很多人可能只停留在第一阶段:知道编程规范有用,整个公司都要求使用驼峰式命名。而后面的阶段,比如为什么和怎么做,就并不了解了。
|
||||
|
||||
但在Google,对于编程规范的信仰,可能超出很多人的想象,我给你简单介绍几点。
|
||||
|
||||
1. 每一个语言都有专门的委员会(Style Committee)制定全公司强制的编程规范,和负责在编程风格争议时的仲裁人(Style Arbiters)。
|
||||
1. 在每个语言相应的编程规范群里,每天都有大量的讨论和辩论。新达成的共识会被写出“大字报”张贴在厕所里,以至于每个人甚至来访者都能用坐着的时候那零碎的5分钟阅读。
|
||||
1. 每一个代码提交,类似于Git里diff的概念,都需要至少两次代码评审(code review),一次针对业务逻辑,一次针对可读性(readability review)。所谓的可读性评审,着重在代码风格规范上。只有通过考核的人,才能够成为可读性评审人(readability reviewer)。
|
||||
1. 有大量的开发自动化工具,确保以上的准则得到强制实施。例如,代码提交前会有linter做静态规则检查,不通过是无法提交代码的。
|
||||
|
||||
看到这里,不知道你有怎样的感受?我自己十分认同这样的工程师文化,所以今天,我会给你介绍清楚两点:
|
||||
|
||||
- Python的编程规范为什么重要,这对于业务开发来说,究竟有没有帮助?
|
||||
- 有哪些流程和工具,可以整合到已有的开发流程中,让你的编程规范强制自动执行呢?
|
||||
|
||||
在讲解过程中,我会适时引用两个条例来举例,分别是:
|
||||
|
||||
- 《8号Python增强规范》(Python Enhacement Proposal #8),以下简称PEP8;
|
||||
- 《Google Python 风格规范》(Google Python Style Guide),以下简称Google Style,这是源自Google内部的风格规范。公开发布的社区版本,是为了让Google旗下所有Python开源项目的编程风格统一。([http://google.github.io/styleguide/pyguide.html](http://google.github.io/styleguide/pyguide.html))
|
||||
|
||||
相对来说,Google Style是比PEP8更严格的一个编程规范。因为PEP8的受众是个人和小团队开发者,而Google Style能够胜任大团队,企业级,百万行级别代码库。他们的内容,后面我也会简单说明。
|
||||
|
||||
## 统一的编程规范为什么重要?
|
||||
|
||||
用一句话来概括,统一的编程规范能提高开发效率。而开发效率,关乎三类对象,也就是阅读者、编程者和机器。他们的优先级是**阅读者的体验 >> 编程者的体验 >> 机器的体验**。
|
||||
|
||||
### 阅读者的体验>>编程者的体验
|
||||
|
||||
写过代码的人可能都有体会,在我们的实际工作中,真正在打字的时间,远比阅读或者debug的时间要少。事实正是如此,研究表明,软件工程中80%的时间都在阅读代码。所以,为了提高开发效率,我们要优化的,不是你的**打字时间**,而是**团队阅读的体验**。
|
||||
|
||||
其实,不少的编程规范,本来就是为了优化读者体验而存在的。举个例子,对于命名原则,我想很多人应该都有所理解,PEP8第38条规定命名必须有意义,不能是无意义的单字母。
|
||||
|
||||
有些人可能会说,啊,编程规范好烦哟,变量名一定要我写完整,打起来好累。但是当你作为阅读者时,一定能分辨下面两种代码的可读性不同:
|
||||
|
||||
```
|
||||
# 错误示例
|
||||
if (a <= 0):
|
||||
return
|
||||
elif (a > b):
|
||||
return
|
||||
else:
|
||||
b -= a
|
||||
|
||||
# 正确示例
|
||||
if (transfer_amount <= 0):
|
||||
raise Exception('...')
|
||||
elif (transfer_amount > balance):
|
||||
raise Exception('...')
|
||||
else:
|
||||
balance -= transfer_amount
|
||||
|
||||
```
|
||||
|
||||
再举一个例子,Google Style 2.2条规定,Python代码中的import对象,只能是package或者module。
|
||||
|
||||
```
|
||||
# 错误示例
|
||||
from mypkg import Obj
|
||||
from mypkg import my_func
|
||||
|
||||
my_func([1, 2, 3])
|
||||
|
||||
# 正确示例
|
||||
import numpy as np
|
||||
import mypkg
|
||||
|
||||
np.array([6, 7, 8])
|
||||
|
||||
```
|
||||
|
||||
以上错误示例在语法上完全合法(因为没有符号冲突name collisions),但是对于读者来讲,它们的可读性太差了。因为my_func这样的名字,如果没有一个package name提供上下文语境,读者很难单独通过my_func这个名字来推测它的可能功能,也很难在debug时根据package name找到可能的问题。
|
||||
|
||||
反观正确示例,虽然array是如此大众脸的名字,但因为有了numpy这个package的暗示,读者可以一下子反应过来,哦,这是一个numpy array。不过这里要注意区别,这个例子和符号冲突(name collisions)是正交(orthogonal)的两个概念,即使没有符号冲突,我们也要遵循这样的import规范。
|
||||
|
||||
### 编程者的体验 >> 机器的体验
|
||||
|
||||
说完了阅读者的体验,再来聊聊编程者的体验。我常常见到的一个错误倾向,是过度简化自己的代码,包括我自己也有这样的问题。一个典型的例子,就是盲目地使用Python的list comprehension。
|
||||
|
||||
```
|
||||
# 错误示例
|
||||
result = [(x, y) for x in range(10) for y in range(5) if x * y > 10]
|
||||
|
||||
```
|
||||
|
||||
我敢打赌,一定很少有人能一口气写出来这么复杂的list comprehension。这不仅容易累着自己,也让阅读者看得很累。其实,如果你用一个简单的for loop,会让这段代码更加简洁明了,自己也更为轻松。
|
||||
|
||||
```
|
||||
# 正确示例
|
||||
result = []
|
||||
for x in range(10):
|
||||
for y in range(5):
|
||||
if x * y > 10:
|
||||
result.append((x, y))
|
||||
|
||||
```
|
||||
|
||||
### 机器的体验也很重要
|
||||
|
||||
讲完了编程者和阅读者的重要性,我们不能忽视了机器的体验。我们最终希望代码能正确、高效地在电脑上执行。但是,一些危险的编程风格,不仅会影响程序正确性,也容易成为代码效率的瓶颈。
|
||||
|
||||
我们先来看看 is 和 == 的使用区别。你能看出下面的代码的运行结果吗?
|
||||
|
||||
```
|
||||
# 错误示例
|
||||
x = 27
|
||||
y = 27
|
||||
print(x is y)
|
||||
|
||||
x = 721
|
||||
y = 721
|
||||
print(x is y)
|
||||
|
||||
```
|
||||
|
||||
看起来is是比较内存地址,那么两个结果应该都是一样的,可是实际上打印出来的,却分别是True和False!
|
||||
|
||||
原因是在CPython(Python的C实现)的实现中,把-5到256的整数做成了singleton,也就是说,这个区间里的数字都会引用同一块内存区域,所以上面的27和下面的27会指向同一个地址,运行结果为True。
|
||||
|
||||
但是-5到256之外的数字,会因为你的重新定义而被重新分配内存,所以两个721会指向不同的内存地址,结果也就是False了。
|
||||
|
||||
所以,即使你已经清楚,is比较对象的内存地址,你也应该在代码风格中,避免去用is比较两个Python整数的地址。
|
||||
|
||||
```
|
||||
# 正确示例
|
||||
x = 27
|
||||
y = 27
|
||||
print(x == y)
|
||||
|
||||
x = 721
|
||||
y = 721
|
||||
print(x == y)
|
||||
|
||||
```
|
||||
|
||||
看完这个例子,我们再看==在比较值的时候,是否总能如你所愿呢?同样的,你可以自己先判断一下运行结果。
|
||||
|
||||
```
|
||||
# 错误示例
|
||||
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 和 == 的使用更安全。不过,光注意这两点就可以了吗?不要忘记,Python中还有隐式布尔转换。比如:
|
||||
|
||||
```
|
||||
# 错误示例
|
||||
def pay(name, salary=None):
|
||||
if not salary:
|
||||
salary = 11
|
||||
print(name, "is compensated", salary, "dollars")
|
||||
|
||||
```
|
||||
|
||||
如果有人调用 pay(“Andrew”, 0) ,会打印什么呢?“Andrew is compensated 11 dollars”。当你明确想要比较对象是否是None时,一定要显式地用 is None。
|
||||
|
||||
```
|
||||
# 正确示例
|
||||
def pay(name, salary=None):
|
||||
if salary is not None:
|
||||
salary = 11
|
||||
print(name, "is compensated", salary, "dollars")
|
||||
|
||||
```
|
||||
|
||||
这就是为什么,PEP8和Google Style都特别强调了,何时使用is, 何时使用 ==,何时使用隐式布尔转换。
|
||||
|
||||
不规范的编程习惯也会导致程序效率问题,我们看下面的代码有什么问题:
|
||||
|
||||
```
|
||||
# 错误示例
|
||||
adict = {i: i * 2 for i in xrange(10000000)}
|
||||
|
||||
for key in adict.keys():
|
||||
print("{0} = {1}".format(key, adict[key]))
|
||||
|
||||
```
|
||||
|
||||
keys()方法会在遍历前生成一个临时的列表,导致上面的代码消耗大量内存并且运行缓慢。正确的方式,是使用默认的iterator。默认的iterator不会分配新内存,也就不会造成上面的性能问题:
|
||||
|
||||
```
|
||||
# 正确示例
|
||||
for key in adict:
|
||||
|
||||
```
|
||||
|
||||
这也就是为什么Google Style 2.8对于遍历方式的选择作出了限制。
|
||||
|
||||
相信读到这里,对于代码风格规范的重要性,你已经有了进一步的理解。如果能够做到下一步,会让你和你的团队脱胎换骨,那就是和开发流程的完全整合。
|
||||
|
||||
## 整合进开发流程的自动化工具
|
||||
|
||||
前面我们已经提到了,编程规范的终极目标是提高开发效率。显然,如果每次写代码,都需要你在代码规范上额外花很多时间的话,就达不到我们的初衷了。
|
||||
|
||||
首先,你需要根据你的具体工作环境,选择或者制定适合自己公司/团队的规范。市面上可以参考的规范,也就是我在开头提到的那两个,PEP8和Google Style。
|
||||
|
||||
没有放之四海而皆准的规范,你需要因地制宜。例如在Google,因为历史原因C++不使用异常,引入异常对整个代码库带来的风险已经远大于它的益处,所以在它的C++代码规范中,禁止使用异常。
|
||||
|
||||
其次,一旦确定了整个团队同意的代码规范,就一定要强制执行。停留在口头和大脑的共识,只是水中月镜中花。如何执行呢?**靠强制代码评审和强制静态或者动态linter**。
|
||||
|
||||
当然,需要注意的是,我这里“强制”的意思,不是说如果不做就罚款。那就太low了,完全没有极客精神。我指的“强制”,是把共识写进代码里,让机器来自动化这些流程。比如:
|
||||
|
||||
- 在代码评审工具里,添加必须的编程规范环节;
|
||||
- 把团队确定的代码规范写进Pylint里([https://www.pylint.org/](https://www.pylint.org/)),能够在每份代码提交前自动检查,不通过的代码无法提交。
|
||||
|
||||
整合之后,你的团队工作流程就会变成这样:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/29/e3/29fd89f89825b18273083a8e03b044e3.png" alt="">
|
||||
|
||||
## 总结
|
||||
|
||||
学到这里,相信你对代码风格的重要性有了全新的认识。代码风格之所以重要,是因为它关乎阅读者的体验、编程者的体验和执行代码的机器体验。
|
||||
|
||||
当然,仅仅意识到代码风格重要,是远远不够的。我还具体分享了一些自动化代码风格检查的切实方法,比如强制代码评审和强制静态或者动态linter。总之还是那句话,我们强调编程规范,最终一定是为了提高开发效率,而不是做额外功。
|
||||
|
||||
## 思考题
|
||||
|
||||
在你个人或者团队的项目经验中,是否也因为编程规范的问题,踩过坑或者吵过架呢?欢迎留言和我分享,也欢迎你把这篇文章分享出去。
|
||||
|
||||
|
||||
449
极客时间专栏/Python核心技术与实战/规范篇/27 | 学会合理分解代码,提高代码可读性.md
Normal file
449
极客时间专栏/Python核心技术与实战/规范篇/27 | 学会合理分解代码,提高代码可读性.md
Normal file
@@ -0,0 +1,449 @@
|
||||
<audio id="audio" title="27 | 学会合理分解代码,提高代码可读性" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2e/52/2e3790648ac8d760d0c8809a0b25fc52.mp3"></audio>
|
||||
|
||||
你好,我是景霄。今天我们不讲任何技术知识点,继续来一起聊聊代码哲学。
|
||||
|
||||
有句话说得好,好的代码本身就是一份文档。同样功能的一份程序,一个组件,一套系统,让不同的人来写,写出来的代码却是千差万别。
|
||||
|
||||
有些人的设计风格和代码风格犹如热刀切黄油,从顶层到底层的代码看下来酣畅淋漓,注释详尽而又精简;深入到细节代码,无需注释也能理解清清楚楚。
|
||||
|
||||
而有些人,代码勉勉强强能跑起来,遇到稍微复杂的情况可能就会出 bug;深入到代码中 debug,则发现处处都是魔术数、函数堆在一起。一个文件上千行,设计模式又是混淆不堪,让人实在很难阅读,更别提修改和迭代开发。
|
||||
|
||||
Guido van Rossum(吉多·范罗苏姆,Python创始人 )说过,代码的阅读频率远高于编写代码的频率。毕竟,即使是在编写代码的时候,你也需要对代码进行反复阅读和调试,来确认代码能够按照期望运行。
|
||||
|
||||
话不多说,进入正题。
|
||||
|
||||
## PEP 8 规范
|
||||
|
||||
上节课我们简单提起过 PEP 8 ,今天我们继续来详细解读。
|
||||
|
||||
PEP是 Python Enhancement Proposal 的缩写,翻译过来叫“Python 增强规范”。正如我们写文章,会有句式、标点、段落格式、开头缩进等标准的规范一样,Python 书写自然也有一套较为官方的规范。PEP 8 就是这样一种规范,它存在的意义,就是让 Python 更易阅读,换句话,增强代码可读性。
|
||||
|
||||
事实上,Pycharm 已经内置了 PEP 8 规范检测器,它会自动对编码不规范的地方进行检查,然后指出错误,并推荐修改方式。下面这张图就是其界面。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/23/5f/23f0288a5ba4388f69e5a1c3a59eb55f.png" alt="">
|
||||
|
||||
因此,在学习今天的内容时,我推荐你使用 Pycharm IDE 进行代码检查,看一下自己的代码格式哪里有问题。尤其对于初学者,从某些程度来说,代码规范甚至是比代码准确更重要的事情,因为实际工作中,代码可读性的重要性一定比你想象的多得多。
|
||||
|
||||
### 缩进规范
|
||||
|
||||
首先,我们来看代码块内的缩进。
|
||||
|
||||
Python 和 C++ / Java 最大的不同在于,后者完全使用大括号来区分代码块,而前者依靠不同行和不同的缩进来进行分块。有一个很有名的比赛,叫作 [C 语言混乱代码大赛](http://www.ioccc.org/years-spoiler.html),其中有很多非常精彩的作品,你能看到书写的代码排成各种形状,有的是一幅画,或者一个卡通头像,但是能执行出惊人的结果。
|
||||
|
||||
而放到 Python ,显然就不能实现同样的技巧了。不过,以小换大,我们有了“像阅读英语”一样清晰的 Python 代码,也还是可以接受的。
|
||||
|
||||
话说回来,Python 的缩进其实可以写成很多种,Tab、双空格、四空格、空格和 Tab 混合等。而PEP 8 规范告诉我们,**请选择四个空格的缩进,不要使用 Tab,更不要 Tab 和空格混着用。**
|
||||
|
||||
第二个要注意的是,**每行最大长度请限制在 79 个字符。**
|
||||
|
||||
这个原则主要有两个优点,第一个优点比较好理解。很多工程师在编程的时候,习惯一个屏幕并列竖排展示多个源代码。如果某个源代码的某些行过长,你就需要拖动横向滚动条来阅读,或者需要软回车将本行内容放入下一行,这就极大地影响了编码和阅读效率。
|
||||
|
||||
至于第二个优点,需要有一定经验的编程经验后更容易理解:因为当代码的嵌套层数过高,比如超过三层之后,一行的内容就很容易超过 79 个字符了。所以,这条规定另一方面也在制约着程序员,不要写迭代过深的代码,而是要思考继续把代码分解成其他函数或逻辑块,来优化自己的代码结构。
|
||||
|
||||
### 空行规范
|
||||
|
||||
接着我们来看代码块之间的空行。
|
||||
|
||||
我们知道,Python 中的空行对 Python 解释器的执行没有影响,但对阅读体验有很深刻的影响。
|
||||
|
||||
PEP 8 规定,**全局的类和函数的上方需要空两个空行,而类的函数之间需要空一个空行**。当然,函数内部也可以使用空行,和英语的段落一样,用来区分不同意群之间的代码块。但是记住最多空一行,千万不要滥用。
|
||||
|
||||
另外,Python 本身允许把多行合并为一行,使用分号隔开,但这是 PEP 8 不推荐的做法。所以,即使是使用控制语句 if / while / for,你的执行语句哪怕只有一行命令,也请另起一行,这样可以更大程度提升阅读效率。
|
||||
|
||||
至于代码的尾部,每个代码文件的最后一行为空行,并且只有这一个空行。
|
||||
|
||||
### 空格规范
|
||||
|
||||
我们再来看一下,代码块中,每行语句中空格的使用。
|
||||
|
||||
函数的参数列表中,调用函数的参数列表中会出现逗号,请注意逗号后要跟一个空格,这是英语的使用习惯,也能让每个参数独立阅读,更清晰。
|
||||
|
||||
同理,冒号经常被用来初始化字典,冒号后面也要跟一个空格。
|
||||
|
||||
另外,Python 中我们可以使用`#`进行单独注释,请记得要在`#`后、注释前加一个空格。
|
||||
|
||||
对于操作符,例如`+`,`-`,`*`,`/`,`&`,`|`,`=`,`==`,`!=`,请在两边都保留空格。不过与此对应,括号内的两端并不需要空格。
|
||||
|
||||
### 换行规范
|
||||
|
||||
现在再回到缩进规范,注意我们提到的第二点,控制每行的最大长度不超过 79 个字符,但是有时候,函数调用逻辑过长而不得不超过这个数字时,该怎么办呢?
|
||||
|
||||
请看下面这段代码,建议你先自己阅读并总结其特点:
|
||||
|
||||
```
|
||||
def solve1(this_is_the_first_parameter, this_is_the_second_parameter, this_is_the_third_parameter,
|
||||
this_is_the_forth_parameter, this_is_the_fifth_parameter, this_is_the_sixth_parameter):
|
||||
return (this_is_the_first_parameter + this_is_the_second_parameter + this_is_the_third_parameter +
|
||||
this_is_the_forth_parameter + this_is_the_fifth_parameter + this_is_the_sixth_parameter)
|
||||
|
||||
|
||||
def solve2(this_is_the_first_parameter, this_is_the_second_parameter, this_is_the_third_parameter,
|
||||
this_is_the_forth_parameter, this_is_the_fifth_parameter, this_is_the_sixth_parameter):
|
||||
return this_is_the_first_parameter + this_is_the_second_parameter + this_is_the_third_parameter + \
|
||||
this_is_the_forth_parameter + this_is_the_fifth_parameter + this_is_the_sixth_parameter
|
||||
|
||||
|
||||
(top_secret_func(param1=12345678, param2=12345678, param3=12345678, param4=12345678, param5=12345678).check()
|
||||
.launch_nuclear_missile().wait())
|
||||
|
||||
|
||||
top_secret_func(param1=12345678, param2=12345678, param3=12345678, param4=12345678, param5=12345678).check() \
|
||||
.launch_nuclear_missile().wait()
|
||||
|
||||
```
|
||||
|
||||
事实上,这里有两种经典做法。
|
||||
|
||||
第一种,通过括号来将过长的运算进行封装,此时虽然跨行,但是仍处于一个逻辑引用之下。solve1 函数的参数过多,直接换行,不过请注意,要考虑第二行参数和第一行第一个参数对齐,这样可以让函数变得非常美观的同时,更易于阅读。当然,函数调用也可以使用类似的方式,只需要用一对括号将其包裹起来。
|
||||
|
||||
第二种,则是通过换行符来实现。这个方法更为直接,你可以从 solve2 和第二个函数调用看出来。
|
||||
|
||||
关于代码细节方面的规范,我主要强调这四个方面。习惯不是一天养成的,但一定需要你特别留心和刻意练习。我能做的,便是告诉你这些需要留心的地方,并带你感受实际项目的代码风格。
|
||||
|
||||
下面的代码选自开源库 Google TensorFlow Keras,为了更加直观突出重点,我删去了注释和大部分代码,你意会即可。我希望,通过阅读这段代码,你能更真实地了解到,前沿的项目是怎么在增强阅读性上下功夫的。
|
||||
|
||||
```
|
||||
class Model(network.Network):
|
||||
def fit(self,
|
||||
x=None,
|
||||
y=None,
|
||||
batch_size=None,
|
||||
epochs=1,
|
||||
verbose=1,
|
||||
callbacks=None,
|
||||
validation_split=0.,
|
||||
validation_data=None,
|
||||
shuffle=True,
|
||||
class_weight=None,
|
||||
sample_weight=None,
|
||||
initial_epoch=0,
|
||||
steps_per_epoch=None,
|
||||
validation_steps=None,
|
||||
validation_freq=1,
|
||||
max_queue_size=10,
|
||||
workers=1,
|
||||
use_multiprocessing=False,
|
||||
**kwargs):
|
||||
# Legacy support
|
||||
if 'nb_epoch' in kwargs:
|
||||
logging.warning(
|
||||
'The `nb_epoch` argument in `fit` has been renamed `epochs`.')
|
||||
epochs = kwargs.pop('nb_epoch')
|
||||
if kwargs:
|
||||
raise TypeError('Unrecognized keyword arguments: ' + str(kwargs))
|
||||
self._assert_compile_was_called()
|
||||
|
||||
func = self._select_training_loop(x)
|
||||
return func.fit(
|
||||
self,
|
||||
x=x,
|
||||
y=y,
|
||||
batch_size=batch_size,
|
||||
epochs=epochs,
|
||||
verbose=verbose,
|
||||
callbacks=callbacks,
|
||||
validation_split=validation_split,
|
||||
validation_data=validation_data,
|
||||
shuffle=shuffle,
|
||||
class_weight=class_weight,
|
||||
sample_weight=sample_weight,
|
||||
initial_epoch=initial_epoch,
|
||||
steps_per_epoch=steps_per_epoch,
|
||||
validation_steps=validation_steps,
|
||||
validation_freq=validation_freq,
|
||||
max_queue_size=max_queue_size,
|
||||
workers=workers,
|
||||
use_multiprocessing=use_multiprocessing)
|
||||
|
||||
```
|
||||
|
||||
### 文档规范
|
||||
|
||||
接下来我们说说文档规范。先来看看最常用的 import 函数。
|
||||
|
||||
首先,所有 import 尽量放在开头,这个没什么说的,毕竟到处 import 会让人很难看清楚文件之间的依赖关系,运行时 import 也可能会导致潜在的效率问题和其他风险。
|
||||
|
||||
其次,不要使用 import 一次导入多个模块。虽然我们可以在一行中 import 多个模块,并用逗号分隔,但请不要这么做。`import time, os` 是 PEP 8 不推荐的做法。
|
||||
|
||||
如果你采用 `from module import func` 这样的语句,请确保 func 在本文件中不会出现命名冲突。不过,你其实可以通过 `from module import func as new_func` 来进行重命名,从而避免冲突。
|
||||
|
||||
### 注释规范
|
||||
|
||||
有句话这么说:错误的注释,不如没有注释。所以,当你改动代码的时候,一定要注意检查周围的注释是否需要更新。
|
||||
|
||||
对于大的逻辑块,我们可以在最开始相同的缩进处以 `#` 开始写注释。即使是注释,你也应该把它当成完整的文章来书写。如果英文注释,请注意开头大写及结尾标点,注意避免语法错误和逻辑错误,同时精简要表达的意思。中文注释也是同样的要求。一份优秀的代码,离不开优秀的注释。
|
||||
|
||||
至于行注释,如空格规范中所讲,我们可以在一行后面跟两个空格,然后以 `#` 开头加入注释。不过,请注意,行注释并不是很推荐的方式。
|
||||
|
||||
```
|
||||
# This is an example to demonstrate how to comment.
|
||||
# Please note this function must be used carefully.
|
||||
def solve(x):
|
||||
if x == 1: # This is only one exception.
|
||||
return False
|
||||
return True
|
||||
|
||||
```
|
||||
|
||||
### 文档描述
|
||||
|
||||
再来说说文档描述,我们继续以 TensorFlow 的代码为例。
|
||||
|
||||
```
|
||||
class SpatialDropout2D(Dropout):
|
||||
"""Spatial 2D version of Dropout.
|
||||
This version performs the same function as Dropout, however it drops
|
||||
entire 2D feature maps instead of individual elements. If adjacent pixels
|
||||
within feature maps are strongly correlated (as is normally the case in
|
||||
early convolution layers) then regular dropout will not regularize the
|
||||
activations and will otherwise just result in an effective learning rate
|
||||
decrease. In this case, SpatialDropout2D will help promote independence
|
||||
between feature maps and should be used instead.
|
||||
Arguments:
|
||||
rate: float between 0 and 1. Fraction of the input units to drop.
|
||||
data_format: 'channels_first' or 'channels_last'.
|
||||
In 'channels_first' mode, the channels dimension
|
||||
(the depth) is at index 1,
|
||||
in 'channels_last' mode is it at index 3.
|
||||
It defaults to the `image_data_format` value found in your
|
||||
Keras config file at `~/.keras/keras.json`.
|
||||
If you never set it, then it will be "channels_last".
|
||||
Input shape:
|
||||
4D tensor with shape:
|
||||
`(samples, channels, rows, cols)` if data_format='channels_first'
|
||||
or 4D tensor with shape:
|
||||
`(samples, rows, cols, channels)` if data_format='channels_last'.
|
||||
Output shape:
|
||||
Same as input
|
||||
References:
|
||||
- [Efficient Object Localization Using Convolutional
|
||||
Networks](https://arxiv.org/abs/1411.4280)
|
||||
"""
|
||||
def __init__(self, rate, data_format=None, **kwargs):
|
||||
super(SpatialDropout2D, self).__init__(rate, **kwargs)
|
||||
if data_format is None:
|
||||
data_format = K.image_data_format()
|
||||
if data_format not in {'channels_last', 'channels_first'}:
|
||||
raise ValueError('data_format must be in '
|
||||
'{"channels_last", "channels_first"}')
|
||||
self.data_format = data_format
|
||||
self.input_spec = InputSpec(ndim=4)
|
||||
|
||||
```
|
||||
|
||||
你应该可以发现,类和函数的注释,为的是让读者快速理解这个函数做了什么,它输入的参数和格式,输出的返回值和格式,以及其他需要注意的地方。
|
||||
|
||||
至于docstring 的写法,它是用三个双引号开始、三个双引号结尾。我们首先用一句话简单说明这个函数做什么,然后跟一段话来详细解释;再往后是参数列表、参数格式、返回值格式。
|
||||
|
||||
### 命名规范
|
||||
|
||||
接下来,我来讲一讲命名。你应该听过这么一句话,“计算机科学的两件难事:缓存失效和命名。”命名对程序员来说,是一个不算省心的事。一个具有误导性的名字,极有可能在项目中埋下潜在的 bug。这里我就不从命名分类方法来给你划分了,我们只讲一些最实用的规范。
|
||||
|
||||
先来看变量命名。变量名请拒绝使用 a b c d 这样毫无意义的单字符,我们应该使用能够代表其意思的变量名。一般来说,变量使用小写,通过下划线串联起来,例如:`data_format`、`input_spec`、`image_data_set`。唯一可以使用单字符的地方是迭代,比如 `for i in range(n)` 这种,为了精简可以使用。如果是类的私有变量,请记得前面增加两个下划线。
|
||||
|
||||
对于常量,最好的做法是全部大写,并通过下划线连接,例如:`WAIT_TIME`、`SERVER_ADDRESS`、`PORT_NUMBER`。
|
||||
|
||||
对于函数名,同样也请使用小写的方式,通过下划线连接起来,例如:`launch_nuclear_missile()`、`check_input_validation()`。
|
||||
|
||||
对于类名,则应该首字母大写,然后合并起来,例如:`class SpatialDropout2D()`、`class FeatureSet()`。
|
||||
|
||||
总之,还是那句话,不要过于吝啬一个变量名的长度。当然,在合理描述这个变量背后代表的对象后,一定的精简能力也是必要的。
|
||||
|
||||
## 代码分解技巧
|
||||
|
||||
最后,我们再讲一些很实用的代码优化技巧。
|
||||
|
||||
编程中一个核心思想是,不写重复代码。重复代码大概率可以通过使用条件、循环、构造函数和类来解决。而另一个核心思想则是,减少迭代层数,尽可能让 Python 代码扁平化,毕竟,人的大脑无法处理过多的栈操作。
|
||||
|
||||
所以,在很多业务逻辑比较复杂的地方,就需要我们加入大量的判断和循环。不过,这些一旦没写好,程序看起来就是地狱了。
|
||||
|
||||
我们来看下面几个示例,来说说写好判断、循环的细节问题。先来看第一段代码:
|
||||
|
||||
```
|
||||
if i_am_rich:
|
||||
money = 100
|
||||
send(money)
|
||||
else:
|
||||
money = 10
|
||||
send(money)
|
||||
|
||||
```
|
||||
|
||||
这段代码中,同样的send语句出现了两次,所以我们完全可以合并一下,把代码改造成下面这样:
|
||||
|
||||
```
|
||||
if i_am_rich:
|
||||
money = 100
|
||||
else:
|
||||
money = 10
|
||||
send(money)
|
||||
|
||||
```
|
||||
|
||||
再来看一个例子:
|
||||
|
||||
```
|
||||
def send(money):
|
||||
if is_server_dead:
|
||||
LOG('server dead')
|
||||
return
|
||||
else:
|
||||
if is_server_timed_out:
|
||||
LOG('server timed out')
|
||||
return
|
||||
else:
|
||||
result = get_result_from_server()
|
||||
if result == MONEY_IS_NOT_ENOUGH:
|
||||
LOG('you do not have enough money')
|
||||
return
|
||||
else:
|
||||
if result == TRANSACTION_SUCCEED:
|
||||
LOG('OK')
|
||||
return
|
||||
else:
|
||||
LOG('something wrong')
|
||||
return
|
||||
|
||||
```
|
||||
|
||||
这段代码层层缩进,显而易见的难看。我们来改一下:
|
||||
|
||||
```
|
||||
def send(money):
|
||||
if is_server_dead:
|
||||
LOG('server dead')
|
||||
return
|
||||
|
||||
if is_server_timed_out:
|
||||
LOG('server timed out')
|
||||
return
|
||||
|
||||
result = get_result_from_server()
|
||||
|
||||
if result == MONET_IS_NOT_ENOUGH:
|
||||
LOG('you do not have enough money')
|
||||
return
|
||||
|
||||
if result == TRANSACTION_SUCCEED:
|
||||
LOG('OK')
|
||||
return
|
||||
|
||||
LOG('something wrong')
|
||||
|
||||
```
|
||||
|
||||
新的代码是不是就清晰多了?
|
||||
|
||||
另外,我们知道,一个函数的粒度应该尽可能细,不要让一个函数做太多的事情。所以,对待一个复杂的函数,我们需要尽可能地把它拆分成几个功能简单的函数,然后合并起来。那么,应该如何拆分函数呢?
|
||||
|
||||
这里,我以一个简单的二分搜索来举例说明。我给定一个非递减整数数组,和一个 target,要求你找到数组中最小的一个数 x,可以满足 `x*x > target`。一旦不存在,则返回 -1。
|
||||
|
||||
这个功能应该不难写吧。你不妨先自己写一下,写完后再对照着来看下面的代码,找出自己的问题。
|
||||
|
||||
```
|
||||
def solve(arr, target):
|
||||
l, r = 0, len(arr) - 1
|
||||
ret = -1
|
||||
while l <= r:
|
||||
m = (l + r) // 2
|
||||
if arr[m] * arr[m] > target:
|
||||
ret = m
|
||||
r = m - 1
|
||||
else:
|
||||
l = m + 1
|
||||
if ret == -1:
|
||||
return -1
|
||||
else:
|
||||
return arr[ret]
|
||||
|
||||
|
||||
print(solve([1, 2, 3, 4, 5, 6], 8))
|
||||
print(solve([1, 2, 3, 4, 5, 6], 9))
|
||||
print(solve([1, 2, 3, 4, 5, 6], 0))
|
||||
print(solve([1, 2, 3, 4, 5, 6], 40))
|
||||
|
||||
```
|
||||
|
||||
我给出的第一段代码这样的写法,在算法比赛和面试中已经 OK 了。不过,从工程角度来说,我们还能继续优化一下:
|
||||
|
||||
```
|
||||
def comp(x, target):
|
||||
return x * x > target
|
||||
|
||||
|
||||
def binary_search(arr, target):
|
||||
l, r = 0, len(arr) - 1
|
||||
ret = -1
|
||||
while l <= r:
|
||||
m = (l + r) // 2
|
||||
if comp(arr[m], target):
|
||||
ret = m
|
||||
r = m - 1
|
||||
else:
|
||||
l = m + 1
|
||||
return ret
|
||||
|
||||
|
||||
def solve(arr, target):
|
||||
id = binary_search(arr, target)
|
||||
|
||||
if id != -1:
|
||||
return arr[id]
|
||||
return -1
|
||||
|
||||
|
||||
print(solve([1, 2, 3, 4, 5, 6], 8))
|
||||
print(solve([1, 2, 3, 4, 5, 6], 9))
|
||||
print(solve([1, 2, 3, 4, 5, 6], 0))
|
||||
print(solve([1, 2, 3, 4, 5, 6], 40))
|
||||
|
||||
```
|
||||
|
||||
你可以看出,第二段代码中,我把不同功能的代码拿了出来。其中,comp() 函数作为核心判断,拿出来后可以让整个程序更清晰;同时,我也把二分搜索的主程序拿了出来,只负责二分搜索;最后的 solve() 函数拿到结果,决定返回不存在,还是返回值。这样一来,每个函数各司其职,阅读性也能得到一定提高。
|
||||
|
||||
最后,我们再来看一下如何拆分类。老规矩,先看代码:
|
||||
|
||||
```
|
||||
class Person:
|
||||
def __init__(self, name, sex, age, job_title, job_description, company_name):
|
||||
self.name = name
|
||||
self.sex = sex
|
||||
self.age = age
|
||||
self.job_title = job_title
|
||||
self.job_description = description
|
||||
self.company_name = company_name
|
||||
|
||||
```
|
||||
|
||||
你应该能看得出来,job 在其中出现了很多次,而且它们表达的是一个意义实体,这种情况下,我们可以考虑将这部分分解出来,作为单独的类。
|
||||
|
||||
```
|
||||
class Person:
|
||||
def __init__(self, name, sex, age, job_title, job_description, company_name):
|
||||
self.name = name
|
||||
self.sex = sex
|
||||
self.age = age
|
||||
self.job = Job(job_title, job_description, company_name)
|
||||
|
||||
class Job:
|
||||
def __init__(self, job_title, job_description, company_name):
|
||||
|
||||
self.job_title = job_title
|
||||
self.job_description = description
|
||||
self.company_name = company_name
|
||||
|
||||
```
|
||||
|
||||
你看,改造后的代码,瞬间就清晰了很多。
|
||||
|
||||
## 总结
|
||||
|
||||
今天这节课,我们简单讲述了如何提高 Python 代码的可读性,主要介绍了PEP 8 规范,并通过实例的说明和改造,让你清楚如何对 Python 程序进行可读性优化。
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,我想留一个思考题。这次的思考题开放一些,希望你在评论区讲一讲,你自己在初学编程时,不注意规范问题而犯下的错误,和这些错误会导致什么样的后果,比如对后来读代码的人有严重的误导,或是埋下了潜在的 bug 等等。
|
||||
|
||||
希望你在留言区分享你的经历,你也可以把这篇文章分享出去,让更多的人互相交流心得体会,留下真实的经历,并在经历中进步成长。
|
||||
|
||||
|
||||
231
极客时间专栏/Python核心技术与实战/规范篇/28 | 如何合理利用assert?.md
Normal file
231
极客时间专栏/Python核心技术与实战/规范篇/28 | 如何合理利用assert?.md
Normal file
@@ -0,0 +1,231 @@
|
||||
<audio id="audio" title="28 | 如何合理利用assert?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8f/e1/8fb093ab510ab57d805081cfba734ae1.mp3"></audio>
|
||||
|
||||
你好,我是景霄。
|
||||
|
||||
相信你平时在写代码时,肯定或多或少看到过assert的存在。我也曾在日常的code review中,被一些同事要求增加assert语句,让代码更加健壮。
|
||||
|
||||
不过,尽管如此,我发现在很多情况下,assert还是很容易被忽略,人们似乎对这么一个“不起眼”的东西并不关心。但事实上,这个看似“不起眼”的东西,如果能用好,对我们的程序大有裨益。
|
||||
|
||||
说了这么多,那么究竟什么是assert,我们又该如何合理地使用assert呢?今天这节课,我就带你一起来学习它的用法。
|
||||
|
||||
## 什么是assert?
|
||||
|
||||
Python的assert语句,可以说是一个debug的好工具,主要用于测试一个条件是否满足。如果测试的条件满足,则什么也不做,相当于执行了pass语句;如果测试条件不满足,便会抛出异常AssertionError,并返回具体的错误信息(optional)。
|
||||
|
||||
它的具体语法是下面这样的:
|
||||
|
||||
```
|
||||
assert_stmt ::= "assert" expression ["," expression]
|
||||
|
||||
```
|
||||
|
||||
我们先来看一个简单形式的`assert expression`,比如下面这个例子:
|
||||
|
||||
```
|
||||
assert 1 == 2
|
||||
|
||||
```
|
||||
|
||||
它就相当于下面这两行代码:
|
||||
|
||||
```
|
||||
if __debug__:
|
||||
if not expression: raise AssertionError
|
||||
|
||||
```
|
||||
|
||||
再来看`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时加入括号,比如下面这个例子:
|
||||
|
||||
```
|
||||
assert(1 == 2, 'This should fail')
|
||||
# 输出
|
||||
<ipython-input-8-2c057bd7fe24>:1: SyntaxWarning: assertion is always true, perhaps remove parentheses?
|
||||
assert(1 == 2, 'This should fail')
|
||||
|
||||
```
|
||||
|
||||
如果你按照这样来写,无论表达式对与错(比如这里的1 == 2显然是错误的),assert检查永远不会fail,程序只会给你SyntaxWarning。
|
||||
|
||||
正确的写法,应该是下面这种不带括号的写法:
|
||||
|
||||
```
|
||||
assert 1 == 2, 'This should fail'
|
||||
# 输出
|
||||
AssertionError: This should fail
|
||||
|
||||
```
|
||||
|
||||
总的来说,assert在程序中的作用,是对代码做一些internal的self-check。使用assert,就表示你很确定。这个条件一定会发生或者一定不会发生。
|
||||
|
||||
举个例子,比如你有一个函数,其中一个参数是人的性别,因为性别只有男女之分(这里只指生理性别),你便可以使用assert,以防止程序的非法输入。如果你的程序没有bug,那么assert永远不会抛出异常;而它一旦抛出了异常,你就知道程序存在问题了,并且可以根据错误信息,很容易定位出错误的源头。
|
||||
|
||||
## assert 的用法
|
||||
|
||||
讲完了assert的基本语法与概念,我们接下来通过一些实际应用的例子,来看看assert在Python中的用法,并弄清楚 assert 的使用场景。
|
||||
|
||||
第一个例子,假设你现在使用的极客时间正在做专栏促销活动,准备对一些专栏进行打折,所以后台需要写一个apply_discount()函数,要求输入为原来的价格和折扣,输出是折后的价格。那么,我们可以大致写成下面这样:
|
||||
|
||||
```
|
||||
def apply_discount(price, discount):
|
||||
updated_price = price * (1 - discount)
|
||||
assert 0 <= updated_price <= price, 'price should be greater or equal to 0 and less or equal to original price'
|
||||
return updated_price
|
||||
|
||||
```
|
||||
|
||||
可以看到,在计算新价格的后面,我们还写了一个assert语句,用来检查折后价格,这个值必须大于等于0、小于等于原来的价格,否则就抛出异常。
|
||||
|
||||
我们可以试着输入几组数,来验证一下这个功能:
|
||||
|
||||
```
|
||||
apply_discount(100, 0.2)
|
||||
80.0
|
||||
|
||||
apply_discount(100, 2)
|
||||
AssertionError: price should be greater or equal to 0 and less or equal to original price
|
||||
|
||||
```
|
||||
|
||||
显然,当discount是0.2时,输出80,没有问题。但是当discount为2时,程序便抛出下面这个异常:
|
||||
|
||||
```
|
||||
AssertionError:price should be greater or equal to 0 and less or equal to original price
|
||||
|
||||
```
|
||||
|
||||
这样一来,如果开发人员修改相关的代码,或者是加入新的功能,导致discount数值的异常时,我们运行测试时就可以很容易发现问题。正如我开头所说,assert的加入,可以有效预防bug的发生,提高程序的健壮性。
|
||||
|
||||
再来看一个例子,最常见的除法操作,这在任何领域的计算中都经常会遇到。同样还是以极客时间为例,假如极客时间后台想知道每个专栏的平均销售价格,那么就需要给定销售总额和销售数目,这样平均销售价格便很容易计算出来:
|
||||
|
||||
```
|
||||
def calculate_average_price(total_sales, num_sales):
|
||||
assert num_sales > 0, 'number of sales should be greater than 0'
|
||||
return total_sales / num_sales
|
||||
|
||||
```
|
||||
|
||||
同样的,我们也加入了assert语句,规定销售数目必须大于0,这样就可以防止后台计算那些还未开卖的专栏的价格。
|
||||
|
||||
除了这两个例子,在实际工作中,assert还有一些很常见的用法,比如下面的场景:
|
||||
|
||||
```
|
||||
def func(input):
|
||||
assert isinstance(input, list), 'input must be type of list'
|
||||
# 下面的操作都是基于前提:input必须是list
|
||||
if len(input) == 1:
|
||||
...
|
||||
elif len(input) == 2:
|
||||
...
|
||||
else:
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
这里函数func()里的所有操作,都是基于输入必须是list 这个前提。是不是很熟悉的需求呢?那我们就很有必要在开头加一句assert的检查,防止程序出错。
|
||||
|
||||
当然,我们也要根据具体情况具体分析。比如上面这个例子,之所以能加assert,是因为我们很确定输入必须是list,不能是其他数据类型。
|
||||
|
||||
如果你的程序中,允许input是其他数据类型,并且对不同的数据类型都有不同的处理方式,那你就应该写成if else的条件语句了:
|
||||
|
||||
```
|
||||
def func(input):
|
||||
if isinstance(input, list):
|
||||
...
|
||||
else:
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
## assert错误示例
|
||||
|
||||
前面我们讲了这么多 assert的使用场景,可能给你一种错觉,也可能会让你有些迷茫:很多地方都可以使用assert, 那么,很多if条件语句是不是都可以换成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检查。那么我想让你思考一下,这样写到底对不对呢?
|
||||
|
||||
答案显然是否定的。你可能觉得,从代码功能角度来说,这没错啊。但是在实际工程中,基本上没人会这么写。为什么呢?
|
||||
|
||||
要注意,前面我说过,assert的检查是可以被关闭的,比如在运行Python程序时,加入`-O`这个选项就会让assert失效。因此,一旦assert的检查被关闭,user_is_admin()和course_exist()这两个函数便不会被执行。这就会导致:
|
||||
|
||||
- 任何用户都有权限删除专栏课程;
|
||||
- 并且,不管这个课程是否存在,他们都可以强行执行删除操作。
|
||||
|
||||
这显然会给程序带来巨大的安全漏洞。所以,正确的做法,是使用条件语句进行相应的检查,并合理抛出异常:
|
||||
|
||||
```
|
||||
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)
|
||||
|
||||
```
|
||||
|
||||
再来看一个例子,如果你想打开一个文件,进行数据读取、处理等一系列操作,那么下面这样的写法,显然也是不正确的:
|
||||
|
||||
```
|
||||
def read_and_process(path):
|
||||
assert file_exist(path), 'file must exist'
|
||||
with open(path) as f:
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
因为assert的使用,表明你强行指定了文件必须存在,但事实上在很多情况下,这个假设并不成立。另外,打开文件操作,也有可能触发其他的异常。所以,正确的做法是进行异常处理,用try和except来解决:
|
||||
|
||||
```
|
||||
def read_and_process(path):
|
||||
try:
|
||||
with open(path) as f:
|
||||
...
|
||||
except Exception as e:
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
总的来说,assert并不适用run-time error 的检查。比如你试图打开一个文件,但文件不存在;再或者是你试图从网上下载一个东西,但中途断网了了等等,这些情况下,还是应该参照我们前面所讲的[错误与异常](https://time.geekbang.org/column/article/97462)的内容,进行正确处理。
|
||||
|
||||
## 总结
|
||||
|
||||
今天这节课,我们一起学习了assert的用法。assert通常用来对代码进行必要的self check,表明你很确定这种情况一定发生,或者一定不会发生。需要注意的是,使用assert时,一定不要加上括号,否则无论表达式对与错,assert检查永远不会fail。另外,程序中的assert语句,可以通过`-O`等选项被全局disable。
|
||||
|
||||
通过这节课的几个使用场景,你能看到,assert的合理使用,可以增加代码的健壮度,同时也方便了程序出错时开发人员的定位排查。
|
||||
|
||||
不过,我们也不能滥用assert。很多情况下,程序中出现的不同情况都是意料之中的,需要我们用不同的方案去处理,这时候用条件语句进行判断更为合适。而对于程序中的一些run-time error,请记得使用异常处理。
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,给你留一个思考题。在平时的工作学习中,你用过assert吗?如果用过的话,是在什么情况下使用的?有遇到过什么问题吗?
|
||||
|
||||
欢迎在留言区写下你的经历,还有今天学习的心得和疑惑,与我一起分享。也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流,一起进步。
|
||||
|
||||
|
||||
251
极客时间专栏/Python核心技术与实战/规范篇/29 | 巧用上下文管理器和With语句精简代码.md
Normal file
251
极客时间专栏/Python核心技术与实战/规范篇/29 | 巧用上下文管理器和With语句精简代码.md
Normal file
@@ -0,0 +1,251 @@
|
||||
<audio id="audio" title="29 | 巧用上下文管理器和With语句精简代码" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3b/cc/3ba1f3fc566ae7bf51227d934e1e9acc.mp3"></audio>
|
||||
|
||||
你好,我是景霄。
|
||||
|
||||
我想你对Python中的with语句一定不陌生,在专栏里它也曾多次出现,尤其是在文件的输入输出操作中,不过我想,大部分人可能习惯了它的使用,却并不知道隐藏在其背后的“秘密”。
|
||||
|
||||
那么,究竟with语句要怎么用,与之相关的上下文管理器(context manager)是什么,它们之间又有着怎样的联系呢?这节课,我就带你一起揭开它们的神秘面纱。
|
||||
|
||||
## 什么是上下文管理器?
|
||||
|
||||
在任何一门编程语言中,文件的输入输出、数据库的连接断开等,都是很常见的资源管理操作。但资源都是有限的,在写程序时,我们必须保证这些资源在使用过后得到释放,不然就容易造成资源泄露,轻者使得系统处理缓慢,重则会使系统崩溃。
|
||||
|
||||
光说这些概念,你可能体会不到这一点,我们可以看看下面的例子:
|
||||
|
||||
```
|
||||
for x in range(10000000):
|
||||
f = open('test.txt', 'w')
|
||||
f.write('hello')
|
||||
|
||||
```
|
||||
|
||||
这里我们一共打开了10000000个文件,但是用完以后都没有关闭它们,如果你运行该段代码,便会报错:
|
||||
|
||||
```
|
||||
OSError: [Errno 23] Too many open files in system: 'test.txt'
|
||||
|
||||
```
|
||||
|
||||
这就是一个典型的资源泄露的例子。因为程序中同时打开了太多的文件,占据了太多的资源,造成系统崩溃。
|
||||
|
||||
为了解决这个问题,不同的编程语言都引入了不同的机制。而在Python中,对应的解决方式便是上下文管理器(context manager)。上下文管理器,能够帮助你自动分配并且释放资源,其中最典型的应用便是with语句。所以,上面代码的正确写法应该如下所示:
|
||||
|
||||
```
|
||||
for x in range(10000000):
|
||||
with open('test.txt', 'w') as f:
|
||||
f.write('hello')
|
||||
|
||||
```
|
||||
|
||||
这样,我们每次打开文件`“test.txt”`,并写入`‘hello’`之后,这个文件便会自动关闭,相应的资源也可以得到释放,防止资源泄露。当然,with语句的代码,也可以用下面的形式表示:
|
||||
|
||||
```
|
||||
f = open('test.txt', 'w')
|
||||
try:
|
||||
f.write('hello')
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
```
|
||||
|
||||
要注意的是,最后的finally block尤其重要,哪怕在写入文件时发生错误异常,它也可以保证该文件最终被关闭。不过与with语句相比,这样的代码就显得冗余了,并且还容易漏写,因此我们一般更倾向于使用with语句。
|
||||
|
||||
另外一个典型的例子,是Python中的threading.lock类。举个例子,比如我想要获取一个锁,执行相应的操作,完成后再释放,那么代码就可以写成下面这样:
|
||||
|
||||
```
|
||||
some_lock = threading.Lock()
|
||||
some_lock.acquire()
|
||||
try:
|
||||
...
|
||||
finally:
|
||||
some_lock.release()
|
||||
|
||||
```
|
||||
|
||||
而对应的with语句,同样非常简洁:
|
||||
|
||||
```
|
||||
some_lock = threading.Lock()
|
||||
with somelock:
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
我们可以从这两个例子中看到,with语句的使用,可以简化了代码,有效避免资源泄露的发生。
|
||||
|
||||
## 上下文管理器的实现
|
||||
|
||||
### 基于类的上下文管理器
|
||||
|
||||
了解了上下文管理的概念和优点后,下面我们就通过具体的例子,一起来看看上下文管理器的原理,搞清楚它的内部实现。这里,我自定义了一个上下文管理类FileManager,模拟Python的打开、关闭文件操作:
|
||||
|
||||
```
|
||||
class FileManager:
|
||||
def __init__(self, name, mode):
|
||||
print('calling __init__ method')
|
||||
self.name = name
|
||||
self.mode = mode
|
||||
self.file = None
|
||||
|
||||
def __enter__(self):
|
||||
print('calling __enter__ method')
|
||||
self.file = open(self.name, self.mode)
|
||||
return self.file
|
||||
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
print('calling __exit__ method')
|
||||
if self.file:
|
||||
self.file.close()
|
||||
|
||||
with FileManager('test.txt', 'w') as f:
|
||||
print('ready to write to file')
|
||||
f.write('hello world')
|
||||
|
||||
## 输出
|
||||
calling __init__ method
|
||||
calling __enter__ method
|
||||
ready to write to file
|
||||
calling __exit__ method
|
||||
|
||||
```
|
||||
|
||||
需要注意的是,当我们用类来创建上下文管理器时,必须保证这个类包括方法`”__enter__()”`和方法`“__exit__()”`。其中,方法`“__enter__()”`返回需要被管理的资源,方法`“__exit__()”`里通常会存在一些释放、清理资源的操作,比如这个例子中的关闭文件等等。
|
||||
|
||||
而当我们用with语句,执行这个上下文管理器时:
|
||||
|
||||
```
|
||||
with FileManager('test.txt', 'w') as f:
|
||||
f.write('hello world')
|
||||
|
||||
```
|
||||
|
||||
下面这四步操作会依次发生:
|
||||
|
||||
1. 方法`“__init__()”`被调用,程序初始化对象FileManager,使得文件名(name)是`"test.txt"`,文件模式(mode)是`'w'`;
|
||||
1. 方法`“__enter__()”`被调用,文件`“test.txt”`以写入的模式被打开,并且返回FileManager对象赋予变量f;
|
||||
1. 字符串`“hello world”`被写入文件`“test.txt”`;
|
||||
1. 方法`“__exit__()”`被调用,负责关闭之前打开的文件流。
|
||||
|
||||
因此,这个程序的输出是:
|
||||
|
||||
```
|
||||
calling __init__ method
|
||||
calling __enter__ method
|
||||
ready to write to file
|
||||
calling __exit__ meth
|
||||
|
||||
```
|
||||
|
||||
另外,值得一提的是,方法`“__exit__()”`中的参数`“exc_type, exc_val, exc_tb”`,分别表示exception_type、exception_value和traceback。当我们执行含有上下文管理器的with语句时,如果有异常抛出,异常的信息就会包含在这三个变量中,传入方法`“__exit__()”`。
|
||||
|
||||
因此,如果你需要处理可能发生的异常,可以在`“__exit__()”`添加相应的代码,比如下面这样来写:
|
||||
|
||||
```
|
||||
class Foo:
|
||||
def __init__(self):
|
||||
print('__init__ called')
|
||||
|
||||
def __enter__(self):
|
||||
print('__enter__ called')
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_tb):
|
||||
print('__exit__ called')
|
||||
if exc_type:
|
||||
print(f'exc_type: {exc_type}')
|
||||
print(f'exc_value: {exc_value}')
|
||||
print(f'exc_traceback: {exc_tb}')
|
||||
print('exception handled')
|
||||
return True
|
||||
|
||||
with Foo() as obj:
|
||||
raise Exception('exception raised').with_traceback(None)
|
||||
|
||||
# 输出
|
||||
__init__ called
|
||||
__enter__ called
|
||||
__exit__ called
|
||||
exc_type: <class 'Exception'>
|
||||
exc_value: exception raised
|
||||
exc_traceback: <traceback object at 0x1046036c8>
|
||||
exception handled
|
||||
|
||||
```
|
||||
|
||||
这里,我们在with语句中手动抛出了异常“exception raised”,你可以看到,`“__exit__()”`方法中异常,被顺利捕捉并进行了处理。不过需要注意的是,如果方法`“__exit__()”`没有返回True,异常仍然会被抛出。因此,如果你确定异常已经被处理了,请在`“__exit__()”`的最后,加上`“return True”`这条语句。
|
||||
|
||||
同样的,数据库的连接操作,也常常用上下文管理器来表示,这里我给出了比较简化的代码:
|
||||
|
||||
```
|
||||
class DBConnectionManager:
|
||||
def __init__(self, hostname, port):
|
||||
self.hostname = hostname
|
||||
self.port = port
|
||||
self.connection = None
|
||||
|
||||
def __enter__(self):
|
||||
self.connection = DBClient(self.hostname, self.port)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.connection.close()
|
||||
|
||||
with DBConnectionManager('localhost', '8080') as db_client:
|
||||
|
||||
```
|
||||
|
||||
与前面FileManager的例子类似:
|
||||
|
||||
- 方法`“__init__()”`负责对数据库进行初始化,也就是将主机名、接口(这里是localhost和8080)分别赋予变量hostname和port;
|
||||
- 方法`“__enter__()”`连接数据库,并且返回对象DBConnectionManager;
|
||||
- 方法`“__exit__()”`则负责关闭数据库的连接。
|
||||
|
||||
这样一来,只要你写完了DBconnectionManager这个类,那么在程序每次连接数据库时,我们都只需要简单地调用with语句即可,并不需要关心数据库的关闭、异常等等,显然大大提高了开发的效率。
|
||||
|
||||
### 基于生成器的上下文管理器
|
||||
|
||||
诚然,基于类的上下文管理器,在Python中应用广泛,也是我们经常看到的形式,不过Python中的上下文管理器并不局限于此。除了基于类,它还可以基于生成器实现。接下来我们来看一个例子。
|
||||
|
||||
比如,你可以使用装饰器contextlib.contextmanager,来定义自己所需的基于生成器的上下文管理器,用以支持with语句。还是拿前面的类上下文管理器FileManager来说,我们也可以用下面形式来表示:
|
||||
|
||||
```
|
||||
from contextlib import contextmanager
|
||||
|
||||
@contextmanager
|
||||
def file_manager(name, mode):
|
||||
try:
|
||||
f = open(name, mode)
|
||||
yield f
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
with file_manager('test.txt', 'w') as f:
|
||||
f.write('hello world')
|
||||
|
||||
```
|
||||
|
||||
这段代码中,函数file_manager()是一个生成器,当我们执行with语句时,便会打开文件,并返回文件对象f;当with语句执行完后,finally block中的关闭文件操作便会执行。
|
||||
|
||||
你可以看到,使用基于生成器的上下文管理器时,我们不再用定义`“__enter__()”`和`“__exit__()”`方法,但请务必加上装饰器@contextmanager,这一点新手很容易疏忽。
|
||||
|
||||
讲完这两种不同原理的上下文管理器后,还需要强调的是,基于类的上下文管理器和基于生成器的上下文管理器,这两者在功能上是一致的。只不过,
|
||||
|
||||
- 基于类的上下文管理器更加flexible,适用于大型的系统开发;
|
||||
- 而基于生成器的上下文管理器更加方便、简洁,适用于中小型程序。
|
||||
|
||||
无论你使用哪一种,请不用忘记在方法`“__exit__()”`或者是finally block中释放资源,这一点尤其重要。
|
||||
|
||||
## 总结
|
||||
|
||||
这节课,我们先通过一个简单的例子,了解了资源泄露的易发生性,和其带来的严重后果,从而引入了应对方案——即上下文管理器的概念。上下文管理器,通常应用在文件的打开关闭和数据库的连接关闭等场景中,可以确保用过的资源得到迅速释放,有效提高了程序的安全性,
|
||||
|
||||
接着,我们通过自定义上下文管理的实例,了解了上下文管理工作的原理,并一起学习了基于类的上下文管理器和基于生成器的上下文管理器,这两者的功能相同,具体用哪个,取决于你的具体使用场景。
|
||||
|
||||
另外,上下文管理器通常和with语句一起使用,大大提高了程序的简洁度。需要注意的是,当我们用with语句执行上下文管理器的操作时,一旦有异常抛出,异常的类型、值等具体信息,都会通过参数传入`“__exit__()”`函数中。你可以自行定义相关的操作对异常进行处理,而处理完异常后,也别忘了加上`“return True”`这条语句,否则仍然会抛出异常。
|
||||
|
||||
## 思考题
|
||||
|
||||
那么,在你日常的学习工作中,哪些场景使用过上下文管理器?使用过程中又遇到了哪些问题,或是有什么新的发现呢?欢迎在下方留言与我讨论,也欢迎你把这篇文章分享出去,我们一起交流,一起进步。
|
||||
|
||||
|
||||
293
极客时间专栏/Python核心技术与实战/规范篇/30 | 真的有必要写单元测试吗?.md
Normal file
293
极客时间专栏/Python核心技术与实战/规范篇/30 | 真的有必要写单元测试吗?.md
Normal file
@@ -0,0 +1,293 @@
|
||||
<audio id="audio" title="30 | 真的有必要写单元测试吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a4/32/a4934a46d694862967c94b6ed3a0f132.mp3"></audio>
|
||||
|
||||
你好,我是景霄。
|
||||
|
||||
说到unit test(即单元测试,下文统一用中文称呼),大部分人的反应估计有这么两种:要么就是,单元测试啊,挺简单的呀,做不做无所谓吧;要么就是,哎呀,项目进度太赶,单元测试拖一拖之后再来吧。
|
||||
|
||||
显然,这两种人,都没有正确认识到单元测试的价值,也没能掌握正确的单元测试方法。你是不是觉得自己只要了解Python的各个feature,能够编写出符合规定功能的程序就可以了呢?
|
||||
|
||||
其实不然,完成产品的功能需求只是很基础的一部分,如何保证所写代码的稳定、高效、无误,才是我们工作的关键。而学会合理地使用单元测试,正是帮助你实现这一目标的重要路径。
|
||||
|
||||
我们总说,测试驱动开发(TDD)。今天我就以Python为例,教你设计编写Python的单元测试代码,带你熟悉并掌握这一重要技能。
|
||||
|
||||
## 什么是单元测试?
|
||||
|
||||
单元测试,通俗易懂地讲,就是编写测试来验证某一个模块的功能正确性,一般会指定输入,验证输出是否符合预期。
|
||||
|
||||
实际生产环境中,我们会对每一个模块的所有可能输入值进行测试。这样虽然显得繁琐,增加了额外的工作量,但是能够大大提高代码质量,减小bug发生的可能性,也更方便系统的维护。
|
||||
|
||||
说起单元测试,就不得不提 [Python unittest库](https://docs.python.org/3/library/unittest.html),它提供了我们需要的大多数工具。我们来看下面这个简单的测试,从代码中了解其使用方法:
|
||||
|
||||
```
|
||||
import unittest
|
||||
|
||||
# 将要被测试的排序函数
|
||||
def sort(arr):
|
||||
l = len(arr)
|
||||
for i in range(0, l):
|
||||
for j in range(i + 1, l):
|
||||
if arr[i] >= arr[j]:
|
||||
tmp = arr[i]
|
||||
arr[i] = arr[j]
|
||||
arr[j] = tmp
|
||||
|
||||
|
||||
# 编写子类继承unittest.TestCase
|
||||
class TestSort(unittest.TestCase):
|
||||
|
||||
# 以test开头的函数将会被测试
|
||||
def test_sort(self):
|
||||
arr = [3, 4, 1, 5, 6]
|
||||
sort(arr)
|
||||
# assert 结果跟我们期待的一样
|
||||
self.assertEqual(arr, [1, 3, 4, 5, 6])
|
||||
|
||||
if __name__ == '__main__':
|
||||
## 如果在Jupyter下,请用如下方式运行单元测试
|
||||
unittest.main(argv=['first-arg-is-ignored'], exit=False)
|
||||
|
||||
## 如果是命令行下运行,则:
|
||||
## unittest.main()
|
||||
|
||||
## 输出
|
||||
..
|
||||
----------------------------------------------------------------------
|
||||
Ran 2 tests in 0.002s
|
||||
|
||||
OK
|
||||
|
||||
```
|
||||
|
||||
这里,我们创建了一个排序函数的单元测试,来验证排序函数的功能是否正确。代码里我做了非常详细的注释,相信你能够大致读懂,我再来介绍一些细节。
|
||||
|
||||
首先,我们需要创建一个类`TestSort`,继承类`‘unittest.TestCase’`;然后,在这个类中定义相应的测试函数test_sort(),进行测试。注意,测试函数要以`‘test’`开头,而测试函数的内部,通常使用assertEqual()、assertTrue()、assertFalse()和assertRaise()等assert语句对结果进行验证。
|
||||
|
||||
最后运行时,如果你是在IPython或者Jupyter环境下,请使用下面这行代码:
|
||||
|
||||
```
|
||||
unittest.main(argv=['first-arg-is-ignored'], exit=False)
|
||||
|
||||
```
|
||||
|
||||
而如果你用的是命令行,直接使用unittest.main()就可以了。你可以看到,运行结果输出`’OK‘`,这就表示我们的测试通过了。
|
||||
|
||||
当然,这个例子中的被测函数相对简单一些,所以写起对应的单元测试来也非常自然,并不需要很多单元测试的技巧。但实战中的函数往往还是比较复杂的,遇到复杂问题,高手和新手的最大差别,便是单元测试技巧的使用。
|
||||
|
||||
## 单元测试的几个技巧
|
||||
|
||||
接下来,我将会介绍Python单元测试的几个技巧,分别是mock、side_effect和patch。这三者用法不一样,但都是一个核心思想,即**用虚假的实现,来替换掉被测试函数的一些依赖项,让我们能把更多的精力放在需要被测试的功能上。**
|
||||
|
||||
### mock
|
||||
|
||||
mock是单元测试中最核心重要的一环。mock的意思,便是通过一个虚假对象,来代替被测试函数或模块需要的对象。
|
||||
|
||||
举个例子,比如你要测一个后端API逻辑的功能性,但一般后端API都依赖于数据库、文件系统、网络等。这样,你就需要通过mock,来创建一些虚假的数据库层、文件系统层、网络层对象,以便可以简单地对核心后端逻辑单元进行测试。
|
||||
|
||||
Python mock则主要使用mock或者MagicMock对象,这里我也举了一个代码示例。这个例子看上去比较简单,但是里面的思想很重要。下面我们一起来看下:
|
||||
|
||||
```
|
||||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
class A(unittest.TestCase):
|
||||
def m1(self):
|
||||
val = self.m2()
|
||||
self.m3(val)
|
||||
|
||||
def m2(self):
|
||||
pass
|
||||
|
||||
def m3(self, val):
|
||||
pass
|
||||
|
||||
def test_m1(self):
|
||||
a = A()
|
||||
a.m2 = MagicMock(return_value="custom_val")
|
||||
a.m3 = MagicMock()
|
||||
a.m1()
|
||||
self.assertTrue(a.m2.called) #验证m2被call过
|
||||
a.m3.assert_called_with("custom_val") #验证m3被指定参数call过
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(argv=['first-arg-is-ignored'], exit=False)
|
||||
|
||||
## 输出
|
||||
..
|
||||
----------------------------------------------------------------------
|
||||
Ran 2 tests in 0.002s
|
||||
|
||||
OK
|
||||
|
||||
```
|
||||
|
||||
这段代码中,我们定义了一个类的三个方法m1()、m2()、m3()。我们需要对m1()进行单元测试,但是m1()取决于m2()和m3()。如果m2()和m3()的内部比较复杂, 你就不能只是简单地调用m1()函数来进行测试,可能需要解决很多依赖项的问题。
|
||||
|
||||
这一听就让人头大了吧?但是,有了mock其实就很好办了。我们可以把m2()替换为一个返回具体数值的value,把m3()替换为另一个mock(空函数)。这样,测试m1()就很容易了,我们可以测试m1()调用m2(),并且用m2()的返回值调用m3()。
|
||||
|
||||
可能你会疑惑,这样测试m1()不是基本上毫无意义吗?看起来只是象征性地测了一下逻辑呀?
|
||||
|
||||
其实不然,真正工业化的代码,都是很多层模块相互逻辑调用的一个树形结构。单元测试需要测的是某个节点的逻辑功能,mock掉相关的依赖项是非常重要的。这也是为什么会被叫做单元测试unit test,而不是其他的integration test、end to end test这类。
|
||||
|
||||
### Mock Side Effect
|
||||
|
||||
第二个我们来看Mock Side Effect,这个概念很好理解,就是 mock的函数,属性是可以根据不同的输入,返回不同的数值,而不只是一个return_value。
|
||||
|
||||
比如下面这个示例,例子很简单,测试的是输入参数是否为负数,输入小于0则输出为1 ,否则输出为2。代码很简短,你一定可以看懂,这便是Mock Side Effect的用法。
|
||||
|
||||
```
|
||||
from unittest.mock import MagicMock
|
||||
def side_effect(arg):
|
||||
if arg < 0:
|
||||
return 1
|
||||
else:
|
||||
return 2
|
||||
mock = MagicMock()
|
||||
mock.side_effect = side_effect
|
||||
|
||||
mock(-1)
|
||||
1
|
||||
|
||||
mock(1)
|
||||
2
|
||||
|
||||
```
|
||||
|
||||
### patch
|
||||
|
||||
至于patch,给开发者提供了非常便利的函数mock方法。它可以应用Python的decoration模式或是context manager概念,快速自然地mock所需的函数。它的用法也不难,我们来看代码:
|
||||
|
||||
```
|
||||
from unittest.mock import patch
|
||||
|
||||
@patch('sort')
|
||||
def test_sort(self, mock_sort):
|
||||
...
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
在这个test里面,mock_sort 替代sort函数本身的存在,所以,我们可以像开始提到的mock object一样,设置return_value和side_effect。
|
||||
|
||||
另一种patch的常见用法,是mock类的成员函数,这个技巧我们在工作中也经常会用到,比如说一个类的构造函数非常复杂,而测试其中一个成员函数并不依赖所有初始化的object。它的用法如下:
|
||||
|
||||
```
|
||||
with patch.object(A, '__init__', lambda x: None):
|
||||
…
|
||||
|
||||
```
|
||||
|
||||
代码应该也比较好懂。在with语句里面,我们通过patch,将A类的构造函数mock为一个do nothing的函数,这样就可以很方便地避免一些复杂的初始化(initialization)。
|
||||
|
||||
其实,综合前面讲的这几点来看,你应该感受到了,单元测试的核心还是mock,mock掉依赖项,测试相应的逻辑或算法的准确性。在我看来,虽然Python unittest库还有很多层出不穷的方法,但只要你能掌握了MagicMock和patch,编写绝大部分工作场景的单元测试就不成问题了。
|
||||
|
||||
## 高质量单元测试的关键
|
||||
|
||||
这节课的最后,我想谈一谈高质量的单元测试。我很理解,单元测试这个东西,哪怕是正在使用的人也是“百般讨厌”的,不少人很多时候只是敷衍了事。我也嫌麻烦,但从来不敢松懈,因为在大公司里,如果你写一个很重要的模块功能,不写单元测试是无法通过code review的。
|
||||
|
||||
低质量的单元测试,可能真的就是摆设,根本不能帮我们验证代码的正确性,还浪费时间。那么,既然要做单元测试,与其浪费时间糊弄自己,不如追求高质量的单元测试,切实提高代码品质。
|
||||
|
||||
那该怎么做呢?结合工作经验,我认为一个高质量的单元测试,应该特别关注下面两点。
|
||||
|
||||
### Test Coverage
|
||||
|
||||
首先我们要关注Test Coverage,它是衡量代码中语句被cover的百分比。可以说,提高代码模块的Test Coverage,基本等同于提高代码的正确性。
|
||||
|
||||
为什么呢?
|
||||
|
||||
要知道,大多数公司代码库的模块都非常复杂。尽管它们遵从模块化设计的理念,但因为有复杂的业务逻辑在,还是会产生逻辑越来越复杂的模块。所以,编写高质量的单元测试,需要我们cover模块的每条语句,提高Test Coverage。
|
||||
|
||||
我们可以用Python的coverage tool 来衡量Test Coverage,并且显示每个模块为被coverage的语句。如果你想了解更多更详细的使用,可以点击这个链接来学习:[https://coverage.readthedocs.io/en/v4.5.x/](https://coverage.readthedocs.io/en/v4.5.x/) 。
|
||||
|
||||
### 模块化
|
||||
|
||||
高质量单元测试,不仅要求我们提高Test Coverage,尽量让所写的测试能够cover每个模块中的每条语句;还要求我们从测试的角度审视codebase,去思考怎么模块化代码,以便写出高质量的单元测试。
|
||||
|
||||
光讲这段话可能有些抽象,我们来看这样的场景。比如,我写了一个下面这个函数,对一个数组进行处理,并返回新的数组:
|
||||
|
||||
```
|
||||
def work(arr):
|
||||
# pre process
|
||||
...
|
||||
...
|
||||
# sort
|
||||
l = len(arr)
|
||||
for i in range(0, l):
|
||||
for j in range(i + 1, j):
|
||||
if arr[i] >= arr[j]:
|
||||
tmp = arr[i]
|
||||
arr[i] = arr[j]
|
||||
arr[j] = tmp
|
||||
# post process
|
||||
...
|
||||
...
|
||||
Return arr
|
||||
|
||||
```
|
||||
|
||||
这段代码的大概意思是,先有个预处理,再排序,最后再处理一下然后返回。如果现在要求你,给这个函数写个单元测试,你是不是会一筹莫展呢?
|
||||
|
||||
毕竟,这个函数确实有点儿复杂,以至于你都不知道应该是怎样的输入,并要期望怎样的输出。这种代码写单元测试是非常痛苦的,更别谈cover每条语句的要求了。
|
||||
|
||||
所以,正确的测试方法,应该是先模块化代码,写成下面的形式:
|
||||
|
||||
```
|
||||
def preprocess(arr):
|
||||
...
|
||||
...
|
||||
return arr
|
||||
|
||||
def sort(arr):
|
||||
...
|
||||
...
|
||||
return arr
|
||||
|
||||
def postprocess(arr):
|
||||
...
|
||||
return arr
|
||||
|
||||
def work(self):
|
||||
arr = preprocess(arr)
|
||||
arr = sort(arr)
|
||||
arr = postprocess(arr)
|
||||
return arr
|
||||
|
||||
```
|
||||
|
||||
接着再进行相应的测试,测试三个子函数的功能正确性;然后通过mock子函数,调用work()函数,来验证三个子函数被call过。
|
||||
|
||||
```
|
||||
from unittest.mock import patch
|
||||
|
||||
def test_preprocess(self):
|
||||
...
|
||||
|
||||
def test_sort(self):
|
||||
...
|
||||
|
||||
def test_postprocess(self):
|
||||
...
|
||||
|
||||
@patch('%s.preprocess')
|
||||
@patch('%s.sort')
|
||||
@patch('%s.postprocess')
|
||||
def test_work(self,mock_post_process, mock_sort, mock_preprocess):
|
||||
work()
|
||||
self.assertTrue(mock_post_process.called)
|
||||
self.assertTrue(mock_sort.called)
|
||||
self.assertTrue(mock_preprocess.called)
|
||||
|
||||
```
|
||||
|
||||
你看,这样一来,通过重构代码就可以使单元测试更加全面、精确,并且让整体架构、函数设计都美观了不少。
|
||||
|
||||
## 总结
|
||||
|
||||
回顾下这节课,整体来看,单元测试的理念是先模块化代码设计,然后针对每个作用单元,编写单独的测试去验证其准确性。更好的模块化设计和更多的Test Coverage,是提高代码质量的核心。而单元测试的本质就是通过mock,去除掉不影响测试的依赖项,把重点放在需要测试的代码核心逻辑上。
|
||||
|
||||
讲了这么多,还是想告诉你,单元测试是个非常非常重要的技能,在实际工作中是保证代码质量和准确性必不可少的一环。同时,单元测试的设计技能,不只是适用于Python,而是适用于任何语言。所以,单元测试必不可少。
|
||||
|
||||
## 思考题
|
||||
|
||||
那么,你在平时的学习工作中,曾经写过单元测试吗?在编写单元测试时,用到过哪些技巧或者遇到过哪些问题吗?欢迎留言与我交流,也欢迎你把这篇文章分享出去。
|
||||
|
||||
|
||||
281
极客时间专栏/Python核心技术与实战/规范篇/31 | pdb & cProfile:调试和性能分析的法宝.md
Normal file
281
极客时间专栏/Python核心技术与实战/规范篇/31 | pdb & cProfile:调试和性能分析的法宝.md
Normal file
@@ -0,0 +1,281 @@
|
||||
<audio id="audio" title="31 | pdb & cProfile:调试和性能分析的法宝" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/70/07/70eaf90b67bbacab3af721301623ca07.mp3"></audio>
|
||||
|
||||
你好,我是景霄。
|
||||
|
||||
在实际生产环境中,对代码进行调试和性能分析,是一个永远都逃不开的话题。调试和性能分析的主要场景,通常有这么三个:
|
||||
|
||||
- 一是代码本身有问题,需要我们找到root cause并修复;
|
||||
- 二是代码效率有问题,比如过度浪费资源,增加latency,因此需要我们debug;
|
||||
- 三是在开发新的feature时,一般都需要测试。
|
||||
|
||||
在遇到这些场景时,究竟应该使用哪些工具,如何正确的使用这些工具,应该遵循什么样的步骤等等,就是这节课我们要讨论的话题。
|
||||
|
||||
## 用pdb进行代码调试
|
||||
|
||||
### pdb的必要性
|
||||
|
||||
首先,我们来看代码的调试。也许不少人会有疑问:代码调试?说白了不就是在程序中使用print()语句吗?
|
||||
|
||||
没错,在程序中相应的地方打印,的确是调试程序的一个常用手段,但这只适用于小型程序。因为你每次都得重新运行整个程序,或是一个完整的功能模块,才能看到打印出来的变量值。如果程序不大,每次运行都非常快,那么使用print(),的确是很方便的。
|
||||
|
||||
但是,如果我们面对的是大型程序,运行一次的调试成本很高。特别是对于一些tricky的例子来说,它们通常需要反复运行调试、追溯上下文代码,才能找到错误根源。这种情况下,仅仅依赖打印的效率自然就很低了。
|
||||
|
||||
我们可以想象下面这个场景。比如你最常使用的极客时间App,最近出现了一个bug,部分用户无法登陆。于是,后端工程师们开始debug。
|
||||
|
||||
他们怀疑错误的代码逻辑在某几个函数中,如果使用print()语句debug,很可能出现的场景是,工程师们在他们认为的10个最可能出现bug的地方,都使用print()语句,然后运行整个功能块代码(从启动到运行花了5min),看打印出来的结果值,是不是和预期相符。
|
||||
|
||||
如果结果值和预期相符,并能直接找到错误根源,显然是最好的。但实际情况往往是,
|
||||
|
||||
- 要么与预期并不相符,需要重复以上步骤,继续debug;
|
||||
- 要么虽说与预期相符,但前面的操作只是缩小了错误代码的范围,所以仍得继续添加print()语句,再一次运行相应的代码模块(又要5min),进行debug。
|
||||
|
||||
你可以看到,这样的效率就很低下了。哪怕只是遇到稍微复杂一点的case,两、三个工程师一下午的时间可能就没了。
|
||||
|
||||
可能又有人会说,现在很多的IDE不都有内置的debug工具吗?
|
||||
|
||||
这话说的也没错。比如我们常用的Pycharm,可以很方便地在程序中设置断点。这样程序只要运行到断点处,便会自动停下,你就可以轻松查看环境中各个变量的值,并且可以执行相应的语句,大大提高了调试的效率。
|
||||
|
||||
看到这里,你不禁会问,既然问题都解决了,那为什么还要学习pdb呢?其实在很多大公司,产品的创造与迭代,往往需要很多编程语言的支持;并且,公司内部也会开发很多自己的接口,尝试把尽可能多的语言给结合起来。
|
||||
|
||||
这就使得,很多情况下,单一语言的IDE,对混合代码并不支持UI形式的断点调试功能,或是只对某些功能模块支持。另外,考虑到不少代码已经挪到了类似Jupyter的Notebook中,往往就要求开发者使用命令行的形式,来对代码进行调试。
|
||||
|
||||
而Python的pdb,正是其自带的一个调试库。它为Python程序提供了交互式的源代码调试功能,是命令行版本的IDE断点调试器,完美地解决了我们刚刚讨论的这个问题。
|
||||
|
||||
### 如何使用pdb
|
||||
|
||||
了解了pdb的重要性与必要性后,接下来,我们就一起来看看,pdb在Python中到底应该如何使用。
|
||||
|
||||
首先,要启动pdb调试,我们只需要在程序中,加入`“import pdb”`和`“pdb.set_trace()”`这两行代码就行了,比如下面这个简单的例子:
|
||||
|
||||
```
|
||||
a = 1
|
||||
b = 2
|
||||
import pdb
|
||||
pdb.set_trace()
|
||||
c = 3
|
||||
print(a + b + c)
|
||||
|
||||
```
|
||||
|
||||
当我们运行这个程序时时,它的输出界面是下面这样的,表示程序已经运行到了`“pdb.set_trace()”`这行,并且暂停了下来,等待用户输入。
|
||||
|
||||
```
|
||||
> /Users/jingxiao/test.py(5)<module>()
|
||||
-> c = 3
|
||||
|
||||
```
|
||||
|
||||
这时,我们就可以执行,在IDE断点调试器中可以执行的一切操作,比如打印,语法是`"p <expression>"`:
|
||||
|
||||
```
|
||||
(pdb) p a
|
||||
1
|
||||
(pdb) p b
|
||||
2
|
||||
|
||||
```
|
||||
|
||||
你可以看到,我打印的是a和b的值,分别为1和2,与预期相符。为什么不打印c呢?显然,打印c会抛出异常,因为程序目前只运行了前面几行,此时的变量c还没有被定义:
|
||||
|
||||
```
|
||||
(pdb) p c
|
||||
*** NameError: name 'c' is not defined
|
||||
|
||||
```
|
||||
|
||||
除了打印,常见的操作还有`“n”`,表示继续执行代码到下一行,用法如下:
|
||||
|
||||
```
|
||||
(pdb) n
|
||||
-> print(a + b + c)
|
||||
|
||||
```
|
||||
|
||||
而命令`”l“`,则表示列举出当前代码行上下的11行源代码,方便开发者熟悉当前断点周围的代码状态:
|
||||
|
||||
```
|
||||
(pdb) l
|
||||
1 a = 1
|
||||
2 b = 2
|
||||
3 import pdb
|
||||
4 pdb.set_trace()
|
||||
5 -> c = 3
|
||||
6 print(a + b + c)
|
||||
|
||||
```
|
||||
|
||||
命令`“s“`,就是 step into 的意思,即进入相对应的代码内部。这时,命令行中会显示`”--Call--“`的字样,当你执行完内部的代码块后,命令行中则会出现`”--Return--“`的字样。
|
||||
|
||||
我们来看下面这个例子:
|
||||
|
||||
```
|
||||
def func():
|
||||
print('enter func()')
|
||||
|
||||
a = 1
|
||||
b = 2
|
||||
import pdb
|
||||
pdb.set_trace()
|
||||
func()
|
||||
c = 3
|
||||
print(a + b + c)
|
||||
|
||||
# pdb
|
||||
> /Users/jingxiao/test.py(9)<module>()
|
||||
-> func()
|
||||
(pdb) s
|
||||
--Call--
|
||||
> /Users/jingxiao/test.py(1)func()
|
||||
-> def func():
|
||||
(Pdb) l
|
||||
1 -> def func():
|
||||
2 print('enter func()')
|
||||
3
|
||||
4
|
||||
5 a = 1
|
||||
6 b = 2
|
||||
7 import pdb
|
||||
8 pdb.set_trace()
|
||||
9 func()
|
||||
10 c = 3
|
||||
11 print(a + b + c)
|
||||
|
||||
(Pdb) n
|
||||
> /Users/jingxiao/test.py(2)func()
|
||||
-> print('enter func()')
|
||||
(Pdb) n
|
||||
enter func()
|
||||
--Return--
|
||||
> /Users/jingxiao/test.py(2)func()->None
|
||||
-> print('enter func()')
|
||||
|
||||
(Pdb) n
|
||||
> /Users/jingxiao/test.py(10)<module>()
|
||||
-> c = 3
|
||||
|
||||
```
|
||||
|
||||
这里,我们使用命令`”s“`进入了函数func()的内部,显示`”--Call--“`;而当我们执行完函数func()内部语句并跳出后,显示`”--Return--“`。
|
||||
|
||||
另外,
|
||||
|
||||
- 与之相对应的命令`”r“`,表示step out,即继续执行,直到当前的函数完成返回。
|
||||
- 命令`”b [ ([filename:]lineno | function) [, condition] ]“`可以用来设置断点。比方说,我想要在代码中的第10行,再加一个断点,那么在pdb模式下输入`”b 11“`即可。
|
||||
- 而`”c“`则表示一直执行程序,直到遇到下一个断点。
|
||||
|
||||
当然,除了这些常用命令,还有许多其他的命令可以使用,这里我就不在一一赘述了。你可以参考对应的官方文档([https://docs.python.org/3/library/pdb.html#module-pdb](https://docs.python.org/3/library/pdb.html#module-pdb)),来熟悉这些用法。
|
||||
|
||||
## 用cProfile进行性能分析
|
||||
|
||||
关于调试的内容,我主要先讲这么多。事实上,除了要对程序进行调试,性能分析也是每个开发者的必备技能。
|
||||
|
||||
日常工作中,我们常常会遇到这样的问题:在线上,我发现产品的某个功能模块效率低下,延迟(latency)高,占用的资源多,但却不知道是哪里出了问题。
|
||||
|
||||
这时,对代码进行profile就显得异常重要了。
|
||||
|
||||
这里所谓的profile,是指对代码的每个部分进行动态的分析,比如准确计算出每个模块消耗的时间等。这样你就可以知道程序的瓶颈所在,从而对其进行修正或优化。当然,这并不需要你花费特别大的力气,在Python中,这些需求用cProfile就可以实现。
|
||||
|
||||
举个例子,比如我想计算[斐波拉契数列](https://en.wikipedia.org/wiki/Fibonacci_number),运用递归思想,我们很容易就能写出下面这样的代码:
|
||||
|
||||
```
|
||||
def fib(n):
|
||||
if n == 0:
|
||||
return 0
|
||||
elif n == 1:
|
||||
return 1
|
||||
else:
|
||||
return fib(n-1) + fib(n-2)
|
||||
|
||||
def fib_seq(n):
|
||||
res = []
|
||||
if n > 0:
|
||||
res.extend(fib_seq(n-1))
|
||||
res.append(fib(n))
|
||||
return res
|
||||
|
||||
fib_seq(30)
|
||||
|
||||
```
|
||||
|
||||
接下来,我想要测试一下这段代码总的效率以及各个部分的效率。那么,我就只需在开头导入cProfile这个模块,并且在最后运行cProfile.run()就可以了:
|
||||
|
||||
```
|
||||
import cProfile
|
||||
# def fib(n)
|
||||
# def fib_seq(n):
|
||||
cProfile.run('fib_seq(30)')
|
||||
|
||||
```
|
||||
|
||||
或者更简单一些,直接在运行脚本的命令中,加入选项`“-m cProfile”`也很方便:
|
||||
|
||||
```
|
||||
python3 -m cProfile xxx.py
|
||||
|
||||
```
|
||||
|
||||
运行完毕后,我们可以看到下面这个输出界面:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2b/49/2b15939d6da0fd14d07e4c7c15fb3c49.png" alt="">
|
||||
|
||||
这里有一些参数你可能比较陌生,我来简单介绍一下:
|
||||
|
||||
- ncalls,是指相应代码/函数被调用的次数;
|
||||
- tottime,是指对应代码/函数总共执行所需要的时间(注意,并不包括它调用的其他代码/函数的执行时间);
|
||||
- tottime percall,就是上述两者相除的结果,也就是`tottime / ncalls`;
|
||||
- cumtime,则是指对应代码/函数总共执行所需要的时间,这里包括了它调用的其他代码/函数的执行时间;
|
||||
- cumtime percall,则是cumtime和ncalls相除的平均结果。
|
||||
|
||||
了解这些参数后,再来看这张图。我们可以清晰地看到,这段程序执行效率的瓶颈,在于第二行的函数fib(),它被调用了700多万次。
|
||||
|
||||
有没有什么办法可以提高改进呢?答案是肯定的。通过观察,我们发现,程序中有很多对fib()的调用,其实是重复的,那我们就可以用字典来保存计算过的结果,防止重复。改进后的代码如下所示:
|
||||
|
||||
```
|
||||
def memoize(f):
|
||||
memo = {}
|
||||
def helper(x):
|
||||
if x not in memo:
|
||||
memo[x] = f(x)
|
||||
return memo[x]
|
||||
return helper
|
||||
|
||||
@memoize
|
||||
def fib(n):
|
||||
if n == 0:
|
||||
return 0
|
||||
elif n == 1:
|
||||
return 1
|
||||
else:
|
||||
return fib(n-1) + fib(n-2)
|
||||
|
||||
|
||||
def fib_seq(n):
|
||||
res = []
|
||||
if n > 0:
|
||||
res.extend(fib_seq(n-1))
|
||||
res.append(fib(n))
|
||||
return res
|
||||
|
||||
fib_seq(30)
|
||||
|
||||
```
|
||||
|
||||
这时,我们再对其进行profile,你就会得到新的输出结果,很明显,效率得到了极大的提高。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2f/e3/2f5d33bf3151eb8099a54e1340bfd9e3.png" alt="">
|
||||
|
||||
这个简单的例子,便是cProfile的基本用法,也是我今天想讲的重点。当然,cProfile还有很多其他功能,还可以结合stats类来使用,你可以阅读相应的 [官方文档](https://docs.python.org/3.7/library/profile.html) 来了解。
|
||||
|
||||
## 总结
|
||||
|
||||
这节课,我们一起学习了Python中常用的调试工具pdb,和经典的性能分析工具cProfile。pdb为Python程序提供了一种通用的、交互式的高效率调试方案;而cProfile则是为开发者提供了每个代码块执行效率的详细分析,有助于我们对程序的优化与提高。
|
||||
|
||||
关于它们的更多用法,你可以通过它们的官方文档进行实践,都不太难,熟能生巧。
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,留一个开放性的交流问题。你在平时的工作中,常用的调试和性能分析工具是什么呢?有发现什么独到的使用技巧吗?你曾用到过pdb、cProfile或是其他相似的工具吗?
|
||||
|
||||
欢迎在下方留言与我讨论,也欢迎你把这篇文章分享出去。我们一起交流,一起进步。
|
||||
|
||||
|
||||
86
极客时间专栏/Python核心技术与实战/规范篇/32 | 答疑(三):如何选择合适的异常处理方式?.md
Normal file
86
极客时间专栏/Python核心技术与实战/规范篇/32 | 答疑(三):如何选择合适的异常处理方式?.md
Normal file
@@ -0,0 +1,86 @@
|
||||
<audio id="audio" title="32 | 答疑(三):如何选择合适的异常处理方式?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/af/fb/af75cfd5d3ee79f171f71ab740bf95fb.mp3"></audio>
|
||||
|
||||
你好,我是景霄。
|
||||
|
||||
不知不觉中,我们又一起完成了第三大章规范篇的学习。我非常高兴看到很多同学一直在坚持积极地学习,并且留下了很多高质量的留言,值得我们互相思考交流。也有一些同学反复推敲,指出了文章中一些表达不严谨或是不当的地方,我也表示十分感谢。
|
||||
|
||||
大部分留言,我都在相对应的文章中回复过了。而一些手机上不方便回复,或是很有价值很典型的问题,我专门摘录了出来,作为今天的答疑内容,集中回复。
|
||||
|
||||
## 问题一:应该使用哪种异常处理方式?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c7/42/c766890764672c7c924092d9dfd0c942.png" alt="">
|
||||
|
||||
第一个问题是code2同学的疑惑。下面这两种处理的风格,哪一种风格更有效、更优雅?
|
||||
|
||||
- 第一种,在代码中对数据进行检测,并直接处理与抛出异常。
|
||||
- 第二种,在异常处理代码中进行处理。
|
||||
|
||||
其实,第一种方法,可以翻译成下面的“if…elif…”语句:
|
||||
|
||||
```
|
||||
if [condition1]:
|
||||
raise Exception1('exception 1')
|
||||
elif [condition2]:
|
||||
raise Exception2('exception 2')
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
而第二种方法,则对应着下面异常处理的代码:
|
||||
|
||||
```
|
||||
try:
|
||||
...
|
||||
except Exception as e:
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
这两种方法很大的一个区别是,第一种方法一旦抛出异常,那么程序就会终止;而在第二种方法中,如果抛出异常,会被程序捕获(catch),程序还会继续运行。这也是我们选择这两种方法的重要依据。当然,在实际工作中,到底使用哪一种方法,还是取决于具体的场景。
|
||||
|
||||
比方说,一个模块的功能是对输入进行检测,如果输入不合法,则弹出对话框进行提示,并终止程序。那么,这种情况下,使用第一种方法更加合理。
|
||||
|
||||
但是,如果换成一个产品的服务器端,它需要应对各种可能发生的情况,以保证服务器不崩溃。比如在连接数据库时,如果网络异常,无法连接,那就需要捕获(catch)这个异常(exception),进行记录,并同时保证其他功能不受影响。这种情况下,我们通常会选择第二种方式。
|
||||
|
||||
## 问题二:先写出能跑起来的代码,后期再优化可以吗?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d5/b0/d5efddbe80757e61e33596ce41440bb0.png" alt="">
|
||||
|
||||
第二个问题,夜路破晓同学提到了很多程序员传授的“经验之谈”,即先写出能跑起来的代码,后期再优化。很明显,这种认知是错误的。我们从一开始写代码时,就必须对功能和规范这两者双管齐下。
|
||||
|
||||
代码功能完整和规范完整的优先级是不分先后的,应该是同时进行的。如果你一开始只注重代码的功能完整,而不关注其质量、规范,那么规范问题很容易越积越多。这样就会导致产品的bug越来越多,相应的代码库越发难以维护,到最后不得已只能推倒重来。
|
||||
|
||||
我在Facebook工作时就遇到过这样的情况,参与过类似的项目。当时,某些功能模块因为赶时间,code review很宽松,代码写得很不规范,留下了隐患。时间一长,bug越来越多,legacy越来越多。到最后,万分无奈的情况下,我们几个工程师专门立项,花了三个多月时间,重写了这一模块的代码,才解决了这个问题。
|
||||
|
||||
## 问题三:代码中写多少注释才合适?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f2/71/f22b2ec07051b244ed4a852189745671.png" alt="">
|
||||
|
||||
第三个问题,小侠龙旋风同学留言说,自己的同事要求代码中有70%的注释,这显然有点过了。但是反过来说,如果你的代码中没有注释或者注释很少,仅凭规范的变量名肯定是远远不够的。
|
||||
|
||||
通常来说,我们会在类的开头、函数的开头或者是某一个功能块的开头加上一段描述性的注释,来说明这段代码的功能,并指明所有的输入和输出。除此之外,我们也要求在一些比较tricky的代码上方加上注释,帮助阅读者理解代码的含义。
|
||||
|
||||
总的来说,代码中到底需要有多少注释,其实并没有一个统一的要求,还是要根据代码量和代码的复杂度来决定。不过,我们平常书写时,只要满足这样的规范就可以了。
|
||||
|
||||
另外,必须提醒一点,如果在写好之后修改了代码,那么代码对应的注释一定也要做出相应的修改,不然很容易造成“文不对题”的现象,给别人也给你自己带来困扰。
|
||||
|
||||
## 问题四:项目的API文档重要吗?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fc/c5/fc59ac08ab33764afa439056e75acac5.png" alt="">
|
||||
|
||||
第四个问题,是未来已来同学的留言。他提到了项目的API文档的问题,这一点说得非常好,在这里我也简单介绍一下。
|
||||
|
||||
我在专栏中主要讲的是代码的规范问题,但很多情况下,光有规范的代码还是远远不够的。因为一个系统,一个产品,甚至一个功能模块的代码,都有可能非常复杂。少则几千行,动辄几十万行,尤其是对于刚加入的新人来说,在ramp up阶段光看代码可能就是一个噩梦了。
|
||||
|
||||
因此,在这方面做得比较规范的公司,通常也会要求书写文档。项目的文档,主要是对相应的系统、产品或是功能模块做一个概述,有助于后人理解。以一个service为例,其对应的文档通常会包括下面几部分:
|
||||
|
||||
- 第一点,系统的概述,包括各个组成部分以及工作流程的介绍;
|
||||
- 第二点,每个组成部分的具体介绍,包括必要性、设计原理等等;
|
||||
- 第三点,系统的performance,包括latency等等参数;
|
||||
- 第四点主要说明如何对系统的各个部分进行修改,主要给出相应的code pointer及对应的测试方案。
|
||||
|
||||
这些内容,也希望屏幕前的你能够牢记。
|
||||
|
||||
今天我主要回答这些问题,同时也欢迎你继续在留言区写下疑问和感想,我会持续不断地解答。希望每一次的留言和答疑,都能给你带来新的收获和价值。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user