Mayaのツール開発で書くパターン集


この記事は、[MayaPython アドベントカレンダー](https://qiita.com/advent-calendar/2017/maya-python) 9日目の記事です。
Mayaのツール開発でよく使っているフレーズをまとめて見ました。

デザインパターンのことは書いていませんので、間違えて入ってこられた方はご了承下さい。


## Maya 編

### sys.pathを通す / pyc 作らない

```python:sys
import sys
sys.dont_write_bytecode = True

libpath = "//server/dev/app/maya/tools/python"
if not libpath in sys.path:
    sys.path.append(libpath)
```

個人環境であれば気にすることはないですが、
スタジオでツールを共有するシチュエーションでは、よく使います。


### Qt.pyを使う
https://github.com/mottosso/Qt.py

PySide2, PySide, PyQt5, PyQt4 をラッピングしてくれるサードパーティモジュールです。
例外処理で長いファイルヘッダーを自前で書くべきではありません。
シングルファイルなので、ツールに組み込んで配布しやすいのも良いですね。

私は、C++ like に書きたいので、スターインポートしています。

```python:Qt
from Qt.QtGui import *
from Qt.QtCore import *
from Qt.QtWidgets import *
```

shiboken or shiboken2 (sip) の wrapInstance も以下のコードで共用出来ます。

```python
from Qt import QtCompat
QtCompat.wrapInstance()
```

Houdini と同じく、Qt.py をプリインストールしてくれれば、わざわざ布教する必要もないのですが...
技術ブログ界隈で使っておられる方が多いようなので、これからの標準でしょうか。


### シーンを元に戻す
```python:undo
import maya.cmds as cmds

cmds.undoInfo(ock=1)
cmds.refresh(su=1)

# 書きたい処理

cmds.refresh(su=0)
cmds.undoInfo(cck=1)
cmds.undo()
```

スクリプト処理前に戻ります。
シーン状態を整理するので、エクスポートの時に使うことが多いかも。


### スタンドアロン or スクリプトを区別する

```python:QApplication
# Qt.py import 済み状態で

if not QApplication.instance():
    app = QApplication(sys.argv)

    # view を呼ぶ処理

    sys.exit(app.exec_())

else:
    # view を呼ぶ処理

```

PySide を、Mayaスクリプトエディタでもスタンドアロンでも起動できるようにします。
view と controller を適切に分離すれば、スタンドアロン経由の mayapy 起動なんてこともできますね。


## ロジック編
私がよく書くパターンを紹介します。

### 疑わしきは try except

```python:traceback
import traceback

try:
    # …
except Exception:
    print (traceback.format_exc())
```

文字列を定義するよりわかりやすいエラーメッセージが見えるので使います。


### for と if を減らす
forとifを減らすことは、可読性をあげつつメンテナンスしやすいコードになるので、
意識して減らします。いくつか方法があります。

#### 1. itertools.prodect 使う

```python
import itertools

for m, n in itertools.product(members, numbers):
    ...
```

要素数が同じ配列同士なら zip 関数ですね。itertools は色々なパターンの
アルゴリズムが入っているので、for文の中で条件分岐するようなコードがあれば、
itertools の出番かもしれません。車輪の再発明をする必要はありません。

#### 2. all or any

```python
if all([f for f in exp_files if os.path.exists(f)]):
    ...
```

リスト内包表記と組み合わせて、要素の中身を全て洗います。
for文とif文だけで書くと、面倒くさくて読みにくいコードになりそう。

#### 3. None 回避の or

```python
import maya.cmds as cmds
from Qt.QtGui import *

cmds.file(q=1, sn=1) or QFileDialog.getOpenFileName()
```

if 文で None チェックするより書き方がスマートです。


```python
for item in cmds.listRelatives() or []:
    ...
```

`listRelatives` や `listConnections` など要素が None になる可能性がある関数でエラーが起きません。`ls`コマンドは空のlistを返すという違いがあるので、戻り値の型を気にします。


### Python3
```python:python3
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
```

Mayaのツール開発でデバッガーが付いたエディタで開発する方が少数である以上、
print文が多く含まれることが予測されるツールは、未来の Python3 でエラーになります。
問題は未然に防いでおきたいので、print 関数で書いておきます。

参考: https://docs.python.jp/3.6/howto/pyporting.html#prevent-compatibility-regressions



## 書き方編
もはや Python の書き方じゃないかと突っ込まれそうですが、
MayaでPythonを書いているとありがちな内容なのでまとめます。


### 引数を２個以上記入する際は改行する

```python
import maya.cmds as cmds

cmds.setAttr(
    "{}.dso".format(shape),
    "{}/{}/{}.ass".format(
        os.path.dirname(os.path.dirname(dir)), "element/ASS", file
    ),
    typ="string"
)
```

MELプロシージャは、Bad Python と言えるほどに引数が多いので、
視認性を考えて改行します。リスト内包表記も同様です。


### シーケンスを引数へ一度に展開する
xform や getattr で得たlistに都度インデックスアクセスして書くより、アスタリスクで要素展開するとシンプルです。
MVector や MColor 等は `__getitem__` を実装しているので同じように展開できます。(スライシングは無理)
 
```python
import maya.cmds as cmds
import maya.api.OpenMaya as om

v0 = om.MVector(1, 4, 1) #cmds.xform("pSphere1", t=1, q=1)
cmds.setAttr("pSphere1.translate", *v0)
```

引数の長さはきっちり合わせる必要があります。


### listのアンパックに _ を使う

使わないトランスフォームなど、変数名を割り当てずに明示的に使用しないことを表現するために、
Swift like なアンダースコアを使います。

```python
a, _ = cmds.listRelatives()
```

インデックスによるリストへのアクセスと使い分けます。


### パス置換

```python
import os
string.replace(os.sep, "/")
```

Windows環境はよく使いますね。



## 運用編
ツールを試して貰う際に起きやすい問題に対処します。


### 察しない。ログを出す
ローカル環境でうまくいっていたことが他環境でうまくいかないことが多いので、
Mayaのバージョン、チェックしているファイル、作成日時など、参考にできる情報は全てログに流します。

```python
cmds.about(q=1, v=1), cmds.about(q=1, cd=1), cmds.about(q=1, ct=1), cmds.file(q=1, sn=1)
```
`logging` や `print` とか。進んでいる職場ではチケットシステムに統合されているのでしょうか？


### 絵で説得する
ロジックは完璧なのに求める絵が違うことが、稀に発生します。
それはアーティストにとってエラーと同義なので、早い段階で絵を共有しておくと問題になりにくいです。

自前のコードを載せようと思いましたが、既に開発済みのものがあるので、有難く使わせて頂きます。
https://github.com/guncys-inc/saveScreenShot


### プラグインのチェック
```python:plugins
# 2016 later
for p in cmds.unknownPlugin(q=1, l=1) or []:
    cmds.unknownPlugin(p, r=1)
```

個々の環境の違いで、消耗したくないですね。


## まとめ
過去に解決済みの課題を同じようなロジックで書くことが、割とあります。
特にテクニカルは共同でコードを書く経験が少ないので、車輪の再発明をしがちです。

既知のものが多いかと思いますが、1つでも参考になることがあれば幸いです。
皆様が持っている知識があれば、ぜひ教えてください。