Pythonのタイムアウトデコレータ実行値を動的に変えたい
この記事はpythonほぼわからん人間が描いた試行錯誤の軌跡である
以下の要件があった。
- 新規の機能を追加したい → 新規関数追加すればいいや
- タイムアウト機能をつけたい → 新規でtimeoutのデコレータ作ってつければいいや
- タイムアウト時間は呼び出し元に応じて変化させたい → デコレータでやるならこれは実装どうしよう・・・・
デコレータは以下の通り、printはデバッグ用
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を用意しなければならないようにした
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に乗るのはとても悔しい