【Python】変数の変化をソースコードにリアルタイム保存【一時ファイルからの開放】

お久しぶりです。時の流れは早いもので、前回の記事の投稿が学生時代（春休み）だった私にも先日後輩社員ができてしまいました。
# はじめに
- GUIを有するプログラムなどユーザーが設定値を変更する場合
- 初期値からキャリブレーションなどで値を毎回調節している場合

などなど、プログラムの実行間で値を引き継ぎたい場面は多いと思います。
基本的にそのような場合は、設定値を保存するファイルを**別に作成して**、毎回それを読み出すといった形を取ると思います。
しかし、スカラー値１つなどのために設定ファイルを別に保存するのって煩わしいというか仰々しいというか虚無感というか…？なんだか損したような気持ちになりますよね。

どうせインタプリタ言語なんだし、ソースコード内に直接保存できればいいのに…

　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　…実現してみましょう。

![2024-04-16 21-27-32_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501281/ff70c537-403c-c89b-a0aa-d4399e50859e.gif)
# 構想
- 実行中に値を更新したらソースコードの定義部分を自動的に書き換えたい
- 値の更新は仕方ないが、それ以外の扱いは通常の変数（数値や配列）のように扱いたい（四則演算やイテレーションなど）

etc.

# 実装

```python
import inspect
import re
import os

class Storable:
    def __init__(self, value, /, *, format="{}"):
        self.__value = value
        self.__format = format
        self.pattern = re.compile(r"(?<=Storable\()(.+?)(?=(?:,\s*format.+)?\)$)")
        info = inspect.getframeinfo(inspect.currentframe().f_back)
        self.__def_lineno = info.lineno - 1
        self.__file = info.filename
        self.__ispyfile = os.path.exists(self.__file)

    @property
    def value(self):
        return self.__value
    
    @value.setter
    def value(self, v):
        self.__value = v
        if self.__ispyfile:
            self.__save__()
    
    def __save__(self):
        with open(self.__file, 'r+') as f:
            if isinstance(self.__value, str):
                val = self.__format.format(self.__value.__repr__().replace("\\r", r"\\r").replace("\\n", r"\\n"))
            else:
                val = self.__format.format(" ".join([i.strip() for i in self.__value.__repr__().splitlines()]))
            lines = f.readlines()
            lines[self.__def_lineno] = self.pattern.sub(val, lines[self.__def_lineno])
            f.seek(0)
            f.writelines(lines)
            f.truncate()
            
if __name__ == "__main__":
    import numpy as np
    s = Storable(0, format="np.{}")
    s.value = np.random.randn(2,3)

```

<details><summary>特殊メソッドオーバーライド版</summary>

こちらを使用すると`s.value + 1`や`for i in s.value:`などといちいち`.value`をつけなくても、多くのメソッドが使用可能になります。

```python
import inspect
import re
import os

class Storable:
    def __init__(self, value, /, *, format="{}"):
        self.__value = value
        self.__format = format
        self.pattern = re.compile(r"(?<=Storable\()(.+?)(?=(?:,\s*format.+)?\)$)")
        info = inspect.getframeinfo(inspect.currentframe().f_back)
        self.__def_lineno = info.lineno - 1
        self.__file = info.filename
        self.__ispyfile = os.path.exists(self.__file)

    @property
    def value(self):
        return self.__value
    
    @value.setter
    def value(self, v):
        self.__value = v
        if self.__ispyfile:
            self.__save__()
    
    def __save__(self):
        with open(self.__file, 'r+') as f:
            if isinstance(self.__value, str):
                val = self.__format.format(self.__value.__repr__().replace("\\r", r"\\r").replace("\\n", r"\\n"))
            else:
                val = self.__format.format(" ".join([i.strip() for i in self.__value.__repr__().splitlines()]))
            lines = f.readlines()
            lines[self.__def_lineno] = self.pattern.sub(val, lines[self.__def_lineno])
            f.seek(0)
            f.writelines(lines)
            f.truncate()

    def __abs__(self, *args, **kwargs):
        return self.__value.__abs__(*args, **kwargs)

    def __add__(self, b):
        result = self.__value.__add__(b)
        return result if result is not NotImplemented else b.__radd__(self.__value)

    def __and__(self, b):
        result = self.__value.__and__(b)
        return result if result is not NotImplemented else b.__rand__(self.__value)

    def __bool__(self, *args, **kwargs):
        return self.__value.__bool__(*args, **kwargs)

    def __ceil__(self, *args, **kwargs):
        return self.__value.__ceil__(*args, **kwargs)

    def __class_getitem__(self, *args, **kwargs):
        return self.__value.__class_getitem__(*args, **kwargs)

    def __contains__(self, *args, **kwargs):
        return self.__value.__contains__(*args, **kwargs)

    def __concat__(self, *args, **kwargs):
        return self.__value.__concat__(*args, **kwargs)

    def __delitem__(self, *args, **kwargs):
        return self.__value.__delitem__(*args, **kwargs)

    def __divmod__(self, b):
        result = self.__value.__divmod__(b)
        return result if result is not NotImplemented else b.__rdivmod__(self.__value)

    def __eq__(self, b):
        result = self.__value.__eq__(b)
        return result if result is not NotImplemented else b.__eq__(self.__value)

    def __float__(self, *args, **kwargs):
        return self.__value.__float__(*args, **kwargs)

    def __floor__(self, *args, **kwargs):
        return self.__value.__floor__(*args, **kwargs)

    def __floordiv__(self, b):
        result = self.__value.__floordiv__(b)
        return result if result is not NotImplemented else b.__rfloordiv__(self.__value)

    def __format__(self, *args, **kwargs):
        return self.__value.__format__(*args, **kwargs)

    def __ge__(self, b):
        result = self.__value.__ge__(b)
        return result if result is not NotImplemented else b.__le__(self.__value)

    def __getformat__(self, *args, **kwargs):
        return self.__value.__getformat__(*args, **kwargs)

    def __getitem__(self, *args, **kwargs):
        return self.__value.__getitem__(*args, **kwargs)

    def __getnewargs__(self, *args, **kwargs):
        return self.__value.__getnewargs__(*args, **kwargs)

    def __gt__(self, b):
        result = self.__value.__gt__(b)
        return result if result is not NotImplemented else b.__lt__(self.__value)

    def __hash__(self, *args, **kwargs):
        return self.__value.__hash__(*args, **kwargs)

    def __iadd__(self, *args, **kwargs):
        self.value = self.__value.__add__(*args, **kwargs)
        return self

    def __iconcat__(self, *args, **kwargs):
        self.value = self.__value.__concat__(*args, **kwargs)
        return self

    def __ilshift__(self, *args, **kwargs):
        self.value = self.__value.__lshift__(*args, **kwargs)
        return self

    def __imatmul__(self, *args, **kwargs):
        self.value = self.__value.__matmul__(*args, **kwargs)
        return self

    def __imod__(self, *args, **kwargs):
        self.value = self.__value.__mod__(*args, **kwargs)
        return self
    
    def __imul__(self, *args, **kwargs):
        self.value = self.__value.__mul__(*args, **kwargs)
        return self
    
    def __ior__(self, *args, **kwargs):
        self.value = self.__value.__or__(*args, **kwargs)
        return self
    
    def __ipow__(self, *args, **kwargs):
        self.value = self.__value.__pow__(*args, **kwargs)
        return self
    
    def __irshift__(self, *args, **kwargs):
        self.value = self.__value.__rshift__(*args, **kwargs)
        return self
    
    def __isub__(self, *args, **kwargs):
        self.value = self.__value.__sub__(*args, **kwargs)
        return self
    
    def __itruediv__(self, *args, **kwargs):
        self.value = self.__value.__truediv__(*args, **kwargs)
        return self
    
    def __ixor__(self, *args, **kwargs):
        self.value = self.__value.__xor__(*args, **kwargs)
        return self

    def __index__(self, *args, **kwargs):
        return self.__value.__index__(*args, **kwargs)
    
    def __indexof__(self, *args, **kwargs):
        return self.__value.__indexof__(*args, **kwargs)

    def __int__(self, *args, **kwargs):
        return self.__value.__int__(*args, **kwargs)

    def __invert__(self, *args, **kwargs):
        return self.__value.__invert__(*args, **kwargs)

    def __iter__(self, *args, **kwargs):
        return self.__value.__iter__(*args, **kwargs)

    def __le__(self, b):
        result = self.__value.__le__(b)
        return result if result is not NotImplemented else b.__ge__(self.__value)

    def __len__(self, *args, **kwargs):
        return self.__value.__len__(*args, **kwargs)

    def __lshift__(self, b):
        result = self.__value.__lshift__(b)
        return result if result is not NotImplemented else b.__rlshift__(self.__value)

    def __lt__(self, b):
        result = self.__value.__lt__(b)
        return result if result is not NotImplemented else b.__gt__(self.__value)

    def __mod__(self, b):
        result = self.__value.__mod__(b)
        return result if result is not NotImplemented else b.__rmod__(self.__value)

    def __mul__(self, b):
        result = self.__value.__mul__(b)
        return result if result is not NotImplemented else b.__rmul__(self.__value)

    def __ne__(self, *args, **kwargs):
        return self.__value.__ne__(*args, **kwargs)

    def __neg__(self, *args, **kwargs):
        return self.__value.__neg__(*args, **kwargs)

    def __or__(self, b):
        result = self.__value.__or__(b)
        return result if result is not NotImplemented else b.__ror__(self.__value)

    def __pos__(self, *args, **kwargs):
        return self.__value.__pos__(*args, **kwargs)

    def __pow__(self, b):
        result = self.__value.__pow__(b)
        return result if result is not NotImplemented else b.__rpow__(self.__value)

    def __radd__(self, b):
        result = self.__value.__radd__(b)
        return result if result is not NotImplemented else b.__add__(self.__value)

    def __rand__(self, b):
        result = self.__value.__rand__(b)
        return result if result is not NotImplemented else b.__and__(self.__value)

    def __rdivmod__(self, b):
        result = self.__value.__rdivmod__(b)
        return result if result is not NotImplemented else b.__divmod__(self.__value)

    def __repr__(self, *args, **kwargs):
        return self.__value.__repr__(*args, **kwargs)

    def __reversed__(self, *args, **kwargs):
        return self.__value.__reversed__(*args, **kwargs)

    def __rfloordiv__(self, b):
        result = self.__value.__rfloordiv__(b)
        return result if result is not NotImplemented else b.__floordiv__(self.__value)

    def __rlshift__(self, b):
        result = self.__value.__rlshift__(b)
        return result if result is not NotImplemented else b.__lshift__(self.__value)

    def __rmod__(self, b):
        result = self.__value.__rmod__(b)
        return result if result is not NotImplemented else b.__mod__(self.__value)

    def __rmul__(self, b):
        result = self.__value.__rmul__(b)
        return result if result is not NotImplemented else b.__mul__(self.__value)

    def __ror__(self, b):
        result = self.__value.__ror__(b)
        return result if result is not NotImplemented else b.__or__(self.__value)

    def __round__(self, *args, **kwargs):
        return self.__value.__round__(*args, **kwargs)

    def __rpow__(self, b):
        result = self.__value.__rpow__(b)
        return result if result is not NotImplemented else b.__pow__(self.__value)

    def __rrshift__(self, b):
        result = self.__value.__rrshift__(b)
        return result if result is not NotImplemented else b.__rshift__(self.__value)

    def __rshift__(self, b):
        result = self.__value.__rshift__(b)
        return result if result is not NotImplemented else b.__rrshift__(self.__value)

    def __rsub__(self, b):
        result = self.__value.__rsub__(b)
        return result if result is not NotImplemented else b.__sub__(self.__value)

    def __rtruediv__(self, b):
        result = self.__value.__rtruediv__(b)
        return result if result is not NotImplemented else b.__truediv__(self.__value)

    def __rxor__(self, b):
        result = self.__value.__rxor__(b)
        return result if result is not NotImplemented else b.__xor__(self.__value)

    def __setformat__(self, *args, **kwargs):
        return self.__value.__setformat__(*args, **kwargs)

    def __setitem__(self, *args, **kwargs):
        return self.__value.__setitem__(*args, **kwargs)

    def __sizeof__(self, *args, **kwargs):
        return self.__value.__sizeof__(*args, **kwargs)

    def __str__(self, *args, **kwargs):
        return self.__value.__str__(*args, **kwargs)

    def __sub__(self, b):
        result = self.__value.__sub__(b)
        return result if result is not NotImplemented else b.__rsub__(self.__value)

    def __truediv__(self, b):
        result = self.__value.__truediv__(b)
        return result if result is not NotImplemented else b.__truediv__(self.__value)

    def __trunc__(self, *args, **kwargs):
        return self.__value.__trunc__(*args, **kwargs)

    def __xor__(self, b):
        result = self.__value.__xor__(b)
        return result if result is not NotImplemented else b.__rxor__(self.__value)
        
```

</details>

# 動作確認

## 使用方法

`Storable(初期値, format=修飾)`でインスタンス化して、`.value`に値を代入するだけです。

`format`は内部で`format.format(更新後の値)`のように使用されます。
例えば、`ndarray`は出力の際`array([1,2,3])`のように出力されますが、`format="np.{}"`としておけば`np.array([1,2,3])`と記録されます。

特殊メソッドオーバーライド版を使用すれば、以下のように自然に違いを意識せずに記述することができます。

```python
s = Storable(1)
a = s + 2
print(a, type(a)) # -> 3 int

s.value = [1, 2, 3, 4, 5]
for i in s:
    print(i)
# 1
# 2
# 3
# 4
# 5

s.value = 10
s += 10
print(s) # -> 20
print(type(s)) # -> <class 'Storable.Storable'>
```

## 動作の様子
こんなUIなしタイマーだって作れます。
![2024-04-18 22-03-38.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501281/240e4caf-109e-8bc9-3eb7-005319dfbe6f.gif)


# 動作の仕組み
- inpectでインスタンス化の際にその行番号を記録します。
- 値の更新にsetterを用いることで、値の更新のたびにソースコードを書き換える処理が呼び出されます。
- 事前に記録した行番号の位置に移動し、引数`value`に対応する値が記述されている箇所を正規表現でピックアップし、書き換えます。

### 注意
インスタンス化の際に行番号を取得しているため、何らかの原因で実行中に行番号がずれるようなことがあると値が更新されません。
文字列や配列で改行が入らないように簡単な処理を施していますが、それらをすり抜けた際には予期せぬ動作をするかもしれません。

# おわりに
これで小規模な一時記録ファイルから開放されますねﾔｯﾀｰ