Sharing

2012年7月29日 星期日

Python args and kwargs parsing


python 在 argument 的處理上有很大的彈性, 以前在寫 C/C++ 時, 總會為了參數要怎麼傳, 怎麼樣才能讓參數穿透層層關卡到達最下層而煩惱, 程式要寫的有架構常常必須要分層次, 彼此之間要用定好 interface 隔開, 但間接造成的問題是, 如果在最上層要新增新的參數, 你很可能要一層一層的改下去, 也是很頭痛, 但 python 在這部份就比較有方法可以做到.

先看這兩篇在介紹 args 和 kwargs, args 是 list (有順序性), kwargs 是 dict (無順序性)
http://www.saltycrane.com/blog/2008/01/how-to-use-args-and-kwargs-in-python/
http://docs.python.org/tutorial/controlflow.html#keyword-arguments

第一個例子, 說明 *args 會把 argument 按照順序收集起來, 所以 test_var_args 除了第一個 "fargs" 一定要傳之外, 其它的參數可以任意的接在後來
>>> def test_var_args(farg, *args):
...    print "formal arg:", farg
...    for arg in args:
...        print "another arg:", arg
>>> test_var_args(1, "two", 3)
formal arg: 1
another arg: two
another arg: 3

第二個例子, 說明 **kwargs 會把 keyword argument 收集起來放進 kwargs 這個字典中, test_var_args 除了第一個 "fargs" 一定要傳之外, 其它的參數可以用 keyword argument 的方式加進去
>>> def test_var_kwargs(farg, **kwargs):
...     print "formal arg:", farg
...     for key in kwargs:
...         print "another keyword arg: %s: %s" % (key, kwargs[key])
...
>>> test_var_kwargs(farg=1, myarg2="two", myarg3=3)
formal arg: 1
another keyword arg: myarg2: two
another keyword arg: myarg3: 3

最後是把兩個結合起來
>>> def test_vars(farg, *args, **kwargs):
...     print "formal arg:", farg
...     for arg in args:
...        print "another arg:", arg
...     for key in kwargs:
...         print "another keyword arg: %s: %s" % (key, kwargs[key])
...
>>> test_vars(1, 2, 3, myarg4="four", myarg5=5)
formal arg: 1
another arg: 2
another arg: 3
another keyword arg: myarg4: four
another keyword arg: myarg5: 5

所以有了 args 和 kwargs, python 的函式在傳參數時就可以做到不定個數、不定長度. 那就可以玩一些變化讓這些參數具有穿透力. 這有什麼用處呢? 在某些情況, 假設你有 f1, 內部會用到 f2, 你為了讓呼叫的人也可以控制到 f2, 所以你必須也要在 f1 參數上也加上 f2 的參數. 就像下面這個例子

>>> def f1(a, b=1):
...     f2(b)
...
>>> def f2(b=1): pass
...

這樣的寫法會有什麼困擾呢?

第一個是有關於預設值, 如果當我們呼叫 f1 時, 預期在不給 b 的狀況下, 能夠直接使用 f2 的設定值, 我們就只能在 f1 中也針對 b 設定一樣的預設值, 否則就會不同步, 產生錯誤的行為
第二個是如果當 f2 增加參數時或改變參數預設值時, f1 也必須要跟著修改

所以我們可以利用 args/kwargs 來讓參數有穿透力, 我們在 f1 只關心 a 這個參數, 於是我們把 a 拿走, 剩下的全部傳進 f2, 而 f2 只需要 b, 於是它把 b 拿走, 剩下的傳進 f3.

>>> def f3(c):
...     print c
...
>>> def f2(b, *args, **kwargs):
...     print b
...     f3(*args, **kwargs)
...
>>> def f1(a, *args, **kwargs):
...     print a
...     f2(*args, **kwargs)
...
>>> f1(1, 2, 3)
1
2
3
>>> f1(1, 2, c=3)
1
2
3
>>> f1(1, b=2, c=3)
1
2
3


如果我們在 f1 新增一個參數 d, 為了向前相容, 所以我們給他一個預設值, 所以原來的程式碼也還可以繼續使用, 新的程式碼如果需要修改參數 d, 也只需要在呼叫時, 多一個 keyword argument 即可, f2/f3 完全不需要修改, 真的是很方便.

>>> def f1(c, d=2):
...     print c,d
...
>>> f3(3, c=5, b=4)
3
4
5 2
>>> f3(3, c=5, b=4, d=6)
3
4
5 6


再來下一個問題:是否有辦法觀察一個函式的參數有那些? 而且是否有預設值, 我們可以利用 inspect 這個模組.
http://stackoverflow.com/questions/196960/can-you-list-the-keyword-arguments-a-python-function-receives
http://docs.python.org/library/inspect.html?highlight=inspect#inspect

>>> def func(a,b,c=42, *args, **kwargs): pass
...
>>> inspect.getargspec(func)
ArgSpec(args=['a', 'b', 'c'], varargs='args', keywords='kwargs', defaults=(42,))


除了原來連結寫的幾個函式很有用處之外, 我也寫了一個小工具, 主要是用來應付如果要乎叫的函式不能傳進 kwargs 時, 就必須要先把 kwargs 過濾過, 把可接受的部份留下, 然後去除掉不能使用的部份

def filter_args(func, kwargs):
    args, varargs, varkw, defaults = inspect.getargspec(func)
    args_with_default = args[-len(defaults):]
    valid_kw = dict()

    if not varkw:
        for arg in kwargs:
            # remove unaccepted argument
            if arg not in args:
                continue
            # argument = None but with default value
            elif not kwargs[arg] and arg in args_with_default:
                continue
            valid_kw.update({arg:kwargs[arg]})

    return valid_kw


這在 command line 的參數處理滿有用處的,

def shellcmd(func, *argv, **kwargs):
    kwargs = filter_args(func, kwargs)
    func(*argv, **kwargs)



沒有留言: