LoginSignup
22
28

Pythonで終了時に必ず何か実行したい (続編)

Last updated at Posted at 2023-10-29

Pythonで終了時に必ず何か実行したい (続編)

はじめに

3年ほど前に「Python で終了時に必ず何か実行したい」という記事を書きました。

3年も経つと、あちこちにこのコードのコピペが乱立してきました。
ふと、with句やdecoratorを使えばもうすこしきれいになるのではと思い、作ったものをpypiに公開してみました。

名前はFinalizerです。

pip install finalizer

で使えます。

ソースは

です。

使い方

使い方は簡単で、with Finalizer(掃除用の関数):のように指定します。
with句を抜けたときに掃除用関数が実行されます。
また、with句の実行中はCtrl-Cやkillコマンド(SIGTERM)はトラップされていますので、Ctrl-Cやkillコマンドで止めた場合も、掃除用関数は実行されます。

from time import sleep
from finalizer import Finalizer

def cleanup() -> None:
    print("cleanup start")
    sleep(3)
    print("cleanup end")

def task() -> None:
    print("task start")
    with Finalizer(cleanup):
        sleep(3)
        print("task end")

掃除用関数に引数が必要な場合は、

def cleanup(param1: str, param2: int) -> None:
    print("cleanup start", param1, param2)
    sleep(3)
    print("cleanup end")

def task2() -> None:
    print("task start")
    with Finalizer(cleanup, "test", param2=42):
        sleep(3)
        print("task end")

のように書くことができます。
with句はネストしても大丈夫です。

また、decoratorとしても指定できるので、次のように書くこともできます。

def cleanup() -> None:
    print("cleanup start")
    sleep(3)
    print("cleanup end")

@Finalizer(cleanup)
def task() -> None:
    print("task start")
    sleep(3)
    print("task end")

仕組みの開設

実際のソースコードより少しシンプルにしてありますが、骨子はこんな感じです。

from __future__ import annotations
from typing import Callable, Any
import signal
import sys
from contextlib import ContextDecorator


def sig_handler(signum, frame) -> None:
    sys.exit(1)


class Finalizer(ContextDecorator):
    def __init__(self, func: Callable[..., None], *args, **kw):
        self.__cleanup: Callable[..., None] = func
        self.__args: tuple[Any, ...] = args
        self.__kw: dict[str, Any] = kw
        self.__prev_sigterm: signal.Handlers = signal.SIG_DFL

    def __enter__(self):
        self.__prev_sigterm = signal.getsignal(signal.SIGTERM)
        signal.signal(signal.SIGTERM, sig_handler)

    def __exit__(self, exc_type, exc_value, traceback):
        cur_sigint = signal.getsignal(signal.SIGINT)
        signal.signal(signal.SIGTERM, signal.SIG_IGN)
        signal.signal(signal.SIGINT, signal.SIG_IGN)

        self.__cleanup(*self.__args, **self.__kw)

        signal.signal(signal.SIGTERM, self.__prev_sigterm)
        signal.signal(signal.SIGINT, cur_sigint)

with句実行時に__init__()__enter__()が呼ばれます。
つまり、with句実行時に掃除用関数とその引数を保存しておきます。
同時に、現在のSIGTERMのハンドラも保存しておき、SIGTERMにsys.exit(1)を実行する関数をセットします。
SIGTERMにsys.exitをさせるようにしておかないと、Pythonで終了時に必ず何か実行したいで解説したtry~finallyの時と同様に、__exit__()が実行されずにそのまま終了してしまいます。

with句を抜けるときには__exit__()が実行されます。
掃除用関数実行中に再びCtrl-Cやkillコマンドで止められないようにSIGINTとSGTERMをIgnoreし、処理が終わったら元に戻します。

ちなみに、ContextDecratorを継承しておくことで、自動的にdecoratorにもなります。便利ですね。

おわりに

ごちゃごちゃせず、シンプルに書くことができるようになりました。
multi process環境でも動作します。

が、multi threadの環境では子threadでsignalを扱えないので、うまく動作しません。
もちろん、親threadでは使えるのですが、子threadは終了するまで待つしかありません。
結果として、掃除用関数は呼ばれるのですが、docker stopのような場合はkillされた後10秒後にkill -9されてしまうので、残念な結果になってしまいます。
何かいいアイデアがあれば、コメントやプルリクお待ちしています。

22
28
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
22
28