ロコガイド テックブログ

「地域のくらしを、かしこく、たのしく」する、株式会社ロコガイドの技術部ブログです。主にトクバイ・ロコナビのサービス開発について発信しています。

「地域のくらしを、かしこく、たのしく」する、株式会社ロコガイドの技術部ブログです。
主にトクバイ・ロコナビのサービス開発について発信しています。

ヘルパを使ってLuigiワークフローの依存関係をスッキリ書こう!

f:id:ar_tama:20191004165548j:plain

みなさん、ととのってますか〜?
最近サウナにどっぷりハマってしまった id:ar_tama です。このエントリは名古屋・栄のサウナラボで サ活 リモートワークをしながら書いています。

さて、最近ロコガイドでは社内の業務改善として、今まで手動で行っていた業務を自動化するプロジェクトが行われており、そのワークフロー管理にPython・Luigiを用いています。
日本語では2016~2017年のエントリが多く見られ、最近のアップデートに関する(日本語の)記述が少なく感じたため、何回かに分けて知見を書き溜めていこうと思います。

※ このエントリは主にこちらの ドキュメント の焼き直し+αです。
更に理解が深まるはずなので、ぜひ併せて読んでみてください:)

パラメータ爆発をなんとかしたい

以下の例(ドキュメントから抜粋)では、TaskCを起点としたワークフローで受け取ったパラメータを、TaskB→TaskAへと順に渡しています。

class TaskA(luigi.ExternalTask):
    param_a = luigi.Parameter()

class TaskB(luigi.Task):
    param_b = luigi.Parameter()
    param_a = luigi.Parameter()

    def requires(self):
        return TaskA(param_a=self.param_a)

class TaskC(luigi.Task):
    param_c = luigi.Parameter()
    param_b = luigi.Parameter()
    param_a = luigi.Parameter()

    def requires(self):
        return TaskB(param_b=self.param_b, param_a=self.param_a)

# invoke
if __name__ == '__main__':
    luigi.build(TaskC(param_c=1, param_b=2, param_a=3))

いくつかのタスクを組み合わせてひとつのワークフローとする場合、このように延々と値をリレーしていかないといけません。め、面倒ですね……。

依存関係をスッキリ書きたい

ところで上の例、ぱっと見て「TaskC→TaskB→TaskAと依存している」ことがわかりましたでしょうか?
全体感がすぐに分かるコード量であればこの書き方でもよさそうですが、順調にコードが育った際に、それぞれのタスクの関係性がなかなかわかりにくくなりそうです。
更にこの依存が複数タスクに渡る場合、認識コストが更に跳ね上がります。

それ、 @requires ヘルパでできるよ!

そこで登場するのが @requires ヘルパです。これはPythonのデコレータとして定義されており、このように使います:

class TaskA(luigi.ExternalTask):
    param_a = luigi.Parameter()

@requires(TaskA)
class TaskB(luigi.Task):
    param_b = luigi.Parameter()

@requires(TaskB)
class TaskC(luigi.Task):
    param_c = luigi.Parameter()

# invoke
if __name__ == '__main__':
    luigi.build(TaskC(param_c=1, param_b=2, param_a=3))

めちゃめちゃスッキリしましたね!

パラメータの引き渡し処理、および requires 関数の定義が不要になりました。
依存先のタスクへは @requires が暗黙的に引き渡してくれる上に、クラス宣言の上にマークされるため、 requires 関数を定義するより見通しが良くなったように感じます。
複数タスクの依存を定義できないという過去のエントリも見かけましたが、2019年10月現在、複数定義は正式にサポートされています🎉

また、 requires 関数を個別に定義したい場合は、その機能のみを引いた @inherits ヘルパも用意されています。
こちらも複数対応されているようですね。(コード

余談:複数のinputはどう処理されるの?

依存関係が複数になった場合はどのように受け取るのか、書き始めるまで分からなかったのですが
ここを読むと分かるように、input()requires 関数の戻り値と同義なので、 @requires ヘルパを使った場合は

  • 依存タスクが1つの場合: 依存先の output()
  • 依存タスクが複数の場合: @requires で定義された順の output() のlist

がそれぞれ返ってきます。シンプルですね:)

おわりに

Pythonでのガッツリとした開発は初めてだったのですが、書き味がシンプルかつコードリーディングもしやすく、Rubyとはまた違った魅力がありますね。
ロコガイドではRuby on Railsでの開発が主ですが、PythonやGoのプロダクトもちらほら存在しています。ご興味をお持ちいただけたら、ぜひぜひ遊びにきてください!