當前位置:
首頁 > 知識 > 程序員必知的 Python 陷阱與缺陷列表

程序員必知的 Python 陷阱與缺陷列表

(點擊

上方藍字

,快速關注我們)




來源:xybaby


www.cnblogs.com/xybaby/p/7183854.html


如有好文章投稿,請點擊 → 這裡了解詳情




我個人對陷阱的定義是這樣的:代碼看起來可以工作,但不是以你「想當然「」的方式。如果一段代碼直接出錯,拋出了異常,我不認為這是陷阱。比如,Python程序員應該都遇到過的「UnboundLocalError」, 示例:




>>>

a

=

1


>>>

def

func

()

:


...

    

a

+=

1


...

    

print

a


...

>>>

func

()


Traceback

(

most recent call

last

)

:


File

"<stdin>"

,

line

1

,

in

<

module

>

File

"<stdin>"

,

line

2

,

in

func


UnboundLocalError

:

local

variable

"a"

referenced before

assignment




對於「UnboundLocalError」,還有更高級的版本:





import

random


 


def

func

(

ok

)

:


    

if

ok

:


        

a

=

random

.

random

()


    

else

:


        

import

random


        

a

=

random

.

randint

(

1

,

10

)


    

return

a


 


func

(

True

)

# UnboundLocalError: local variable "random" referenced before assignment




可能對於很多python新手來說,這個Error讓人摸不著頭腦。但我認為這不算陷阱,因為這段代碼一定會報錯,而不是默默的以錯誤的方式運行。不怕真小人,就怕偽君子。我認為缺陷就好比偽君子。




那麼Python中哪些真正算得上陷阱呢?




第一:以mutable對象作為默認參數




這個估計是最廣為人知的了,Python和其他很多語言一樣,提供了默認參數,默認參數確實是個好東西,可以讓函數調用者忽略一些細節(比如GUI編程,Tkinter,QT),對於lambda表達式也非常有用。但是如果使用了可變對象作為默認參數,那麼事情就不那麼愉快了。





>>>

def

f

(

lst

=

[])

:


...

    

lst

.

append

(

1

)


...

    

return

lst


...


>>>

f

()


[

1

]


>>>

f

()


[

1

,

1

]




驚喜不驚喜?!究其原因,python中一切都是對象,函數也不列外,默認參數只是函數的一個屬性。而默認參數在函數定義的時候已經求值了。





  Default parameter values are evaluated when the function definition is executed.




stackoverflow上有一個更適當的例子來說明默認參數是在定義的時候求值,而不是調用的時候。 





>>>

import

time


>>>

def

report

(

when

=

time

.

time

())

:


...

return

when


...


>>>

report

()


1500113234.487932


>>>

report

()


1500113234.487932




python docoment 給出了標準的解決辦法:





  A way around this is to use None as the default, and explicitly test for it in the body of the function





>>>

def

report

(

when

=

None

)

:


...

  

if

when

is

None

:


...

  

when

=

time

.

time

()


...

return

when


...


>>>

report

()


1500113446.746997


>>>

report

()


1500113448.552873




第二: x += y vs x = x + y




一般來說,二者是等價的,至少看起來是等價的(這也是陷阱的定義 — 看起來都OK,但不一定正確)。





>>>

x

=

1

;

x

+=

1

;

print

x


2

 


>>>

x

=

1

;

x

=

x

+

1

;

print

x


2


>>>

x

=

[

1

];

x

+=

[

2

];

print

x


[

1

,

2

]


>>>

x

=

[

1

];

x

=

x

+

[

2

];

print

x


[

1

,

2

]




呃,被光速打臉了?





>>>

x

=

[

1

];

print

id

(

x

);

x

=

x

+

[

2

];

print

id

(

x

)

 


4357132800


4357132728


>>>

x

=

[

1

];

print

id

(

x

);

x

+=

[

2

];

print

id

(

x

)


4357132800


4357132800




前者x指向一個新的對象,後者x在原來的對象是修改,當然,那種效果是正確的取決於應用場景。至少,得知道,二者有時候並不一樣




 第三,神奇的小括弧–()




小括弧(parenthese)在各種編程語言中都有廣泛的應用,python中,小括弧還能表示元組(tuple)這一數據類型, 元組是immutable的序列。





>>>

a

=

(

1

,

2

)


>>>

type

(

a

)


<

type

"tuple"

>


>>>

type

(())


<

type

"tuple"

>




但如果只有一個元素呢





>>>

a

=

(

1

)


>>>

type

(

a

)


<

type

"int"

>




神奇不神奇,如果要表示只有一個元素的元組,正確的姿勢是:





>>>

a

=

(

1

,)


>>>

type

(

a

)

 


<

type

"tuple"

>




第四:生成一個元素是列表的列表




這個有點像二維數組,當然生成一個元素是字典的列表也是可以的,

更通俗的說,生成一個元素是可變對象的序列




很簡單嘛:





>>>

a

=

[[]]

*

10


>>>

a


[[],

[],

[],

[],

[],

[],

[],

[],

[],

[]]


>>>

a

[

0

].

append

(

10

)


>>>

a

[

0

]

 


[

10

]




看起來很不錯,簡單明了,but





>>>

a

[

1

]


[

10

]


>>>

a


[[

10

],

[

10

],

[

10

],

[

10

],

[

10

],

[

10

],

[

10

],

[

10

],

[

10

],

[

10

]]




我猜,這應該不是你預期的結果吧,究其原因,還是因為python中list是可變對象,上述的寫法大家都指向的同一個可變對象,正確的姿勢





>>>

a

=

[[]

for

_

in

xrange

(

10

)]


>>>

a

[

0

].

append

(

10

)


>>>

a


[[

10

],

[],

[],

[],

[],

[],

[],

[],

[],

[]]




第五,在訪問列表的時候,修改列表




列表(list)在python中使用非常廣泛,當然經常會在訪問列表的時候增加或者刪除一些元素。比如,下面這個函數,試圖刪掉列表中為3的倍數的元素:





>>>

def

modify_lst

(

lst

)

:


...

for

idx

,

elem

in

enumerate

(

lst

)

:


...

if

elem

%

3

==

0

:


...

del

lst

[

idx

]




測試一下,





>>>

lst

=

[

1

,

2

,

3

,

4

,

5

,

6

]


>>>

modify_lst

(

lst

)


>>>

lst


[

1

,

2

,

4

,

5

]




好像沒什麼錯,不過這只是運氣好





>>>

lst

=

[

1

,

2

,

3

,

6

,

5

,

4

]


>>>

modify_lst

(

lst

)


>>>

lst


[

1

,

2

,

6

,

5

,

4

]




上面的例子中,6這個元素就沒有被刪除。如果在modify_lst函數中print idx, item就可以發現端倪:lst在變短,但idx是遞增的,所以在上面出錯的例子中,當3被刪除之後,6變成了lst的第2個元素(從0開始)。在C++中,如果遍歷容器的時候用迭代器刪除元素,也會有同樣的問題。




如果邏輯比較簡單,使用list comprehension是不錯的注意




 第六,閉包與lambda




這個也是老生長談的例子,在其他語言也有類似的情況。先看一個例子:





>>>

def

create_multipliers

()

:


...

  

return

[

lambda

x

:

i

*

x

for

i

in

range

(

5

)]


...


>>>

for

multiplier

in

create_multipliers

()

:


...

print

multiplier

(

2

)


...




create_multipliers函數的返回值時一個列表,列表的每一個元素都是一個函數 -- 將輸入參數x乘以一個倍數i的函數。預期的結果時0,2,4,6,8. 但結果是5個8,意外不意外。




由於出現這個陷阱的時候經常使用了lambda,所以可能會認為是lambda的問題,但lambda表示不願意背這個鍋。問題的本質在與python中的屬性查找規則,LEGB(local,enclousing,global,bulitin),在上面的例子中,i就是在閉包作用域(enclousing),而Python的閉包是 遲綁定 , 這意味著閉包中用到的變數的值,是在內部函數被調用時查詢得到的。




 解決辦法也很簡單,那就是變閉包作用域為局部作用域。





>>>

def

create_multipliers

()

:


...

return

[

lambda

x

,

i

=

i

:

i

*

x

for

i

in

range

(

5

)]


...






第七,定義__del__




大多數計算機專業的同學可能都是先學的C、C++,構造、析構函數的概念應該都非常熟。於是,當切換到python的時候,自然也想知道有沒有相應的函數。比如,在C++中非常有名的RAII,即通過構造、析構來管理資源(如內存、文件描述符)的聲明周期。那在python中要達到同樣的效果怎麼做呢,即需要找到一個對象在銷毀的時候一定會調用的函數,於是發現了__init__, __del__函數,可能簡單寫了兩個例子發現確實也能工作。但事實上可能掉進了一個陷阱,在python documnet是有描述的:





  Circular references which are garbage are detected when the option cycle detector is enabled (it』s on by default), but can only be cleaned up if there are no Python-level __del__() methods involved.




簡單來說,如果在循環引用中的對象定義了__del__,那麼python gc不能進行回收,因此,存在內存泄漏的風險




第八,不同的姿勢import同一個module




示例在stackoverflow的例子上稍作修改,假設現在有一個package叫mypackage,裡面包含三個python文件:mymodule.py, main.py, __init__.py。mymodule.py代碼如下:





l

=

[]


class

A

(

object

)

:


    pass




main.py代碼如下:





def

add

(

x

)

:


    

from

mypackage

import

mymodule


    

mymodule

.

l

.

append

(

x

)


    

print

"updated list"

,

mymodule

.

l

,

id

(

mymodule

)


 


def

get

()

:


    

import

mymodule


    print

"module in get"

,

id

(

mymodule

)


    

return

mymodule

.

l


 


if

__name__

==

"__main__"

:


    

import

sys


    

sys

.

path

.

append

(

"../"

)


    

add

(

1

)


    


    

ret

=

get

()


    

print

"lets check"

,

ret




運行python main.py,結果如下:





updated list [1] 4406700752


module in get 4406700920


lets check []




 從運行結果可以看到,在add 和 get函數中import的mymodule不是同一個module,ID不同。當然,在python2.7.10中,需要main.py的第13行才能出現這樣的效果。你可能會問,誰會寫出第13行這樣的代碼呢?事實上,在很多項目中,為了import的時候方便,會往sys.path加入一堆路徑。那麼在項目中,大家同意一種import方式就非常有必要了




第九,python升級




python3.x並不向後兼容,所以如果從2.x升級到3.x的時候得小心了,下面列舉兩點:




在python2.7中,range的返回值是一個列表;而在python3.x中,返回的是一個range對象。




map()、filter()、 dict.items()在python2.7返回列表,而在3.x中返回迭代器。當然迭代器大多數都是比較好的選擇,更加pythonic,但是也有缺點,就是只能遍歷一次。在instagram的分享中,也提到因為這個導致的一個坑爹的bug。




 第十,gil




以GIL結尾,因為gil是Python中大家公認的缺陷!




從其他語言過來的同學可能看到python用threading模塊,拿過來就用,結果發現效果不對啊,然後就會噴,什麼鬼。




總結:




毫無疑問的說,python是非常容易上手,也非常強大的一門語言。python非常靈活,可定製化很強。同時,也存在一些陷阱,搞清楚這些陷阱能夠更好的掌握、使用這麼語言。本文列舉了一些python中的一些缺陷,這是一份不完全列表,歡迎大家補充。




看完本文有收穫?請轉

發分享給更多人


關注「P

ython開發者」,提升Python技能


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

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


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

GAFT:一個使用 Python 實現的遺傳演算法框架
優秀的程序員都有哪些習慣?
Python 中的非同步編程:Asyncio
機器學習演算法實踐:Logistic 回歸與梯度上升演算法
愛學習的程序員都關注了這些

TAG:Python開發者 |

您可能感興趣

Gucci Sylvie真偽測評,帶你躲開假貨陷阱
Kubernetes監控方面要避免的四個常見陷阱
十道 Python 面試問題陷阱
Shell 腳本編程陷阱
流浪vagante洞窟陷阱躲避技巧
PyTorch經驗指南:技巧與陷阱
iPhone X挑戰超大陷阱夾 不知道它會不會疼
China Insight 中美兩國應如何避免修昔底德陷阱?
Mata心思縝密,開局戰術至少三處陷阱,只為一級幹掉Iboy
買switch避免陷阱,內部爆料黑店手段
Google Play在韓國遭調查,被指濫用市場地位;新華社評趙薇夫婦被罰:資本市場不容套路陷阱
流媒體巨頭 Netflix,躲不掉的內容陷阱
垃圾軟體攻佔APP Store教育榜首,這些陷阱家長要防範
歡喜為女兒網購iPhone 8 Plus,為何卻掉入陷阱?
美聯儲Bullard:政策評估應以避免通縮陷阱為目標
黃子韜《Beggar》MV首發,是浪漫邂逅還是甜蜜陷阱?
網購iPhone容易進入的幾處陷阱,其中它最重要!
生活哪有easy模式,不過都是溫柔陷阱
大規模MySQL運維陷阱:使用MyCat踩坑篇
iPhone套路太深,這些陷阱你知多少?網友:還是華為良心