2022年11月23日

[python] Record Function Call

最近因為需要在程式中埋下紀錄使用者過程中用到了那些 function,這樣方便日後追蹤重現結果。當然最簡單的方法是在每個 function 內都明確地用 logger 之類的寫進 log 裡,不過 python 有更簡單更不容易出錯的方式可以達到效果,因為用 logger 很容易在未來程式改版時有所疏漏。

方法基本上有三種:proxy, build-in __getattribute__(), decorator

    proxy

    說穿了就是另外包一層 proxy function/class,所有 function call 都會透過 proxy function/class 記錄。其實效果跟用 logger 差不多,差別是程式有改版的話不用擔心漏改,因為一定會出現不一致導致 runtime error。

    https://stackoverflow.com/a/13770097


    __getattribute__()

    python class 內部不論是 property/member data 還是 function/method 其實都被視為一種 attribute,實際上的 function call 的過程中會拿使用者用到的名字丟進去 __getattribute__ 這個 build-in function,拿到真正的 handle 後才能去使用,因此我們其實可以重新實作 __getattribute__ 這支 function 來中途攔截所有的 funciton call

    https://stackoverflow.com/a/4724111

    class CommandWatcher(object):                                                                 
        def __init__(self):
            self.cmd_list = []
    
        # reimplement __getattribute__
        def __getattribute__(self, name):
            attr = super().__getattribute__(name)
            if callable(attr):
                def wrapped(*args, **kwargs):
                    arg = [str(v) for v in args] + [f"{k}={v}" for k, v in kwargs.items()]
                    cmd = f"{name}(" + ", ".join(arg) + ")"
                    self.cmd_list.append(cmd)
                    return attr(*args, **kwargs)
                return wrapped
            else:
                return attr

    decorator

    利用 __getattribute__ 的方式雖然相當簡單明瞭,缺點是如果 function A 內用了 private function B,而我其實不希望 function B 也被記錄下來干擾日後判讀或重現,這時就會有困擾。其中一個方式是在 __getattribute__ 的過程中加上須滿足特定條件的 function 我才紀錄,例如 _ 開頭的 function 我就不紀錄;另一種方式就是這邊要使用的 decorator。唯有被加上 decorator 的 function 才會被記錄,缺點同於使用 logger,然而比 logger 更容易被使用是其優點。

    class CommandWatcher(object):                                                                 
        def __init__(self):
            self.cmd_list = []
            
        # decorator usage
        def record(func):
            def wrapped(self, *args, **kwargs):
                arg = [str(v) for v in args] + [f"{k}={v}" for k, v in kwargs.items()]
                cmd = f"{func.__name__}(" + ", ".join(arg) + ")"
                self.cmd_list.append(cmd)
                return func(self, *args, **kwargs)
            return wrapped
            
    class MyClass(CommandWatcher):
        def __init__(self):
            super().__init__()
    
        @CommandWatcher.record
        def a(self, v):
            print(f"a with {v}")

這邊需要特別注意的是 __getattribute__() 與 decorator 無法同時使用,因為 decorator 本質上是幫 function 重新包裝成 2nd order function,同時使用時反而會記錄到外層包裝過的 wrapper 而非我們真正想要的 function 本身。範例為了方便起見同時使用了兩者,然而實際使用時務必擇一使用

完整範例如下:

沒有留言:

張貼留言