DCCツールで頻出する前後処理をコンテキストマネージャで再利用可能にする

## 環境

* Windows
* Autodesk Maya 2025.3

## 前処理、後処理の再利用

DCCツールで前処理・後処理をパターン化して再利用できるようにするためには、with文に渡すためのコンテキストマネージャを作成すると便利です。

## コンテキストマネージャの作成について

### コンテキストマネージャ型

コンテキストマネージャは ```__enter__``` メソッドと ```__exit__``` メソッドを持つクラスを定義することで作成可能です。

```python
class SampleContext:
    def __init__(self, s):
        self._s = s

    def __enter__(self):
        print("enter")
        return self._s  # as キーワードで参照できる

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("exit")


def sample(s):
    with SampleContext(s) as f:
        print(f)
```

with文実行時には下記の順番で実行されます。

1. ```__enter__``` メソッドの処理
1. withブロック内の処理
1. ```__exit__``` メソッドの処理

```python:出力結果
>>> sample("sample")
enter
sample
exit
```

### @contextlib.contextmanager

標準ライブラリにある ```@contextlib.contextmanager``` デコレータを使用すると簡潔に定義できます。

```python
from contextlib import contextmanager


@contextmanager
def sample_context(s):
    print("enter")
    try:
        yield s  # as キーワードで参照できる
    finally:
        print("exit")


def sample(s):
    with sample_context(s) as f:
        print(f)
```

yield文より前に書かれたコードが前処理として、yield文より後に書かれたコートが後処理として実行されます。

```python:出力結果
>>> sample("sample")
enter
sample
exit
```

```@contextlib.contextmanager``` で作成するその他のメリットとしてデコレータとしての利用も可能な点が挙げられます。

```python
from contextlib import contextmanager


@contextmanager
def sample_context():
    print("enter")
    try:
        yield
    finally:
        print("exit")


@sample_context()
def sample_deco():
    print("sample_deco")
```

```python:出力結果
>>> sample_deco()
enter
sample_deco
exit
```

似たようなものとしてクラスベースのコンテキストマネージャをデコレータとして使用できるようにする基底クラス `ContextDecorator` などもあります。

## Maya での利用例

DCCツールでの利用例として、Mayaの例を下記に記載します。

### mayapy での初期化

バッチ処理での利用例です。

```python
from contextlib import contextmanager
import maya.standalone


@contextmanager
def mayapy_initialize():
    maya.standalone.initialize()
    try:
        yield
    finally:
        maya.standalone.uninitialize()
```

```python:利用例
if __name__ == "__main__":
    import maya.cmds as cmds

    with mayapy_initialize():
        print(cmds.polyCube())
```

### undo/redo

Qt for Python で実装したGUIのコールバック関数にデコレートする例です。

```python
from contextlib import contextmanager

from maya import cmds


@contextmanager
def undoredo():
    cmds.undoInfo(ock=True)
    cmds.refresh(su=True)
    try:
        yield
    finally:
        cmds.refresh(su=False)
        cmds.undoInfo(cck=True)
```

```python:利用例
from PySide6 import QtWidgets

from maya import cmds
from maya.app.general.mayaMixin import MayaQWidgetBaseMixin


class Window(MayaQWidgetBaseMixin, QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        widget = QtWidgets.QWidget()
        self.setCentralWidget(widget)

        layout = QtWidgets.QVBoxLayout()
        widget.setLayout(layout)

        self._button = QtWidgets.QPushButton("Apply")
        self._button.clicked.connect(self._button_clicked)
        layout.addWidget(self._button)

    @undoredo()
    def _button_clicked(self, *args):
        cmds.polySphere()
        cmds.polyCube()
        cmds.polyCylinder()
```

## 参考

* [with文とコンテキストマネージャ](https://docs.python.org/ja/3.13/reference/datamodel.html#with-statement-context-managers)
* [コンテキストマネージャ型](https://docs.python.org/ja/3.13/library/stdtypes.html#typecontextmanager)
* [@contextlib.contextmanager](https://docs.python.org/ja/3/library/contextlib.html#contextlib.contextmanager)
* [Maya セッション外で Python スクリプトを実行する](https://help.autodesk.com/view/MAYADEV/2025/JPN/?guid=Maya_DEVHELP_Maya_Python_API_PythonOutsideMayaSession_html)