🙆

Dynamo で使う CPython の注意点

2021/06/23に公開

英語版の資料がありました…

https://github.com/DynamoDS/Dynamo/wiki/Work-in-progress-to-improve-Python-3-support

背景

Revit や Civil 3D に搭載されている Dynamo は、2021 版では 2.5 (Civil 3D) or 2.6 (Revit) だったものが、2022 版では一気に 2.10 までアップグレードされました。

その間にどんな改善があったかは、各バージョンのリリースノートで確認ができまして、例えば 2.9 は下記にあります。
https://dynamobim.org/dynamo-core-2-9-release/

2021 版から 2022 版への大きな機能改善と言うと、2.7 でリリースされた CPython への対応、および 2.9 でリリースされた Node Autocomplete が挙げられます。

Dynamo 上で使用する上で、IronPython2 と CPython3 では、公式の注意書きにある「Python 2 と Python 3 で記法が違う」ということ以外にも違いがあります。

ここでは、実際にコードを移管してみて上手く動かなかったところを随時、まとめていきます。

型の取得

例えば、取得した obj の型が Line かどうかを判定したい時、IronPython2 では

isLineType = obj.GetType() == Line

と書けば、実行時に「Line は型を表す」と勝手に判断してくれました。しかし Cpython3 では

isLineType = obj.GetType() == clr.GetClrType(Line)

と明示的に表現する必要があります。C# で typeof(Line) と書くような感じです。
ホントは IronPython2 でも clr.GetClrType(Line) と書くべきですね。

With ステートメント中のエラーメッセージ

Dynamo for Civil 3D では、Python の with(C# の using)を書くことが多いです。
LockDocument()StartTransaction() など、安全にドキュメントにアクセスするための
下準備が色々あり、それらをいちいち Dispose() するのが面倒だからです(多分)。

この with 内部で意図的にエラーを発生させてみます。たとえば

rightList = ['hoge', 'fuga']
wrongList = None
result = someFuncWithList(wrongList)

みたいに、「for loop を回そうとしたけど NoneType 受け取った、どうしよう」
みたいなエラーを発生させます(正式な名称が不明)。

エラーが出ると、CPython3 ではこう書かれます。

No method matches given arguments for OnExit:
(<class 'Autodesk.AutoCAD.ApplicationServices.DocumentLock'>,
<class 'type'>, <class 'TypeError'>, <class 'traceback'>)
['  File "<string>", line xxx, in <module>\n']

DocumentLock クラスにエラーが発生しているので、LockDocument() が原因のように思えます。

しかし、IronPython2 ではこう書かれます。

IronPythonEvaluatorEvaluateIronPythonScript 操作に失敗しました。
Traceback (most recent call last):
  File "<string>", line xxx, in <module>
  File "<string>", line xxx, in Func
TypeError: iteration over non-sequence of type NoneType

「あぁ、リストを期待されていたのに NoneType 渡しちゃったな」というのが分かります。

というわけで、with 節内のエラーは、まず with 節から出して判断するのが良さそうです。
with 節から出すと CPython3 も IronPython2 と同じエラーメッセージが出ます。
ちなみに、この違いがなぜ発生するかは全く分かりません…。

リスト処理(API 直アクセス)

例えば、下記のコードを実行します。
ここでは Civil 3D 線形スタイルの、ObjectId の「集合」を取得します。

alignmentStyleIds = CivilApplication.ActiveDocument.Styles.AlignmentStyles

例は何でも良いですが、Revit や Civil 3D はこのように「集合」を取得する場合があります。
alignmentStyleIds の型が何であるかは、下記のように書くと取得できます。

OUT = type(alignId)

見てみると、IronPython2 の場合の型は IronPython.Runtime.Types.PythonType で、
CPython3 の場合は Autodesk.Civil.DatabaseServices.Styles.AlignmentStyleCollection です。

AlignmentStyleCollectionIEnumerable<> を継承しているので、
for loop で繰り返し計算は出来ますが、alignmentStyleIds[0] のように
インデックスを指定して値を取得することが出来ません。

この場合、下記のような内包表記で集合を展開してあげます。

alignmentStyleIdsPy = [item for item in alignmentStyleIds]

そうすると、型が class 'list' になり、インデックスで要素を取得できます。

リスト処理(Dynamo ノード)

例えば、下記のコードを実行します。

list = [Geometry.Intersect(item, IN[1]) for item in IN[0]]

IN[0] の型は Geometry[] で、IN[1] の型は Geometry です。

list の型が何であるかは、下記のように書くと取得できます。

OUT = type(list)

見てみると、IronPython2 の場合の型は IronPython.Runtime.Types.PythonType で、CPython3 の場合の型は class 'list' です。

これに対して Dynamo ノードでリスト処理(e.g. List.Flatten)を行うと、IronPython2 の型は IronPython.Runtime.Types.PythonType のままですが、CPython3 の型は System.Collections.Generic.List[System.Object] に変わります。

このように、リスト処理の結果として型が変わることで、上手くプログラムが回らなくなる場合があります。その場合、Pythonic な書き方をすると型を変えずに済みます。

例えば List.Flatten の場合は、

× List.Flatten(list, -1)
○ [item for sublist in list for item in sublist]

とすると、型が class 'list' のままになり、上手くスクリプトが回ります。

参考:
https://stackoverflow.com/questions/952914/how-to-make-a-flat-list-out-of-a-list-of-lists

Discussion