Kivyの弱参照の注意点

Kivyは至るところで弱参照を用いていて、それに関する注意点が幾つかあります。

### ２つの変数が参照しているWidgetの同一性を確かめるのに`is演算子`が使えるか？

```python
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)
```

```text
<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にならない間違いというのが一番やっかいで、実際私はこれのせいで一時間近く潰れました。

でこれに対する対策なんですが、以下の三つが思い浮かびます。

```python
print(weak_ref == direct_ref)  # A
print(weak_ref.uid is direct_ref.uid)  # B
print(weak_ref.__self__ is direct_ref.__self__)  # C
```

```text
True
True
True
```

`uid`はWidgetに割り当てられた一意の整数値です(B行)。`__self__`はWidgetの直接参照を得られるpropertyで実装コードは以下のようになっています。

```python
class Widget(WidgetBase):
    @property
    def __self__(self):
        return self
```

これのおかげで変数が弱参照だろうと直接参照だろうと常に直接参照が得られるわけです。なのでC行は確実に直接参照同士を`is演算子`で比較する方法と言えます。

### WeakMethodに注意

次のコードを実行すると

```python
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')
```

```text
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()`を使うことで上記の問題を避けられます。

```python
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'))
```

```text
F
E
D
C
B
A
```

`fbind()`は既定では常に直接参照を使うからです。一応`fbind()`に`WeakMethod`を使うように指示した場合にどうなるかも確かめておきます。

```python
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)
```

```text
F
E
D
C
A
```

`ref=True`がその為の引数で、結果Bだけ呼ばれなくなりました。

Clockも`WeakMethod`を使っています。Clockの場合は`EventDispatcher.fbind()`のように必ず直接参照を使ってくれるmethodは無いため、そうしたいならこちら側でpartialかlambdaで包んであげないといけません。

```python
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関連の処理を進められる
```

```text
B
C
D
```