1. 程式人生 > >Python注意事項和誤區

Python注意事項和誤區

Python是一種解釋性、面向物件並具有動態語義的高階程式語言。它內建了高階的資料結構,結合了動態型別和動態繫結的優點,這使得它在快速應用開發中非常有吸引力,並且可作為指令碼或膠水語言來連線現有的元件或服務。Python支援模組和包,從而鼓勵了程式的模組化和程式碼重用。

關於這篇文章

Python簡單易學的語法可能會使Python開發者–尤其是那些程式設計的初學者–忽視了它的一些微妙的地方並低估了這門語言的能力。

有鑑於此,本文列出了一個“10強”名單,枚舉了甚至是高階Python開發人員有時也難以捕捉的錯誤。

常見錯誤 1: 濫用表示式作為函式引數的預設值

Python允許為函式的引數提供預設的可選值。儘管這是語言的一大特色,但是它可能會導致一些易變預設值的混亂。例如,看一下這個Python函式的定義:

>>> def foo(bar=[]):        # bar is optional and defaults to [] if not specified  
...    bar.append("baz")    # but this line could be problematic, as we'll see...  
...    return ba

一個常見的錯誤是認為在函式每次不提供可選引數呼叫時可選引數將設定為預設指定值。在上面的程式碼中,例如,人們可能會希望反覆(即不明確指定bar引數)地呼叫foo()時總返回’baz’,由於每次foo()呼叫時都假定(不設定bar引數)bar被設定為[](即一個空列表)。

但是讓我們看一下這樣做時究竟會發生什麼:

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

耶?為什麼每次foo()呼叫時都要把預設值"baz"追加到現有列表中而不是建立一個新的列表呢?

答案是函式引數的預設值只會評估使用一次—在函式定義的時候。因此,bar引數在初始化時為其預設值(即一個空列表),即foo()首次定義的時候,但當呼叫foo()時(即,不指定bar引數時)將繼續使用bar原本已經初始化的引數。

下面是一個常見的解決方法:

>>> 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

常規用一下。

>>> 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

我們只改了A.x,為什麼C.x也改了?

在python中,類變數在內部當做字典來處理,其遵循常被引用的方法解析順序(MRO)。所以在上面的程式碼中,由於class C中的x屬性沒有找到,它會向上找它的基類(儘管Python支援多重繼承,但上面的例子中只有A)。換句話說,class C中沒有它自己的x屬性,其獨立於A。因此,C.x事實上是A.x的引用。

常見錯誤 3: 為 except 指定錯誤的引數

假設你有如下一段程式碼:

>>> 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中,使用語法 except Exception, e 是將一個異常物件繫結到第二個可選引數(在這個例子中是 e)上,以便在後面使用。所以,在上面這個例子中,IndexError 這個異常並不是被except語句捕捉到的,而是被繫結到一個名叫 IndexError的引數上時引發的。

在一個except語句中捕獲多個異常的正確做法是將第一個引數指定為一個含有所有要捕獲異常的元組。並且,為了程式碼的可移植性,要使用as關鍵詞,因為Python 2 和Python 3都支援這種語法:

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

常見錯誤 4: 不理解Python的作用域

Python是基於 LEGB 來進行作用於解析的, LEGB 是 Local, Enclosing, Global, Built-in 的縮寫。看起來“見文知意”,對嗎?實際上,在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 的錯誤。 (你可以在這裡瞭解到更多)

尤其是當開發者使用 lists 時,這個問題就更加常見. 請看下面這個例子:

>>> 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

嗯?為什麼 foo2 報錯,而foo1沒有問題呢?

原因和之前那個例子的一樣,不過更加令人難以捉摸。foo1 沒有對 lst 進行賦值操作,而 foo2 做了。要知道, lst += [5] 是 lst = lst + [5] 的縮寫,我們試圖對 lst 進行賦值操作(Python把他當成了局部變數)。此外,我們對 lst 進行的賦值操作是基於 lst 自身(這再一次被Python當成了局部變數),但此時還未定義。因此出錯!

常見錯誤 5:當迭代時修改一個列表(List)

下面程式碼中的問題應該是相當明顯的:

>>> 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 “”, line 2, in

IndexError: list index out of range

當迭代的時候,從一個 列表 (List)或者陣列中刪除元素,對於任何有經驗的開發者來說,這是一個眾所周知的錯誤。儘管上面的例子非常明顯,但是許多高階開發者在更復雜的程式碼中也並非是故意而為之的。

幸運的是,Python包含大量簡潔優雅的程式設計範例,若使用得當,能大大簡化和精煉程式碼。這樣的好處是能得到更簡化和更精簡的程式碼,能更好的避免程式中出現當迭代時修改一個列表(List)這樣的bug。一個這樣的範例是遞推式列表(list comprehensions)。而且,遞推式列表(list comprehensions)針對這個問題是特別有用的,通過更改上文中的實現,得到一段極佳的程式碼:

>>> 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中的“後期繫結”行為——閉包中用到的變數只有在函式被呼叫的時候才會被賦值。所以,在上面的程式碼中,任何時候,當返回的函式被呼叫時,Python會在該函式被呼叫時的作用域中查詢 i 對應的值(這時,迴圈已經結束,所以 i 被賦上了最終的值——4)。

解決的方法有一點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

在這裡,我們利用了預設引數來生成一個匿名的函式以便實現我們想要的結果。有人說這個方法很巧妙,有人說它難以理解,還有人討厭這種做法。但是,如果你是一個 Python 開發者,理解這種行為很重要。

你定義一個函式,函式內的變數並不是立刻就把值綁定了,而是等呼叫的時候再查詢這個變數,如圖,定義函式的時候沒有 foo 變數,但是仍然可以,只要呼叫的時候環境裡有就行。

一個道理,在 for 裡面 i 的值是不斷改寫的,但是 lambda 裡面只是儲存了 i 的符號,呼叫的時候再查詢。這就是你說的後期繫結。

為什麼你加了預設引數就成功了呢?因為在建立函式的時候就要獲取預設引數的值,放到 lambda 的環境中,所以這裡相當於存在一個賦值,從而 lambda 函式環境中有了一個獨立的 i。

最後,優雅的寫法是用生成器:

for multiplier in (lambda x : i * x for i in range(5)):
    print(multiplier(2))

這樣惰性求值就可以避免 i 的改寫。
或者:

def create_multipliers():
    for i in range(5):
        yield lambda x: i * x
 
for multiplier in create_multipliers():
    print(multiplier(2))

常見錯誤 7: 建立迴圈依賴模組

讓我們假設你有兩個檔案,a.pyb.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中,僅僅引入一個迴圈依賴的模組是沒有問題的。如果一個模組已經被引入了,Python並不會去再次引入它。但是,根據每個模組要訪問其他模組中的函式和變數位置的不同,就很可能會遇到問題。

所以,回到我們這個例子,當我們引入 a.py 時,再引入 b.py 不會產生任何問題,因為當引入的時候,b.py 不需要 a.py 中定義任何東西。b.py 中唯一引用 a.py 中的東西是呼叫 a.f()。 但是那個呼叫是發生在g() 中的,並且 a.pyb.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的過程中,Python嘗試去引入 a.py但是a.py 要呼叫f(),而f() 有嘗試去訪問 b.x。但是此時 b.x 還沒有被定義呢。所以發生了 AttributeError 異常。

至少,解決這個問題很簡單,只需修改b.py,使其在g()中引入 a.py

x = 1 
def g():  
    import a    # 只有當g()被呼叫的時候才會引入a  
    print a.f()

現在,當我們再引入b,沒有任何問題:

>>> 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標準庫中的模組,由於你定義了一個同名的模組,就會使該模組錯誤的引入了你的模組,而不是 stdlib 中的模組。這就會出問題了。

因此,我們必須要注意這個問題,以避免使用和Python標準庫中相同的模組名。修改你包中的模組名要比通過 Python Enhancement Proposal (PEP) 給Python提建議來修改標準庫的模組名容易多了。

常見錯誤 #9: 未能解決Python 2和Python 3之間的差異

請看下面這個 filefoo.py:

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

bad()

File “foo.py”, line 17, in bad

print(e)

UnboundLocalError: local variable ‘e’ referenced before assignment

出什麼問題了? “問題”就是,在 Python 3 中,異常的物件在 except 程式碼塊之外是不可見的。(這樣做的原因是,它將儲存一個對記憶體中堆疊幀的引用週期,直到垃圾回收器執行並且從記憶體中清除掉引用。瞭解更多技術細節請參考這裡) 。

一種解決辦法是在 except 程式碼塊的外部作用域中定義一個對異常物件的引用,以便訪問。下面的例子使用了該方法,因此最後的程式碼可以在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()

在Py3k中執行:

$ python3 foo.py 1

key error

1

$ python3 foo.py 2

value error

2

正常!

(順便提一下, 我們的 Python Hiring Guide 討論了當我們把程式碼從Python 2 遷移到 Python 3時的其他一些需要知道的重要差異。)

常見錯誤 10: 誤用__del__方法

假設你有一個名為 calledmod.py 的檔案:

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

並且有一個名為 another_mod.py 的檔案:

import mod

mybar = mod.Bar()

你會得到一個 AttributeError 的異常。

為什麼呢?因為,正如這裡所說,當直譯器退出的時候,模組中的全域性變數都被設定成了 None。所以,在上面這個例子中,當 del 被呼叫時,foo 已經被設定成了None。

解決方法是使用 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的一些關鍵的細微之處,像本文中所提到的那些(但不限於這些),可以幫助我們更好的去使用語言,從而避免一些常見的陷阱。

問題一:以下的程式碼的輸出將是什麼? 說出你的答案並解釋。

class Parent(object):
    x = 1
 
class Child1(Parent):
    pass
 
class Child2(Parent):
    pass
 
print Parent.x, Child1.x, Child2.x
Child1.x = 2
print Parent.x, Child1.x, Child2.x
Parent.x = 3
print Parent.x, Child1.x, Child2.x

答案

以上程式碼的輸出是:

1
1
1

1
2
1

3
2
3

使你困惑或是驚奇的是關於最後一行的輸出是 3 2 3 而不是 3 2 1。為什麼改變了 Parent.x的值還會改變 Child2.x 的值,但是同時 Child1.x 值卻沒有改變?

這個答案的關鍵是,在 Python 中,類變數在內部是作為字典處理的。如果一個變數的名字沒有在當前類的字典中發現,將搜尋祖先類(比如父類)直到被引用的變數名被找到(如果這個被引用的變數名既沒有在自己所在的類又沒有在祖先類中找到,會引發一個 AttributeError 異常 )。

因此,在父類中設定 x = 1 會使得類變數 X 在引用該類和其任何子類中的值為 1。這就是因為第一個 print 語句的輸出是 1 1 1。

隨後,如果任何它的子類重寫了該值(例如,我們執行語句 Child1.x = 2),然後,該值僅僅在子類中被改變。這就是為什麼第二個 print 語句的輸出是 1 2 1。

最後,如果該值在父類中被改變(例如,我們執行語句 Parent.x = 3),這個改變會影響到任何未重寫該值的子類當中的值(在這個示例中被影響的子類是 Child2)。這就是為什麼第三個 print 輸出是 3 2 3。

問題二:以下的程式碼的輸出將是什麼? 說出你的答案並解釋?

def div1(x,y):
    print("%s/%s = %s" % (x, y, x/y))
 
def div2(x,y):
    print("%s//%s = %s" % (x, y, x//y))
 
div1(5,2)
div1(5.,2)
div2(5,2)
div2(5.,2.)

答案

這個答案實際依賴於你使用的是 Python 2 還是 Python 3。

在 Python 3 中,期望的輸出是:

5/2
=
2.5

5.0/2
=
2.5

5//2
 = 2

5.0//2.0
 = 2.0

在 Python 2 中,儘管如此,以上程式碼的輸出將是:

5/2 = 2
5.0/2 = 2.5
5//2 = 2
5.0//2.0 = 2.0

預設,如果兩個運算元都是整數,Python 2 自動執行整型計算。結果,5/2 值為 2,然而 5./2 值為 ```2.5``。注意,儘管如此,你可以在 Python 2 中過載這一行為(比如達到你想在 Python 3 中的同樣結果),通過新增以下匯入:

from __future__ import division

也需要注意的是“雙劃線”(//)操作符將一直執行整除,而不管運算元的型別,這就是為什麼 5.0//2.0 值為 2.0。注: 在 Python 3 中,/ 操作符是做浮點除法,而 // 是做整除(即商沒有餘數,比如 10 // 3 其結果就為 3,餘數會被截除掉,而 (-7) // 3 的結果卻是 -3。這個演算法與其它很多程式語言不一樣,需要注意,它們的整除運算會向0的方向取值。而在 Python 2 中,/ 就是整除,即和 Python 3 中的 // 操作符一樣,)

問題三:以下程式碼將輸出什麼?

list = ['a', 'b', 'c', 'd', 'e']
print list[10:]

答案

以上程式碼將輸出 [],並且不會導致一個 IndexError。

正如人們所期望的,試圖訪問一個超過列表索引值的成員將導致 IndexError(比如訪問以上列表的 list[10])。儘管如此,試圖訪問一個列表的以超出列表成員數作為開始索引的切片將不會導致 IndexError,並且將僅僅返回一個空列表。

一個討厭的小問題是它會導致出現 bug ,並且這個問題是難以追蹤的,因為它在執行時不會引發錯誤。

問題四:以下的程式碼的輸出將是什麼? 說出你的答案並解釋?

def
multipliers():
    return[lambdax:i *xfor i in range(4)]
    
print[m(2) for m in multipliers()]

你將如何修改 multipliers 的定義來產生期望的結果

答案

以上程式碼的輸出是 [6, 6, 6, 6] (而不是 [0, 2, 4, 6])。

這個的原因是 Python 的閉包的後期繫結導致的 late binding,這意味著在閉包中的變數是在內部函式被呼叫的時候被查詢。所以結果是,當任何 multipliers() 返回的函式被呼叫,在那時,i 的值是在它被呼叫時的周圍作用域中查詢,到那時,無論哪個返回的函式被呼叫,for 迴圈都已經完成了,i 最後的值是 3,因此,每個返回的函式 multiplies 的值都是 3。因此一個等於 2 的值被傳遞進以上程式碼,它們將返回一個值 6 (比如: 3 x 2)。

(順便說下,正如在 The Hitchhiker’s Guide to Python 中指出的,這裡有一點普遍的誤解,是關於 lambda 表示式的一些東西。一個 lambda 表示式建立的函式不是特殊的,和使用一個普通的 def 建立的函式展示的表現是一樣的。)

這裡有兩種方法解決這個問題。

最普遍的解決方案是建立一個閉包,通過使用預設引數立即繫結它的引數。例如:

def multipliers():
    return [lambda x, i=i : i * x for i in range(4)]

另外一個選擇是,你可以使用 functools.partial 函式:

from functools import p