如何编写可维护 Python 代码(持续更新)
如何编写可维护代码,从一开始就规避代码的质量劣化。本文侧重 Python 编程语言。
参考资料:
- [1] 王垠: 编程的智慧
- [2] 陈皓: Go编程模式
例子
for 循环体内一开始就应该尽可能只是转调用一个子函数,而不是就地写for循环的body,因为代码非常快的就暴涨,很快for循环的body就成了一堆需要独立成为子函数的代码。因此,一旦for循环的body超过3行,就要注意。
// bad,很快这里的代码就会膨胀到你觉的加代码好费劲的地步
def do_something(xxx_list):
for key,value in enumerate(xxx_list):
// proces 1
if key ....:
if value ....:
......
// process 2
if xxx:
if yyy:
...
else:
...
else:
....
// good,不要担心小函数多,每个都很好处理
def do_something(xxx_list):
for key,value in enumerate(xxx_list):
process(key,value)
def process(key,value):
ret = a(key, value)
if ret !=0:
return
ret = b(key,value)
if ret != 0:
return
return
def a(key,value):
if key ....:
if value ....:
......
return 0
def b(key,value):
if xxx:
if yyy:
...
else:
...
else:
....
return 0
参考[1],避免使用全局变量和类成员(class member)来传递信息,尽量使用局部变量和参数
。面向对象的好处是可以封装数据和函数,但是成员变量也是函数内的“全局变量”,比较好的方式还是用class做封装,但是class内的method之间,尽可能是写成输入都由函数参数控制,输出都用返回值控制的方式。只在class的public方法处,把内部的这样的干净的method和member之间做组装。
class SmartClass:
def __init__(self,xxx,yyy,zzz):
self.xxx = xxx
self.yyy = yyy
self.zzz = zzz
# 仅在组装函数里使用成员变量
def run(self):
ret, a,b = self.__step_1(self.xxx, self.yyy)
if ret != 0:
return ret, None
ret, c = self.__step_2(a, b, self.zzz)
if ret != 0:
return ret, None
return 0, c
# 内部函数的输入全部通过函数参数指定,可以调用其他的内部函数
def __step_1(self, xxx, yyy):
...
ret, a = self.__step_1_1(xxx)
if ret != 0:
return ret, None, None
...
ret, b = self.__step_1_2(yyy)
if ret !=0:
return ret, None, None
...
return 0, a, b
def __step_2(self, a, b, zzz):
...
return 0, c
任何程序,从入口开始,一定要可以本地测试。如果一定依赖线上环境才能做完整的测试,那么就难以做问题的复现诊断。到线上才复现,则问题的暴露要来回很多次。
# bad: 入口函数从一开始就有副作用,依赖 argv, environ 等
# 不方便测试
def main():
if sys.argv[1] ....
...
mm = os.environ.get("mm")
...
if __name__=="__main__":
main()
# good: 输入依赖函数参数
# 更进一步,输入参数最好是结构化的,有验证能力的模型
# 例如 python 可以用 pydantic
# 这样,整个入口都可以方便做UT
from pydantic import BaseModel
class Model(BaseModel):
xxx: str,
mm: Field("", string)
def main(model: Model):
xxx = model.xxx
mm = model.mm
...
if __name__=="__main__":
model = Model(...)
main(model)
绝大部分时候,都应该把一个函数写成一组【线性展开】的代码,线性展开的代码,就是扁平的流水线代码,每个环节做一件事,输入和输出依赖明确。例如:
def do_something():
err, a_result = a()
if err:
logger.error(f"xxxx, call a, err:{err}, result:{a_result}")
return err
err, b_result = b(a_result)
if err:
logger.error(f"xxxx, call b, err:{err}, result:{b_result}")
return err
...
return 0, result
工程上,排解问题绝大部分情况下依赖日志。写好程序里的日志是严肃软件的标志。日志如果很长怎么解决?想办法拆,不同语言的支持能力不同。Python在这个问题上倒是有好的方案。
# bad
def test(arg1, arg2):
ret, value1,value2 = sub(arg1)
if ret!=0:
logger.error(f'test, , arg1:{arg1}, arg2:{arg2}, ret:{ret}, value1:{value1}, value2:{value2}')
return
可以通过多行字符串自动拼接机制来改进:
# good
def test(arg1, arg2):
ret, value1,value2 = sub(arg1)
if ret!=0:
logger.error(f'test, '
f', arg1:{arg1}, arg2:{arg2}'
f', ret:{ret}'
f', value1:{value1}, value2:{value2}')
return
字典在 Python 代码中的使用频率非常高,遍历 Python 字典的代码也很容易碎片化。
# bad
for key in xxx_map:
value = xxx_map[key]
...
index=0
for key in xxx_map:
value = xxx_map[key]
index +=1
比较好的方式是,根据需要选择合适的字典迭代器来简化代码
# good
for key, value in xxx_map.items():
...
for key in xxx_map.keys():
...
for value in xxx_map.values():
...
for index, key in enumerate(xxx_map):
...
for index, item in enumerate(xxx_map.items()):
key,value = item
...
列表也是 Python 非常高频的数据结构。一些常用的快捷代码,不要写for循环遍历
# 列表去重
arr = list(set(arr))
# 列表左边减右边
arr = list(set(a)-set(b))
# 列表交集
arr = list(set(a)&set(b))
使用列表推导表达式
# list 列表推导式
arr = [e for e in arr if e is not None]
# dict 列表推导式
map = {key: value for key,value in map.items() if key in ["a"]}
不要就地做某个变量的一堆分支处理
# bad,局部变量 check_test_types 在初始化的地方就地做了很多处理
# 代码的边界不清晰,立刻导致函数内的代码碎片化
def test(customize):
a = ...
b = ...
c = ....
check_test_types = customize.get('check_test_types', ['ut_manual'])
check_test_types = [t for t in check_test_types if t in ['ut','ut_manual','ut_gsan','bench_ict','sim_cit','all']]
if 'all' in check_test_types:
check_test_types = ['all']
elif 'ut' in check_test_types:
check_test_types = [t for t in check_test_types if t not in ['ut_manual','ut_gsan']]
if self.customize.get('scope', "")=="module" and customize.get('enable_daily_coverage_checker',False):
check_test_types = ['ut_with_daily_ct_and_gsan']
比较好的是独立一个小函数处理
# good: 使用函数,让代码边界更清晰,层级更对称
def test(customize):
a = ...
b = ...
c = ....
check_test_types = parse_check_test_types(customize)
def parse_check_test_types(customize):
check_test_types = customize.get('check_test_types', ['ut_manual'])
check_test_types = [t for t in check_test_types if t in ['ut','ut_manual','ut_gsan','bench_ict','sim_cit','all']]
if 'all' in check_test_types:
check_test_types = ['all']
elif 'ut' in check_test_types:
check_test_types = [t for t in check_test_types if t not in ['ut_manual','ut_gsan']]
if self.customize.get('scope', "")=="module" and customize.get('enable_daily_coverage_checker',False):
check_test_types = ['ut_with_daily_ct_and_gsan']
return check_test_types
判断类型
# bad
if type(a)==type({}) or type(a)==type(""):
..
使用 isinstance
# good
if isinstance(a, (dict, str)):
...
字符串处理/长字符串
# bad
simple = f"xxxxxx"
info = f"...,ljfdlkajlajflajflkajfdlafjdlajflkajfdaljf, afdalfjaljf laj f, afdajljalfjl aj, af afda a:{xxx}"
编写合适的字符串
# good
# 非参数化字符串,不要用f-string
simpe = "xxxxx"
# 通过括号表达式,分行编写,自动拼接
info = (
"...,ljfdlkajlajflajflkajfdlafjdlajflkajfdaljf"
", afdalfjaljf laj f, afdajljalfjl aj"
f", af afda a:{xxx}"
)
API的必要参数缺失
# bad
with open("xx.txt", "r") as file:
...
resp = requests.get(url)
添加必要的参数
# good
# 总是指定编码
with open("xx.txt", "r", encoding="utf-8") as file:
...
# 总是指定超时时间
resp = requests.get(url, timeout=10)
使用python静态代码检测工具。碎片的有:pylint, flak8, isort,autopep8, black 集大成者是 ruff
- https://pythonspeed.com/articles/pylint-flake8-ruff/
- https://docs.pylint.org/tutorial.html 可以从 pylint 的教程里学到很多标准相关的case
ruff
和其他代码质量工具建议不要将 lambda
表达式赋值给一个 Python 变量,而建议使用局部 def
函数,主要有以下几个原因:
-
可读性:
- 使用
def
定义的函数有一个明确的名称和函数体,通常更容易阅读和理解。 lambda
表达式通常用于定义简单的匿名函数,当它们被赋值给变量时,代码的意图可能不如使用def
定义的函数那么清晰。
- 使用
-
调试和错误信息:
- 使用
def
定义的函数有一个明确的名称,当出现错误时,错误信息中会包含函数名,这有助于调试。 lambda
表达式没有名称,当出现错误时,错误信息中不会包含有用的函数名,可能会增加调试的难度。
- 使用
-
代码风格和一致性:
- PEP 8(Python 的官方风格指南)建议避免将复杂的
lambda
表达式赋值给变量,而是使用def
定义函数。这有助于保持代码风格的一致性。
- PEP 8(Python 的官方风格指南)建议避免将复杂的
# 不推荐的做法
add = lambda x, y: x + y
print(add(2, 3))
# 推荐的做法
def add(x, y):
return x + y
print(add(2, 3))
--to be continue--