目录

如何写出整洁的 Python 代码 中

0x00 前言

本文是《提升你的 Python 项目代码健壮性和性能》系列的第七篇文章。

上篇《如何写出整洁的代码 上》 从变量命名 / 函数 / 注释整洁 / 格式整洁上写出干净的代码

https://zhuanlan.zhihu.com/p/59510165

本文还是通过代码上的一些小技巧和一些原则来让代码更加整齐。

0x01 避免过深的缩进

场景,你在做一个 B2B2C 的商城系统。商家的活动需要在某些比较严格的条件下才能参与(假设有五个字段吧)。

如果不动手捋一捋判断的路径,上来就动手写代码,则很容易写出如下的代码。

if cond1:
	dosomething()
	if cond2:
		dosomething()
		if cond3 and cond4:
			dosomething()
			else:
				dosomething()
			if condx:
			dosomething()
else:
	if cond2:
		dosomething()
		if cond3 and cond4:
			dosomething()
			if condx:
			dosomething()

想想你这个时候才判断了 5 个字段… 如果想都不想就开始写这种代码的话,就做好修改的时候崩溃吧。

当你写出 if 超过两层缩进的时候,代码的复杂度就值得注意了。

这个时候,应该火速的拿出纸和笔出来,快速的捋一捋所有的变量和情况,

『以减少缩进为目标』

能提前判断掉的就提前判断掉

# 能提前判断掉的就提前判断掉
if cond2:
	raise AlreadPaid():
if cond3:
	raise ActivityExhaused():
if cond4:
	raise ActivityCancel():

代码的缩进越浅,代表着代码越容易维护,用学长的话说,老手才知道『九浅一深』的奥妙……

0x02 使用异常

使用异常而不是状态码,这点重点点名一下 Go 语言的状态码….

遇到异常返回状态码写业务的话,很容易抓狂。如果你在深层抛出一个错误,使用状态码的话,必须一层一层的返回状态码。

  1. 遇到问题,抛出异常,raise ApiException 。就可以在上层捕获错误进行处理。这样的 话,就没有必要每次都在函数签名上返回状态码了。
  2. 让代码更佳清晰的是 try except finally 机制。try 块定义了一个范围,try 中的结果可以看执行中有没有遇到一些奇奇怪怪的情况,然后把 try 块里面的东西取消掉。甚至抛出一个错误,丢到 catch 里面执行。这种接近于事务的做法是状态码机制没法比的。

拿第一小节的代码来说,可以在最深层抛出异常,然后在最外面统一处理异常,有些异常可能只是报 apiexception, 有的异常可能还要针对情况打日志,或者有的情况是你在写代码的时候没有考虑到的情况,都可以进行各种灵活的处理。

这样的话,代码就非常的清晰了。

0x03 类与 OOP

注意!

  1. OOP 语言让封装 / 继承 / 多态更佳方便快捷安全。
  2. 封装 / 多态 / 继承 并不是 OOP 对象的专利,实际上利用指针 C 也可以写出来具备封装 / 继承 / 多态的程序。只是相对危险一些罢了。

比如,你有这么个场景,计算购物车里面东西的价格:

  1. 面向过程思考方式,用户我选了一些商品,然后把商品放到购物车里,然后我算一下物品价格。
  2. OOP 思考方式,用户我需要一个购物车,帮我把这些物品放到购物车里面,购物车告诉我价格。这个时候,你封装一个购物车类会比较合适。

你这个时候就问了,这不就是一个 calcPrice(cart) 和 cart.calcPrice 的区别么?

区别在哪?

  1. 『真实世界建模』购物车的是一个对真实世界的建模。
  2. 『职责转移』。计算价格这件事情就是『购物车』这个 Object 的事情了。
  3. 封装性:我不需要维护一个物品集合。都交给购物车来做这件事情。

什么情况下需要类,用 OOP 的方式思考是合理的,明显的,清晰的,就可以了。

当然,采用了 OOP, 可以更快的结合继承 / 多态来完成『依赖反转』。

这个名词听起来不明觉厉,但其实很简单。

# base.py
class Human:
	def perform(self):
		pass

# foo/man.py
class Man(Human):
	def perform(self):
		print("大哥,真不会唱歌")

# bar/woman.py
class Woman(Human):
	def perform(self):
		print("大哥,真不会跳舞")

如果老大哥让你跳舞,就必须要把你的代码给 import 到老大哥的源代码里面。

# bar/bigbother.py
from foo.man import man_instance
from bar.woman import woman_instance

man_instance.perform()
woman_instance.perform()

这样会带来一个问题,产生了源代码上面的依赖。这样带来结果是老大哥依赖于几个具体 man 和 woman, 这是不合理的,应该是铁打的老大哥,流水的 man 和 woman

# 源码不依赖 bigbother.py

def order_perform(h):
	h.perform()

humans = scan_humans()

for human in humans:
	order_perform(human)

利用多态,则将这个问题完美的解决了。当然,考虑到动态语言,本身就可以很『多态』…. 你甚至 都不需要继承了…

策略层与实现完美分离。甚至可以分开进行独立部署。

结论:

  1. 对真实世界的建模
  2. 代码清晰为主,如果能用简单函数解决的事情,就不要封装成类。
  3. 以多态为手段对源代码中的依赖关系进行控制的能力。借此,可以构建 出插件式架构,让高层策略性组件和底层实现性组件分离。底层实现可以编译成插件,实 现独立于高层组件的开发和部署。

0x04 SOLID 设计原则

  • SRP 单一职责 原则
  • OCP 开闭原则
  • LSP 里式替换原则
  • ISP 接口隔离原则
  • DIP 依赖反转原则

原则是原则,是追求,是启迪思路的思想,但也要随机应变。

  1. 假如你不了解业务,强行用依赖反转原则写了抽象层,后面 PM 过来说,我有这么一个思路。那么,你的代码写起来就很痛苦了。
  2. 假如你不了解场景,在使用单一职责的时候,往往就会业务区分不明确。

在现实场景中,往往是先保持足够的清晰简单的代码,随着代码的演进,用上面的原则再次思 考一下可不可以做的更好。

单一职责

比如说,单一职责原则听起来很简单,一个函数只完成一个功能(事情)。

但现实情况是这种往往只是一个追求,站在不同的角度有不同的看法:

比如说,

  1. 你说,你今天想学习。这是一件事情。
  2. 你说,你今天上午想学习数学。这是一件事情。
  3. 你说,你今天上午想学习高数第三章,接着做完笔记,回头抽卡默背一遍公式。这是三 件事情,并且也可以是一件事情。

你的拆分粒度决定了一件事情的指代范围。

开闭原则

开闭原则强调的事情是计算机系统应该在不需要修改的前提下被扩展。将系统划分为一系列组件,并且将这些组件的依赖关系按照层次结构进行组织,使得高阶组件不会因为低阶组件被修改而受到影响

里氏替换原则

在不改变软件行为的基础上,衍生类可以替换掉基类

接口隔离原则

任何层次的软件设计如果依赖了它并不需要的东西的时候,就会带来意料之外的麻烦。

依赖反转原则

什么叫做依赖反转?

依赖反转就是设计软件的时候设计稳定的抽象层。针对抽象的东西编程。

0x05 边界和第三方库的挑选

在软件包膨胀的今天,应该如何挑选第三方库呢?

我给出几个挑选的原则。

  1. 靠谱依赖原则:如果 flask 是靠谱的,那么,flask 依赖的 click 包,werkzeuk 包一定是靠谱的。
  2. 浓缩精华原则:如果一个库依赖少,代码清晰简单,那么可以采用。
  3. 活跃维护原则:如果维护很活跃,证明前景相对较好。
  4. 多人维护原则:如果是多人维护,则不会因一个人的喜好和个人状态而断了维护。别问我怎么知道的,都是泪

挑选有这个原则,那么,使用有什么原则么?

就一条,尽量减少依赖库对你现有代码的侵入性。

比如,你用了 cryptography 之后,应该封装一个接口用来调用 cryptography 防止以后这个项目挂了,这样你可以只修改该接口,和 pycrypto 对接。

这和里氏替换的思路也是比较类似的

0xDD 结论

所谓『不能谋万世者不能谋一时,不能谋全局者不能谋一隅』

在软件开发中,其实最重要的过程是梳理流程,流程梳理的足够清楚,代码就足够简单。

不管是避免深缩进,还是使用异常,还是 Solid 原则。都是建立在全局观足够高,对当前的流程非常熟悉的基础上的。

当然,考虑到需求变更的不确定性,代码还是足够简单清晰为上策。

0xEE 参考

  • 《架构整洁之道》
  • 《代码整洁之道》
  • Photo by Joseph Kellner on Unsplash