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

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

## はじめに

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

https://qiita.com/qualitia_cdev/items/f536002791671c6238e3

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

名前は**Finalizer**です。

https://pypi.org/project/finalizer/

```sh
pip install finalizer
```

で使えます。

ソースは

https://github.com/hirachan/finalizer

です。


## 使い方

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

```python
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")
```

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

```python
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としても指定できるので、次のように書くこともできます。

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

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

## 仕組みの開設

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

```python
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で終了時に必ず何か実行したい](https://qiita.com/qualitia_cdev/items/f536002791671c6238e3)で解説した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されてしまうので、残念な結果になってしまいます。
何かいいアイデアがあれば、コメントやプルリクお待ちしています。