天天看點

Python 程式員最常犯的十個錯誤Python 程式員最常犯的十個錯誤

Python 程式員最常犯的十個錯誤Python 程式員最常犯的十個錯誤

<a></a>

在python中,我們可以為函數的某個參數設定預設值,使該參數成為可選參數。雖然這是一個很好的語言特性,但是當預設值是可變類型時,也會導緻一些令人困惑的情況。我們來看看下面這個python函數定義:

<code>&gt;&gt;&gt; def foo(bar=[]): # bar是可選參數,如果沒有提供bar的值,則預設為[],</code>

<code>... bar.append("baz") # 但是稍後我們會看到這行代碼會出現問題。</code>

<code>... return bar</code>

python程式員常犯的一個錯誤,就是想當然地認為:在每次調用函數時,如果沒有為可選參數傳入值,那麼這個可選參數就會被設定為指定的預設值。在上面的代碼中,你們可能覺得重複調用foo()函數應該會一直傳回'baz',因為你們預設每次<code>foo()</code>函數執行時(沒有指定<code>bar</code>變量的值),<code>bar</code>變量都被設定為[](也就是,一個新的空清單)。

但是,實際運作結果卻是這樣的:

<code>&gt;&gt;&gt; foo()</code>

<code>["baz"]</code>

<code>["baz", "baz"]</code>

<code>["baz", "baz", "baz"]</code>

很奇怪吧?為什麼每次調用<code>foo()</code>函數時,都會把"baz"這個預設值添加到已有的清單中,而不是重新建立一個新的空清單呢?

答案就是,可選參數預設值的設定在python中隻會被執行一次,也就是定義該函數的時候。是以,隻有當<code>foo()</code>函數被定義時,<code>bar</code>參數才會被初始化為預設值(也就是,一個空清單),但是之後每次<code>foo()</code>函數被調用時,都會繼續使用<code>bar</code>參數原先初始化生成的那個清單。

當然,一個常見的解決辦法就是:

<code>&gt;&gt;&gt; def foo(bar=none):</code>

<code>... if bar is none: # or if not bar:</code>

<code>... bar = []</code>

<code>... bar.append("baz")</code>

<code>...</code>

我們來看下面這個例子:

<code>&gt;&gt;&gt; class a(object):</code>

<code>... x = 1</code>

<code>&gt;&gt;&gt; class b(a):</code>

<code>... pass</code>

<code>&gt;&gt;&gt; class c(a):</code>

<code>&gt;&gt;&gt; print a.x, b.x, c.x</code>

<code>1 1 1</code>

這個結果很正常。

<code>&gt;&gt;&gt; b.x = 2</code>

<code>1 2 1</code>

嗯,結果和預計的一樣。

<code>&gt;&gt;&gt; a.x = 3</code>

<code>3 2 3</code>

在python語言中,類變量是以字典的形式進行處理的,并且遵循方法解析順序(method resolution order,mro)。是以,在上面的代碼中,由于類c中并沒有<code>x</code>這個屬性,解釋器将會查找它的基類(base class,盡管python支援多重繼承,但是在這個例子中,c的基類隻有a)。換句話說,c并不沒有獨立于a、真正屬于自己的<code>x</code>屬性。是以,引用<code>c.x</code>實際上就是引用了<code>a.x</code>。如果沒有處理好這裡的關系,就會導緻示例中出現的這個問題。

請看下面這段代碼:

<code>&gt;&gt;&gt; try:</code>

<code>... l = ["a", "b"]</code>

<code>... int(l[2])</code>

<code>... except valueerror, indexerror: # to catch both exceptions, right?</code>

<code>traceback (most recent call last):</code>

<code>file "&lt;stdin&gt;", line 3, in &lt;module&gt;</code>

<code>indexerror: list index out of range</code>

這段代碼的問題在于,<code>except</code>語句并不支援以這種方式指定異常。在python 2.x中,需要使用變量<code>e</code>将異常綁定至可選的第二個參數中,才能進一步檢視異常的情況。是以,在上述代碼中,<code>except</code>語句并沒有捕獲indexerror異常;而是将出現的異常綁定到了一個名為<code>indexerror</code>的參數中。

要想在<code>except</code>語句中正确地捕獲多個異常,則應将第一個參數指定為元組,然後在元組中寫下希望捕獲的異常類型。另外,為了提高可移植性,請使用<code>as</code>關鍵詞,python 2和python 3均支援這種用法。

<code>... except (valueerror, indexerror) as e:</code>

<code>&gt;&gt;&gt;</code>

python中的變量名解析遵循所謂的<code>legb</code>原則,也就是“l:本地作用域;e:上一層結構中def或lambda的本地作用域;g:全局作用域;b:内置作用域”(local,enclosing,global,builtin),按順序查找。看上去是不是很簡單?不過,事實上這個原則的生效方式還是有着一些特殊之處。說到這點,我們就不得不提下面這個常見的python程式設計錯誤。請看下面的代碼:

<code>&gt;&gt;&gt; x = 10</code>

<code>&gt;&gt;&gt; def foo():</code>

<code>... x += 1</code>

<code>... print x</code>

<code>file "&lt;stdin&gt;", line 1, in &lt;module&gt;</code>

<code>file "&lt;stdin&gt;", line 2, in foo</code>

<code>unboundlocalerror: local variable 'x' referenced before assignment</code>

出了什麼問題?

上述錯誤的出現,是因為當你在某個作用域内為變量指派時,該變量被python解釋器自動視作該作用域的本地變量,并會取代任何上一層作用域中相同名稱的變量。

正是因為這樣,才會出現一開始好好的代碼,在某個函數内部添加了一個指派語句之後卻出現了<code>unboundlocalerror</code>,難怪會讓許多人吃驚。

在使用清單時,python程式員尤其容易陷入這個圈套。

請看下面這個代碼示例:

<code>&gt;&gt;&gt; lst = [1, 2, 3]</code>

<code>&gt;&gt;&gt; def foo1():</code>

<code>... lst.append(5) # 這裡沒問題</code>

<code>&gt;&gt;&gt; foo1()</code>

<code>&gt;&gt;&gt; lst</code>

<code>[1, 2, 3, 5]</code>

<code></code>

<code>&gt;&gt;&gt; def foo2():</code>

<code>... lst += [5] # ... 但這裡就不對了!</code>

<code>&gt;&gt;&gt; foo2()</code>

<code>unboundlocalerror: local variable 'lst' referenced before assignment</code>

呃?為什麼函數<code>foo1</code>運作正常,<code>foo2</code>卻出現了錯誤?

答案與上一個示例相同,但是卻更難捉摸清楚。<code>foo1</code>函數并沒有為<code>lst</code>變量進行指派,但是<code>foo2</code>卻有指派。我們知道,<code>lst += [5]</code>隻是<code>lst = lst + [5]</code>的簡寫,從中我們就可以看出,<code>foo2</code>函數在嘗試為<code>lst</code>指派(是以,被python解釋器認為是函數本地作用域的變量)。但是,我們希望為<code>lst</code>賦的值卻又是基于<code>lst</code>變量本身(這時,也被認為是函數本地作用域内的變量),也就是說該變量還沒有被定義。這才出現了錯誤。

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

<code>&gt;&gt;&gt; odd = lambda x : bool(x % 2)</code>

<code>&gt;&gt;&gt; numbers = [n for n in range(10)]</code>

<code>&gt;&gt;&gt; for i in range(len(numbers)):</code>

<code>... if odd(numbers[i]):</code>

<code>... del numbers[i] # bad: deleting item from a list while iterating over it</code>

<code>file "&lt;stdin&gt;", line 2, in &lt;module&gt;</code>

在周遊清單或數組的同時從中删除元素,是任何經驗豐富的python開發人員都會注意的問題。但是盡管上面的示例十分明顯,資深開發人員在編寫更為複雜代碼的時候,也很可能會無意之下犯同樣的錯誤。

幸運的是,python語言融合了許多優雅的程式設計範式,如果使用得當,可以極大地簡化代碼。簡化代碼還有一個好處,就是不容易出現在周遊清單時删除元素這個錯誤。能夠做到這點的一個程式設計範式就是清單解析式。而且,清單解析式在避免這個問題方面尤其有用,下面用清單解析式重新實作上面代碼的功能:

<code>&gt;&gt;&gt; numbers[:] = [n for n in numbers if not odd(n)] # ahh, the beauty of it all</code>

<code>&gt;&gt;&gt; numbers</code>

<code>[0, 2, 4, 6, 8]</code>

<code>&gt;&gt;&gt; def create_multipliers():</code>

<code>... return [lambda x : i * x for i in range(5)]</code>

<code>&gt;&gt;&gt; for multiplier in create_multipliers():</code>

<code>... print multiplier(2)</code>

你可能覺得輸出結果應該是這樣的:

<code>0</code>

<code>2</code>

<code>4</code>

<code>6</code>

<code>8</code>

但是,實際的輸出結果卻是:

吓了一跳吧!

這個結果的出現,主要是因為python中的遲綁定late binding機制,即閉包中變量的值隻有在内部函數被調用時才會進行查詢。是以,在上面的代碼中,每次<code>create_multipliers()</code>所傳回的函數被調用時,都會在附近的作用域中查詢變量i的值(而到那時,循環已經結束,是以變量i最後被賦予的值為4)。

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

<code>... return [lambda x, i=i : i * x for i in range(5)]</code>

請注意!我們在這裡利用了預設參數來實作這個lambda匿名函數。有人可能認為這樣做很優雅,有人會覺得很巧妙,還有人會嗤之以鼻。但是,如果你是一名python程式員,不管怎樣你都應該要了解這種解決方法。

假設你有兩個檔案,分别是<code>a.py</code>和<code>b.py</code>,二者互相引用,如下所示:

<code>a.py</code>檔案中的代碼:

<code>import b</code>

<code>def f():</code>

<code>return b.x</code>

<code>print f()</code>

<code>b.py</code>檔案中的代碼:

<code>import a</code>

<code>x = 1</code>

<code>def g():</code>

<code>print a.f()</code>

首先,我們嘗試導入<code>a.py</code>子產品:

<code>&gt;&gt;&gt; import a</code>

<code>1</code>

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

答案是,僅僅存在循環引用的情況本身并不會導緻問題。如果一個子產品已經被引用了,python可以做到不再次進行引用。但是如果每個子產品試圖通路其他子產品定義的函數或變量的時機不對,那麼你就很可能陷入困境。

那麼回到我們的示例,當我們導入<code>a.py</code>子產品時,它在引用<code>b.py</code>子產品時是不會出現問題的,因為<code>b.py</code>子產品在被引用時,并不需要通路在<code>a.py</code>子產品中定義的任何變量或函數。<code>b.py</code>子產品中對a子產品唯一的引用,就是調用了a子產品的<code>foo()</code>函數。但是那個函數調用發生在<code>g()</code>函數當中,而<code>a.py</code>或<code>b.py</code>子產品中都沒有調用<code>g()</code>函數。是以,不會出現問題。

但是,如果我們試着導入<code>b.py</code>子產品呢(即之前沒有引用<code>a.py</code>子產品的前提下):

<code>&gt;&gt;&gt; import b</code>

<code>file "b.py", line 1, in &lt;module&gt;</code>

<code>file "a.py", line 6, in &lt;module&gt;</code>

<code>file "a.py", line 4, in f</code>

<code>attributeerror: 'module' object has no attribute 'x'</code>

糟糕。情況不太妙!這裡的問題是,在導入<code>b.py</code>的過程中,它試圖引用<code>a.py</code>子產品,而<code>a.py</code>子產品接着又要調用<code>foo()</code>函數,這個<code>foo()</code>函數接着又試圖去通路<code>b.x</code>變量。但是這個時候,<code>b.x</code>變量還沒有被定義,是以才出現了attributeerror異常。

解決這個問題有一種非常簡單的方法,就是簡單地修改下<code>b.py</code>子產品,在<code>g()</code>函數内部才引用<code>a.py</code>:

<code>import a # this will be evaluated only when g() is called</code>

現在我們再導入<code>b.py</code>子產品的話,就不會出現任何問題了:

<code>&gt;&gt;&gt; b.g()</code>

<code>1 # printed a first time since module 'a' calls 'print f()' at the end</code>

<code>1 # printed a second time, this one is our call to 'g'</code>

python語言的一大優勢,就是其本身自帶的強大标準庫。但是,正因為如此,如果你不去刻意注意的話,你也是有可能為自己的子產品取一個和python自帶标準庫子產品相同的名字(例如,如果你的代碼中有一個子產品叫<code>email.py</code>,那麼這就會與python标準庫中同名的子產品相沖突。)

這很可能會給你帶來難纏的問題。舉個例子,在導入子產品a的時候,假如該子產品a試圖引用python标準庫中的子產品b,但卻因為你已經有了一個同名子產品b,子產品a會錯誤地引用你自己代碼中的子產品b,而不是python标準庫中的子產品b。這也是導緻一些嚴重錯誤的原因。

是以,python程式員要格外注意,避免使用與python标準庫子產品相同的名稱。畢竟,修改自己子產品的名稱比提出pep提議修改上遊子產品名稱且讓提議通過,要來得容易的多。

假設有下面這段代碼:

<code>import sys</code>

<code>def bar(i):</code>

<code>if i == 1:</code>

<code>raise keyerror(1)</code>

<code>if i == 2:</code>

<code>raise valueerror(2)</code>

<code>def bad():</code>

<code>e = none</code>

<code>try:</code>

<code>bar(int(sys.argv[1]))</code>

<code>except keyerror as e:</code>

<code>print('key error')</code>

<code>except valueerror as e:</code>

<code>print('value error')</code>

<code>print(e)</code>

<code>bad()</code>

如果是python 2,那麼代碼運作正常:

<code>$ python foo.py 1</code>

<code>key error</code>

<code>$ python foo.py 2</code>

<code>value error</code>

但是現在,我們換成python 3再運作一遍:

<code>$ python3 foo.py 1</code>

<code>file "foo.py", line 19, in &lt;module&gt;</code>

<code>file "foo.py", line 17, in bad</code>

<code>unboundlocalerror: local variable 'e' referenced before assignment</code>

這到底是怎麼回事?這裡的“問題”是,在python 3中,異常對象在<code>except</code>代碼塊作用域之外是無法通路的。(這麼設計的原因在于,如果不這樣的話,堆棧幀中就會一直保留它的引用循環,直到垃圾回收器運作,将引用從記憶體中清除。)

避免這個問題的一種方法,就是在<code>except</code>代碼塊的作用域之外,維持一個對異常對象的引用reference,這樣異常對象就可以通路了。下面這段代碼就使用了這種方法,是以在python 2和python 3中的輸出結果是一緻的:

<code>def good():</code>

<code>exception = none</code>

<code>exception = e</code>

<code>print(exception)</code>

<code>good()</code>

在python 3下運作代碼:

<code>$ python3 foo.py 2</code>

太棒了!

假設你在<code>mod.py</code>的檔案中編寫了下面的代碼:

<code>import foo</code>

<code>class bar(object):</code>

<code>def __del__(self):</code>

<code>foo.cleanup(self.myhandle)</code>

之後,你在<code>another_mod.py</code>檔案中進行如下操作:

<code>import mod</code>

<code>mybar = mod.bar()</code>

如果你運作<code>another_mod.py</code>子產品的話,将會出現attributeerror異常。

為什麼?因為當解釋器結束運作的時候,該子產品的全局變量都會被設定為<code>none</code>。是以,在上述示例中,當<code>__del__</code>方法被調用之前,<code>foo</code>已經被設定成了<code>none</code>。

要想解決這個有點棘手的python程式設計問題,其中一個辦法就是使用<code>atexit.register()</code>方法。這樣的話,當你的程式執行完成之後(即正常退出程式的情況下),你所指定的處理程式就會在解釋器關閉之前運作。

應用了上面這種方法,修改後的<code>mod.py</code>檔案可能會是這樣子的:

<code>import atexit</code>

<code>def cleanup(handle):</code>

<code>foo.cleanup(handle)</code>

<code>def __init__(self):</code>

<code>atexit.register(cleanup, self.myhandle)</code>

這種實作支援在程式正常終止時幹淨利落地調用任何必要的清理功能。很明顯,上述示例中将會由<code>foo.cleanup</code>函數來決定如何處理<code>self.myhandle</code>所綁定的對象。

python是一門強大而又靈活的程式設計語言,提供的許多程式設計機制和範式可以極大地提高工作效率。但是與任何軟體工具或語言一樣,如果對該語言的能力了解有限或無法欣賞,那麼有時候自己反而會被阻礙,而不是受益了。正如一句諺語所說,“自以為知道夠多,但實則會給自己或别人帶來危險”knowing enough to be dangerous。(譯者注:這句諺語的意思是,自以為已經對某件事情了解足夠,但在實際去執行或實施時,卻會給自己和别人帶來危險。)

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

本文來自雲栖社群合作夥伴“linux中國”

原文釋出時間為:2013-04-02.