Python模式—— 一则优化轶事

Python Patterns - An Optimization Anecdote

原作者:Guido van Rossum

http://www.python.org/doc/essays/list2str.html

原创翻译,转载请注明出处

http://blog.csdn.net/colinsun/article/details/7579788


有一天,一个朋友请教我一个简单的问题:“如何能更好的实现将一个整型的list转换成其对应的ACII码值的字符串”。比如,list[97, 98, 99]应该转换成字符串‘abc’。假如我们打算用一个函数来实现的话。

我立马想到的一个最直接的实现版本:

    def f1(list):
        string = ""
        for item in list:
            string = string + chr(item)
        return string

我的那个朋友说,这并不是一个最高效的实现。看看这个:

    def f2(list):
        return reduce(lambda string, item: string + chr(item), list, "")

这个版本和上一个同样利用一组字符来实现,用reduce()来代替循环,从而摆脱for循环以提高效率。

不错,我答道,但是是以每使用一个list成员,牺牲一次调用函数(lambda函数)的花费为代价。我打赌这样会更慢,因为在Python中,调用函数的花费要比整个用循环来实现的花费大。

好吧,我已经比较过。f2()要比f1()多花费60%的时间。

“原来如此”,我朋友说,“我希望他能效率更高点”。“ok”,我说,“这个实现如何”:

    def f3(list):
        string = ""
        for character in map(chr, list):
            string = string + character
        return string

让我们俩吃惊的是,f3()的效率要比f1()高一倍!让我们惊讶的因素有二个:首先,他消耗了更多的存储空间(map(chr, list)的返回值是一个新的和原来大小一样的list);第二,他包含了两个循环而不是一个(一个隐含在map()中,一个是for循环)。

当然,空间换取时间是个绝佳的交易,所以第一个因素并没有那么让我们惊讶。但是,为何两个循环的效率会高过一个呢?

原因有二:

首先,在f1()的实现中,内建函数chr()会在每次迭代的时候被查找,而在f3()中只被查找一次(作为map()的入参)。这种查询的花费是相当的高的,我告诉我的朋友,由于Python动态域规则规定查询首先从当前module的全局字典里开始(未查询到),然后会查询内建字典(查询到)。更糟的是,由于哈希链表的方式而未成功的查询(平均来说)要比成功的查询要更慢。

其次,f3()比f1()更高效的原因是对chr(item)的调用是由字节码解释器执行的,这样会比执行map()函数要稍慢些——在每次for循环中,字节码解释器都要执行三条字节码指令,而map()函数却全部是用C来实现的。

这让我们想到了一个折中的方法,既不会浪费空间,又能提高查询chr()函数的效率:

    def f4(list):
        string = ""
        lchr = chr
        for item in list:
            string = string + lchr(item)
        return string

和我们想的一样,f4()只比f3()慢25%,却仍比f1()快40%。这是因为局部变量查询要比全局或内建变量查询要快得多:Python“编译器”优化了函数实体从而使局部变量的查询不需要使用字典,而是用一个简单的array索引操作便足够。为何f3()效率更高的原因也都是,f4()的效率处于f1()和f3()两者之间的原因,但是其中的第一个原因(更少的查询)要更重要些。(为了得到更精确的数据,我们必须编辑解释器)

到现在为止,我们最好的实现,f3(),只是比最直接的实现f1()快一倍。能够更好吗?

我担心两轮迭代(两个循环)的方式会很成问题。到目前为止,我们只用到了一个只有256个成员的list做测试用数据,这也是我的朋友的需求。但是如果list的成员个数有上千个呢?我们不得不串联更长的字符串,每次增加一个字符。很容易看到,到最后,用这样的方式创建一个长度为N的list,将需要总共拷贝1+2+3+...+(N-1)个字符,或者说N*(N-1)/2个,又或0.5*N**2-0.5*N个。另外,将会有N个分配字符串的操作,为了能满足长度为N的需要,要消耗N**2的资源。的确,一个8倍(于256)长度的list(2048个成员),将需要超过8倍时间来完成;事实上近乎于16倍的时长。我可不敢尝试一个所需64倍时长的list。

这里有一个避免两轮迭代的算法。是为超过256个字符长度的字符串编写的:

   def f5(list):
        string = ""
        for i in range(0, 256, 16): # 0, 16, 32, 48, 64, ...
            s = ""
            for character in map(chr, list[i:i+16]):
                s = s + character
            string = string + s
        return string

不幸的是,如果list只有256个成员,这个实现跑起来比f3()还是有一点慢(不超过20%)。由于写一个普遍版本的实现只会更慢,我没有过多的纠结(除非我们拿他和不使用map()的版本比较,显然又会陷入更慢的循环)。

最终,我尝试了一个彻底不同的方式:只使用隐式的循环。整个操作可以描述为:将每一个list成员用chr()转换;然后将所有转换后的字符相连。我们在第一部分已经使用了一个隐式循环:map()。幸运的是,有些连接字符的函数是用C实现的。尤其像string.joinfields(list_of_strings, delimiter)这样连接一个list的字符串的函数,在相邻的两个字符串之间可使用特定的分隔符。这种方式也适用于用空分隔符来连接一个list的字符在Pyhon中也就是一个字符的字符串)为字符串。瞧:

    import string
    def f6(list):
        return string.joinfields(map(chr, list), "")

这个函数的执行效率要比我们以为最快的实现,f3(),快4-5倍。而且避免了两轮迭代。

最终的胜利者是...

第二天,我想到Python中很少用到的:array模块。他刚好有个操作是将一个Python整数的list转换成一个单字节整数的array,并且每个array都能写到文件中或者转换为一个二进制数据结构的字符串(译者注:‘\x12\x34’)。这里是利用这种操作的函数实现:

    import array
    def f7(list):
        return array.array('B', list).tostring()

效率是f6()的三倍左右,是f3()的12-15倍!而且在中间过程使用更少的存储空间——只分配了2个N字节的对象(加上固定消耗),而f6()开始就分配了一个拥有N个成员的list,这通常会消耗4N个字节(在64位机器上消耗8N个字节)——假设字符对象也被其他类似的对象共享(比如小整型,Python通常会在一个单字节中缓存下字符串长度


“停”,我朋友说,“在你开始阐述负面因素之前”——“对我的程序而言这已经是一个够高效的实现了”。我同意,尽管如此,我仍打算再尝试一种实现:用C来实现整个函数。将会用最小的空间(立马分配一个N字节的字符串)并且能在C代码中因为泛型(支持整数长度为1,2,4字节)而在array模块中节省些指令。不管怎样,他将不可避免的从list中每次只取一个成员,然后又从中取C整型,这两个操作在Python-C API中都相当的消耗资源, 所以对f7()而言,我期望的是最适度的性能优化。花时间去写扩展并测试扩展(相对于轻松实现简短的Python语句而言)就如同依赖非标准的Python扩展一样,我决定不再纠结。



结论(有空再翻)

If you feel the need for speed, go for built-in functions - you can't beat a loop written in C. Check the library manual for a built-in function that does what you want. If there isn't one, here are some guidelines for loop optimization:
  • Rule number one: only optimize when there is a proven speed bottleneck. Only optimize the innermost loop. (This rule is independent of Python, but it doesn't hurt repeating it, since it can save a lot of work. :-)
  • Small is beautiful. Given Python's hefty charges for bytecode instructions and variable look-up, it rarely pays off to add extra tests to save a little bit of work.
  • Use intrinsic operations. An implied loop in map() is faster than an explicit for loop; a while loop with an explicit loop counter is even slower.
  • Avoid calling functions written in Python in your inner loop. This includes lambdas. In-lining the inner loop can save a lot of time.
  • Local variables are faster than globals; if you use a global constant in a loop, copy it to a local variable before the loop. And in Python, function names (global or built-in) are also global constants!
  • Try to use map(), filter() or reduce() to replace an explicit for loop, but only if you can use a built-in function: map with a built-in function beats for loop, but a for loop with in-line code beats map with a lambda function!
  • Check your algorithms for quadratic behavior. But notice that a more complex algorithm only pays off for large N - for small N, the complexity doesn't pay off. In our case, 256 turned out to be small enough that the simpler version was still a tad faster. Your mileage may vary - this is worth investigating.
  • And last but not least: collect data. Python's excellent profile module can quickly show the bottleneck in your code. if you're considering different versions of an algorithm, test it in a tight loop using the time.clock() function.

顺便说一下,这里是我使用过的计时函数。他以a作为入参调用一个函数f,n*10次,并在消耗的时间之后打印函数名,四舍五入以毫秒计。10次重复的调用将计时函数的循环花费的影响降低到最小。你可以进一步的调用100次...同样值得注意的是range(n)表达式的计算未在计时中。如果你担心他的消耗,你可以先使用空循环计算的时间来矫正。

    import time
    def timing(f, n, a):
        print f.__name__,
        r = range(n)
        t1 = time.clock()
        for i in r:
            f(a); f(a); f(a); f(a); f(a); f(a); f(a); f(a); f(a); f(a)
        t2 = time.clock()
        print round(t2-t1, 3)

后记

几天后,我的朋友又来请教问题:如何做反向操作呢?比如,从字符串中创建一个整型(ACSII)list。从我大脑里一闪而过的是“(⊙o⊙)哦,不,又来一次”...

但是这一次,相对不那么费力。有两个候选的实现,首先显然是:

    def g1(string):
        return map(ord, string)
其次是:

   import array
   def g2(string):
        return array.array('b', string).tolist()
 

通过计时可以发现g2()的效率是g1()的5倍左右。前提是:g2()返回-128..127之间的整型,而g1()返回0..255之间的整型。如果你需要正整数,使用g1()要比使用g2()后再去做转换要更快。

(注意:由于是手写的随笔,如果用Type code‘B’初始化array,可以让array存储无符号字节,如此就没有必要使用g1()了) 

(译者注:应该是作者当时手写的时候没有注意到Type code‘B’这个细节,所以才有最后一段描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python中,sort_index()函数是pandas库中DataFrame数据对象的一个方法,用于根据行标签和列名称对数据进行排序。函数的语法如下:sort_index(axis=0, level=None, ascending=True, inplace=False, kind="quicksort", na_position="last", sort_remaining=True, ignore_index=False)。\[1\] 另外,pandas的DataFrame数据对象还有另一种排序方式,即根据指定某一列的值进行排序,这可以通过sort_values()函数实现。\[2\] 需要注意的是,sort_index()函数是根据行标签和列名称进行排序,而sort_values()函数是根据某一列的值进行排序。 除了pandas库中的排序函数,还可以使用其他排序算法,比如冒泡排序和选择排序。冒泡排序是一种简单的排序算法,它通过比较相邻元素的大小并交换位置来实现排序。\[3\]选择排序是另一种常见的排序算法,它通过每次选择最小的元素并将其放置在已排序部分的末尾来实现排序。 希望以上信息对您有所帮助! #### 引用[.reference_title] - *1* *2* [python函数sort_index 和 sort_values排序](https://blog.csdn.net/lss1990lss/article/details/119559207)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [蓝桥杯python组————笔记整理](https://blog.csdn.net/qq_46639904/article/details/124023255)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值