1.2 案例故事
下面是Python程序员小R去其他公司面试的故事。
在本书剩下的案例故事里,你还会多次看到“小R”的身影。
小R这个名字来自作者的英文名(Raymond)的首字母缩写。随着故事的不同,小R有时是一位Python初学者,有时又是一名有多年经验的Python老手。但无论扮演什么角色,他总会在每个故事里获得新的成长。
下面,我们看一看本书的第一个案例故事。
奇怪的冒泡排序算法
上午10点,在T公司的会议室里,小R正在参加一场他准备了好几天的技术面试。
整体来说,他在这场面试中的表现还不错。无论坐在小R对面的面试官提出什么问题,他都能侃侃而谈、对答如流。从单体应用聊到微服务,从虚拟机聊到云计算,每一块小R都说得滴水不漏。就在他认为自己胜券在握,可以通过这家自己憧憬已久的公司面试时,对面的面试官突然说道:“技术问题我问得差不多了。最后有一道编程题,希望你可以做一下。”
说完,面试官低头从包里拿出了一台笔记本电脑,递给了小R。小R有些紧张地接过电脑,发现屏幕上是一道算法题。
题目 冒泡排序算法
请用Python语言实现冒泡排序算法,把较大的数字放在后面。注意:默认所有的偶数都比奇数大。
>>> numbers = [23, 32, 1, 3, 4, 19, 20, 2, 4] >>> magic_bubble_sort(numbers) [1, 3, 19, 23, 2, 4, 4, 20, 32]
“冒泡排序,这不是所有排序算法里最简单的一种吗?虽然加了一点儿变化,但看起来没有什么难度啊。”小R一边在心里这么想着,一边打开编辑器开始写代码。
五分钟后,他把笔记本电脑递给面试官并说道:“写完了!”
代码清单1-1就是他写的代码。
代码清单1-1小R写的冒泡排序函数
def magic_bubble_sort(numbers): j = len(numbers) - 1 while j > 0: for i in range(j): if numbers[i] % 2 == 0 and numbers[i + 1] % 2 == 1: numbers[i], numbers[i + 1] = numbers[i + 1], numbers[i] continue elif (numbers[i + 1] % 2 == numbers[i] % 2) and numbers[i] > numbers[i + 1]: numbers[i], numbers[i + 1] = numbers[i + 1], numbers[i] continue j -= 1 return numbers
这段代码没有任何多余的逻辑,可以通过所有的测试用例。面试官看着小R演示完函数功能后,盯着代码似乎想说点儿什么,但最后只是微微点了点头,说:“好,今天的面试就到这儿吧,有后续面试我再通知你。”
小R高高兴兴地回到家,一心觉得这次面试稳了,可没想到,他后来却再也没接到任何后续面试的通知。
01.问题出在哪里
究竟是哪里出了问题呢?小R思来想去,觉得自己回答问题时表现挺好,最有可能出问题的是最后一道编程题,肯定是漏掉了什么边界情况没处理。
于是他找到一位有着十年编程经验的前辈小Q,凭着记忆把题目和自己的答案还原给对方看。
“题目大概就是这样,这是我当时写的代码。Q哥,你帮忙看看,我是不是有什么情况没考虑到?”小R问道。
小Q盯着他写的代码,足足两分钟没说一句话,然后突然开口道:“小R啊,你这个函数功能实现得没毛病,就是实在太难看懂了。”
“总共就10行代码。难看懂?怎么会呢?”小R在心里泛起了嘀咕。这时,前辈小Q说道:“这样,你把笔记本电脑给我,我来给你稍微改改这段代码,然后你再看看。”
三分钟后,小Q把修改过的代码递了过来,如代码清单1-2所示。
代码清单1-2小Q修改后的冒泡排序函数
def magic_bubble_sort(numbers: List[int]): """有魔力的冒泡排序算法,默认所有的偶数都比奇数大 :param numbers: 需要排序的列表,函数会直接修改原始列表 """ stop_position = len(numbers) - 1 while stop_position > 0: for i in range(stop_position): current, next_ = numbers[i], numbers[i + 1] ➊ current_is_even, next_is_even = current % 2 == 0, next_ % 2 == 0 should_swap = False # 交换位置的两个条件: # - 前面是偶数,后面是奇数 # - 前面和后面同为奇数或者偶数,但是前面比后面大 if current_is_even and not next_is_even: should_swap = True elif current_is_even == next_is_even and current > next_: should_swap = True if should_swap: numbers[i], numbers[i + 1] = numbers[i + 1], numbers[i] stop_position -= 1 return numbers
❶ 注意:此处变量名是next_而非next,这是因为已经有一个内置函数使用了next这个名字。PEP 8规定在这种情况下,应该给变量名增加_后缀来避免冲突
小R盯着这段代码,发现它的核心逻辑和之前没有任何不同。但不知为何,这段代码看上去就是比自己写的代码更舒服。小R若有所思,好像一下明白了自己没通过面试的原因。
故事讲完了。看上去,前辈小Q只是在小R的代码之上做了些“无关痛痒”的改动,但正是这些“无关痛痒”的改动,改善了代码的观感,提升了整个函数的可读性。
02.“无关痛痒”的改动
和小R写的代码相比,前辈小Q的新代码主要进行了以下改进。
(1) 变量名变成了可读的、有意义的名字,比如在旧代码里,“停止位”是无意义的j,新代码里变成了stop_position。
(2) 增加了有意义的临时变量,比如current/next_ 代表前一个/后一个元素、{}_is_even代表元素是否为偶数、should_swap代表是否应该交换元素。
(3) 多了一点儿恰到好处的指引性注释,比如说明交换元素顺序的详细条件。
这些变化让整段代码变得更易读,也让整个算法变得更好理解。所以,哪怕是一段不到10行代码的简单函数,对变量和注释的不同处理方式,也会让代码发生质的变化。