请快速说出以下代码的功能:
- for i in range(n):
- for j in range(m):
- for k in range(l):
- temp_value = X[i][j][k] *12.5
- new_array[i][j][k] = temp_value+ 15
很难,对吧?要想对这段代码进行修改或调试,除非知道作者在想什么,否则将难以实现。即使是作者本人,在编写这段代码几天后也会忘记其用途,因为变量名和“魔数”(magic numbers)并不能帮助记忆代码的功能。
使用数据科学代码时,类似于上面(或者更糟)的示例很常见:代码中含有如x、y、xs、x1、x2、tp、tn、clf、reg、xi、yi、ii这样的变量名和许多未命名的常量值。坦率地说,数据科学家(包括本人)并不擅长于命名变量。
很多人经历过从为一次性分析编写研究导向的数据科学代码到编写生产水平代码的过程,所以不得不摒弃从数据科学书籍、课程和实验室中获得的实践来改进编程方式。可实际应用的机器学习代码与数据科学家的编程方法有许多不同之处,但本文将从两个影响力较大的常见问题开始:
- 无用的/混淆的/不明确的变量名
- 未命名的“魔幻”常数
这两个问题都导致了数据科学研究(或Kaggle项目)和生产机器学习系统之间的脱节。是的,你可以在只运行一次代码的Jupyter Notebook中侥幸逃脱这些问题,但是当任务中关键的机器学习管道需要每天准确无误地运行数百次时,编写可读以及可理解的代码就十分必要了。幸运的是,数据科学家可以采用软件工程中的优秀实践,本文也将对其进行介绍。
注意:本文主要讨论Python,因为它是目前为止工业数据科学中应用最广泛的语言。在Python中:
- 变量名/函数名小写并且用下划线隔开
- 命名常数的名称全部大写
- 类的名称采用驼峰式大小写命名规则
命名变量
命名变量时要记住三个基本原则:
- 变量名必须描述变量所表示的信息。变量名应该用词明确,来体现变量代表的内容。
- 代码读取的次数将多于编写的次数。所以优先考虑代码的可读性而不是编写速度。
- 采用标准的命名规范,才可以做出一个全局决策,而不是做出多个局部决策。
在实践中又如何呢?下面对变量名进行一些改进:
- x和y。如果多次阅读了解的话会知道它们是特性和目标,但是对于阅读代码的其他开发人员来说,这可能并不明确。相反,要使用可以描述这些变量含义的名称,如house_features和house_prices。
- value。value代表什么?它可以是velocity_mph, customers_served, efficiency, revenue_total。像value这样的名称并不能体现变量的用途,而且容易混淆。
- temp。即使只将变量用作临时值存储,也要给它一个有意义的名称。这可能是用于转换单位的值,因此在这种情况下,请明确说明:
- # Don't do this
- temp = get_house_price_in_usd(house_sqft, house_room_count)
- final_value = temp * usd_to_aud_conversion_rate
- # Do this instead
- house_price_in_usd = get_house_price_in_usd(house_sqft,
- house_room_count)
- house_price_in_aud = house_price_in_usd * usd_to_aud_conversion_rate
造成不良变量名的原因
命名变量的问题大多数来源于:
- 试图缩短变量名
- 直接将公式转写为代码
关于第一点,虽然像Fortran这样的语言确实限制了变量名的长度(6个字符以内),但现代编程语言没有限制,所以不要强迫自己使用缩写。也不要使用过长的变量名,但是如果一定要做出选择,要力求可读性。
关于第二点,当编写方程或使用模型时——这是学校忘记强调的一点——记住字母或输入代表实际值!
下列是一个同时犯了两种错误的例子及其改正方式。假设有一个从模型中得出的多项式能够求出一所房子的价格。开发者可能会想直接用代码编写数学公式:
- temp = m1 * x1 + m2 * (x2 ** 2)
- final = temp + b
这段代码看起来像是由机器为机器编写的。虽然计算机最终会运行此代码,但人类进行读取的次数更多,所以编写能让人类理解的代码!
要做到这一点,并不需要考虑公式本身——怎么做——而需要考虑建模的真实对象——是什么。下面是完整的等式(这能很好地测试读者是否理解了模型):
- house_price = price_per_room * rooms + \
- price_per_floor_squared *(floors ** 2)
- house_pricehouse_price = house_price + expected_mean_house_price
如果命名变量时遇到困难,意味着对模型或代码的不了解。编写代码是为了解决实际问题,所以需要理解模型采集的目标。描述性变量名有助于在比公式更高的抽象级别工作,以及帮助开发者关注问题本身。
其他注意事项
命名变量时重要的一点是一致性计数。使用一致的变量名可以减少命名时间,增加解决问题的时间,尤其是添加复合性变量名时。
1. 变量名中的聚合
读者已经了解使用描述性名称的基本思想,将 xs更改为distances,将e 更改为efficiency,将v更改为 velocity。那么求平均速度该使用什么样的变量名?是average_velocity, velocity_mean 还是velocity_average?下列步骤可以解决这个问题:
- 首先,确定常用缩写:avg表示平均值,max表示最大值,std表示标准差等等。确保团队的所有成员达成一致,并把这些记录下来。
- 把缩写放在变量名的末尾。将最具相关性的信息,即变量所描述的实体,放在开头。
按照这些规则,聚合变量可能被命名为为velocity_avg, distance_avg, velocity_min,以及 distance_max。第2条可以依据个人情况酌情选择。
当一个变量表示一个项目的数量时,就会出现一个棘手的问题。如要使用building_num,那么它是指建筑物的总数,还是特定建筑物的某个索引值?为了避免歧义,使用building_count表示建筑物总数,使用building_index表示特定建筑物。这也适于其他问题,如item_count和item_index。item_count也可用item_total代替。这种方法解决了歧义,并保持了将复合名称添加在名称末尾的一致性。
2. 循环索引
非常不幸,典型的循环变量已经变成i、j和k。这可能是造成数据科学中最多错误和困扰的原因。将无说明性的变量名与嵌套循环结合起来(笔者见过使用ii、jj甚至iii的嵌套循环),就会产生不可读、容易出错的代码。这一点可能有些争议,但笔者从不使用i或任何其他单个字母作为循环变量,而是选择描述迭代的内容,例如
- for building_index in range(building_count):
- ....
或
- for row_index in range(row_count):
- for column_index inrange(column_count):
- ....
这格外适用于嵌套循环,此时无需记忆i是代表行还是列,或者与j和k混淆。应该花更多脑力来思考如何创建最佳模型,而不是数组索引的具体顺序。
(在Python中,如果不使用循环变量,则应使用下划线“_”作为占位符。这样就不会对是否使用了索引感到困惑。)
3. 其他需要避免的命名方式
- 避免在变量名中使用数字
- 避免易拼错的单词
- 避免使用不明确的字符
- 避免使用含义相似的变量名
- 避免使用缩写
- 避免发音相似的变量名
应坚持优先考虑可读性而不是方便性这一原则。编程主要是为了与其他程序员进行沟通,因此请适当考虑团队成员。
不使用魔数
魔数是指未命名的常量。它常被用于单位转换,改变时间间隔或增加下标时:
- final_value = unconverted_value * 1.61
- final_quantity = quantity / 60
- valuevalue_with_offset = value + 150
- (这些变量名都很糟糕!)
魔数会导致大量的错误和混乱,因为:
- 只有作者本人知道魔数的含义
- 改变魔数的值需要查找它出现的所有位置,然后人工输入新值
可以定义一个用于转换的函数来代替魔数。这个函数接受一个未经转换的值以及一个转换率作为参数。
- defconvert_usd_to_aud(price_in_usd,
- aud_to_usd_conversion_rate):
- price_in_aus = price_in_usd *usd_to_aud_conversion_rate
如果要在一个项目的许多函数中使用同一个转换率,可以在某处定义一个命名常量。
- USD_TO_AUD_CONVERSION_RATE = 1.61
- price_in_aud = price_in_usd * USD_TO_AUD_CONVERSION_RATE
(在开始编写这个项目之前,需要和其他组员约定usd代表美元,aud代表澳元。记住标准!)
下面是另一个例子:
- # Conversion function approach
- def get_revolution_count(minutes_elapsed,
- revolutions_per_minute):
- revolution_count = minutes_elapsed *revolutions_per_minute
- # Named constant approach
- REVOLUTIONS_PER_MINUTE = 60
- revolution_count = minutes_elapsed *REVOLUTIONS_PER_MINUT
使用在某处定义的命名常量使得改写数值更加容易和一致。如果转换率发生改变,无需搜索整个代码库来改变它每次出现时的值,因为它只在一处被定义。这也把常数的含义告诉了代码的读者。如果参数名能够体现参数的内容,函数参数也是一个可行的解决方式。
一个魔数缺陷的实例来自于笔者在大学时从事的某个研究项目。这个项目需要获取每15分钟更新的能量数据。没有人想到这个数字可能会变化,于是团队编写了大量使用魔数15的函数(或者96,即每日观测次数)。这些函数运行得很好,直到开始以5分钟和1分钟的间隔获取数据。整个团队花费了几周的时间来修改函数,使它们能够接受一个时间间隔作为参数。即使这样,还是遇到了很多由于几个月以来使用魔数而导致的错误。
真实的数据经常改变,比如汇率每分钟都在变化。强行使用具体的数值来编程意味着可能不得不花费大量时间重写代码和修复错误。编程中没有“魔法”的位置,即使是在数据科学中也是如此。
标准和约定的重要性
使用标准的好处在于它们帮助开发者简单地做出全局决策而不是许多的局部决策。无需在每次命名变量时选择声明的位置,而是在项目开始的时候做好决定,然后在整个项目中前后一致地使用这些变量。这样做的目的是花费更少的时间在命名、格式和风格这类数据科学的非核心问题上,用更多的时间解决重要的问题(比如使用机器学习研究环境变化)。
习惯独自工作的开发者可能很难意识到采用标准的优越性。然而即使是独自工作,也可以练习定义自己的规则并且一贯地使用它们。开发者将能够少做一些琐碎的决定,并且这也是为将来进行团队开发工作做准备。在任何需要一人以上的项目中,标准都是必要的。
读者可能会质疑本文中的某些命名选择,这无关紧要。更重要的是采用一组一贯的标准,而不是命名时具体使用的空间或者变量名的最大长度。关键是不再在偶然的难题中花费大量时间,而是专注于解决必然的问题。
结论
记住刚刚学到的,现在回到文章开头的代码:
- for i in range(n):
- for j in range(m):
- for k in range(l):
- temp_value = X[i][j][k] *12.5
- new_array[i][j][k] =temp_value + 150
然后使用描述性的变量名和符号常量对它进行修改。
- PIXEL_NORMALIZATION_FACTOR = 12.5
- PIXEL_OFFSET_FACTOR = 150
- for row_index in range(row_count):
- for column_index in range(column_count):
- for color_channel_index in range(color_channel_count):
- normalized_pixel_value = (
- original_pixel_array[row_index][column_index][color_channel_index]
- * PIXEL_NORMALIZATION_FACTOR
- )
- transformed_pixel_array[row_index][column_index][color_channel_index] =(
- normalized_pixel_value + PIXEL_OFFSET_FACTOR
- )
现在可以看出这段代码是在规格化数组中的像素值,并且加上一个偏移量来建立一个新的数组(忽略这种实现的低效性!)。当这段代码被交付给同事,他们将能够读懂和修改它。而且,当开发者回头看这段代码,试图测试和修正错误,他们也将清楚地知道自己在做什么。
这个话题无聊吗?或许有一点枯燥,但如果花费时间阅读有关软件工程的书目,会发现优秀程序员和普通程序员的区别就在于重复使用这些无趣的技巧,比如好的变量名、短暂的工作周期、测试每一行代码、重构等等。这就是把代码从实验室级别提升到工业生产级别所需要的。并且一旦做到这一点,就能发现使用模型来改变现实生活中的决策非常有趣。
本文讨论了一些改进变量名的方式。
需要牢记的所有重点:
- 变量名应该描述它所代表的对象
- 比起易编写性更重视易读性
- 在项目中使用一贯的标准,从而尽可能减轻琐碎决定的识别难题。
特别要点:
- 使用描述性的变量名
- 使用函数参数或者命名常量而不是“魔数”
- 不要使用特定的机器学习缩写
- 用变量名描述算式或模型的含义
- 把组合而成的变量名放在末尾
- 使用item_count而不是num
- 使用描述性的循环索引而不是i、j、k
- 在整个项目中采用统一的命名和格式规则