Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

概要

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 が返ってきます。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away