概要
import numpy as np
from kivy.properties import ObjectProperty
from kivy.uix.widget import Widget
class NdarrayWrapper(np.ndarray):
def __eq__(self, other):
return np.array_equal(self, other)
class NdarrayProperty(ObjectProperty):
def set(self, obj, val):
if not isinstance(val, NdarrayWrapper):
val = np.asanyarray(val).view(NdarrayWrapper)
return super().set(obj, val)
def get(self, *args, **kwargs):
val = super().get(*args, **kwargs)
return None if val is None else val.view(np.ndarray)
class MyWidget(Widget):
my_property = NdarrayProperty()
はじめに
PythonのGUIフレームワーク Kivy でデータバインディングを利用するとき、普通はPropertyを使います。
しかしこのProperty、値が更新されたかどうかを bool(a == b)
で判定しているため、Numpyの配列と相性がよくありません。
例えば以下のようにすると警告が出て値を更新できません。
import numpy as np
from kivy.properties import ObjectProperty
from kivy.uix.widget import Widget
class MyWidget(Widget):
my_property = ObjectProperty()
w = MyWidget()
w.my_property = np.array([1, 2, 3])
[WARNING] [Property ] Value comparison failed for <ObjectProperty name=texture> with "The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()". Consider setting force_dispatch to True to avoid this.
警告通り force_dispatch=True
にすれば値を更新することはできるようになりますが、値に変化が無くても通知が飛ぶようになり双方向バインディングできません。
解決方法
根本的な原因は ndarray.__eq__
の戻り値が bool
型でないことなので、こいつをオーバーライドしたサブクラスを用意します。
ndarray
とサブクラスの相互変換には ndarray.view()
メソッドが使えます。 (詳細: Subclassing ndarray)
class NdarrayWrapper(np.ndarray):
def __eq__(self, other):
return np.array_equal(self, other)
これを以下のようにして使います。
import numpy as np
from kivy.properties import ObjectProperty
from kivy.uix.widget import Widget
class NdarrayWrapper(np.ndarray):
def __eq__(self, other):
return np.array_equal(self, other)
class MyWidget(Widget):
my_property = ObjectProperty()
w = MyWidget()
w.my_property = np.array([1, 2, 3]).view(NdarrayWrapper)
一応これで動きますが、値更新するたびにこんなことやってられないのでProperty側もサブクラスを作ります。
class NdarrayProperty(ObjectProperty):
def set(self, obj, val):
if not isinstance(val, NdarrayWrapper):
val = np.asanyarray(val).view(NdarrayWrapper)
return super().set(obj, val)
def get(self, *args, **kwargs):
val = super().get(*args, **kwargs)
return None if val is None else val.view(np.ndarray)
これでちゃんと NdarrayWrapper
に変換してから格納され、ndarray
として取り出されます。
注意事項
NdarrayProperty
は ObjectProperty.dispatch()
をオーバーライドしていないので、値変更時の通知で送られてくるデータは NdarrayWrapper
のままです。
そのため要素ごとの比較のつもりで a == b
するとハマるのでお気をつけ下さい。bool
が返ってきます。