プログラミング言語"Python"とワンボードマイコン"Arduino"を利用したSwitchの自動操作環境プラットフォームであるPoke-Controller (以下Poke-Con)は、有志による機能追加(Poke-Controller-modified, Poke-Controller-Modified-Extension)が積極的に取り組まれており、自動化スクリプトの開発・配布・利用も盛んです。
私自身も自動化スクリプトの実装のためPoke-Conを活用しておりますが、自動化スクリプトの実装方法や内部の設計については幾つか気になっている点があり、更なる機能追加やパフォーマンスの改善などを行う場合の障壁になるのではという懸念を抱いています。
とはいえ、その懸念点の解消を理由として現行仕様と全く異なる設計思想での再実装を提案したところで、旧仕様との互換性の観点などからコミュニティに受け入れられるかは別の話ですし、そもそもその懸念点自体の妥当性も不確かです。
そこで、まずは現行の実装を読み解くことで問題点を整理し、そのうえでPython上でSwitchの自動化スクリプトがどうあるべきか、ある種の"ぼくのかんがえるさいきょうの自動化基盤アプリ"のモックアップなども交えながら考えを纏めてみることにします。
今回は初回ということで、Poke-Conの実装で気になっているポイントを幾つかピックアップして説明します。
以下、Poke-Controller-Modified-Extensionを参照しつつ整理を進めてみます。
Poke-Conにおけるマクロスクリプトは、CommandBase
クラスを継承したPythonCommand
クラスあるいはImageProcPythonCommand
クラスをさらに継承した新たなクラスを定義し、その抽象メソッドであるdo
メソッドを具象化する形で記述します。
また、do
メソッドが長大化する場合は、必要に応じてインスタンスメソッドを定義して分割します。実際に書いてみるとこんな感じ。
ここで、foo
メソッドを使いまわすことで、マクロスクリプトを部分的に再利用できないかを考えてみます。このとき取りうる方法としては、主に以下の二パターンが思い浮かびます。
しかしながら、いずれもあまり好ましい方法とは言えません。
前者の主な問題はメンテナンス性や可読性にあります。コピー元のコードに不具合があった場合にコピー先のあらゆるコードを修正する手間がかかるでしょうし、同じようなメソッドが乱立して見通しも悪くなります。
後者に関しても、複数のクラスを同時に継承することで、メソッド解決順序やインスタンス変数の共有に起因した予期せぬバグを引き起こしたりする可能性もあります。そもそも、継承という手段はメソッド単位での再利用という目的に対してはあまりに大がかりかつ複雑で、初心者にもやさしくありません。
総じて、再利用性の低いad-hocなメソッドが乱立する羽目になります。例え多くのマクロにみられるような汎用的な処理であっても、それらを切り出すことが難しくなってしまっているのが現状です。
マクロスクリプトにおけるキー入力コマンドは、PythonCommand
のインスタンスメソッドとして定義されています。
例えば、指定された時間だけキー入力を実行するpress
メソッドの実装は以下のようになっています。
何やら見慣れないKeyPress
インスタンス(keys
)を使ってアレコレしていますね。どうやらこれは外からSender
インスタンスが与えられる形で初期化されているようです。
さらに実装を追ってみましょう。KeyPress
の中身を覗いてinput
メソッドとinputEnd
メソッドを抜粋してみます。
すごいわちゃわちゃしていますね。
どうやらinput
メソッドは、入力情報のフォーマットを行うSendFormat
インスタンス(self.format
)に対して各キー入力を与えてシリアル通信用文字列を生成し、それをSender
インスタンス(self.ser
)のwriteRow
メソッドに与えて通信を行っているようですね。
キー入力の一連の処理をシーケンス図っぽく整理してみるとこんな感じ。
KeyPress
はPythonCommand
から独立したクラスになっているのが少し奇妙に思えます。わざわざKeyPress
を介してSendFormat
の状態を操作し、Sender
で通信を行う理由が見いだせません。
また、各クラスの持つ状態についても気になる点が見受けられます。
KeyPress
, SendFormat
, Sender
クラスの__init__
メソッドを覗いてみましょう。
KeyPress
SendFormat
Sender
KeyPress
はどのボタンがホールドされているか(self.holdButton
)、SendFormat
はL/Rスティックの入力に変更があったか(self.L_stick_changed
/self.R_stick_changed
)、現在の十字キーの入力(self.Hat_pos
) を状態として持っています。キー入力の状態が各クラスに分散しており、誰が何の状態を持つのかの境界があやふやです。
こうした複雑怪奇な設計の元で新たな機能追加や改修を行うのにはしばしば困難を伴います。
Poke-Conにおいて、現在のループ回数や条件分岐の結果、画像認識結果といった情報をログウィンドウに出力するにはprint
関数を利用します。この処理は一見すると何の変哲もない処理に見えますが、実のところ極めて奇妙な振る舞いをしています。
print
関数は与えられた文字列を任意のテキストストリームに出力する関数です。通常はデフォルト引数として標準出力(sys.stdout
)をテキストストリームとして実行します。言い換えれば、テキストストリームとして何らかのオブジェクトを指定しない限り、print
関数と全く無関係のログウィンドウに何かが出力されることはあり得ません。
この奇妙な振る舞いの原因はWindow.py
内の以下の処理にあります。
つまり、標準出力オブジェクトそのものをファイルオブジェクトと同等の振る舞いをするユーザー定義オブジェクトで上書きしている訳です。一種のダックタイピングですね。
とはいえ、print
関数でログ出力をしたいという目先の簡単のためだけに標準出力をリダイレクトしてしまうというのは余りに大がかりな印象を受けます。GUI上に何かしらを表示したいということであれば、キー入力処理などと同様に、PythonCommand
あるいはImageProcPythonCommand
上で専用のメソッドを定義したうえで、そこを経由する形でログ出力するべきだと考えます。