[python] decorator 也可以把階層打平,更改被裝飾函式的傳入參數
在 python 裡面,把程式碼階層打平,會讓程式碼看起來好看很多,增加可讀性,而且有助於把重覆的部份拆解開,增加可維護性、可擴充性。雖然,拆得太散會因為抽象度提高,造成可理解性會降低,而且看一個任務變得要在程式碼的不同地方跑來跑去,也會很困擾。但是適度的打平的好處是多過壞處的。
會產生程式碼階層的地方不外乎是 loop、if...else、try...except 這幾個敘述。loop 可以用 map,if...else 可以用 filter,那 try...except 呢?
以往我遇到 try...except,都是乖乖寫,程式碼就會多好幾層,後來再偷懶一點就是寫一個 try 函式來套
def try_(fn, *arg, **kwarg):
try:
fn(*arg, **kwarg)
except Exception, ex:
print 'app error', str(ex)
raise exdef rfile(f):
x = open(f, 'r')
y = x.read()
x.close()
return yprint try_(rfile, 'xxx')
>>> ================================ RESTART ================================
>>>
app error [Errno 2] No such file or directory: 'xxx'Traceback (most recent call last):
File "D:\ricky\doc\myDropbox\python_client\decorators_application.py", line 14, in <module>
print try_(rfile, 'xxx')
File "D:\ricky\doc\myDropbox\python_client\decorators_application.py", line 6, in try_
raise ex
IOError: [Errno 2] No such file or directory: 'xxx'
如此一來,rfile 函式的程式碼就可以不寫 try...except。但是以這個偷懶寫法寫成的 try_ 函式,呼叫起來很彆扭,函式當第一個參數,要用到的參數得跟在後面。若是以前寫好的程式都要加進 try_ 來呼叫,每個呼叫的地方都得改成多一層呼叫,感覺多此一舉,更麻煩。
利用最近想通的 decorator 來做這件事,寫完像這樣:
def deco_try(func):
def _decotry(*arg, **kwarg):
try:
func(*arg, **kwarg)
except Exception, ex:
print 'app error', str(ex)
raise ex
return _decotry@deco_try
def rfile(f):
x = open(f, 'r')
y = x.read()
x.close()
return yrfile('xxx')
>>> ================================ RESTART ================================
>>>
app error [Errno 2] No such file or directory: 'xxx'Traceback (most recent call last):
File "D:\ricky\doc\myDropbox\python_client\decorators_application.py", line 24, in <module>
rfile('xxx')
File "D:\ricky\doc\myDropbox\python_client\decorators_application.py", line 14, in _decotry
raise ex
IOError: [Errno 2] No such file or directory: 'xxx'
利用 decorator 的方式,我只要在函式宣告的地方加上 @decotry,而不用動到呼叫的地方。同時,原來的 rfile 裡面的程式碼也不會多一層。
接著,我也試著每次要開資料庫連線這件事上使用 decorator 方式簡化工作。原本是
def q(query_string):
conn = sqlite3.connect('dblog.db')
cursor = conn.cursor()
cursor.execute(query_string)
for row in cursor:
print row
cursor.close()
conn.close()
想要把 connect 這些動作搬出去,但是 cursor 就必須從函式傳進來。就得改成這樣:
def openconn2(func):
def _open(*arg, **kwarg):
print 'open connect'
conn = sqlite3.connect('dblog.db')
cursor = conn.cursor()
a = [i for i in arg]
a.append(cursor)
try:
func(*a, **kwarg)
conn.commit()
except Exception, ex:
raise ex
finally:
cursor.close()
conn.close()
print 'close connect'return _open
@openconn2
def q2(query_string, cursor):
cursor.execute(query_string)
for row in cursor:
print rowq2('select * from file_rec')
從這裡可以看到的確是直接先進裝飾程式碼裡,不然變數個數就不對,應該會發生 exception。
為了不要讓 cursor 參數鎖定位置,避免寫程式的手錯發生,後來又改強迫用 keyword 參數型式。
def openconn3(func):
def _open(*arg, **kwarg):
print 'open connect'
conn = sqlite3.connect('dblog.db')
cursor = conn.cursor()
kwarg['cursor'] = cursor
try:
func(*arg, **kwarg)
conn.commit()
except Exception, ex:
raise ex
finally:
cursor.close()
conn.close()
print 'close connect'return _open
@openconn3
def q3(query_string, cursor=None):
cursor.execute(query_string)
for row in cursor:
print rowq3('select * from file_rec')
>>> ================================ RESTART ================================
>>>
open connect
(1, u'40ff3740766171a42d57b15fcfed019a3ea10f0f', u'IMG_1408.JPG', u'../lib/20100829', u'1283070380.0', u'2013-10-25T23:35:25.018000')
close connect這樣改好,以後開 db 的 connection,就不用每個函式都寫一次 open, close,也少掉一層 try...except。不過以後時間久了,突然看到 cursor=None 還居然會動應該會嚇一下吧(呵),要記得 cursor 是 decorator 會幫忙處理。
decorator 在了解之後,的確可以在「簡化程式碼」、「重用程式碼」方面提供非常大的幫助。會不會到了濫用的程度呢?那就等待以後的心得吧。(我覺得開 db 的 connection 的用法可以會算是濫用吧…)(我在想這種 io 的 open、close 是不是用 with 來解決比較好?這應該是兩碼子事。with 可以自動關閉,寫成 decorator 可做到的事比較多,像是一些前置動作。把with寫到 decorator 更好。)