62311


# 環境

  * 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]


CC BY-SA 4.0
2019-05-03T16:58:46.043
54697
2020-03-02T16:01:15.117
2019-05-04T05:07:04.037
29826
19524
question
python
デコレータ用の関数を外部から指定する方法


> ApiClientクラスのユーザが、リトライ用の関数を変更できるようにしたいです。

クラスの利用者が随時処理を描き借るのなら、抽象クラスの利用。デフォルトを定義して必要に応じて書き換えるならば、クラスの継承で表現できませんか？
聞きたいことと乖離していたらすいません。

[code]

    from abc import *
    
    class AbstractBase:
    
        def confirmmthod01(self):
            self.abmethod()
    
        @abstractmethod
        def abmethod(self):
            raise NotImplementedError
    
    
    class ConcClass(AbstractBase):
    
        def abmethod(self):
            print('hello')
    
    
    hoge = ConcClass()
    hoge.confirmmthod01()
    
[/code]

そのほか、引数に「関数」受け取るメソッドを用意するなど、やり方はいろいろありそうです。


CC BY-SA 4.0
2019-05-04T00:29:47.970
54700
2019-05-04T05:07:58.717
2019-05-04T05:07:58.717
29826
10174
54697
answer


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

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


CC BY-SA 4.0
2020-01-17T02:22:09.313
62311
2020-01-17T02:22:09.313
62
54697
answer