Python 程序員最常犯的十個錯誤,做爲小白的你是否是也常常犯?

常見錯誤1:錯誤地將表達式做爲函數的默認參數python

在Python中,咱們能夠爲函數的某個參數設置默認值,使該參數成爲可選參數。雖然這是一個很好的語言特性,可是當默認值是可變類型時,也會致使一些使人困惑的狀況。咱們來看看下面這個Python函數定義:程序員

>>> def foo(bar=[]): # bar是可選參數,若是沒有提供bar的值,則默認爲[],
... bar.append("baz") # 可是稍後咱們會看到這行代碼會出現問題。
... return bar

 

Python程序員常犯的一個錯誤,就是想固然地認爲:在每次調用函數時,若是沒有爲可選參數傳入值,那麼這個可選參數就會被設置爲指定的默認值。在上面的代碼中,大家可能以爲重複調用foo()函數應該會一直返回’baz’,由於大家默認每次foo()函數執行時(沒有指定bar變量的值),bar變量都被設置爲[](也就是,一個新的空列表)。編程

Python 程序員最常犯的十個錯誤,做爲小白的你是否是也常常犯?

 

可是,實際運行結果倒是這樣的:數組

>>> foo()
["baz"]
>>> foo()
["baz", "baz"]
>>> foo()
["baz", "baz", "baz"]

很奇怪吧?爲何每次調用foo()函數時,都會把」baz」這個默認值添加到已有的列表中,而不是從新建立一個新的空列表呢?閉包

答案就是,可選參數默認值的設置在Python中只會被執行一次,也就是定義該函數的時候。所以,只有當foo()函數被定義時,bar參數纔會被初始化爲默認值(也就是,一個空列表),可是以後每次foo()函數被調用時,都會繼續使用bar參數原先初始化生成的那個列表。app

固然,一個常見的解決辦法就是:編程語言

>>> def foo(bar=None):
... if bar is None: # or if not bar:
... bar = []
... bar.append("baz")
... return bar
...
>>> foo()
["baz"]
>>> foo()
["baz"]
>>> foo()
["baz"]

常見問題2:錯誤地使用類變量函數

咱們來看下面這個例子:工具

>>> class A(object):
... x = 1
...
>>> class B(A):
... pass
...
>>> class C(A):
... pass
...
>>> print A.x, B.x, C.x
1 1 1

這個結果很正常。ui

>>> B.x = 2
>>> print A.x, B.x, C.x
1 2 1

嗯,結果和預計的同樣。

>>> A.x = 3
>>> print A.x, B.x, C.x
3 2 3

在Python語言中,類變量是以字典的形式進行處理的,而且遵循方法解析順序(Method Resolution Order,MRO)。所以,在上面的代碼中,因爲類C中並無x這個屬性,解釋器將會查找它的基類(base class,儘管Python支持多重繼承,可是在這個例子中,C的基類只有A)。換句話說,C並不沒有獨立於A、真正屬於本身的x屬性。因此,引用C.x實際上就是引用了A.x。若是沒有處理好這裏的關係,就會致使示例中出現的這個問題。

常見錯誤3:錯誤地指定異常代碼塊(exception block)的參數

請看下面這段代碼:

>>> try:
... l = ["a", "b"]
... int(l[2])
... except ValueError, IndexError: # To catch both exceptions, right?
... pass
...
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
IndexError: list index out of range

這段代碼的問題在於,except語句並不支持以這種方式指定異常。在Python 2.x中,須要使用變量e將異常綁定至可選的第二個參數中,才能進一步查看異常的狀況。所以,在上述代碼中,except語句並無捕獲IndexError異常;而是將出現的異常綁定到了一個名爲IndexError的參數中。

要想在except語句中正確地捕獲多個異常,則應將第一個參數指定爲元組,而後在元組中寫下但願捕獲的異常類型。另外,爲了提升可移植性,請使用as關鍵詞,Python 2和Python 3均支持這種用法。

>>> try:
... l = ["a", "b"]
... int(l[2])
... except (ValueError, IndexError) as e:
... pass
...
>>>

常見錯誤4:錯誤理解Python中的變量名解析

Python中的變量名解析遵循所謂的LEGB原則,也就是「L:本地做用域;E:上一層結構中def或lambda的本地做用域;G:全局做用域;B:內置做用域」(Local,Enclosing,Global,Builtin),按順序查找。看上去是否是很簡單?不過,事實上這個原則的生效方式仍是有着一些特殊之處。說到這點,咱們就不得不提下面這個常見的Python編程錯誤。請看下面的代碼:

>>> x = 10
>>> def foo():
... x += 1
... print x
...
>>> foo()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'x' referenced before assignment

出了什麼問題?

上述錯誤的出現,是由於當你在某個做用域內爲變量賦值時,該變量被Python解釋器自動視做該做用域的本地變量,並會取代任何上一層做用域中相同名稱的變量。

正是由於這樣,纔會出現一開始好好的代碼,在某個函數內部添加了一個賦值語句以後卻出現了UnboundLocalError,難怪會讓許多人吃驚。

在使用列表時,Python程序員尤爲容易陷入這個圈套。

請看下面這個代碼示例:

>>> lst = [1, 2, 3]
>>> def foo1():
... lst.append(5) # 這裏沒問題
...
>>> foo1()
>>> lst
[1, 2, 3, 5]
>>> lst = [1, 2, 3]
>>> def foo2():
... lst += [5] # ... 但這裏就不對了!
...
>>> foo2()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'lst' referenced before assignment

呃?爲何函數foo1運行正常,foo2卻出現了錯誤?

答案與上一個示例相同,可是卻更難捉摸清楚。foo1函數並無爲lst變量進行賦值,可是foo2卻有賦值。咱們知道,lst += [5]只是lst = lst + [5]的簡寫,從中咱們就能夠看出,foo2函數在嘗試爲lst賦值(所以,被Python解釋器認爲是函數本地做用域的變量)。可是,咱們但願爲lst賦的值卻又是基於lst變量自己(這時,也被認爲是函數本地做用域內的變量),也就是說該變量尚未被定義。這纔出現了錯誤。

常見錯誤5:在遍歷列表時更改列表

下面這段代碼的問題應該算是十分明顯:

>>> odd = lambda x : bool(x % 2)
>>> numbers = [n for n in range(10)]
>>> for i in range(len(numbers)):
... if odd(numbers[i]):
... del numbers[i] # BAD: Deleting item from a list while iterating over it
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
IndexError: list index out of range

在遍歷列表或數組的同時從中刪除元素,是任何經驗豐富的Python開發人員都會注意的問題。可是儘管上面的示例十分明顯,資深開發人員在編寫更爲複雜代碼的時候,也極可能會無心之下犯一樣的錯誤。

幸運的是,Python語言融合了許多優雅的編程範式,若是使用得當,能夠極大地簡化代碼。簡化代碼還有一個好處,就是不容易出如今遍歷列表時刪除元素這個錯誤。可以作到這點的一個編程範式就是列表解析式。並且,列表解析式在避免這個問題方面尤爲有用,下面用列表解析式從新實現上面代碼的功能:

>>> odd = lambda x : bool(x % 2)
>>> numbers = [n for n in range(10)]
>>> numbers[:] = [n for n in numbers if not odd(n)] # ahh, the beauty of it all
>>> numbers
[0, 2, 4, 6, 8]

常見錯誤6:不理解Python在閉包中如何綁定變量

請看下面這段代碼:

>>> def create_multipliers():
... return [lambda x : i * x for i in range(5)]
>>> for multiplier in create_multipliers():
... print multiplier(2)
...

你可能以爲輸出結果應該是這樣的:

0
2
4
6
8

可是,實際的輸出結果倒是:

8
8
8
8
8

嚇了一跳吧!

這個結果的出現,主要是由於Python中的遲綁定(late binding)機制,即閉包中變量的值只有在內部函數被調用時纔會進行查詢。所以,在上面的代碼中,每次create_multipliers()所返回的函數被調用時,都會在附近的做用域中查詢變量i的值(而到那時,循環已經結束,因此變量i最後被賦予的值爲4)。

要解決這個常見Python問題的方法中,須要使用一些hack技巧:

>>> def create_multipliers():
... return [lambda x, i=i : i * x for i in range(5)]
...
>>> for multiplier in create_multipliers():
... print multiplier(2)
...
0
2
4
6
8

請注意!咱們在這裏利用了默認參數來實現這個lambda匿名函數。有人可能認爲這樣作很優雅,有人會以爲很巧妙,還有人會嗤之以鼻。可是,若是你是一名Python程序員,無論怎樣你都應該要了解這種解決方法。

常見錯誤7:模塊之間出現循環依賴(circular dependencies)

假設你有兩個文件,分別是a.py和b.py,兩者相互引用,以下所示:

a.py文件中的代碼:

import b
def f():
return b.x
print f()

b.py文件中的代碼:

import a
x = 1
def g():
print a.f()

首先,咱們嘗試導入a.py模塊:

>>> import a
1

代碼運行正常。也許這出乎了你的意料。畢竟,咱們這裏存在循環引用這個問題,想必應該是會出現問題的,難道不是嗎?

答案是,僅僅存在循環引用的狀況自己並不會致使問題。若是一個模塊已經被引用了,Python能夠作到再也不次進行引用。可是若是每一個模塊試圖訪問其餘模塊定義的函數或變量的時機不對,那麼你就極可能陷入困境。

那麼回到咱們的示例,當咱們導入a.py模塊時,它在引用b.py模塊時是不會出現問題的,由於b.py模塊在被引用時,並不須要訪問在a.py模塊中定義的任何變量或函數。b.py模塊中對a模塊惟一的引用,就是調用了a模塊的foo()函數。可是那個函數調用發生在g()函數當中,而a.py或b.py模塊中都沒有調用g()函數。因此,不會出現問題。

可是,若是咱們試着導入b.py模塊呢(即以前沒有引用a.py模塊的前提下):

>>> import b
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "b.py", line 1, in <module>
import a
File "a.py", line 6, in <module>
print f()
File "a.py", line 4, in f
return b.x
AttributeError: 'module' object has no attribute 'x'

糟糕。狀況不太妙!這裏的問題是,在導入b.py的過程當中,它試圖引用a.py模塊,而a.py模塊接着又要調用foo()函數,這個foo()函數接着又試圖去訪問b.x變量。可是這個時候,b.x變量尚未被定義,因此纔出現了AttributeError異常。

解決這個問題有一種很是簡單的方法,就是簡單地修改下b.py模塊,在g()函數內部才引用a.py:

x = 1
def g():
import a # This will be evaluated only when g() is called
print a.f()

如今咱們再導入b.py模塊的話,就不會出現任何問題了:

>>> import b
>>> b.g()
1 # Printed a first time since module 'a' calls 'print f()' at the end
1 # Printed a second time, this one is our call to 'g'

常見錯誤8:模塊命名與Python標準庫模塊名衝突

Python語言的一大優點,就是其自己自帶的強大標準庫。可是,正由於如此,若是你不去刻意注意的話,你也是有可能爲本身的模塊取一個和Python自帶標準庫模塊相同的名字(例如,若是你的代碼中有一個模塊叫email.py,那麼這就會與Python標準庫中同名的模塊相沖突。)

這極可能會給你帶來難纏的問題。舉個例子,在導入模塊A的時候,假如該模塊A試圖引用Python標準庫中的模塊B,但卻由於你已經有了一個同名模塊B,模塊A會錯誤地引用你本身代碼中的模塊B,而不是Python標準庫中的模塊B。這也是致使一些嚴重錯誤的緣由。

所以,Python程序員要格外注意,避免使用與Python標準庫模塊相同的名稱。畢竟,修改本身模塊的名稱比提出PEP提議修改上游模塊名稱且讓提議經過,要來得容易的多。

常見錯誤9:未能解決Python 2與Python 3之間的差別

假設有下面這段代碼:

import sys
def bar(i):
if i == 1:
raise KeyError(1)
if i == 2:
raise ValueError(2)
def bad():
e = None
try:
bar(int(sys.argv[1]))
except KeyError as e:
print('key error')
except ValueError as e:
print('value error')
print(e)
bad()

若是是Python 2,那麼代碼運行正常:

$ python foo.py 1
key error
1
$ python foo.py 2
value error
2

可是如今,咱們換成Python 3再運行一遍:

$ python3 foo.py 1
key error
Traceback (most recent call last):
File "foo.py", line 19, in <module>
bad()
File "foo.py", line 17, in bad
print(e)
UnboundLocalError: local variable 'e' referenced before assignment

這究竟是怎麼回事?這裏的「問題」是,在Python 3中,異常對象在except代碼塊做用域以外是沒法訪問的。(這麼設計的緣由在於,若是不這樣的話,堆棧幀中就會一直保留它的引用循環,直到垃圾回收器運行,將引用從內存中清除。)

避免這個問題的一種方法,就是在except代碼塊的做用域以外,維持一個對異常對象的引用(reference),這樣異常對象就能夠訪問了。下面這段代碼就使用了這種方法,所以在Python 2和Python 3中的輸出結果是一致的:

import sys
def bar(i):
if i == 1:
raise KeyError(1)
if i == 2:
raise ValueError(2)
def good():
exception = None
try:
bar(int(sys.argv[1]))
except KeyError as e:
exception = e
print('key error')
except ValueError as e:
exception = e
print('value error')
print(exception)
good()

在Python 3下運行代碼:

$ python3 foo.py 1
key error
1
$ python3 foo.py 2
value error
2

太棒了!

常見錯誤10:錯誤使用del方法

假設你在mod.py的文件中編寫了下面的代碼:

import foo
class Bar(object):
...
def __del__(self):
foo.cleanup(self.myhandle)

以後,你在another_mod.py文件中進行以下操做:

import mod
mybar = mod.Bar()

若是你運行another_mod.py模塊的話,將會出現AttributeError異常。

爲何?由於當解釋器結束運行的時候,該模塊的全局變量都會被設置爲None。所以,在上述示例中,當__del__方法被調用以前,foo已經被設置成了None。

要想解決這個有點棘手的Python編程問題,其中一個辦法就是使用atexit.register()方法。這樣的話,當你的程序執行完成以後(即正常退出程序的狀況下),你所指定的處理程序就會在解釋器關閉以前運行。

應用了上面這種方法,修改後的mod.py文件可能會是這樣子的:

import foo
import atexit
def cleanup(handle):
foo.cleanup(handle)
class Bar(object):
def __init__(self):
...
atexit.register(cleanup, self.myhandle)

這種實現支持在程序正常終止時乾淨利落地調用任何須要的清理功能。很明顯,上述示例中將會由foo.cleanup函數來決定如何處理self.myhandle所綁定的對象。

綜述

Python是一門強大而又靈活的編程語言,提供的許多編程機制和範式能夠極大地提升工做效率。可是與任何軟件工具或語言同樣,若是對該語言的能力理解有限或沒法欣賞,那麼有時候本身反而會被阻礙,而不是受益了。正如一句諺語所說,「自覺得知道夠多,但實則會給本身或別人帶來危險」(knowing enough to be dangerous)。(譯者注:這句諺語的意思是,自覺得已經對某件事情瞭解足夠,但在實際去執行或實施時,卻會給本身和別人帶來危險。)

不斷地熟悉Python語言的一些細微之處,尤爲是本文中提到的10大常見錯誤,將會幫助你有效地使用這門語言,同時也能避免犯一些比較常見的錯誤。

相關文章
相關標籤/搜索