[python] decorator 也可以把階層打平,更改被裝飾函式的傳入參數

  • 8303
  • 0

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

def rfile(f):
    x = open(f, 'r')
    y = x.read()
    x.close()
    return y

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

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

q2('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 row

q3('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 更好。)

 

 

 

分享