Clickのすゝめ

この記事は、みらい翻訳 Advent Calendar 2020 7日目の記事です。

はじめまして、株式会社みらい翻訳エンジニアリング部の@reonyanarcleです。
自然言語処理エンジニアとして、みらい翻訳のモデル開発に携わっています。

急に今日話をしてみようと思い立ったので書くことにしました！
今日思い立って、今日書いたので拙いかもしれませんがよろしくお願いします！
(勢いは大事ですよね)

## はじめに

今日は、みんな大好き(?)コマンドラインツールを作る話をします！
仕事や研究でプログラムを書いているときに、これをいい感じにコマンドラインでかけたらいいなあー。と思うことってありますよね？
そんな貴方にPythonのコマンドラインを作るモジュールclickをお勧めさせてください！
よくあるclick紹介記事になりますがそれはご容赦ください。
また、今回の話には、以下の内容は含まれていません。

- Python以外を用いたコマンドラインツールの作成について
- argparseに関する説明

## clickとは

少し初めにでも話しましたが、[click](https://click.palletsprojects.com/en/7.x/)はコマンドラインツールを作成するためのPythonモジュールです。
公式ドキュメントにも書いてあるのですが、"必要最小限で美しい"コマンドラインツールを作成することができます。
ちなみにとても余談ですが、clickを作ったのは、jinjaや、flaskなどのプロジェクトを立ち上げた、[Armin Ronacher](https://github.com/mitsuhiko)です。

なぜ、私が標準ライブラリの中に存在するargparseを差し置いて、clickを勧めるのかというと、

- 書いたコードの可読性が高い（個人差がありますが）
- 公式ドキュメントがとても丁寧！

これを言ったら元も子もないですが、公式ドキュメントがとても丁寧なので、私の記事を読まずにclickの公式ドキュメントを読みましょう！笑

## clickのメリット

少し具体的なコードを見ながら話を進めていきましょう。
公式ドキュメントに書いてあるコードに以下のようなものがあります。(少し、自分が読みやすいようにコードを修正してます。)

### clickを用いた時

clickを使って、挨拶するコマンドを作成してみましょう。

```hello.py
import click


@click.command()
@click.option('--count', type=int,
　　　　　　　　　default=1, help='Number of greetings.')
@click.option('--name', type=str,
              prompt='Your name', help='The person to greet.')
def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times."""
    for x in range(count):
        click.echo(f'Hello {name}!')


if __name__ == '__main__':
    hello()
```

clickでは、`@click.command()`というデコレータを関数の上に書くと、関数をコマンドと認識してくれます。
`@click.option()`というデコレータを用いると、オプション引数が定義できます。
また、docstringはコマンドの説明文になります。これが地味に便利です。

早速コマンドを実行してみましょう。

```bash
$ python hello.py --help
Usage: hello.py [OPTIONS]

  Simple program that greets NAME for a total of COUNT times.

Options:
  --count INTEGER  Number of greetings.
  --name TEXT      The person to greet.
  --help           Show this message and exit.
```

helpでは、オプション引数ごとに何の型をとるのかを書いてくれます。
helpに従って、コマンドを打つと、

```bash
$ python hello.py --count=3 --name Alice
Hello Alice!
Hello Alice!
Hello Alice!
```

無事にコマンドが作成されていますね！ちなみに、このコマンドは、`--name`オプションを指定せずにコマンドを打つことが可能です。

``` bash
$ python hello.py --count=3
Your name: Alice
Hello Alice!
Hello Alice!
Hello Alice!
```

`--name`オプションに関する遅延読み込みが可能になってます。

### argparseを用いた時

次に、argparseを使って、同じコマンドを作成してみましょう。

```hello_arg.py
import argparse


def hello():
    parser = argparse.ArgumentParser(description='Simple program that greets NAME for a total of COUNT times.')
    parser.add_argument('--count', dest='integer', type=int,
                        default=1, help='Number of greetings.')
    parser.add_argument('--name', dest='text',
                        help='The person to greet.')
    args = parser.parse_args()

    if not args.text:
        args.text = input('Your name: ')

    for x in range(args.integer):
        print(f'Hello {args.text}!')


if __name__ == "__main__":
    hello()
```

上記のコードを実行してみると、

```bash

python hello_arg.py --help
usage: hello_arg.py [-h] [--count INTEGER] [--name TEXT]

Simple program that greets NAME for a total of COUNT times.

optional arguments:
  -h, --help       show this help message and exit
  --count INTEGER  Number of greetings.
  --name TEXT      The person to greet.
```

```bash
$ python hello_arg.py --count=3 --name Alice
Hello Alice!
Hello Alice!
Hello Alice!
```

```bash
$ python hello_arg.py --count=3
Your name: Alice
Hello Alice!
Hello Alice!
Hello Alice!
```

無事に同じように動きましたね！(usageに微妙な違いがありますが、それは許してください…)

### clickとargparseの比較

さて、clickとargparseで同じコマンドを作成しました。
このコードから二つを比較するのはやや強引ですが、（私の書き方が悪い可能性があるので）比べてみると、

- argparseは関数内で引数を定義するので、コマンドラインの引数が分かりにくく感じる。
  これは個人的に可読性が低くなっている原因かなと思います。
- argparseは引数もオプション引数も`add_argument()`で定義します。
  それに対して、clickは、`@click.argment()`と`@click.option()`と別々になっています。
- argparseを用いて遅延読み込みを行うのは少し面倒です。
  `hello_arg.py`の11,12行目をきれいに書く方法を知ってる人がいたら教えてください。

があります。
作者がclick贔屓なところがありますが、なんとなくclickの良さは知ってもらえたかなと思います！
今回は書きませんが、サブコマンドの作成もかなり違います。（詳しい話は参考文献を見てみてください）

## clickのデメリット

ここまでclickのことを褒めていましたが、デメリットを書かないわけにはいきません。
デメリットは単純でデコレータ地獄になることです。
オプション引数がとても多いとデコレータがたくさんになり、可読性が低くなります。。。

もう一つあると思うのは、途中にあったインタラクティブシェルのような機能がいらない人にはargparseの方がいいかもしれないということです。
私個人としてはオプション引数が多かったり、そもそも引数が長くなるものはインタラクティブシェルで聞く方がいいと思っているので、clickをお勧めします。
これは好みの問題なので議論はしません！

## まとめ

どうでしたかね？なんとなくclickの良さを感じてくれれば幸いです。
clickのより面白い話とか、clickの仕様が気になったら参考文献を見に行ってみてください！
これだけclickをお勧めしてきましたが、コマンドの良さを決めるのはコマンドを書くのに何を使ったかではなく、コマンドの設計の良さに出てくると思っています！笑
特に、普段からgitやaws cli、dockerなどを触っている方ならなんとなくわかるかと思います。
良い設計をしたコマンドを作るのにclickを使って、良いコマンド作成生活をお過ごしください！

~~次は、12/9に@kobarasukimaroがawsについて書いてくれるそうです！~~
明日は@nile_mtが書いてくれるそうです！

楽しみですね！

## 参考文献

- [click公式ドキュメント](https://click.palletsprojects.com/en/7.x/)
- [Python: コマンドラインパーサの Click が便利すぎた](https://blog.amedama.jp/entry/2015/10/14/232045)
- [Python Command Line Tools: Design powerful apps with Click](https://www.amazon.co.jp/Python-Command-Line-Tools-onemillion2021-ebook/dp/B0855FSFYZ/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=click+python&qid=1607341198&sr=8-1)

私はこの三つを読んで勉強しました。
またこの記事には上記を参考にして書きました。