當前位置:
首頁 > 知識 > python編碼最佳實踐之總結

python編碼最佳實踐之總結

  




程序員大咖

點擊右側關注,免費進階高級!




 作者|xiaoqb


 https://yq.aliyun.com/articles/2758




相信用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),顯然數據的存儲方式選擇會直接影響演算法的複雜度。


數據結構的選擇



在列表中查找


對於已經排序的列表考慮用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代價很高.


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來進行,平時使用的時候多注意下,特別當列表比較大的時候,性能的影響就更大。


使用python的collections模塊替代內建容器類型


collections有三種類型:




  • deque:增強功能的類似list類型



  • defaultdict:類似dict類型



  • 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提倡簡單、優雅。所以正確的語法實踐又很有必要,這樣才會寫出優雅易於閱讀的代碼。


語法最佳實踐




字元串操作:優於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




  1. 善用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


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




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




編碼小技巧




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

x, y = y, x  

# elegant and efficient


而不是:

temp = x x = y y = temp




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



  2. 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!


【點擊成為源碼大神】

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python開發 的精彩文章:

Python裝飾器的誕生過程
製作安裝在手機上的爬蟲軟體,python在電腦端爬取小說算什麼?

TAG:Python開發 |