案例
目前写Python的同学越来越多了,但动态语言无类型约束,导致Commit时难以review。先来看一段我们的代码:
优化前的代码:
def handle(self, data, rules):
rule_type = rules['type'] # 1
rule_list = rules['rules'] # 2
res = None
if str(rule_type).lower() != RULES_MAPPING:
raise ValueError("type of rule should be 'rules_mapping'")
for rule in rule_list:
col_name = rule['column'] # 2
if rule['function'].lower() == 'cast': # 3
mapping_dict, other = self.parse_cast(rule['mapping'])
res = data[col_name].apply(self.case_func, args=(mapping_dict, other))
elif rule['function'].lower() == 'in':
mapping_dict, other = self.parse_in(rule['mapping'])
res = data[col_name].apply(self.in_func, args=(mapping_dict, other))
elif rule['function'].lower() == 'binning':
res = data[col_name].apply(self.binning_func, args=(rule['mapping']))
else:
raise ValueError('not supported function') # 4
return res
代化之后的代码:
def handle(self, data: DataFrame, rules: Rule) -> Series: # 1
if rules.type != MappingHandler.RULES_TYPE:
raise ValueError(f"type of rule should be '{MappingHandler.RULES_TYPE}'") # 6
rule_list = rules.nested_rules: # 8
res = None
for item in rule_list:
rule = Rule(item) # 2
col_name = self.get_col_name(rule.column)
func_name = rule.function # 4
if func_name == MappingHandler.KEY_CAST: # 3
mapping_dict, other = self.parse_cast(rule.mapping, data[col_name].dtype) # 5
res = data[col_name].apply(self.case_func, args=(mapping_dict, other))
elif func_name == MappingHandler.KEY_IN:
mapping_dict, other = self.parse_in(rule.mapping, data[col_name].dtype)
res = data[col_name].apply(self.in_func, args=(mapping_dict, other))
elif func_name == MappingHandler.KEY_BINNING:
res = data[col_name].apply(self.binning_func, args=(rule.mapping))
else:
raise ValueError(f'not supported function {func_name}') # 7
return res
class Rule(object): # 2
KEY_TYPE = 'type'
# ... 省略其它的常量定义
def __init__(self, rules: Dict[str, Any]):
self.rules = rules
@property
def type(self) -> str: # 2
return str(self.rules[Rule.KEY_TYPE]).lower() if Rule.KEY_TYPE in self.rules else None
# ... 省略其它属性
咋一看,似乎没有什么大的变化,代码反而变多了。我来说说做了哪些改进:
优化前的存在潜在问题(数字代表问题出现的地方):
- [1] rules实际类型为Dict,对dict的取值方式是采用rules[’type’]这种方法,在Python3之后,若无对应的Key,会抛KeyError异常。
- [2] 从dict取出的值无类型推导,IDE无法联想,也无法类型检查纠错。
- [3] 多次调用
rule['function'].lower()
,最坏场景下需要三次从Dict取值与调用lower()方法。 - [4] 最后的抛异常信息不完整’not supported function’,应该给出具体的function值。
优化后的改进点(数字代表改点的地方):
- [1] 方法的入参与出参采用python typing新语法,通过对变量的类型标注,让IDE帮助你联想与纠错,增加代码的可读性。
- [2] 对Dict对象进行包装为Rule对象,原有直接通过Key下标取值,变成Rule的属性,在属性方法中对是否存在做检查。
- [3] 所有的字面字符串都换成了常量大写,更符合编码规范。注:Python语言上并无常量,通过约定俗成的变量名全大写来表示这是一个常量。
- [4] 减少rule[‘function’].lower()调用次数,先赋值给一临时变量。
- [5] 通过data[col_name].dtype 取出数据实际类型,并在parse_xxx方法对支持类型做检查。
- [6] 第一个ValueError的异常信息中rules_mapping采用常量替换,避免修改了常量时而没有修改异常字符串。
- [7] 补充第二个ValueError的异常信息,把实际的func_name抛出,方便定位问题。
- [8] 变量就近声明,可以减少作用域范围。注:此类优化并不明显,有些代码检查工具规定变量声明离它使用的地方不能超过5行。
说了这么多,有人可能会开始质疑了,优化前的代码也没有什么大问题,可读性也还行,能正常工作。你这样优化是不是有点太过于吹毛求疵了。代码就在这,代码到底要不要这样去打磨,每个人心中都有一杆称,只要哪怕你觉得其中一条优化点对你有所启发,那就采纳吧。
背后的知识
Python是动态脚本语言,不需要显示数据类型声明,缺少静态语言类型检查机制。有人戏称:动态一时爽,重构火葬场。
- 优点:动态语言由于类型到运行时才确定,对于输入输出并不要一一对应,满足其调用约束即可。动态性带来了灵活性,有很多高级用法,编写的代码数量更少,看起来更加简洁,提升了开发效率。
- 缺点:最大问题是代码难以阅读,变量与方法的作用需要查阅大量上下文同时靠人肉去做类型推导,修改重构都很麻烦。
所以现在动态语言的发展趋势是也增加类型机制,如JavaScript之上的TypeScript,Python中的Typing标注。
Python的Typing标注是在3.5版本引入,其语法是可以归纳为两点:
- 在声明变量时,变量的后面可以加一个冒号,后面再写上变量的类型,如
func(x: int, y: List[str])
。 - 在声明方法返回值的时候,可以在方法的后面加一个箭头,后面加上返回值的类型 如
func() -> str
。
其作用显然是增强类型检查,减少开发态出错的机率:
- 类型检查,防止低级错误,减少运行时出现参数和返回值类型不符合。
- 使IDE有能力进行更智能的代码提示与检查,提升开发效率。
- 代码更规范化,明确的类型约束让代码也更容易阅读与理解。
案例中的几项优化点都是让代码增加类型约束,借助于IDE的联想与检查机制,减少低级错误,同时提升代码可读性与可维护性。
可读性
增强代码的可读性可能是一个老生常谈的问题,可读性也是可维护性的前提。但事实上,我们常常受限于时间,好不容易有点时间按自己的思维方式把代码写完,哪再有时间去花得精力把代码优化一点点,让他变得更容易阅读。
在我司也有很典型的错觉,越少的代码越容易让人理解与维护。很多的所谓优秀重构实践都会强调从XXX行代码降到XXX行码,似乎重构代码行数不下降就不好意思说。但是事实上,并不是代码越精简就越容易让人理解与维护。相对于追求最小化代码行数,一个更好的提高可读性方法是最小化人们理解代码所需要的时间。
减少理解代码花费的时间,有两个层次:
- 代码直观上很容易理解。
- IDE等工具能准确地查找代码间调用关系。
在«编写可读代码的艺术»一书中,对于如何提升代码的可读性总结三个层次:
- 表层上的改进:在命名方法(变量名,方法名),变量声明,代码格式,注释等方面的改进。
- 控制流和逻辑的改进:在控制流,逻辑表达式上让代码变得更容易理解。
- 结构上的改进:善于抽取逻辑,借助自然语言的描述来改善代码。
回到案例本身来看,前面列出8个细小的改进,其实也在遵循着上述所讲的表层上改进原则。光代码的可读性就能写一本了,百度百科上有 编写可读代码的艺术 一书的介绍,如果你没有时间,可以看看它的目录,也可以知晓些有哪些技巧手段。
小重构
最近心声与管理优化报上有一篇谈到:写软件是一门"手艺活",要写好就得熟能生巧。虽文章中论据可能会受到挑战,但我是非常赞成其立意的。写好代码需要有匠心,匠心就需要对其倾注爱心,出于爱心就会不断追求,追求中不避艰苦,是追求中自得其乐。软件的生命周期70%的时间是维护期,代码一旦写完,只是刚刚开始,代码需要匠心持续地打磨优化。
但我们写的代码却是一旦提交上库之后,可能忙于下个迭代的需求开发,再也不会回头来看自己写的代码。我们是做商品的工人,而不是做工艺品的手艺人。绝大部人的不会在写完代码来思考是否可有改进的空间,增加可读性、可维护性。及时对自己代码的小重构就是对代码不断打磨,打磨则需要有一颗匠心。
还有一个原因,我们害怕出错,因为重构可会导致问题。没有被问题折磨的程序员在技术上不会有长进的,错误可以让我们很快接近真理。每个迭代写完代码,我们应该来审视自己的代码,不是组织上行为要求,而是出于自己的追求。及时的小重构效果往往会好于被动地需求驱动重构。
重构的理由不需要什么高大上的理论支持,也不需要响天震地的口号,而是像手艺人一样不为繁华易匠心,把代码当成自己的作品,倾注对其的爱心。
结语
写代码是一个不断打磨的过程,不以善小而不为。当你发现IDE不能类型检查纠错时,那就增加类型约束;当你发现变量命名不易理解时,那就思考改个容易理解的;当你发现定位问题不能快速定位时,那就增加日志内容中的关键信息…注重细节,从小事做起,慢慢就养成习惯。对自己的代码匠心打磨,倾注爱心,不断的追求,让它变得越来越好。