Python
numpy
Kivy

force_dispatch=FalseのままKivyのPropertyでNumpyの配列を扱う

概要

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 として取り出されます。

注意事項

NdarrayPropertyObjectProperty.dispatch() をオーバーライドしていないので、値変更時の通知で送られてくるデータは NdarrayWrapper のままです。
そのため要素ごとの比較のつもりで a == b するとハマるのでお気をつけ下さい。bool が返ってきます。