記事の目次

    みなさんこんにちは。最近は3Dプリンタを使って3Dプリンタのパーツをひたすら出力する日々です。ところが、最近は世界中で半導体やそのほか様々なパーツが入手しづらくなっている影響で、部品を購入しようにも入荷まで1ヶ月以上かかるような品物があって困ったナァとなることが多いです。趣味でやっているからまだいいものの、仕事でやっていてこれだと大変ですね。改めてハードウェアの大変さを身に染みて感じているところです。

    TEXT_痴山紘史 / Hiroshi Chiyama(日本CGサービス
    EDIT_尾形美幸 / Miyuki Ogata(CGWORLD)



    処理の内容を適切に管理する

    第34回ではコードのリファクタリングをしながら、コードの形について考えました。形を整えることでコードは読みやすくなり、メンテナンス性も高くなります。今回はコードの整理や処理内容の管理を行う際に "処理や意味の切り分け" を意識する必要性について考えていきます。

    GUIと処理の分離

    映像制作の補助をするためのツールを作成する場合、GUIの作成は避けて通ることができません。主なユーザーであるデザイナーさんはプログラムについてあまり詳しいわけではなく、むしろ苦手意識のある人の方が多いくらいなので、ちょっとした機能でもボタンなどのGUIを使って操作できることを求められます。そのため、ツールを作成する際にもGUIの作成から入ることが多く、すぐにGUIなしでは動かすことのできないプログラムができあがってしまいがちです。

    GUIなしで動かすことのできないプログラムは大きな問題をたくさん抱えることになります。まず、テストを作成することが困難になります。テストを作成すれば何十回でも何百回でも自動的にプログラムがきちんと動いていることを確認できるのに、それがないとプログラムを更新する度に全ての機能を手で触って確認しなければいけません。最初はいいですが、開発が進んで機能が複雑になっていくにつれて動作確認のためのコストが急増していきます。

    さらに、プログラムがGUIの構造にベッタリとくっついてしまうため、抜本的なプログラムの改良を行いづらくなってしまいます。また、使用するGUIライブラリが変更されると、プログラムを移植することが困難になります。DCCTool上のPythonで使用するGUIライブラリですら、この10年で wxWidgets→PyQt→PySide→PySide2 と移り変わってきています。ここ数年はQt系のもので落ち着いてはいるものの、Qtの開発体制も盤石ではないため、何時ながれが変わるかもわかりません。そんな中、作成したプログラムが特定のGUIライブラリに強く依存するというのは避けたいです。

    では、具体的にどうすればいいでしょう?

    GUIプログラムを書いていると、以下のようなコードを書いてしまうことがよくありあます。onRunBtnClicked はGUI上のRunボタンが押されたときに呼ばれる関数です。

    def onRunBtnClicked(self):
        self.do_some_pre_process()
            :
        self.do_some_process()
            :
        self.do_some_post_process()
    

    ボタンが押されたときに実行したい内容を onRunBtnClicked に直接書いてしまっている例です。ボタンを押したときの処理を書いているからいいのでは? と思うかもしれませんが、これはあまりいい内容とは言えないです。この歪みは、別のボタンを押したときに onRunBtnClicked と同じ処理をしたくなった場合に表面化します。

    def onSuperRunBtnClicked(self):
        self.do_some_super_pre_process()
            :
        self.onRunBtnClicked() # おやおや、おやおやおやおやおやおやおやおやおや?
            :
    

    RunBtn が押されていないのに、RunBtn が押されたときのコードが実行されています。こうなると onRunBtnClicked という名前がおかしいことに気づきます。ボタンが押されたときに呼ばれる関数は、文字通り "ボタンが押されたときに実行する" という意味しかないです。その中で何をするかというのは別物だと考えて、中の処理はそれ自体にきちんと名前をつけて別の関数として定義しておくのがいいです。

    def convert_ma_to_abc(self, src, dst):
        self.do_some_pre_process()
            :
        self.do_some_process()
            :
        self.do_some_post_process()
    
        return True
    
    
    def onRunBtnClicked(self):
        src = self.get_src_path()
        dst = self.get_dst_path()
        self.convert_ma_to_abc(src, dst)
    
    
    def onSuperRunBtnClicked(self):
        src = self.get_super_src_path()
        dst = self.get_super_dst_path()
        self.convert_ma_to_abc(src, dst)
    

    GUIのイベントとして呼ばれる onRunBtnClicked 内ではGUIに関連する処理を担当し、そこから呼ぶ処理本体にはGUIの情報を一切伝えないようにすることでGUIに関連する部分と処理本体を分けておくことができます。

    これ以外にも、少しでも油断をすると、すぐにGUIに依存したプログラムができてしまいます。そうならないためには、書いたプログラムを実行するためのサンプルコードやテストコードを用意することを通して、自分がコードのユーザーになることが一番です。

    処理を適切な部分で切り分ける

    処理内容を適切な部分で切り分けることで、コードのメンテナンス性や仕様変更への耐性を高めることができます。ひとつの例として、第9回:HTCondorで処理を行うでご紹介した、データ変換処理をHTCondor上で行うようにするためのながれがあります。

    このときは、指定したディレクトリ以下にあるMayaのデータをabcファイルに変換するスクリプトの処理を

    ・HTCondorにジョブを投入する
    ・HTCondorがジョブを解釈してMayaを実行する
    ・Mayaがスクリプトを実行する
    ・ファイルの変換を行う

    という4つの段階に分割することで、HTCondor上でデータ変換を行うことができるようにしました。この方法のいい点はたくさんあります。

    まず、HTCondorではなくDeadlineを使いたいとなった場合でも、前半のHTCondor対応部分だけつくりなおせば、Mayaでの処理はほぼそのまま使いまわすことができる点です。また、各段階ごとに独立しているため、ひとつひとつの段階に集中して機能をつくり込むことができます。処理内容が整理されていない場合、動作確認をするために毎回レンダーサーバにジョブを投入して実行しなければいけないため、一度の動作確認で数十秒から数分程度かかってしまいます。これでは集中力が細切れになってしまい、この隙にメールをチェックしたりTwitterを眺めてしまって、さらに数分(1時間?)を浪費してしまうでしょう。それに対し、ファイルの変換部分だけMayaから直接実行できるようになっていれば、動作確認を数秒で終わらせることができ、集中力を途切れさせることなく作業を続けられます。ここでの生産性は数倍から数十倍のちがいがあると言えます。

    データのアクセスパターンを変えてみる

    もうひとつ面白い例をご紹介します。何かを処理する際、キャッシュや処理速度、そのほかモロモロの事情からデータへのアクセスパターンを変えたい場合というのが存在します。例えば、あるケースでは水平方向に1行目、2行目、3行目......と順に処理したいものの、別のケースでは垂直方向に1列目、2列目、3列目......と順に処理したいような場合、さらには中心から外側に向かって順にアクセスしたいような場合があるかもしれません。このような場合、普通に書くとそれぞれのパターンごとに別のコードを用意することになるでしょう。例えば、以下のようなコードを別々に書くことになります。

    # 行優先アクセス
    for y in range(10):
        for x in range(10):
        	process_data(x, y)
    
    # 列優先アクセス
    for x in range(10):
        for y in range(10):
        	process_data(x, y)
    

    これでもまあまあいいのですが、ループの中の処理がちょっと複雑になったり、アクセスパターンをいろいろ試したいといった場合にメンテナンスが大変になってきます。こんなときも、"データのアクセスパターンの生成" と "処理" の2つのコードに分けて管理ができるとスッキリします。

    Pythonであれば、yieldを使って実現できます。

    def generate_column_major_access_pattern():
        for y in range(10):
            for x in range(10):
                yield (x, y)
    
    def generate_row_major_access_pattern():
        for x in range(10):
            for y in range(10):
                yield (x, y)
    
    
    for x, y in generate_column_major_access_pattern():
        process_data(x, y)
    

    行優先アクセスの場合は generate_column_major_access_pattern、列優先アクセスの場合は generate_column_major_access_pattern を使用するだけで、ほかには一切変更を加えずにアクセス方法を切り替えることができます。また、この方法であれば各アクセスパターンごとのテストを書くことも簡単にできます。

    まとめ

    コーディングガイドラインなどを基にプログラムの形を整えるのは比較的ハードルが低く着手しやすいのに対して、プログラムの構造や処理の内容に着目して整理をするというのは、形がない分、難しいところがあります。それでも、常に綺麗な構造でプログラムを記述できないかという意識をもってコードを書き続けることで、ある日突然ひらめくことがあります。一度ひらめいてしまったら、何で今までそんな簡単なことに気づかなかったんだろう? と思えるので、不思議なものです。また、ほかの人の書いたプログラムを読むことも勉強になります。最近はオープンソースソフトウェアもたくさんあり、自分が使っているプログラムのソースコードにアクセスできることも多々あるので活用していくといいです。



    第37回の公開は、2021年9月を予定しております。

    プロフィール

    • 痴山紘史
      日本CGサービス(JCGS) 代表

      大学卒業後、株式会社IMAGICA入社。放送局向けリアルタイムCGシステムの構築・運用に携わる。その後、株式会社リンクス・デジワークスにて映画・ゲームなどの映像制作に携わる。2010年独立、現職。映像制作プロダクション向けのパイプラインの開発と提供を行なっている。新人パパ。娘かわいい。
      @chiyama