1
1

More than 3 years have passed since last update.

timeoutデコレータで、動的にタイムアウト時間を設定したかった

Posted at

Pythonのタイムアウトデコレータ実行値を動的に変えたい

この記事はpythonほぼわからん人間が描いた試行錯誤の軌跡である

以下の要件があった。
- 新規の機能を追加したい → 新規関数追加すればいいや
- タイムアウト機能をつけたい → 新規でtimeoutのデコレータ作ってつければいいや
- タイムアウト時間は呼び出し元に応じて変化させたい → デコレータでやるならこれは実装どうしよう・・・・

デコレータは以下の通り、printはデバッグ用

timeout.py
from functools import wraps
import errno
import os
import signal

def timeout(seconds=10, error_message=os.strerror(errno.ETIME)):
    def decorator(func):
        def _handle_timeout(signum, frame):
            raise TimeoutError(error_message)

        def wrapper(*args, **kwargs):

            print("decorator.second: %s" % seconds)

            signal.signal(signal.SIGALRM, _handle_timeout)
            signal.setitimer(signal.ITIMER_REAL, seconds)
            try:
                result = func(*args, **kwargs)
            finally:
                signal.alarm(0)
            return result
        return wraps(func)(wrapper)
    return decorator

ダメだった試行錯誤

  • パターン①:instance変数の利用
class Test:
  def __init__(self, ...):
    self.TimeoutSec = 10
  ...

  @timeout(self.TimeoutSec)
  def execHoge():
    ...

実行結果

Traceback (most recent call last):
  File "./CallTest.py", line 7, in <module>
    import Test
  File "~/Test.py", line 18, in <module>
    class Test:
  File "~/Test.py", line 111, in Test
    @timeout(self.TimeoutSec)
NameError: name 'self' is not defined

デコレータはinstance変数を読み取ってくれなかった。

  • パターン②:class変数の利用
    複数の端末から同時に実行される想定ではあるが、int型でimmutableだから、ヨシ!
    # class変数はバグの温床になりやすいから余り使いたくはないが四の五の言う暇はない
class Test:
  TimeoutSec=1.5
  def __init__(self, ...):
    self.TimeoutSec = 0.05
    print("class変数: %s " % self.TimeoutSec)
  ...

  @timeout(TimeoutSec)
  def execHoge():
    ...

実行結果

class変数: 0.05
decorator.second: 1.5

class作成時に定義しているclass変数の値でdecoretorを呼び出していることが分かった

  • パターン③:global変数の利用 class変数で書いた理由と同様の理由で、ヨシ!
TIMEOUT_SEC=1.5
class Test:
  def __init__(self, ...):
    global TIMEOUT_SEC
    TIMEOUT_SEC = 0.05
    print("global変数: %s " % TIMEOUT_SEC)
  ...

  @timeout(TIMEOUT_SEC)
  def execHoge():
    ...

実行結果

global変数: 0.05
decorator.second: 1.5

globalでもclassでも定義時の値がデコレータに渡されていることがわかる

ここら辺でお手上げになったのでpython分かる先輩にきいたところ、ヘボエンジニアの私が理解できるように説明してくれた。

# あくまで私が理解できるようにしてくれたものなので、正確な仕様とは微妙に違うかも・・・

  • pythonの読込み時に、デコレータは付与してる関数に処理を追加した関数を内部的にこっそり作ってるよ
  • Instanceは読込みが終わって、コンピュータが実行するタイミングで作られるものだから、読込み時にinstance使おうとしてもエラーが出るよ
    • だからパターン①は実行したタイミングでtraceback流れてるんだよ


# python勉強しとこう‥‥

つまり治すべきは実装ではなくデコレータだった

上述しているデコレータを下記のように修正した。
修正概要は以下
- 引数でタイムアウトの秒数指定をやめた
- Instance変数で、timeout_secondを用意しなければならないようにした

timeout.py
 from functools import wraps
 import errno
 import os
 import signal

 def timeout(seconds=10, error_message=os.strerror(errno.ETIME)):
     def decorator(func):
         def _handle_timeout(signum, frame):
             raise TimeoutError(error_message)

         def wrapper(*args, **kwargs):
             seconds = args[0].timeout_seconds
             print("decorator.second: %s" % seconds)
             signal.signal(signal.SIGALRM, _handle_timeout)
             signal.setitimer(signal.ITIMER_REAL, seconds)
             try:
                 result = func(*args, **kwargs)
             finally:
                 signal.alarm(0)
             return result
         return wraps(func)(wrapper)
     return decorator
class Test:
  def __init__(self, ...):
    self.timeout_seconds = 0.05
    print("instance変数: %s " % self.timeout_seconds)
    ...

    @timeout()
    def execHoge():
      ...

実行結果

instance変数: 0.05
decorator.second: 0.05

これにて漸くタイムアウト時間を柔軟に設定できるようになった。

反省点

納期が短かったとはいえ、無理にデコレータで実装せずに、別途なにか方法考えても良かったかもしれないなあ。

汎用的じゃない不細工な実装に仕上がったものがmasterに乗るのはとても悔しい

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1