7

飞哥讲代码13:好代码须匠心打磨

 3 years ago
source link: http://lanlingzi.cn/post/technical/2020/0912_code/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

飞哥讲代码13:好代码须匠心打磨

2020-09-12

  |   技术  

  |  

2986 字 ~6分钟

目前写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行。

说了这么多,有人可能会开始质疑了,优化前的代码也没有什么大问题,可读性也还行,能正常工作。你这样优化是不是有点太过于吹毛求疵了。代码就在这,代码到底要不要这样去打磨,每个人心中都有一杆称,只要哪怕你觉得其中一条优化点对你有所启发,那就采纳吧。

1.1 背后的知识

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不能类型检查纠错时,那就增加类型约束;当你发现变量命名不易理解时,那就思考改个容易理解的;当你发现定位问题不能快速定位时,那就增加日志内容中的关键信息…注重细节,从小事做起,慢慢就养成习惯。对自己的代码匠心打磨,倾注爱心,不断的追求,让它变得越来越好。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK