学习过程中,我们经常会阅读他人写的代码,如果注意观察就会发现,好的代码本身就是一份文档,解决同样的问题,不同的人编写的代码,其可读性千差万别。

有些人的设计风格和代码风格犹如热刀切黄油,从顶层到底层的代码看下来酣畅淋漓,注释详尽又精简;深入到细节代码,无需注释也能理解清清楚楚。而有些人,代码勉勉强强能跑起来,遇到稍微复杂的情况就会出崩溃,且代码中处处都是堆积在一起的变量、函数和类,很难理清代码的实现思路。

Python 创始人 Guido van Rossum(吉多·范罗苏姆)说过,代码的阅读频率远高于编写代码的频率。毕竟是在编写代码的时候,我们自己也需要对代码进行反复阅读和调试,来确认代码能够按照期望运行。

本节,在读者学会如何使用 Puython 函数的基础上,教大家怎么才能合理分解代码,提高代码的可读性。

首先,大家在编程过程中,一定要围绕一个中心思想:不写重复性的代码。因为,重复代码往往是可以通过使用条件、循环、构造函数和类(后续章节会做详细介绍)来解决的。

例如,仔细观察下面的代码:

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)

与此同时,还要学会刻意地减少代码的迭代层数,尽可能让 Python 代码扁平化。例如:

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() 函数拿到结果,决定返回不存在,还是返回值。这样一来,每个函数各司其职,阅读性也能得到一定提高。