existsメソッドってなんなんですか？

本記事は[Maya Advent Calendar 2020](https://qiita.com/advent-calendar/2020/maya) 18日目の記事です。
今年も（？）完走したいので今年遭遇した現象を書きます。
※完全にデバッグしきれている記事ではありません。推測止まりになっていることをご了承ください。

# 事の発端
pymelで何かを処理する前に、処理対象があるかどうかを確認するときにこんな書き方をすることはありませんか？

```python
if node.exists():
    # なにかしらの処理
```

まさにこんな書き方でノードが存在するかどうかを確認したときに事件は起きました。

## maya2017 up5の場合
- maya2017 up5
- PyMEL Version 1.0.9

```python
import pymel.core as pm

hoge = pm.createNode('transform', n='hoge')
print hoge.exists()
# Result: True
```

作ったノードが存在するので結果はTrueが帰ってきますよね。今度は作ったノードを削除してから確認してみましょう。

```python
import pymel.core as pm

hoge = pm.createNode('transform', n='hoge')
pm.delete(hoge)
print hoge.exists()
# Result: False
```

うんうん、それはそう。
## maya2019 up3の場合
問題は同じコードをmaya2019のup3で実行したときです。

- maya2019 up3
- PyMEL Version 1.0.10

```python
import pymel.core as pm

hoge = pm.createNode('transform', n='hoge')
pm.delete(hoge)
print hoge.exists()
# Result: True
```

？
**True** ってどういうことだ？？？

存在しないはずのノードに対して`exists()`メソッドがTrueを返してきていることが判明しました。
今回はこれに関してちょっと調べてみようと思います。

# とりあえず解決したい
ノードの存在を確認して、あったら処理する。みたいな記述はよくやると思います。
この書き方をしていたスクリプトがmaya2019 up3で正常に動作しないのは仕事的にもヤバいので、とりあえずの解決方法を探しました。

```python
import pymel.core as pm

hoge = pm.createNode('transform', n='hoge')
pm.delete(hoge)
print hoge.exists()
# Result: True

print pm.objExists(hoge)
# Result: False
```

なるほど、`pm.objExists()` を使えばとりあえず正しい状態を検出できそうです。

# exists()メソッドの挙動を探ってみる
`exists()`メソッドが何を判断してブーリアンを返すかを [pymel/general.py](https://github.com/LumaPictures/pymel/blob/master/pymel/core/general.py) から順に探っていきます。

```general.py
def exists(self, **kwargs):
    "objExists"
    try:
        # use __apimobject__, not __apiobject__, because that's the one
        # that calls _api.isValidMObjectHandle (ie, we don't want to get
        # an MDagPath, which won't do that validation)
        if self.__apimobject__():
            return True
    except MayaObjectError:
        pass
    return False
```

`self.__apimobject__()`があるかどうかで判断しているようです。では`__apimobject__()`は何をもって判断しているのか？

```general.py
import pymel.api as _api

def __apimobject__(self):
    "Return the MObject for this attribute, if it is valid"
    try:
        handle = self.__apiobjects__['MObjectHandle']
    except:
        handle = _api.MObjectHandle(self.__apimplug__().attribute())
        self.__apiobjects__['MObjectHandle'] = handle
    if _api.isValidMObjectHandle(handle):
        return handle.object()

    raise MayaAttributeError
```

MObjectHandleを取得して`_api.isValidMObjectHandle(handle)`の結果で判断しているみたいですね。これも見てみましょう。
[pymel/api/allapi.py](https://github.com/LumaPictures/pymel/blob/master/pymel/api/allapi.py)

```allapi.py
# fast convenience tests on API objects
def isValidMObjectHandle(obj):
    if isinstance(obj, MObjectHandle):
        return obj.isValid() and obj.isAlive()
    else:
        return False
```
MObjectHndleクラスから作られたインスタンス（オブジェクト）であることをチェックしているようです。
2017, 2019の両方の環境でこのapiを実行するとともに同じ**False**が帰ってきました。

## 検証コード
`exists()`メソッドの挙動から以下のコードのどこかでmaya2017, maya2019に差がでるはず、と推測しました。
では実行してみましょう。

```python
import pymel.api as _api
import pymel.core as pm

hoge = pm.createNode('transform', n='hoge')
pm.delete(hoge)

handle = hoge.__apiobjects__['MObjectHandle']

print handle.isValidMObjectHandle()
```

maya2017では **False** が表示されます。
maya2019では`handle = hoge.__apiobjects__['MObjectHandle']`の部分でhandleが取得できませんでした。
このことからmaya2017ではgeneral.pyのtryが成功し、maya2019では失敗しているのでexceptが実行されているっぽいですね。

```general.py
import pymel.api as _api

def __apimobject__(self):
    "Return the MObject for this attribute, if it is valid"
    try:
        handle = self.__apiobjects__['MObjectHandle'] # maya2017はこっちのhandle？
    except:
        handle = _api.MObjectHandle(self.__apimplug__().attribute()) # maya2019はこっちのhandle？
        self.__apiobjects__['MObjectHandle'] = handle
    if _api.isValidMObjectHandle(handle):
        return handle.object()

    raise MayaAttributeError
```

exceptで`handle = _api.MObjectHandle(self.__apimplug__().attribute())`が実行されています。
このhandleがMObjectHndleインスタンス（オブジェクト）なので、`isValidMObjectHandle(obj)`が、`exists()`メソッドで **True** になると推測できます。

## もう少し調べたいこと
`__apimobject__()`の`_api.MObjectHandle(self.__apimplug__().attribute())`あたりをもう少し詳しく調べたかったのですが、アドベントカレンダーのタイムリミットがきてしまいました。。。
時間ができたらステップ実行などでもう少し詳しくデバッグして追記します！:pray: