デコレータ用の関数を外部から指定する方法


# 環境

  * Python 3.7

# 背景

PythonでWeb
Apiのクライアントライブラリを作成しています。APIのアクセス過多で失敗したときでもリトライするよう、`my_retry`というデコレータを作成しました。

[code]

    def my_retry(self, function):
        """
        HTTP Status Codeが429 or 5XXのときはリトライする. 最大5分間リトライする。
        """
    
        @functools.wraps(function)
        def wrapped(*args, **kwargs):
            def fatal_code(e):
                """Too many Request以外の4XXはretryしない"""
                code = e.response.status_code
                return code != 429 and code < 500
    
            return backoff.on_exception(backoff.expo, requests.exceptions.RequestException,
                                        jitter=backoff.full_jitter,
                                        max_time=300,
                                        giveup=fatal_code)(function)(*args, **kwargs)
    
        return wrapped
    
[/code]

以下、読みやすさのために `my_retry` を簡略化しています。

[code]

    # coding: utf-8
    import functools
    
    def my_retry(function):
        @functools.wraps(function)
        def wrapped(*args, **kwargs):
            print("my_retry calls", function.__name__, end=", ")
            return function(*args, **kwargs)
    
        return wrapped
    
    class ApiClient:
        def __init__(self):
            pass
    
        @my_retry
        def execute_hoge_api(self):
            print("hoge!")
    
    
        @my_retry
        def execute_fuga_api(self):
            print("fuga!")
    
    def main():
        ac = ApiClient()
        ac.execute_hoge_api()
        ac.execute_fuga_api()
    
    
    if __name__ == '__main__':
        main()
    
[/code]

# やりたいこと

`ApiClient`クラスのユーザが、リトライ用の関数を変更できるようにしたいです。  
具体的には、`ApiClient`のコンストラクタ引数にリトライ用関数を指定するなどの方法を考えています。

どのようなコードを書けばよいでしょうか？

# 検討したこと

以下のコードのように、`my_retry`関数を`ApiClient`クラスのインスタンスメソッドにしてみました。  
この場合、`@my_retry`の引数には何を渡せばよいか分からず、詰まってしまいました。

[code]

    class ApiClient:
        def __init__(self, my_retry=None):
            if my_retry is not None:
                self.my_retry = my_retry
            pass
    
        # デコレータの引数には何を指定すればよい？
        @my_retry
        def execute_hoge_api(self):
            print("hoge!")
    
    
        @my_retry
        def execute_fuga_api(self):
            print("fuga!")
    
[/code]




動的なデコレータを直接クラスに定義するわけにいきませんので、デコレータそのものを遅延評価させる必要があります。

[code]

    import functools
    
    
    class ApiClient:
        def __init__(self, my_retry=None):
            if my_retry is not None:
                self.my_retry = my_retry
            pass
    
        def lazy_deco(pv):
            def outer(func):
                @functools.wraps(func)
                def inner(self, *args, **kwargs):
                    return getattr(self, pv)(func)(self, *args, **kwargs)
                return inner
            return outer
    
        @lazy_deco('my_retry')
        def execute_hoge_api(self):
            print("hoge!")
    
        @lazy_deco('my_retry')
        def execute_fuga_api(self):
            print("fuga!")
    
    
    def my_retry(function):
        @functools.wraps(function)
        def wrapped(*args, **kwargs):
            print("my_retry calls", function.__name__, end=", ")
            return function(*args, **kwargs)
    
        return wrapped
    
    
    def main():
        ac = ApiClient(my_retry=my_retry)
        ac.execute_fuga_api()
    
    
    if __name__ == '__main__':
        main()  # => my_retry calls execute_fuga_api, fuga!
    
[/code]




動的なデコレータを直接クラスに定義するわけにいきませんので、デコレータそのものを遅延評価させる必要があります。

[code]

    import functools
    
    
    class ApiClient:
        def __init__(self, my_retry=None):
            if my_retry is not None:
                self.my_retry = my_retry
            pass
    
        def lazy_deco(pv):
            def outer(func):
                @functools.wraps(func)
                def inner(self, *args, **kwargs):
                    return getattr(self, pv)(func)(self, *args, **kwargs)
                return inner
            return outer
    
        @lazy_deco('my_retry')
        def execute_hoge_api(self):
            print("hoge!")
    
        @lazy_deco('my_retry')
        def execute_fuga_api(self):
            print("fuga!")
    
    
    def my_retry(function):
        @functools.wraps(function)
        def wrapped(*args, **kwargs):
            print("my_retry calls", function.__name__, end=", ")
            return function(*args, **kwargs)
    
        return wrapped
    
    
    def main():
        ac = ApiClient(my_retry=my_retry)
        ac.execute_fuga_api()
    
    
    if __name__ == '__main__':
        main()  # => my_retry calls execute_fuga_api, fuga!
    
[/code]

