Kivyは至るところで弱参照を用いていて、それに関する注意点が幾つかあります。
2つの変数が参照しているWidgetの同一性を確かめるのにis演算子が使えるか?
from kivy.lang import Builder
root = Builder.load_string(r'''
FloatLayout:
Widget:
id: hoge
''')
weak_ref = root.ids.hoge
direct_ref = root.children[0]
print(weak_ref)
print(direct_ref)
print('-----------------------------')
print(repr(weak_ref))
print(repr(direct_ref))
print('-----------------------------')
print(weak_ref is direct_ref)
<kivy.uix.widget.Widget object at 0x7fa2d88a06c8>
<kivy.uix.widget.Widget object at 0x7fa2d88a06c8>
-----------------------------
<WeakProxy to <kivy.uix.widget.Widget object at 0x7fa2d88a06c8>>
<kivy.uix.widget.Widget object at 0x7fa2d88a06c8>
-----------------------------
False
このようにKivyがどこで弱参照を用いているか分からないため普通にis演算子を使うのは 危険 と言えます。危険という表現は大げさに聞こえるかもしれませんが本当です。こういうErrorにならない間違いというのが一番やっかいで、実際私はこれのせいで一時間近く潰れました。
でこれに対する対策なんですが、以下の三つが思い浮かびます。
print(weak_ref == direct_ref) # A
print(weak_ref.uid is direct_ref.uid) # B
print(weak_ref.__self__ is direct_ref.__self__) # C
True
True
True
uidはWidgetに割り当てられた一意の整数値です(B行)。__self__はWidgetの直接参照を得られるpropertyで実装コードは以下のようになっています。
class Widget(WidgetBase):
@property
def __self__(self):
return self
これのおかげで変数が弱参照だろうと直接参照だろうと常に直接参照が得られるわけです。なのでC行は確実に直接参照同士をis演算子で比較する方法と言えます。
WeakMethodに注意
次のコードを実行すると
import gc
from functools import partial
from kivy.uix.button import Button
class Hoge:
def __init__(self, value):
self.value = value
def callback(self, *args):
print(self.value)
hoge = Hoge('A')
button = Button()
button.bind(on_press=hoge.callback)
button.bind(on_press=Hoge('B').callback)
button.bind(on_press=partial(Hoge('C').callback))
button.bind(on_press=partial(Hoge.callback, Hoge('D')))
button.bind(on_press=lambda *args: print('E'))
gc.collect() # garbage collection
button.dispatch('on_press')
E
D
C
A
Bだけが呼ばれてないのが分かります。これは最初なかなか不思議に思った現象で、もしKivy側がbind()で渡された関数を直接参照で保持しているのならAからEまで全て呼ばれるはずで、逆にKivy側が弱参照で保持しているのならA以外は呼ばれないはずです。何故このようにどちらでもない結果になったのでしょうか?
理由はbind()が内部で使っているWeakMethodという物にあります。WeakMethodは条件付きの弱参照で、
- instance methodに対しては弱参照を作る
- それ以外のcallableの場合は直接参照を保持する
という特徴があるようです。これを踏まえて表に纏めてみると
| こちら側の参照 | Kivy側の参照 | |
|---|---|---|
| A | 直接 | 弱 |
| B | 無 | 弱 |
| C | 無 | 直接 |
| D | 無 | 直接 |
| E | 無 | 直接 |
Bだけ直接参照が無いですね、なのでgarbage collectionの対象になり呼ばれなかったわけです。
実はEventDispatcherに関して言えば、bind()に代えてfbind()を使うことで上記の問題を避けられます。
button.fbind('on_press', hoge.callback)
button.fbind('on_press', Hoge('B').callback)
button.fbind('on_press', partial(Hoge('C').callback))
button.fbind('on_press', partial(Hoge.callback, Hoge('D')))
button.fbind('on_press', Hoge.callback, Hoge('E')) # Dはpartialを使わずともこのように書ける
button.fbind('on_press', lambda *args: print('F'))
F
E
D
C
B
A
fbind()は既定では常に直接参照を使うからです。一応fbind()にWeakMethodを使うように指示した場合にどうなるかも確かめておきます。
button.fbind('on_press', hoge.callback, ref=True)
button.fbind('on_press', Hoge('B').callback, ref=True)
button.fbind('on_press', partial(Hoge('C').callback), ref=True)
button.fbind('on_press', partial(Hoge.callback, Hoge('D')), ref=True)
button.fbind('on_press', Hoge.callback, Hoge('E'), ref=True)
button.fbind('on_press', lambda *args: print('F'), ref=True)
F
E
D
C
A
ref=Trueがその為の引数で、結果Bだけ呼ばれなくなりました。
ClockもWeakMethodを使っています。Clockの場合はEventDispatcher.fbind()のように必ず直接参照を使ってくれるmethodは無いため、そうしたいならこちら側でpartialかlambdaで包んであげないといけません。
import gc
from functools import partial
from kivy.clock import Clock
class Hoge:
def __init__(self, value):
self.value = value
def callback(self, *args):
print(self.value)
Clock.schedule_once(Hoge('A').callback)
Clock.schedule_once(partial(Hoge('B').callback))
Clock.schedule_once(partial(Hoge.callback, Hoge('C')))
Clock.schedule_once(lambda __: Hoge('D').callback())
gc.collect()
Clock.tick() # 内部用method。こうすることでApp.run()やrunTouchApp()を呼ばずともClock関連の処理を進められる
B
C
D