合理分解代码,提高代码可读性

合理分解代码,提高代码可读性

好的代码本身就是一份文档,从顶层到底层的代码看下来酣畅淋漓,注释详尽而又精简;深入到细节代码,无需注释也能理解清清楚楚。Guido van Rossum(吉多·范罗苏姆,Python 创始人 )说过,代码的阅读频率远高于编写代码的频率。

PEP 8 规范

PEP 是 Python Enhancement Proposal 的缩写,翻译过来叫“Python 增强规范”。

缩进规范

Python 和 C++ / Java 最大的不同在于,后者完全使用大括号来区分代码块,而前者依靠不同行和不同的缩进来进行分块。

有一个很有名的比赛,叫作 C 语言混乱代码大赛,其中有很多非常精彩的作品,你能看到书写的代码排成各种形状,有的是一幅画,或者一个卡通头像,但是能执行出惊人的结果。

Python 的缩进其实可以写成很多种,Tab、双空格、四空格、空格和 Tab 混合等。而 PEP 8 规范告诉我们,请选择四个空格的缩进,不要使用 Tab,更不要 Tab 和空格混着用

还需要注意的是,每行最大长度请限制在 79 个字符

这个原则主要有两个优点,第一个优点比较好理解。很多工程师在编程的时候,习惯一个屏幕并列竖排展示多个源代码。如果某个源代码的某些行过长,你就需要拖动横向滚动条来阅读,或者需要软回车将本行内容放入下一行,这就极大地影响了编码和阅读效率。
至于第二个优点,需要有一定经验的编程经验后更容易理解:因为当代码的嵌套层数过高,比如超过三层之后,一行的内容就很容易超过 79 个字符了。所以,这条规定另一方面也在制约着程序员,不要写迭代过深的代码,而是要思考继续把代码分解成其他函数或逻辑块,来优化自己的代码结构。

空行规范

Python 中的空行对 Python 解释器的执行没有影响,但对阅读体验有很深刻的影响。

PEP 8 规定,全局的类和函数的上方需要空两个空行,而类的函数之间需要空一个空行

另外,Python 本身允许把多行合并为一行,使用分号隔开,但这是 PEP 8 不推荐的做法。

每个代码文件的最后一行为空行,并且只有这一个空行。

空格规范

函数的参数列表中,调用函数的参数列表中会出现逗号,请注意逗号后要跟一个空格,这是英语的使用习惯,也能让每个参数独立阅读,更清晰。

同理,冒号经常被用来初始化字典,冒号后面也要跟一个空格。

另外,Python 中我们可以使用#进行单独注释,请记得要在#后、注释前加一个空格。

对于操作符,例如+,-,*,/,&,|,=,==,!=,请在两边都保留空格。不过与此对应,括号内的两端并不需要空格。

换行规范

需要控制每行的最大长度不超过 79 个字符,但是有时候,函数调用逻辑过长而不得不超过这个数字时,该怎么办?

def solve1(this_is_the_first_parameter, this_is_the_second_parameter, this_is_the_third_parameter,
           this_is_the_forth_parameter, this_is_the_fifth_parameter, this_is_the_sixth_parameter):
    return (this_is_the_first_parameter + this_is_the_second_parameter + this_is_the_third_parameter +
            this_is_the_forth_parameter + this_is_the_fifth_parameter + this_is_the_sixth_parameter)


def solve2(this_is_the_first_parameter, this_is_the_second_parameter, this_is_the_third_parameter,
           this_is_the_forth_parameter, this_is_the_fifth_parameter, this_is_the_sixth_parameter):
    return this_is_the_first_parameter + this_is_the_second_parameter + this_is_the_third_parameter + \
           this_is_the_forth_parameter + this_is_the_fifth_parameter + this_is_the_sixth_parameter


(top_secret_func(param1=12345678, param2=12345678, param3=12345678, param4=12345678, param5=12345678).check()
    .launch_nuclear_missile().wait())


top_secret_func(param1=12345678, param2=12345678, param3=12345678, param4=12345678, param5=12345678).check() \
    .launch_nuclear_missile().wait()

第一种,通过括号来将过长的运算进行封装,此时虽然跨行,但是仍处于一个逻辑引用之下。solve1 函数的参数过多,直接换行,不过请注意,要考虑第二行参数和第一行第一个参数对齐,这样可以让函数变得非常美观的同时,更易于阅读。当然,函数调用也可以使用类似的方式,只需要用一对括号将其包裹起来。

第二种,则是通过换行符来实现。这个方法更为直接,可以从 solve2 和第二个函数调用看出来。

下面的代码选自开源库 Google TensorFlow Keras,通过阅读这段代码,能更真实地了解到,前沿的项目是怎么在增强阅读性上下功夫的:

class Model(network.Network):
    def fit(self,
            x=None,
            y=None,
            batch_size=None,
            epochs=1,
            verbose=1,
            callbacks=None,
            validation_split=0.,
            validation_data=None,
            shuffle=True,
            class_weight=None,
            sample_weight=None,
            initial_epoch=0,
            steps_per_epoch=None,
            validation_steps=None,
            validation_freq=1,
            max_queue_size=10,
            workers=1,
            use_multiprocessing=False,
            **kwargs):
        # Legacy support
        if 'nb_epoch' in kwargs:
            logging.warning(
                'The `nb_epoch` argument in `fit` has been renamed `epochs`.')
            epochs = kwargs.pop('nb_epoch')
        if kwargs:
            raise TypeError('Unrecognized keyword arguments: ' + str(kwargs))
        self._assert_compile_was_called()

        func = self._select_training_loop(x)
        return func.fit(
            self,
            x=x,
            y=y,
            batch_size=batch_size,
            epochs=epochs,
            verbose=verbose,
            callbacks=callbacks,
            validation_split=validation_split,
            validation_data=validation_data,
            shuffle=shuffle,
            class_weight=class_weight,
            sample_weight=sample_weight,
            initial_epoch=initial_epoch,
            steps_per_epoch=steps_per_epoch,
            validation_steps=validation_steps,
            validation_freq=validation_freq,
            max_queue_size=max_queue_size,
            workers=workers,
            use_multiprocessing=use_multiprocessing)

文档规范

  • 所有 import 尽量放在开头,便于看清文件之间的依赖关系。降低运行时 import 时可能导致潜在的效率问题和其他风险。
  • 不要使用 import 一次导入多个模块import time, os是 PEP 8 不推荐的做法。
  • 如果采用 from module import func 这样的语句,请确保 func在本文件中不会出现命名冲突。可以通过 from module import func as new_func 来进行重命名,从而避免冲突。

注释规范

错误的注释,不如没有注释。改动代码的时候,一定要注意检查周围的注释是否需要更新。

  • 对于大的逻辑块,可以在最开始相同的缩进处以 # 开始写注释。
  • 至于行注释,如空格规范中所讲,可以在一行后面跟两个空格,然后以 # 开头加入注释。
# This is an example to demonstrate how to comment.
# Please note this function must be used carefully.
def solve(x):
    if x == 1:  # This is only one exception.
        return False
    return True

文档描述

以 TensorFlow 的代码为例:

class SpatialDropout2D(Dropout):
    """Spatial 2D version of Dropout.
    This version performs the same function as Dropout, however it drops
    entire 2D feature maps instead of individual elements. If adjacent pixels
    within feature maps are strongly correlated (as is normally the case in
    early convolution layers) then regular dropout will not regularize the
    activations and will otherwise just result in an effective learning rate
    decrease. In this case, SpatialDropout2D will help promote independence
    between feature maps and should be used instead.
    Arguments:
        rate: float between 0 and 1. Fraction of the input units to drop.
        data_format: 'channels_first' or 'channels_last'.
            In 'channels_first' mode, the channels dimension
            (the depth) is at index 1,
            in 'channels_last' mode is it at index 3.
            It defaults to the `image_data_format` value found in your
            Keras config file at `~/.keras/keras.json`.
            If you never set it, then it will be "channels_last".
    Input shape:
        4D tensor with shape:
        `(samples, channels, rows, cols)` if data_format='channels_first'
        or 4D tensor with shape:
        `(samples, rows, cols, channels)` if data_format='channels_last'.
    Output shape:
        Same as input
    References:
        - [Efficient Object Localization Using Convolutional
          Networks](https://arxiv.org/abs/1411.4280)
  """
    def __init__(self, rate, data_format=None, **kwargs):
        super(SpatialDropout2D, self).__init__(rate, **kwargs)
        if data_format is None:
            data_format = K.image_data_format()
        if data_format not in {'channels_last', 'channels_first'}:
            raise ValueError('data_format must be in '
                           '{"channels_last", "channels_first"}')
        self.data_format = data_format
        self.input_spec = InputSpec(ndim=4)

docstring 的写法,它是用三个双引号开始、三个双引号结尾。首先用一句话简单说明这个函数做什么,然后跟一段话来详细解释;再往后是参数列表、参数格式、返回值格式。

命名规范

“计算机科学的两件难事:缓存失效和命名。”

  • 变量命名:变量名请拒绝使用 a b c d 这样毫无意义的单字符,应该使用能够代表其意思的变量名。一般来说,变量使用小写,通过下划线串联起来,例如:data_formatinput_specimage_data_set。唯一可以使用单字符的地方是迭代,比如for i in range(n) 这种,为了精简可以使用。如果是类的私有变量,请记得前面增加两个下划线。
  • 常量: 全部大写,并通过下划线连接,例如:WAIT_TIMESERVER_ADDRESSPORT_NUMBER
  • 函数名: 使用小写的方式,通过下划线连接起来,例如:launch_nuclear_missile()check_input_validation()
  • 类名: 首字母大写,然后合并起来,,例如:class SpatialDropout2D()class FeatureSet()

不要过于吝啬一个变量名的长度

代码分解技巧

编程中一个核心思想是,不写重复代码。重复代码大概率可以通过使用条件、循环、构造函数和类来解决。而另一个核心思想则是,减少迭代层数,尽可能让 Python 代码扁平化,毕竟,人的大脑无法处理过多的栈操作。在很多业务逻辑比较复杂的地方,就需要我们加入大量的判断和循环。

通过下面几个示例,来说说写好判断、循环的细节问题:

if i_am_rich:
    money = 100
    send(money)
else:
    money = 10
    send(money)

这段代码中,同样的 send 语句出现了两次,所以完全可以合并一下:

if i_am_rich:
    money = 100
else:
    money = 10
send(money)

再来看一个例子:

def send(money):
    if is_server_dead:
        LOG('server dead')
        return
    else:
        if is_server_timed_out:
            LOG('server timed out')
            return
        else:
            result = get_result_from_server()
            if result == MONEY_IS_NOT_ENOUGH:
                LOG('you do not have enough money')
                return
            else:
                if result == TRANSACTION_SUCCEED:
                    LOG('OK')
                    return
                else:
                    LOG('something wrong')
                    return

这段代码层层缩进,显而易见的难看。改一下:

def send(money):
    if is_server_dead:
        LOG('server dead')
        return

    if is_server_timed_out:
        LOG('server timed out')
        return

    result = get_result_from_server()

    if result == MONET_IS_NOT_ENOUGH:
        LOG('you do not have enough money')
        return

    if result == TRANSACTION_SUCCEED:
        LOG('OK')
        return

    LOG('something wrong')

一个函数的粒度应该尽可能细,不要让一个函数做太多的事情。对待一个复杂的函数,需要尽可能地把它拆分成几个功能简单的函数,然后合并起来。

以一个简单的二分搜索来举例说明我给定一个非递减整数数组,和一个 target,要求找到数组中最小的一个数 x,可以满足 x*x > target。一旦不存在,则返回 -1。

def solve(arr, target):
    l, r = 0, len(arr) - 1
    ret = -1
    while l <= r:
        m = (l + r) // 2
        if arr[m] * arr[m] > target:
            ret = m
            r = m - 1
        else:
            l = m + 1
    if ret == -1:
        return -1
    else:
        return arr[ret]


print(solve([1, 2, 3, 4, 5, 6], 8))
print(solve([1, 2, 3, 4, 5, 6], 9))
print(solve([1, 2, 3, 4, 5, 6], 0))
print(solve([1, 2, 3, 4, 5, 6], 40))

从工程角度来说,还能继续优化一下:

def comp(x, target):
    return x * x > target


def binary_search(arr, target):
    l, r = 0, len(arr) - 1
    ret = -1
    while l <= r:
        m = (l + r) // 2
        if comp(arr[m], target):
            ret = m
            r = m - 1
        else:
            l = m + 1
    return ret


def solve(arr, target):
    id = binary_search(arr, target)

    if id != -1:
        return arr[id]
    return -1


print(solve([1, 2, 3, 4, 5, 6], 8))
print(solve([1, 2, 3, 4, 5, 6], 9))
print(solve([1, 2, 3, 4, 5, 6], 0))
print(solve([1, 2, 3, 4, 5, 6], 40))

第二段代码中,把不同功能的代码拿了出来。其中,comp()函数作为核心判断,拿出来后可以让整个程序更清晰;同时,也把二分搜索的主程序拿了出来,只负责二分搜索;最后的 solve()函数拿到结果,决定返回不存在,还是返回值。这样一来,每个函数各司其职,阅读性也能得到一定提高。

如何拆分类:

class Person:
    def __init__(self, name, sex, age, job_title, job_description, company_name):
        self.name = name
        self.sex = sex
        self.age = age
        self.job_title = job_title
        self.job_description = description
        self.company_name = company_name

job 在其中出现了很多次,而且它们表达的是一个意义实体,这种情况下,可以考虑将这部分分解出来,作为单独的类:

class Person:
    def __init__(self, name, sex, age, job_title, job_description, company_name):
        self.name = name
        self.sex = sex
        self.age = age
        self.job = Job(job_title, job_description, company_name)

class Job:
    def __init__(self, job_title, job_description, company_name):

        self.job_title = job_title
        self.job_description = description
        self.company_name = company_name

总结

缩进规范:

  1. 使用四空格缩进
  2. 每行最大长度79个字符

空行规范:
1. 全局的(文件级别的)类和全局的函数上方要有两个空行
2. 类中的函数上方要有一个空行
3. 函数内部不同意群的代码块之前要有一个空行
4. 不要把多行语句合并为一行(即不要使用分号分隔多条语句)
5. 当使用控制语句if/while/for时,即使执行语句只有一行命令,也需要另起一行
6. 代码文件尾部有且只有一个空行

空格规范:
1. 函数的参数之间要有一个空格
2. 列表、元组、字典的元素之间要有一个空格
3. 字典的冒号之后要有一个空格
4. 使用#注释的话,#后要有一个空格
5. 操作符(例如+,-,*,/,&,|,=,==,!=)两边都要有一个空格,不过括号(包括小括号、中括号和大括号)内的两端不需要空格

换行规范:
1. 一般我们通过代码逻辑拆分等手段来控制每行的最大长度不超过79个字符,但有些时候单行代码还是不可避免的会超过这个限制,这个时候就需要通过换行来解决问题了。
2. 两种实现换行的方法:
第一种,通过小括号进行封装,此时虽然跨行,但是仍处于一个逻辑引用之下。比如函数参数列表的场景、过长的运算语句场景
第二种,直接通过换行符()实现

文档规范:
1. 所有import尽量放在代码文件的头部位置
2. 每行import只导入一个对象
3. 当我们使用from xxx import xxx时,import后面跟着的对象要是一个package(包对应代码目录)或者module(模块对应代码文件),不要是一个类或者一个函数

注释规范:
1. 代码块注释,在代码块上一行的相同缩进处以 # 开始书写注释
2. 代码行注释,在代码行的尾部跟2个空格,然后以 # 开始书写注释,行注释尽量少写
3. 英文注释开头要大写,结尾要写标点符号,避免语法错误和逻辑错误,中文注释也是相同要求
4. 改动代码逻辑时,一定要及时更新相关的注释

文档描述规范:
三个双引号开始、三个双引号结尾;
首先用一句话简单说明这个函数做什么,然后跟一段话来详细解释;
再往后是参数列表、参数格式、返回值格式的描述。

命名规范:
1. 变量名,要全部小写,多个词通过下划线连接,可以使用单字符变量名的场景,比如for i in range(n)这种变量没有实际意义的情况
2. 类的私有变量名,变量名前需要加2个下划线
3. 常量名,要全部大写,多个词通过下划线连接
4. 函数名,要全部小写,多个词通过下划线连接
5. 类名,要求驼峰形式,即单词首字母大写,多个词的话,每个词首字母大写然后直接拼接
6. 命名需要做到名如其意,不要吝啬名字的长度

代码分解技巧:
1. 不写重复代码,重复代码可以通过使用条件、循环、函数和类来解决
2. 减少迭代层数,让代码扁平化
3. 函数拆分,函数的粒度尽可能细,也就是一个函数不要做太多事情
4. 类的拆分,如果一个类中有多个属性描述的是同一类事物,就可以把这些属性拆分出来新建一个单独的类
5. 模块化,若项目偏大,要为不同功能模块创建独立的目录或文件,通过import互相关联

赞赏
Nemo版权所有丨如未注明,均为原创丨本网站采用BY-NC-SA协议进行授权,转载请注明转自:https://nemo.cool/545.html

Nemo

文章作者

发表回复

textsms
account_circle
email

合理分解代码,提高代码可读性
好的代码本身就是一份文档,从顶层到底层的代码看下来酣畅淋漓,注释详尽而又精简;深入到细节代码,无需注释也能理解清清楚楚。Guido van Rossum(吉多·范罗苏姆,Python 创始人 )说过…
扫描二维码继续阅读
2020-05-23