“Python有什麼好學的”這句話可不是反問句,而是問句哦。
主要是煎魚覺得太多的人覺得Python的語法較為簡單,寫出來的程式碼只要符合邏輯,不需要太多的學習即可,即可從一門其他語言跳來用Python寫(當然這樣是好事,誰都希望入門簡單)。
於是我便記錄一下,如果要學Python的話,到底有什麼好學的。記錄一下Python有什麼值得學的,對比其他語言有什麼特別的地方,有什麼樣的程式碼寫出來更Pythonic。一路回味,一路學習。
引上下文管理器
太極生兩儀,兩儀為陰陽。
道有陰陽,月有陰晴,人有生死,門有開關。
你看這個門,它能開能關,就像這個物件,它能建立能釋放。(扯遠了
程式設計這行,幾十年來都繞不開記憶體洩露這個問題。記憶體洩露的根本原因,就是把某個物件建立了,但是卻沒有去釋放它。直到程式結束前那一刻,這個未被釋放的物件還一直佔著記憶體,即使程式已經不用這個物件了。洩露的量少的話還好,量大的話就直接打滿記憶體,然後程式就被kill了。
聰明的程式設計師經過了這十幾年的努力,創造出很多高階程式語言,這些程式語言已經不再需要讓程式設計師過度關注記憶體的問題了。但是在程式設計時,一些常見的物件釋放、流關閉還是要程式設計師顯式地寫出來。
最常見的就是檔案操作了。
常見的檔案操作方式
原始的Python檔案操作方式,很簡單,也很common(也很java):
def read_file_1():
f = open(`file_demo.py`, `r`)
try:
print(f.read())
except Exception as e:
pass
f.close()
就是這麼簡簡單單的,先open然後讀寫再close,中間讀寫加個異常處理。
其中close就是釋放資源了,在這裡如果不close,可能:
- 資源不釋放,直到不可控的垃圾回收來了,甚至直到程式結束
- 中間對檔案修改時,修改的資訊還沒來得及寫入檔案
- 整個程式碼顯得不規範
因此寫上close函式理論上已經必須的了,可是xxx.close()
這樣寫上去,在邏輯複雜的時候讓人容易遺漏,同時也顯得不雅觀。
這時,各種語言生態有各種解決方案。
像Java,就直接jvm+依賴注入,直接把物件的生命週期管理接管了,只留下物件的使用功能給程式設計師;像golang,defer一下就好。而python最常用的則是with,即上下文管理器
使用上下文管理器
用with之後的檔案讀寫會變成:
def read_file_2():
with open(`file_demo.py`, `r`) as f:
print(f.read())
我們看到用了with之後,程式碼沒有了open建立,也沒有了close釋放。而且也沒有了異常處理,這樣子我們一看到程式碼,難免會懷疑它的健壯性。
為了更好地理解上下文管理器,我們先實現試試。
實現上下文管理器
我們先感性地對with進行猜測。
從呼叫with的形式上看,with像是一個函式,包裹住了open和close:
# 大概意思而已 with = open + do + close
def with():
open(xxxx)
doSomething(xxxx)
close(xxxx)
而Python的庫中已有的方案(contextmanager)也和上面的虛擬碼具有一定的相似性:
from contextlib import contextmanager
@contextmanager
def c(s):
print(s + `start`)
yield s
print(s + `end`)
“列印start”相當於open,而“列印end”相當於close,yield語法和修飾器(@)不熟悉的同學可以複習一下這些文章:生成器和修飾器。
然後我們呼叫這個上下文管理器試試,注意煎魚還給上下文管理器加了引數s,輸出的時候會帶上:
def test_context():
with c(`123`) as cc:
print(`in with`)
print(type(cc))
if __name__ == `__main__`:
test_context()
我們看到,start和end前都有實參s=123。
現實一個上下文管理器就是這麼簡單。
異常處理
但是我們必須要注重異常處理,假如上面的上下文管理器中拋異常了怎麼辦呢:
def test_context():
with c(`123`) as cc:
print(`in with`)
print(type(cc))
raise Exception
結果:
顯然,這樣弱雞的異常處理,煎魚時忍不了的。而且最重要的是,後面的close釋放居然沒有執行!
我們可以在實現上下管理器時,接入異常處理:
@contextmanager
def c():
print(`start`)
try:
yield
finally:
print(`end`)
def test_except():
try:
with c() as cc:
print(`in with`)
raise Exception
except:
print(`catch except`)
呼叫test_except函式輸出:
我們在上下文管理器的實現中加入了try-finally,保證出現異常的時候,上下文管理器也能執行close。同時在呼叫with前也加入try結構,保證整個函式的正常執行。
然而,加入了這些東西之後,整個函式變得複雜又難看。
因此,煎魚覺得,想要程式碼好看,抽象的邏輯需要再次昇華,即從函式的層面升為物件(類)的層面。
實現上下文管理器類
其實用類實現上下文管理器,從邏輯理解上簡單了很多,而且不需要引入那一個庫:
class ContextClass(object):
def __init__(self, s):
self.s = s
def __enter__(self):
print(self.s + `call enter`)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(self.s + `call exit`)
def test(self):
print(self.s + `call test`)
從程式碼的字面意思上,我們就能感受得出來,__enter__
即為我們理解的open函式,__exit__
就是close函式。
接下來,我們呼叫一下這個上下文管理器:
def test_context():
with ContextClass(`123`) as c:
print(`in with`)
c.test()
print(type(c))
print(isinstance(c, ContextClass))
print(``)
c = ContextClass(`123`)
print(type(c))
print(isinstance(c, ContextClass))
if __name__ == `__main__`:
test_context()
輸出結果:
功能上和直接用修飾器一致,只是在實現的過程中,邏輯更清晰了。
異常處理
回到我們原來的話題:異常處理。
直接用修飾器實現的上下文管理器處理異常時可以說是很難看了,那麼我們的類選手表現又如何呢?
為了方便比較,煎魚把未進行異常處理的和已進行異常處理的一起寫出來,然後煎魚呼叫一個不存在的方法來拋異常:
class ContextClass(object):
def __init__(self, s):
self.s = s
def __enter__(self):
print(self.s + `call enter`)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(self.s + `call exit`)
class ContextExceptionClass(object):
def __init__(self, s):
self.s = s
def __enter__(self):
print(self.s + `call enter`)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(self.s + `call exit`)
return True
def test_context():
with ContextExceptionClass(`123`) as c:
print(`in with`)
t = c.test()
print(type(t))
# with ContextClass(`456`) as c:
# print(`in with`)
# t = c.test()
# print(type(t))
if __name__ == `__main__`:
test_context()
輸出不一樣的結果:
結果發現,看了半天,兩個類只有最後一句不一樣:異常處理的類中__exit__
函式多一句返回,而且還是return了True。
而且這兩個類都完成了open和close兩部,即使後者拋異常了。
而在__exit__
中加return True
的意思就是不把異常丟擲。
如果想要詳細地處理異常,而不是像上面治標不治本的隱藏異常,則需要在__exit__
函式中處理異常即可,因為該函式中有著異常的資訊。
不信?稍微再改改:
class ContextExceptionClass(object):
def __init__(self, s):
self.s = s
def __enter__(self):
print(self.s + `call enter`)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(self.s + `call exit`)
print(str(exc_type) + ` ` + str(exc_val) + ` ` + str(exc_tb))
return True
輸出與預期異常資訊一致:
先這樣吧
若有錯誤之處請指出,更多地請關注造殼。