AliasPropetyを選んだ理由
次のように機能する物をPythonの通常のプロパティではなくKivyのプロパティで実装したかった。
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という正に今欲しい物のように聞こえる機能がありますがこれだと
- 閾値自体はKivyのプロパティではないので監視できない。
- 閾値を越えた時に例外をあげてしまう。
- 閾値自体に変化が起きた時には値が閾値を越えているかの確認をしない。
という点が不都合だったのでAliasProperty
を選びました。
セッター内で縛ってみる
値を特定の範囲に縛る事をこれから正規化と呼ぶことにします。この正規化をどのタイミングでするかなんですが、普通に考えるとmax
やmin
やvalue
に値が書き込まれたときじゃないでしょうか?なので私がこのValueClamper
を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版を実装するとこうなります。
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)
しかし実際に使ってみると
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、つまり変化が無いにも拘らず通知が来ています。
Kivyのプロパティは値に変化が起きた時に通知してくれる仕組みを持っています。詳しくは以下の記事を。
ゲッター内で縛ってみる
上での試みがうまくいかなかったのはvalue
とmin
とmax
のいずれに書き込んでも_set_value()
が呼び出されると思い込んでいたからでした。実際にはvalue
に書き込んだ時のみであり、セッターは正規化には不適切(あるいは不十分)な場所であると言えます。
セッターが駄目ならゲッターしかありません。
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
は期待通り正規化されましたが、過剰な通知が来ている点は変わりありません。加えて今度は別の問題も生じています。
vc = ValueClapmer(min=0, max=100)
print("--------")
vc.value
vc.value
getter
getter
--------
getter
getter
正規化は本来max
やmin
やvalue
に値が書き込まれた時のみ行えば十分なはずですが、それに加えてvalue
を読み出す際にも行われてしまっています。
cache
幸い今出てきた無駄に正規化の処理が走る問題はcacheで解決できました。
class ValueClapmer(EventDispatcher):
value = AliasProperty(_get_value, _set_value, bind=("min", "max"), cache=True)
vc = ValueClapmer(min=0, max=100)
print("--------")
vc.value
vc.value
getter
--------
このcacheは説明を読む限り、ゲッターが返した値を記憶しておいてゲッターが再び呼ばれたら記憶しておいた値と比較、結果異なっていれば通知を行うものだと思うのですが、実際に動かしてみるとセッターがTrueを返した場合は比較の結果に拘らず通知が発生してしまうようです。なので通知を正しく機能させるためにはセッター側でも正規化された値を算出し、それを元の値と比較する必要がありそうです。
最終的な実装
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
は今回のような用途の為にあるのでは無いのかもしれない。