如何编写可维护 Python 代码(持续更新)

如何编写可维护代码,从一开始就规避代码的质量劣化。本文侧重 Python 编程语言。

参考资料:

例子

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

ruff 和其他代码质量工具建议不要将 lambda 表达式赋值给一个 Python 变量,而建议使用局部 def 函数,主要有以下几个原因:

  1. 可读性

    • 使用 def 定义的函数有一个明确的名称和函数体,通常更容易阅读和理解。
    • lambda 表达式通常用于定义简单的匿名函数,当它们被赋值给变量时,代码的意图可能不如使用 def 定义的函数那么清晰。
  2. 调试和错误信息

    • 使用 def 定义的函数有一个明确的名称,当出现错误时,错误信息中会包含函数名,这有助于调试。
    • lambda 表达式没有名称,当出现错误时,错误信息中不会包含有用的函数名,可能会增加调试的难度。
  3. 代码风格和一致性

    • PEP 8(Python 的官方风格指南)建议避免将复杂的 lambda 表达式赋值给变量,而是使用 def 定义函数。这有助于保持代码风格的一致性。
# 不推荐的做法
add = lambda x, y: x + y
print(add(2, 3))
# 推荐的做法
def add(x, y):
    return x + y

print(add(2, 3))

--to be continue--

posted @ 2024-08-27 11:55  ffl  阅读(18)  评论(0编辑  收藏  举报