python編碼最佳實踐之總結

xiaoqb發表於2016-01-12

該文章轉自阿里巴巴技術協會(ATA

作者:空溟

   相信用python的同學不少,本人也一直對python情有獨鍾,毫無疑問python作為一門解釋性動態語言沒有那些編譯型語言高效,但是python簡潔、易讀以及可擴充套件性等特性使得它大受青睞。

     工作中很多同事都在用python,但往往很少有人關注它的效能和慣用法,一般都是現學現用,畢竟python不是我們的主要語言,我們一般只是使用它來做一些系統管理的工作。但是我們為什麼不做的更好呢?python zen中有這樣一句:There should be one– and preferably only one –obvious way to do it. Although that way may not be obvious at first unless you`re Dutch. 大意就是python鼓勵使用一種最優的方法去完成一件事,這也是和ruby等的一個差異。所以一種好的python編寫習慣個人認為很重要,本文就重點從效能角度出發對python的一些慣用法做一個簡單總結,希望對大家有用~

    提到效能,最容易想到的是降低複雜度,一般可以通過測量程式碼迴路複雜度(cyclomatic complexitly)和Landau符號(大O)來分析, 比如dict查詢是O(1),而列表的查詢卻是O(n),顯然資料的儲存方式選擇會直接影響演算法的複雜度。

一、資料結構的選擇:

     1. 在列表中查詢:

   對於已經排序的列表考慮用bisect模組來實現查詢元素,該模組將使用二分查詢實現

def find(seq, el) :
    pos = bisect(seq, el)
    if pos == 0 or ( pos == len(seq) and seq[-1] != el ) :
        return -1
    return pos - 1

    而快速插入一個元素可以用:

 bisect.insort(list, element) 

這樣就插入元素並且不需要再次呼叫 sort() 來保序,要知道對於長list代價很高.

    2. set代替列表: 

    比如要對一個list進行去重,最容易想到的實現:

seq = [`a`, `a`, `b`]
res = []
for i in seq:
    if i not in res:
        res.append(i)

顯然上面的實現的複雜度是O(n2),若改成:

seq = [`a`, `a`, `b`]
res = set(seq)

複雜度馬上降為O(n),當然這裡假定set可以滿足後續使用。

另外,set的union,intersection,difference等操作要比列表的迭代快的多,因此如果涉及到求列表交集,並集或者差集等問題可以轉換為set來進行,平時使用的時候多注意下,特別當列表比較大的時候,效能的影響就更大。

    3. 使用python的collections模組替代內建容器型別:

collections有三種型別:

  1. deque:增強功能的類似list型別
  2. defaultdict:類似dict型別
  3. namedtuple:類似tuple型別

       列表是基於陣列實現的,而deque是基於雙連結串列的,所以後者在中間or前面插入元素,或者刪除元素都會快很多。 

       defaultdict為新的鍵值新增了一個預設的工廠,可以避免編寫一個額外的測試來初始化對映條目,比dict.setdefault更高效,引用python文件的一個例子:

#使用profile stats工具進行效能分析

>>> from pbp.scripts.profiler import profile, stats
>>> s = [(`yellow`, 1), (`blue`, 2), (`yellow`, 3),
... (`blue`, 4), (`red`, 1)]
>>> @profile(`defaultdict`)
... def faster():
... d = defaultdict(list)
... for k, v in s:
... d[k].append(v)
...
>>> @profile(`dict`)
... def slower():
... d = {}
... for k, v in s:
... d.setdefault(k, []).append(v)
...
>>> slower(); faster()
Optimization: Solutions
[ 306 ]
>>> stats[`dict`]
{`stones`: 16.587882671716077, `memory`: 396,
`time`: 0.35166311264038086}
>>> stats[`defaultdict`]
{`stones`: 6.5733464259021686, `memory`: 552,
`time`: 0.13935494422912598}

可見效能提升了快3倍。defaultdict用一個list工廠作為引數,同樣可用於內建型別,比如long等。

 

除了實現的演算法、架構之外,python提倡簡單、優雅。所以正確的語法實踐又很有必要,這樣才會寫出優雅易於閱讀的程式碼。

二、語法最佳實踐:

  1. 字串操作:優於python字串物件是不可改變的,因此對任何字串的操作如拼接,修改等都將產生一個新的字串物件,而不是基於原字串,因此這種持續的 copy會在一定程度上影響Python的效能:

        (1)用join代替 `+` 操作符,後者有copy開銷;

        (2)同時當對字串可以使用正規表示式或者內建函式來處理的時候,選擇內建函式。如str.isalpha(),str.isdigit(),str.startswith((‘x’, ‘yz’)),str.endswith((‘x’, ‘yz’))

        (3)字元格式化操作優於直接串聯讀取:

     str = "%s%s%s%s" % (a, b, c, d)  # efficient
     str = "" + a + b + c + d + ""  # slow

    2. 善用list comprehension(列表解析)  & generator(生成器) & decorators(裝飾器),熟悉itertools等模組

       (1) 列表解析,我覺得是python2中最讓我印象深刻的特性,舉例1:

      >>> # the following is not so Pythonic  
      >>> numbers = range(10)
      >>> i = 0 
      >>> evens = [] 
      >>> while i < len(numbers): 
      >>>    if i %2 == 0: evens.append(i) 
      >>>    i += 1 
      >>> [0, 2, 4, 6, 8] 

      >>> # the good way to iterate a range, elegant and efficient
      >>> evens = [ i for i in range(10) if i%2 == 0] 
      >>> [0, 2, 4, 6, 8] 

   舉例2:

def _treament(pos, element):
    return `%d: %s` % (pos, element)
f = open(`test.txt`, `r`)
if __name__ == `__main__`:
    #list comps 1
    print sum(len(word) for line in f for word in line.split())
    #list comps 2
    print [(x + 1, y + 1) for x in range(3) for y in range(4)]
    #func
    print filter(lambda x: x % 2 == 0, range(10))
    #list comps3
    print [i for i in range(10) if i % 2 == 0]
    #list comps4 pythonic
    print [_treament(i, el) for i, el in enumerate(range(10))]

output:
24
[(1, 1), (1, 2), (1, 3), (1, 4), (2, 1), (2, 2), (2, 3), (2, 4), (3, 1), (3, 2), (3, 3), (3, 4)]
[0, 2, 4, 6, 8]
[0, 2, 4, 6, 8]
[`0: 0`, `1: 1`, `2: 2`, `3: 3`, `4: 4`, `5: 5`, `6: 6`, `7: 7`, `8: 8`, `9: 9`]

沒錯,就是這麼優雅簡單。

   (2) 生成器表示式在python2.2引入,它使用`lazy evaluation`思想,因此在使用記憶體上更有效。引用python核心程式設計中計算檔案中最長的行的例子:

f = open(`/etc/motd, `r`)
longest = max(len(x.strip()) for x in f)
f.close()
return longest

這種實現簡潔而且不需要把檔案檔案所有行讀入記憶體。

       (3) python在2.4引入裝飾器,又是一個讓人興奮的特性,簡單來說它使得函式和方法封裝(接收一個函式並返回增強版本的函式)更容易閱讀、理解。`@`符號是裝飾器語法,你可以裝飾一個函式,記住呼叫結果供後續使用,這種技術被稱為memoization的,下面是用裝飾器完成一個cache功能:

import time
import hashlib
import pickle
from itertools import chain
cache = {}
def is_obsolete(entry, duration):
    return time.time() - entry[`time`] > duration

def compute_key(function, args, kw):
    #序列化/反序列化一個物件,這裡是用pickle模組對函式和引數物件進行序列化為一個hash值
    key = pickle.dumps((function.func_name, args, kw))
    #hashlib是一個提供MD5和sh1的一個庫,該結果儲存在一個全域性字典中
    return hashlib.sha1(key).hexdigest()

def memoize(duration=10):
    def _memoize(function):
        def __memoize(*args, **kw):
            key = compute_key(function, args, kw)

            # do we have it already
            if (key in cache and
                not is_obsolete(cache[key], duration)):
                print `we got a winner`
                return cache[key][`value`]

            # computing
            result = function(*args, **kw)
            # storing the result
            cache[key] = {`value`: result,-
                            `time`: time.time()}
            return result
        return __memoize
    return _memoize

@memoize()
def very_very_complex_stuff(a, b, c):
    return a + b + c

print very_very_complex_stuff(2, 2, 2)
print very_very_complex_stuff(2, 2, 2)


@memoize(1)
def very_very_complex_stuff(a, b):
    return a + b

print very_very_complex_stuff(2, 2)
time.sleep(2)
print very_very_complex_stuff(2, 2)

執行結果:

6

we got a winner

6

4

4

裝飾器在很多場景用到,比如引數檢查、鎖同步、單元測試框架等,有興趣的人可以自己進一步學習。

    3.  善用python強大的自省能力(屬性和描述符):自從使用了python,真的是驚訝原來自省可以做的這麼強大簡單,關於這個話題,限於內容比較多,這裡就不贅述,後續有時間單獨做一個總結,學習python必須對其自省好好理解。

三、 編碼小技巧:

  1. 在python3之前版本使用xrange代替range,因為range()直接返回完整的元素列表而xrange()在序列中每次呼叫只產生一個整數元素,開銷小。(在python3中xrange不再存在,裡面range提供一個可以 遍歷任意長度的範圍的iterator)
  2. if done is not None比語句if done != None更快;
  3. 儘量使用”in”操作符,簡潔而快速: for i in seq: print i
  4. `x < y < z`代替`x < y and y < z`;
  5. while 1要比while True更快, 因為前者是單步運算,後者還需要計算;
  6. 儘量使用build-in的函式,因為這些函式往往很高效,比如add(a,b)要優於a+b;
  7. 在耗時較多的迴圈中,可以把函式的呼叫改為內聯的方式,內迴圈應該保持簡潔。
  8. 使用多重賦值來swap元素:

          x, y = y, x  # elegant and efficient

             而不是:

          temp = x 
          x = y 
          y = temp 

      9. 三元操作符(python2.5後):V1 if X else V2,避免使用(X and V1) or V2,因為後者當V1=””時,就會有問題。

      10. python之switch case實現:因為switch case語法完全可用if else代替,所以python就沒  有switch case語法,但是我們可以用dictionary或lamda實現:

switch case結構:
switch (var)
{
    case v1: func1();
    case v2: func2();
    ...
    case vN: funcN();
    default: default_func();
}

dictionary實現:

values = {
           v1: func1,
           v2: func2,
           ...
           vN: funcN,
         }
values.get(var, default_func)()

lambda實現:

{
  `1`: lambda: func1,
  `2`: lambda: func2,
  `3`: lambda: func3
}[value]()

用try…catch來實現帶Default的情況,個人推薦使用dict的實現方法。

 

    這裡只總結了一部分python的實踐方法,希望這些建議可以幫助到每一位使用python的同學,優化效能不是重點,高效解決問題,讓自己寫的程式碼更加易於維護,更加pythonic!


相關文章