[Python] 多路选择语句的写法与性能测试

作者 huhamhire,暂无评论,2013年7月31日 20:07 程序实践

Python这玩意儿已经学了到现在差不多快两年了,这么些时间用下来,我最大的感触就是,Python是一个简洁而强大的工具。平时遇到的大部分小问题都可以拿Python来解决,比如我前两周在做的新版hosts客户端工具就是Python与PyQT结合的产物,上学期的操作系统课程实验我也用Python很轻松的给解决了所有问题。Python是个好东西,不过我这人经常喜欢动些歪脑经,没事儿瞎折腾一下,所以就出现了我这一回要写的问题。

一、问题描述

ifelse

之前在拿Python写代码的时候,经常会遇到多路选择的问题。其实任何语言都会遇到这问题,只不过Python在数据类型上的灵活性带来很多有意思的事情。

There should be one -- and preferably only one -- obvious way to do it.

—The Zen of Python

虽然我接触程序设计到现在还不到十年,接触过的语言也并不多,不过大部分语言都有类似于swich语句的设定。但是基于前面这段所描述的Python编程理念,Python中只用一种多路选择的写法。


 1 if condition(1):
 2     operation(1)
 3 elif condition(2):
 4     operation(1)
 5 elif condition(3):
 6     operation(3)
 7 ...
 8 ...
 9 else:
10     operation(n)

这种写法很常见,也是最基本的选择语句的写法,不过可能是看久了有些审美疲劳的缘故,最近每次看到了就觉得这个写法一下子要占好多行,遇到屏幕小的话看起来还有点麻烦,总想换个方法来替代选择语句。于是基于Python的列表和字典这两种数据类型,我想到了用下面两种办法代替。

1.方案一——使用列表选择

构建一个列表,里面放上各个路径的操作或者是函数入口,然后通过一个flag来选择相应的参数。具体实施起来就像下面这样:


1 [
2     operation(1),
3     operation(2),
4     operation(3),
5     ...
6     operation(n),
7 ][flag]

2.方案二——使用字典选择

接近于上面一个方案的改进版本,构建一个字典,然后可以通过key来进行选择,而不再是比较一个数了。具体方案如下:


1 {
2     key(1): operation(1),
3     key(2): operation(2),
4     key(3): operation(3),
5     ...
6     Key(n): operation(n),
7 }[key]

以上这两种都是代码展开以后的写法,如果选项简单一点,缩在一行里面解决也是完全可以的。而且不单单是多路选择,所有的选择语句都可以使用这两种方式替代实现以减少代码量。

代码量是可以减少了,阅读起来所见的字符数量也相应的减少(其实未必降低了可读性)。但是问题也随之而来了,这三种写法他们执行起来的效率怎么样?为了实际进行对比,我准备通过两个小程序来进行测试。

二、设计测试程序

为了回答前面的问题,我设计了下面的两个程序来进行实验测试,测试的内容主要是语句执行的时间。

首先第一个程序用来测试在两个分支的一般选择情况下,这三种的语句执行1000000次分别所消耗的时间,并将时间反馈到终端:


 1 #!/usr/bin/python
 2 # -*- coding: UTF-8 -*-
 3 import math
 4 
 5 def select_if(flag):
 6     if flag:
 7         num = math.log(1000, 10)
 8     else:
 9         num = math.log(10000, 10)
10 
11 def select_list(flag):
12     num = [math.log(1000, 10), math.log(10000, 10)][flag]
13 
14 def select_dict(flag):
15     num = {1: math.log(1000, 10), 0: math.log(10000, 10)}[flag]
16 
17 if __name__ == "__main__":
18     import time
19     test_times = 1000000
20     funcs = [select_if, select_list, select_dict]
21     for func in funcs:
22         start_time = time.time()
23         for i in range(test_times):
24             func(1)
25         finish_time = time.time()
26         print "%i times of %s(1) finished in %.4fs" % (
27             test_times, func.__name__, finish_time - start_time)

接下来是第二个程序用来测试在多个分支的情况下,这三种的语句执行1000000次分别所消耗的时间。为了体现多分支选择的复杂性,这里设置了五个分支,并且测试时直接选择执行最后一个分支:


 1 #!/usr/bin/python
 2 # -*- coding: UTF-8 -*-
 3 import math
 4 
 5 def select_if(flag):
 6     if flag == 0:
 7         num = math.log(1, 10)
 8     elif flag == 1:
 9         num = math.log(10, 10)
10     elif flag == 2:
11         num = math.log(100, 10)
12     elif flag == 3:
13         num = math.log(1000, 10)
14     elif flag == 4:
15         num = math.log(10000, 10)
16 
17 def select_list(flag):
18     num = [
19         math.log(1, 10),
20         math.log(10, 10),
21         math.log(100, 10),
22         math.log(1000, 10),
23         math.log(10000, 10),
24         ][flag]
25 
26 def select_dict(flag):
27     num = {
28         0: math.log(1, 10),
29         1: math.log(10, 10),
30         2: math.log(100, 10),
31         3: math.log(1000, 10),
32         4: math.log(10000, 10),
33         }[flag]
34 
35 if __name__ == "__main__":
36     import time
37     test_times = 1000000
38     funcs = [select_if, select_list, select_dict]
39     for func in funcs:
40         start_time = time.time()
41         for i in range(test_times):
42             func(4)
43         finish_time = time.time()
44         print "%i times of %s(4) finished in %.4fs" % (
45             test_times, func.__name__, finish_time - start_time)

程序写好,下面就测试一下来分析分析结果。

三、实验结果与分析

现在看下在只有两路分支的情况下,进行同样的操作时,时间消耗的情况。为了避免偶然的误差,这里给出了三次的测试结果。


 1 # Test 1
 2 1000000 times of select_if(1) finished in 1.0050s
 3 1000000 times of select_list(1) finished in 1.8630s
 4 1000000 times of select_dict(1) finished in 3.3430s
 5 
 6 # Test 2
 7 1000000 times of select_if(1) finished in 0.8960s
 8 1000000 times of select_list(1) finished in 1.6840s
 9 1000000 times of select_dict(1) finished in 3.3850s
10 
11 # Test 3
12 1000000 times of select_if(1) finished in 1.1490s
13 1000000 times of select_list(1) finished in 2.1730s
14 1000000 times of select_dict(1) finished in 3.1130s

从这里的结果来看。if…else…语句的执行速度最快,一百万次操作的时间消耗大致在1秒左右;使用list实现同样的操作,其时间开销在2秒附近;而是用dict的话,时间消耗就要到3秒以上了。

接下来再来看一下在多路选择情况下的结果,同样给出三次测试数据。


 1 # Test 1
 2 1000000 times of select_if(4) finished in 1.1480s
 3 1000000 times of select_list(4) finished in 5.3590s
 4 1000000 times of select_dict(4) finished in 5.1380s
 5 
 6 # Test 2
 7 1000000 times of select_if(4) finished in 1.1520s
 8 1000000 times of select_list(4) finished in 4.9800s
 9 1000000 times of select_dict(4) finished in 5.6050s
10 
11 # Test 3
12 1000000 times of select_if(4) finished in 1.1490s
13 1000000 times of select_list(4) finished in 5.7660s
14 1000000 times of select_dict(4) finished in 5.1580s

前面在两个分支的选择效率上面list的效率要比dict高一个档次,但是在增加了分支选择以后,list选择的效率降低很明显,以至于在五个分支的时候就已经低于dict的效率了。

下面来分析一下结果。我在实验以前,从算法的角度对结果又一个预期。if语句与列表在进行多路选择操作的时候,其时间复杂度为 O(n);而Python中的字典直接采用了哈希表来存放数据,理论上其时间复杂度为 O(1)

从时间复杂度的角度来看,似乎在大量选择的情况下,字典的效率要更好。但实际测试下来,还是if语句的效率最高。这个结果也是说的过去的,毕竟if语句是一门语言最核心的功能,Python在实现这个语句的时候,说不定也是直接以C语言的方式执行的。而像list和dict这两种存储方式虽然也是Python比较核心大的功能,但其索引方式实现起来应该还是离不开if语句,因而效率跟if语句相比就不可同日而语了。if语句如果是汇编实现的话,也可以直接用类似于TEST+JNZ的指令高效地处理,列表和字典之类的因为其数据结构更高级,处理起来自然没这么简单。

当然,hash也不是一点优势都没有。在前面的测试中,当使用五个分支的时候,相比于通用采用复杂数据结构的列表,哈希表的算法优势就已经体现出来了,时间消耗的增量远没有列表方案来的这么夸张。不过字典在多路选择上的应用也就到此位置,想要完全替代if语句是不太可能的。毕竟if实现简单而dict实现较为复杂,而且即使在极端情况下,也几乎不会遇到上万路的分支选择语句,因为这种选择的代码结构本身就是不规范的。

到了这里,本次实验应该有个结果了。正如同前面提到的The Zen of Python中的那句时所描述的那样,在Python中间无论是采用列表还是字典来替代选择语句,从运行效率上来说肯定是不是具有可比性的。

不过话又说回来,这两种方式在缩减代码行数,以及提升代码的可读性方面还是可以起到一定的作用的。另外考虑到这里秒级别的时间差距实在1000000次测试的情况下才出现的,如果选择语句不是放在大量的循环中间,其误差可以忽略不计。如此看来,使用列表或者字典进行选择也并非不可取,可以看个人喜好并根据实际情况而定,即使这样做不符合Python的编程理念。虽然Python讲究同一个问题,同一个解决方案,但是我觉得程序员在语言风格方面应该可以有相对个性化的选择。

关键词:Python , 小实验
登录后进行评论