AliasPropertyで値を特定の範囲に縛る仕組みを実装してみて気付いたこと

# AliasPropetyを選んだ理由

次のように機能する物をPythonの通常のプロパティではなくKivyのプロパティで実装したかった。

```python
vc = ValueClapmer(value=200, min=0, max=100)
print(vc.value)  # => 100
vc.value = -100
print(vc.value)  # => 0
vc.min = 50
print(vc.value)  # => 50
vc.value = 200
print(vc.value)  # => 100
vc.max = 70
print(vc.value)  # => 70
```

Kivyには[BoundedNumericProperty](https://kivy.org/doc/master/api-kivy.properties.html#kivy.properties.BoundedNumericProperty)という正に今欲しい物のように聞こえる機能がありますがこれだと

- 閾値自体はKivyのプロパティではないので監視できない。
- 閾値を越えた時に例外をあげてしまう。
- 閾値自体に変化が起きた時には値が閾値を越えているかの確認をしない。

という点が不都合だったので`AliasProperty`を選びました。

# セッター内で縛ってみる

値を特定の範囲に縛る事をこれから正規化と呼ぶことにします。この正規化をどのタイミングでするかなんですが、普通に考えると`max`や`min`や`value`に値が書き込まれたときじゃないでしょうか？なので私がこの`ValueClamper`をPythonのプロパティで実装するとしたら以下のようになります。

```python
# minとmaxの関係が min <= max である事を確かめる処理が抜けていますが目を瞑って下さい。
class ValueClapmer:
    def __init__(self, *, value=0, min=0, max=0):
        self._value = value
        self._min = min
        self._max = max
        self._clamp()

    def _clamp(self):
        self._value = max(self._min, min(self._max, self._value))

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, v):
        self._value = v
        self._clamp()

    @property
    def min(self):
        return self._min

    @min.setter
    def min(self, v):
        self._min = v
        self._clamp()

    @property
    def max(self):
        return self._max

    @max.setter
    def max(self, v):
        self._max = v
        self._clamp()
```

このように各セッターが呼ばれた時に正規化(`_clamp`)し、内部変数`_value`には常に正規化後の値をしまうというのが最初に思い浮かぶ実装です。そしてこれに倣ってKivy版を実装するとこうなります。

```python
from kivy.event import EventDispatcher
from kivy.properties import NumericProperty, AliasProperty


class ValueClapmer(EventDispatcher):
    min = NumericProperty()
    max = NumericProperty()

    def _get_value(self):
        return self._value

    def _set_value(self, value):
        value = max(self.min, min(self.max, value))
        if self._value != value:
            self._value = value
            return True
 
    value = AliasProperty(_get_value, _set_value, bind=("min", "max"))

    def __init__(self, **kwargs):
        self._value = 0
        super().__init__(**kwargs)
```

しかし実際に使ってみると

```python
vc = ValueClapmer(min=0, max=100)
vc.bind(value=lambda __, value: print("value:", value))
vc.value = 200
vc.value = -100
vc.min = 50
print(vc.value)
```

```
value: 100  # 期待通り
value: 0    # 期待通り
value: 0    # ??? (期待していたのは50)
0           # ??? (期待していたのは50)
```

見ての通り下限値を50に引き上げたのに`value`は0のままでした。そしておかしいのはそれだけではありません。上のコードではKivyプロパティの通知の仕組みを利用して`print`しているわけですが、0から0、つまり変化が無いにも拘らず通知が来ています。

:::note info
Kivyのプロパティは値に変化が起きた時に通知してくれる仕組みを持っています。詳しくは以下の記事を。
:::

https://qiita.com/gotta_dive_into_python/items/b7d8d19dd49599182c9d

# ゲッター内で縛ってみる

上での試みがうまくいかなかったのは`value`と`min`と`max`のいずれに書き込んでも`_set_value()`が呼び出されると思い込んでいたからでした。実際には`value`に書き込んだ時のみであり、セッターは正規化には不適切(あるいは不十分)な場所であると言えます。

セッターが駄目ならゲッターしかありません。

```python
class ValueClapmer(EventDispatcher):
    min = NumericProperty()
    max = NumericProperty()

    def _get_value(self):
        self._value = v = max(self.min, min(self.max, self._value))
        return v

    def _set_value(self, value):
        self._value = value
        return True
 
    value = AliasProperty(_get_value, _set_value, bind=("min", "max"))

    def __init__(self, **kwargs):
        self._value = 0
        super().__init__(**kwargs)


vc = ValueClapmer(min=0, max=100)
vc.bind(value=lambda __, value: print("value:", value))
vc.value = 200
vc.value = -100
vc.value = -100
vc.min = 50
print(vc.value)
```

```
value: 100  # 期待通り
value: 0    # 期待通り
value: 0    # 過剰な通知
value: 50   # 期待通り
50          # 期待通り
```

すると`value`は期待通り正規化されましたが、過剰な通知が来ている点は変わりありません。加えて今度は別の問題も生じています。

```python:ゲッターにprint("getter")を仕込んだ状態で実行
vc = ValueClapmer(min=0, max=100)
print("--------")
vc.value
vc.value
```

```
getter
getter
--------
getter
getter
```

正規化は本来`max`や`min`や`value`に値が書き込まれた時のみ行えば十分なはずですが、それに加えて`value`を読み出す際にも行われてしまっています。

# cache

幸い今出てきた無駄に正規化の処理が走る問題はcacheで解決できました。

```python:抜粋
class ValueClapmer(EventDispatcher):
    value = AliasProperty(_get_value, _set_value, bind=("min", "max"), cache=True)
```

```python:ゲッターにprint("getter")を仕込んだ状態で実行
vc = ValueClapmer(min=0, max=100)
print("--------")
vc.value
vc.value
```

```
getter
--------
```

このcacheは説明を読む限り、ゲッターが返した値を記憶しておいてゲッターが再び呼ばれたら記憶しておいた値と比較、結果異なっていれば通知を行うものだと思うのですが、実際に動かしてみるとセッターがTrueを返した場合は比較の結果に拘らず通知が発生してしまうようです。なので通知を正しく機能させるためにはセッター側でも正規化された値を算出し、それを元の値と比較する必要がありそうです。

# 最終的な実装

```python
class ValueClapmer(EventDispatcher):
    min = NumericProperty()
    max = NumericProperty()

    def _clamp(self, value):
        return max(self.min, min(self.max, value))

    def _get_value(self):
        self._value = v = self._clamp(self._value)
        return v

    def _set_value(self, value):
        value = self._clamp(value)
        if self._value != value:
            self._value = value
            return True

    value = AliasProperty(_get_value, _set_value, bind=("min", "max"), cache=True)

    def __init__(self, **kwargs):
        self._value = 0
        super().__init__(**kwargs)
```

というわけでセッターとゲッターの両方で正規化された値を計算するという醜い実装となってしまいました。もしセッターの`return True`が通知を行うか否かを決めるものではなくゲッターを呼ぶか否かを決める物であったなら、通知を行うか否かは依然としてキャッシュされた値との比較で決まるためセッター内でその判断を下す必要はなく、もっと綺麗に実装できていたところです。

# 感想

とにかく分かりにくかった。もしかすると`AliasProperty`は今回のような用途の為にあるのでは無いのかもしれない。