This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View 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能够胜任大团队企业级百万行级别代码库。他们的内容后面我也会简单说明。
## 统一的编程规范为什么重要?
用一句话来概括,统一的编程规范能提高开发效率。而开发效率,关乎三类对象,也就是阅读者、编程者和机器。他们的优先级是**阅读者的体验 &gt;&gt; 编程者的体验 &gt;&gt; 机器的体验**。
### 阅读者的体验&gt;&gt;编程者的体验
写过代码的人可能都有体会在我们的实际工作中真正在打字的时间远比阅读或者debug的时间要少。事实正是如此研究表明软件工程中80%的时间都在阅读代码。所以,为了提高开发效率,我们要优化的,不是你的**打字时间**,而是**团队阅读的体验**。
其实不少的编程规范本来就是为了优化读者体验而存在的。举个例子对于命名原则我想很多人应该都有所理解PEP8第38条规定命名必须有意义不能是无意义的单字母。
有些人可能会说,啊,编程规范好烦哟,变量名一定要我写完整,打起来好累。但是当你作为阅读者时,一定能分辨下面两种代码的可读性不同:
```
# 错误示例
if (a &lt;= 0):
return
elif (a &gt; b):
return
else:
b -= a
# 正确示例
if (transfer_amount &lt;= 0):
raise Exception('...')
elif (transfer_amount &gt; 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规范。
### 编程者的体验 &gt;&gt; 机器的体验
说完了阅读者的体验再来聊聊编程者的体验。我常常见到的一个错误倾向是过度简化自己的代码包括我自己也有这样的问题。一个典型的例子就是盲目地使用Python的list comprehension。
```
# 错误示例
result = [(x, y) for x in range(10) for y in range(5) if x * y &gt; 10]
```
我敢打赌一定很少有人能一口气写出来这么复杂的list comprehension。这不仅容易累着自己也让阅读者看得很累。其实如果你用一个简单的for loop会让这段代码更加简洁明了自己也更为轻松。
```
# 正确示例
result = []
for x in range(10):
for y in range(5):
if x * y &gt; 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
原因是在CPythonPython的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, &quot;is compensated&quot;, salary, &quot;dollars&quot;)
```
如果有人调用 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, &quot;is compensated&quot;, salary, &quot;dollars&quot;)
```
这就是为什么PEP8和Google Style都特别强调了何时使用is 何时使用 ==,何时使用隐式布尔转换。
不规范的编程习惯也会导致程序效率问题,我们看下面的代码有什么问题:
```
# 错误示例
adict = {i: i * 2 for i in xrange(10000000)}
for key in adict.keys():
print(&quot;{0} = {1}&quot;.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。总之还是那句话我们强调编程规范最终一定是为了提高开发效率而不是做额外功。
## 思考题
在你个人或者团队的项目经验中,是否也因为编程规范的问题,踩过坑或者吵过架呢?欢迎留言和我分享,也欢迎你把这篇文章分享出去。

View 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 中我们可以使用`#`进行单独注释,请记得要在`#`后、注释前加一个空格。
对于操作符,例如`+``-``*``/``&amp;``|``=``==``!=`,请在两边都保留空格。不过与此对应,括号内的两端并不需要空格。
### 换行规范
现在再回到缩进规范,注意我们提到的第二点,控制每行的最大长度不超过 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):
&quot;&quot;&quot;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 &quot;channels_last&quot;.
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)
&quot;&quot;&quot;
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 '
'{&quot;channels_last&quot;, &quot;channels_first&quot;}')
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 &gt; target`。一旦不存在,则返回 -1。
这个功能应该不难写吧。你不妨先自己写一下,写完后再对照着来看下面的代码,找出自己的问题。
```
def solve(arr, target):
l, r = 0, len(arr) - 1
ret = -1
while l &lt;= r:
m = (l + r) // 2
if arr[m] * arr[m] &gt; 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 &gt; target
def binary_search(arr, target):
l, r = 0, len(arr) - 1
ret = -1
while l &lt;= 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 等等。
希望你在留言区分享你的经历,你也可以把这篇文章分享出去,让更多的人互相交流心得体会,留下真实的经历,并在经历中进步成长。

View 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 ::= &quot;assert&quot; expression [&quot;,&quot; 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')
# 输出
&lt;ipython-input-8-2c057bd7fe24&gt;: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 &lt;= updated_price &lt;= 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时程序便抛出下面这个异常
```
AssertionErrorprice 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 &gt; 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吗如果用过的话是在什么情况下使用的有遇到过什么问题吗
欢迎在留言区写下你的经历,还有今天学习的心得和疑惑,与我一起分享。也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流,一起进步。

View 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: &lt;class 'Exception'&gt;
exc_value: exception raised
exc_traceback: &lt;traceback object at 0x1046036c8&gt;
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”`这条语句,否则仍然会抛出异常。
## 思考题
那么,在你日常的学习工作中,哪些场景使用过上下文管理器?使用过程中又遇到了哪些问题,或是有什么新的发现呢?欢迎在下方留言与我讨论,也欢迎你把这篇文章分享出去,我们一起交流,一起进步。

View 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] &gt;= 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=&quot;custom_val&quot;)
a.m3 = MagicMock()
a.m1()
self.assertTrue(a.m2.called) #验证m2被call过
a.m3.assert_called_with(&quot;custom_val&quot;) #验证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 &lt; 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
其实综合前面讲的这几点来看你应该感受到了单元测试的核心还是mockmock掉依赖项测试相应的逻辑或算法的准确性。在我看来虽然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] &gt;= 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而是适用于任何语言。所以单元测试必不可少。
## 思考题
那么,你在平时的学习工作中,曾经写过单元测试吗?在编写单元测试时,用到过哪些技巧或者遇到过哪些问题吗?欢迎留言与我交流,也欢迎你把这篇文章分享出去。

View 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()”`这行,并且暂停了下来,等待用户输入。
```
&gt; /Users/jingxiao/test.py(5)&lt;module&gt;()
-&gt; c = 3
```
这时我们就可以执行在IDE断点调试器中可以执行的一切操作比如打印语法是`"p &lt;expression&gt;"`
```
(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
-&gt; print(a + b + c)
```
而命令`”l“`则表示列举出当前代码行上下的11行源代码方便开发者熟悉当前断点周围的代码状态
```
(pdb) l
1 a = 1
2 b = 2
3 import pdb
4 pdb.set_trace()
5 -&gt; 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
&gt; /Users/jingxiao/test.py(9)&lt;module&gt;()
-&gt; func()
(pdb) s
--Call--
&gt; /Users/jingxiao/test.py(1)func()
-&gt; def func():
(Pdb) l
1 -&gt; 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
&gt; /Users/jingxiao/test.py(2)func()
-&gt; print('enter func()')
(Pdb) n
enter func()
--Return--
&gt; /Users/jingxiao/test.py(2)func()-&gt;None
-&gt; print('enter func()')
(Pdb) n
&gt; /Users/jingxiao/test.py(10)&lt;module&gt;()
-&gt; 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 &gt; 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 &gt; 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或是其他相似的工具吗
欢迎在下方留言与我讨论,也欢迎你把这篇文章分享出去。我们一起交流,一起进步。

View 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及对应的测试方案。
这些内容,也希望屏幕前的你能够牢记。
今天我主要回答这些问题,同时也欢迎你继续在留言区写下疑问和感想,我会持续不断地解答。希望每一次的留言和答疑,都能给你带来新的收获和价值。