CUBE SUGAR CONTAINER

技術系のこと書きます。

Golang でマージソートを書いてみる

以前、勉強がてら Golang でクイックソートを書いたので、ついでにマージソートも書いてみる。

blog.amedama.jp

サンプルコードは次の通り。 例によってナイーブな実装なので実用性は低い。

package main

import (
    "fmt"
    "time"
    "math/rand"
)


func merge(left, right []int) (ret []int) {
    ret = []int{}
    for len(left) > 0 && len(right) > 0 {
        var x int
        // ソート済みのふたつのスライスからより小さいものを選んで追加していく (これがソート処理)
        if right[0] > left[0] {
            x, left = left[0], left[1:]
        } else {
            x, right = right[0], right[1:]
        }
        ret = append(ret, x)
    }
    // 片方のスライスから追加する要素がなくなったら残りは単純に連結できる (各スライスは既にソートされているため)
    ret = append(ret, left...)
    ret = append(ret, right...)
    return
}

func sort(left, right []int) (ret []int) {
    // ふたつのスライスをそれぞれ再帰的にソートする
    if len(left) > 1 {
        l, r := split(left)
        left = sort(l, r)
    }
    if len(right) > 1 {
        l, r := split(right)
        right = sort(l, r)
    }
    
    // ソート済みのふたつのスライスをひとつにマージする
    ret = merge(left, right)
    return
}

func split(values []int) (left, right []int) {
    // スライスを真ん中でふたつに分割する
    left = values[:len(values) / 2]
    right = values[len(values) / 2:]
    return
}

func Sort(values []int) (ret []int) {
    left, right := split(values)
    ret = sort(left, right)
    return
}

func main() {
    // UNIX 時間をシードにして乱数生成器を用意する
    t := time.Now().Unix()
    s := rand.NewSource(t)
    r := rand.New(s)

    // ランダムな値の入った配列を作る
    N := 10
    values := make([]int, N)
    for i := 0; i < N; i++ {
        values[i] = r.Intn(N)
    }
    
    // ソートして結果を出力する
    sortedValues := Sort(values)
    fmt.Println(sortedValues)
}

実行結果は次の通り。

$ go run mergesort.go
[0 1 2 3 5 6 6 8 8 9]

ばっちり。

おまけ

Python 版

#!/usr/bin/env python
# -*- coding: utf-8 -*-


def _merge(left, right):
    ret = []
    while len(left) > 0 and len(right) > 0:
        if left[0] > right[0]:
            ret.append(right.pop(0))
        else:
            ret.append(left.pop(0))

    ret += left + right
    return ret


def _sort(left, right):
    if len(left) > 1:
        l, r = _split(left)
        left = _sort(l, r)
    if len(right) > 1:
        l, r = _split(right)
        right = _sort(l, r)

    ret = _merge(left, right)
    return ret


def _split(values):
    left = values[:len(values) // 2]
    right = values[len(values) // 2:]
    return left, right


def sort(values):
    left, right = _split(values)
    ret = _sort(left, right)
    return ret


def main():
    import random
    N = 10
    values = [random.randint(0, N) for _ in range(N)]
    sorted_list = sort(values)
    print(sorted_list)


if __name__ == '__main__':
    main()

Python の DB-API (Database API) とは何か

Python でデータベース周りについて調べていると、ドキュメントの中にちょくちょく DB-API (2.0) という単語が出てくる。 果たしてこの DB-API とは何者なのか、というのが今回の主題。

結論から言ってしまうと、DB-API というのは Python でリレーショナルデータベースを操作するために定義された API の仕様を指している。 仕様というのがポイントで、これは具体的な実装を指しているわけではない。 DB-API は Python に関する仕様を決める PEP という枠組みの中で PEP0249 というドキュメントに書かれている。

PEP 249 -- Python Database API Specification v2.0 | Python.org

なぜ DB-API が必要になるのだろうか? それは、リレーショナルデータベースに SQLite3 や MySQL など、数々の実装がある点から説明できる。 それらは SQL を扱うリレーショナルデータベースという点で共通しているにも関わらず、それを操作するためのモジュールがそれぞれバラバラに API を決めていてはユーザにとって利便性が低くなる。 そこで登場するのが DB-API という仕様で、モジュールが仕様を満たすように作られることで、ユーザは使っているリレーショナルデータベースとモジュールに関わらず同じインターフェースを通して操作できるようになる

ここからは実際に色々なデータベース操作用モジュールで DB-API を触ってみることにする。 環境には Mac OS X を使っている。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11
BuildVersion:   15A284

SQLite3 (sqlite3)

まずは Python 組み込みの SQLite3 モジュールから。 組み込みなので Python さえ入っていれば特に何をするでもなく使うことができる。

Python の REPL を使って動作を確認していく。 インポートは sqlite3 という名前でできる。

$ python -q
>>> import sqlite3

例えば DB-API では apilevel や threadsafety といったグローバル変数が定義されている。

>>> sqlite3.apilevel
'2.0'
>>> sqlite3.threadsafety
1

また、データベースへの接続は connect() 関数を使うことになっている。

>>> sqlite3.connect
<built-in function connect>

たしかに DB-API 2.0 が実装されているようだ。

MySQL (PyMySQL)

次は MySQL 用のモジュールとして PyMySQL を試してみる。

Homebrew で MySQL をインストールした上で pymysql を入れる。

$ brew install mysql
$ pip install pymysql

モジュールは pymysql という名前でインポートできる。 次のように、やはり別のモジュールであっても DB-API の仕様に則って同じグローバル変数、同じ関数が実装されている。

$ python -q
>>> import pymysql
>>> pymysql.apilevel
'2.0'
>>> pymysql.threadsafety
1
>>> pymysql.connect
<function Connect at 0x100b44400>

Postgresql (psycopg2)

次は Postgresql 用のモジュール psycopg2 を試してみる。

Homebrew で Postgresql をインストールした上で、psycopg2 を入れる。

$ brew install postgresql
$ pip install psycopg2

モジュールは psycopg2 という名前でインポートできる。 やはり、このモジュールも DB-API に則って必要なグローバル変数と関数が用意されている。 しかし、先のふたつとは threadsafety の値が異なっていることもわかる。

$ python -q
>>> import psycopg2
>>> psycopg2.apilevel
'2.0'
>>> psycopg2.threadsafety
2
>>> psycopg2.connect
<function connect at 0x10bd6d598>

DB-API を使ってみる

次は実際に DB-API を使ってみることにする。 リレーショナルデータベースには MySQL を使う。

動作確認用のデータベースとテーブルを作って行をひとつ追加しておく。

$ mysql.server start 
$ mysql -u root

mysql> create database if not exists test;
Query OK, 1 row affected, 1 warning (0.00 sec)

mysql> use test;
Database changed

mysql> create table users (
    ->     id Integer auto_increment,
    ->     name Text not null,
    ->     primary key (id)
    -> );
Query OK, 0 rows affected (0.01 sec)

mysql> insert into users (name) values ("foo");
Query OK, 1 row affected (0.00 sec)

mysql> quit
Bye

次に、Python の DB-API 経由で行を取得してみる。 connect() 関数でデータベースへの接続を確立したら、cursor() でカーソルオブジェクトを取得する。 カーソルオブジェクト経由で SQL を実行して結果を取得する。

$ python -q
>>> import pymysql
>>> connection = pymysql.connect('localhost', user='root', db='test')
>>> with connection.cursor() as cursor:
...     sql = 'select `id`, `name` from `users` where `name`=%s'
...     cursor.execute(sql, 'foo')
...     result = cursor.fetchone()
...     print(result)
...
1
(1, 'foo')

ばっちり。

使い終わったらコネクションを閉じる。

>>> connection.close()

まとめ

今回は Python の DB-API について調べてみた。 DB-API は Python でリレーショナルデータベースを扱うモジュールに求められる仕様のことで、巷のモジュールはそれを実装している。 とはいえ、実際に Python でリレーショナルデータベースを扱う際には、インピーダンスミスマッチの問題があるので SQL を直接発行するような低レベルの DB-API を直接使う場面というのは少ないと思う。 現実的には O/R マッパー (例えば SQLAlchemy など) が内部的に DB-API を使うことで、間接的にその恩恵に預かることになるだろう。

Python: デコレータとコンテキストマネージャの両方として動作する API の作り方

以前、仕事でデータベースのトランザクションを管理する Python の API を考えているときに、同僚と「デコレータで作るべき」「いやコンテキストマネージャの方が扱いやすい」みたいなやり取りをしたことがあった。 でも、今考えるとどちらとしても動作するように作っておけばよかったんだよね。

ということで、今回はデコレータとしても動作するしコンテキストマネージャとしても動作するような API を作る方法について書くことにする。 補足しておくと、デコレータもコンテキストマネージャも対象となる処理の前後に特定の処理を挿入できるというところに共通点がある。 今回紹介する内容も、対象となる処理の前後に特定の処理を挿入するというものになっている。

サンプルコードその一

以下のサンプルコードでは、clamp() 関数がデコレータとしてもコンテキストマネージャとしても動作するように作られている。 clamp() 関数の内容は、対象となる処理の前後に print() 関数の実行を加えるというもの。 @clamp デコレータで修飾された decorator() 関数や、clamp() をコンテキストマネージャとして with 文で print() 関数をネストさせている contextmanager() 関数がそれに当たる。 とはいえ clamp() 関数自体は難しいことをやっているわけではなく、引数がある場合にはデコレータ (_decorator) を、ない場合にはコンテキストマネージャ (_contextmanager()) を返しているに過ぎない。 尚、コンテキストマネージャは本来であればクラスに特殊メソッド __enter__() と __exit__() を実装することになるが、今回は楽をするために @contextlib.contextmanager() でジェネレータを修飾することで作成している。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import contextlib


@contextlib.contextmanager
def _contextmanager():
    print('Beofre')
    try:
        yield
    finally:
        print('After')


def _decorator(func):
    def __decorator(*args, **kwargs):
        print('Beofre')
        try:
            ret = func(*args, **kwargs)
        finally:
            print('After')
            return ret
    return __decorator


def clamp(func=None):
    '''
    引数が指定されればデコレータとして、指定されなければコンテキストマネージャとして使える
    '''
    if func is not None:
        return _decorator(func)
    else:
        return _contextmanager()


# デコレータとして使うパターン
@clamp
def decorator():
    print('Hello, World!')


def contextmanager():
    # コンテキストマネージャとして使うパターン
    with clamp():
        print('Hello, World!')


def main():
    print('---デコレータ---')
    decorator()
    print('---コンテキストマネージャ---')
    contextmanager()


if __name__ == '__main__':
    main()

実行結果は次の通り。 思惑通り 'Hello, World!' の前後に 'Before' と 'After' が出力されている。

$ python clamp1.py
---デコレータ---
Beofre
Hello, World!
After
---コンテキストマネージャ---
Beofre
Hello, World!
After

サンプルコードその二

次はより実用的な例として、デコレータが修飾した関数の引数にオブジェクトをインジェクトしたり、コンテキストマネージャが as キーワードで受け取れるオブジェクトを返すようにしてみよう。 これは例えばデータベースのトランザクションを管理する API であれば、セッションを表すオブジェクトがそれに当たることになると思う。

次のサンプルコードでは、MyClass クラスのインスタンスをデコレータやコンテキストマネージャ経由でユーザに使えるようにしたい、というのを想定している。 コンテキストマネージャの場合はシンプルに yield で返したいオブジェクトを指定すれば、それが with 文の as キーワードに続く変数で受け取れるようになる。 デコレータの場合はそれよりも多少複雑で、まずデコレータの引数でインジェクト対象の変数名を受け取っている。 その上で、デコレータの中で修飾対象となる関数の引数を inspect モジュールを使って調べた上でオブジェクトを引数にインジェクトしている。 ちなみに以下のコードではキーワード付き変数にはインジェクトできない点には注意が必要。 デコレータの場合もコンテキストマネージャの場合も、受け取ったオブジェクトのメソッド (MyClass#echo()) を使ってメッセージを出力しているところがポイントになる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import contextlib
import inspect


# デコレータで修飾された関数や with 文のスコープ内で使いたいオブジェクトを想定したクラス
class MyClass(object):

    def echo(self, msg):
        print(msg)


@contextlib.contextmanager
def _contextmanager():
    print('Beofre')
    try:
        # yield で返したオブジェクトが with 文で as に続く変数に代入される
        yield MyClass()
    finally:
        print('After')


def _decorator(name):
    def __decorator(func):
        def ___decorator(*args, **kwargs):
            print('Beofre')
            try:
                # デコレータでラップする対象の関数から引数名をリストで取得する
                args_names = inspect.getargspec(func)[0]
                # 名前を手がかりにオブジェクトをインジェクトすべき場所を見つける
                target_pos = args_names.index(name)
                # 元々の引数を配列にする (タプルは Immutable なオブジェクトのため)
                args_list = list(args)
                # 必要なオブジェクトをインジェクトする
                args_list.insert(target_pos, MyClass())
                # 加工後の引数を使って関数を呼び出す
                ret = func(*args_list, **kwargs)
            finally:
                print('After')
                return ret
        return ___decorator
    return __decorator


def clamp(name=None):
    '''
    引数が指定されればデコレータとして、指定されなければコンテキストマネージャとして使える
    '''
    if name is not None:
        return _decorator(name)
    else:
        return _contextmanager()


# デコレータとして使うパターン
@clamp('obj')  # オブジェクトをインジェクトする引数名を指定する
def decorator(obj):
    obj.echo('Hello, World!')


def contextmanager():
    # コンテキストマネージャとして使うパターン
    with clamp() as obj:
        obj.echo('Hello, World!')


def main():
    print('---デコレータ---')
    decorator()
    print('---コンテキストマネージャ---')
    contextmanager()


if __name__ == '__main__':
    main()

上記を実行してみよう。 実行結果自体は先程と変わらない。

$ python clamp2.py
---デコレータ---
Beofre
Hello, World!
After
---コンテキストマネージャ---
Beofre
Hello, World!
After

ばっちり。

やっぱり、かっこいい API を設計したいよね。

Python: docstring を書いてみよう

Python の docstring というのは、モジュールやクラス、関数などにつける説明文のこと。 単純なコメントとの違いは、より仕様に近い内容を記述しておくことで API のドキュメントとして利用できる他、別のドキュメントから内容を読み込んで使ったりすることができる。

今回は docstring の書かれたサンプルコードを元に、REPL からその内容を参照したり、ドキュメンテーションツール Sphinx でその内容を読み込むというのを試してみる。

docstring の書かれたソースコードを用意する

以下が docstring の書かれた Python のソースコード。 内容については FizzBuzz 問題を題材にしている。 モジュールや関数の先頭で “”“ や ‘’‘ を使ってコメントを書いておくと、それが自動的に docstring として扱われることになる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
このモジュールでは FizzBuzz 問題を実装しています。
"""

from __future__ import print_function


def fizzbuzz(n):
    """
    この関数は、与えられた整数が 3 で割り切れるときは 'Fizz' を、
    5 で割り切れるときは 'Buzz' を、その両者で割り切れるときは
    'FizzBuzz' を、いずれでも割り切れないときは数値をそのまま
    文字列に変換して返します。
    """
    if n % 3 == 0 and n % 5 == 0:
        return 'FizzBuzz'

    if n % 3 == 0:
        return 'Fizz'

    if n % 5 == 0:
        return 'Buzz'

    return str(n)


def main():
    """
    モジュールを実行した際のエントリポイントです。
    """
    import sys
    n = int(sys.argv[1])
    ret = fizzbuzz(n)
    print(ret)


if __name__ == '__main__':
    main()

今回の主眼とは異なるものの、動作についても一応は確認しておく。

$ python fizzbuzz.py 1
1
$ python fizzbuzz.py 2
2
$ python fizzbuzz.py 3
Fizz
$ python fizzbuzz.py 4
4
$ python fizzbuzz.py 5
Buzz
$ python fizzbuzz.py 15
FizzBuzz

REPL から docstring の内容を確認する

docstring の最も単純な使い方としては、REPL を使って内容を確認するというものだとおもう。 docstring が書かれているモジュールや関数などに対して組み込み関数 help() を実行することで、内容を読むことができる。

以下のようにして実行するとページャ経由で docstring が読めるので確認してもらいたい。

$ python -q
>>> import fizzbuzz
>>> help(fizzbuzz)
>>> help(fizzbuzz.fizzbuzz)

ちなみに docstring の内容は、それが書かれたオブジェクトの特殊属性の中に記録されている。 Python 自体がそれを管理するという面から言っても、単なるコメントとは異なることがわかる。

>>> fizzbuzz.__doc__
'\nこのモジュールでは FizzBuzz 問題を実装しています。\n'
>>> fizzbuzz.fizzbuzz.__doc__
"\n    この関数は、与えられた整数が 3 で割り切れるときは 'Fizz' を、\n    5 で割り切れるときは 'Buzz' を、その両者で割り切れるときは\n    'FizzBuzz' を、いずれでも割り切れないときは数値をそのまま\n    文字列に変換して返します。\n    :param int n: FizzBuzz にかける値\n    :rtype: str\n    :return: FizzBuzz の結果\n    "

Sphinx のドキュメントから docstring を読み込む

次に応用編として、Sphinx を使って書かれたドキュメントから docstring の内容を読み込んでみることにしよう。 補足しておくと Sphinx というは Python で書かれたドキュメンテーションツールのこと。 Sphinx では、autodoc という拡張を使うことで docstring の内容を読み込むことができる。

Sphinx をインストールする

まずは Sphinx を PyPI からインストールする。

$ pip install sphinx

初期設定を行う

次に sphinx-quickstart コマンドを使って必要なファイルと設定を用意する。 プロジェクト名、著者、バージョン番号を入力して、autodoc 拡張の組み込みに yes と答える以外はエンターを連打して構わない。

$ sphinx-quickstart
Welcome to the Sphinx 1.3.1 quickstart utility.

Please enter values for the following settings (just press Enter to
accept a default value, if one is given in brackets).

Enter the root path for documentation.
> Root path for the documentation [.]:

You have two options for placing the build directory for Sphinx output.
Either, you use a directory "_build" within the root path, or you separate
"source" and "build" directories within the root path.
> Separate source and build directories (y/n) [n]:

Inside the root directory, two more directories will be created; "_templates"
for custom HTML templates and "_static" for custom stylesheets and other static
files. You can enter another prefix (such as ".") to replace the underscore.
> Name prefix for templates and static dir [_]:

The project name will occur in several places in the built documentation.
> Project name: fizzbuzz-doc
> Author name(s): momijiame

Sphinx has the notion of a "version" and a "release" for the
software. Each version can have multiple releases. For example, for
Python the version is something like 2.5 or 3.0, while the release is
something like 2.5.1 or 3.0a1.  If you don't need this dual structure,
just set both to the same value.
> Project version: 0.0.1
> Project release [0.0.1]:

If the documents are to be written in a language other than English,
you can select a language here by its language code. Sphinx will then
translate text that it generates into that language.

For a list of supported codes, see
http://sphinx-doc.org/config.html#confval-language.
> Project language [en]: ja

The file name suffix for source files. Commonly, this is either ".txt"
or ".rst".  Only files with this suffix are considered documents.
> Source file suffix [.rst]:

One document is special in that it is considered the top node of the
"contents tree", that is, it is the root of the hierarchical structure
of the documents. Normally, this is "index", but if your "index"
document is a custom template, you can also set this to another filename.
> Name of your master document (without suffix) [index]:

Sphinx can also add configuration for epub output:
> Do you want to use the epub builder (y/n) [n]:

Please indicate if you want to use one of the following Sphinx extensions:
> autodoc: automatically insert docstrings from modules (y/n) [n]: y
> doctest: automatically test code snippets in doctest blocks (y/n) [n]:
> intersphinx: link between Sphinx documentation of different projects (y/n) [n]:
> todo: write "todo" entries that can be shown or hidden on build (y/n) [n]:
> coverage: checks for documentation coverage (y/n) [n]:
> pngmath: include math, rendered as PNG images (y/n) [n]:
> mathjax: include math, rendered in the browser by MathJax (y/n) [n]:
> ifconfig: conditional inclusion of content based on config values (y/n) [n]:
> viewcode: include links to the source code of documented Python objects (y/n) [n]:

A Makefile and a Windows command file can be generated for you so that you
only have to run e.g. `make html' instead of invoking sphinx-build
directly.
> Create Makefile? (y/n) [y]:
> Create Windows command file? (y/n) [y]: n

Creating file ./conf.py.
Creating file ./index.rst.
Creating file ./Makefile.

Finished: An initial directory structure has been created.

You should now populate your master file ./index.rst and create other documentation
source files. Use the Makefile to build the docs, like so:
   make builder
where "builder" is one of the supported builders, e.g. html, latex or linkcheck.

これで必要なもの一式が作られた。

Makefile    __pycache__ _build      _static     _templates  conf.py     fizzbuzz.py index.rst

試しに初期状態で HTML 形式のドキュメントをビルドしてみよう。

$ make singlehtml

デフォルトでは結果が _build ディレクトリ以下に格納されるので、それを開く。

$ open _build/singlehtml/index.html

初期状態だとこんな感じになった。

f:id:momijiame:20151015210751p:plain

docstring の内容を読み込む

docstring を Sphinx のドキュメントから読むには設定が必要になる。

まず、sphinx-quickstart コマンドで autodoc 拡張を使うかの確認に yes と答えてさえいれば、設定ファイルの中で autodoc 拡張が有効になっているはず。

$ grep -A 2 ^extensions conf.py
extensions = [
    'sphinx.ext.autodoc',
]

次に、Sphinx が Python のモジュールを見つけ出す必要があるので、パスに (fizzbuzz.py のある) 現在のディレクトリを追加しておく。 実際には現在のディレクトリをパスに追加する処理がコメントアウトされた状態で存在するので、そのコメントを外すだけ。

$ sed -i -e "s:^#\(sys.path\):\1:" conf.py
$ grep ^sys.path conf.py
sys.path.insert(0, os.path.abspath('.'))

最後に、実際のドキュメントの書かれた index.rst の中で automodule ディレクティブを使って fizzbuzz モジュールを読み込むように指定する。

$ cat << EOF > index.rst
Welcome to fizzbuzz-doc's documentation!
========================================

.. automodule:: fizzbuzz
   :members:

EOF

再度 HTML 形式でドキュメントをビルドし直そう。 ビルドの中で特にエラーが表示されなければ上手くいっている。

$ make singlehtml
$ open _build/singlehtml/index.html

完成したドキュメントはこのようになった。

f:id:momijiame:20151015212137p:plain

これで作成したモジュールのドキュメントをソースコードから生成することが可能になった。 docstring を書いておけば、いざ API のドキュメンテーションが必要になった場合でも、ソースコードの内容をひたすらコピペするような作業は必要なさそうだ。

Golang でクイックソートを書いてみる

Golang の勉強がてら、どれくらい楽に書けるかという確認を兼ねてクイックソートを書いてみた。 ナイーブな実装なので実用性はないけど。

package main

import (
    "fmt"
    "time"
    "math/rand"
)

func sort(values []int) (ret []int) {
    // 要素数が 1 以下の配列はそれ以上細分化してソートする必要がない
    if len(values) < 2 {
        return values
    }

    // 配列の先頭をピボット (基準値) に選ぶ
    pivot := values[0]

    // ピボットを基準にして値の大小で配列をふたつに分割する
    left := []int{}
    right := []int{}
    for _, v := range values[1:] {
        if v > pivot {
            right = append(right, v)
        } else {
            left = append(left, v)
        }
    }

    // 分割した配列をそれぞれ再帰的にソートする
    left = sort(left)
    right = sort(right)

    /*
    left(小さい) + pivot(基準値) + right(大きい) の順番で配列を組み立てる
    ここが実際のソート処理
    */
    ret = append(left, pivot)
    ret = append(ret, right...)

    return
}

func main() {
    // UNIX 時間をシードにして乱数生成器を用意する
    t := time.Now().Unix()
    s := rand.NewSource(t)
    r := rand.New(s)

    // ランダムな値の入った配列を作る
    N := 10
    values := make([]int, N)
    for i := 0; i < N; i++ {
        values[i] = r.Intn(N)
    }

    // ソートして結果を出力する
    sortedValues := sort(values)
    fmt.Println(sortedValues)
}

実行結果は次の通り。

$ go run quicksort.go
[0 0 1 3 3 4 5 8 8 9]

スクリプト言語ほどではないにせよ、それなりに書きやすいかな。

おまけ

Python 版

#!/usr/bin/env python
# -*- coding: utf-8 -*-


def sort(values):
    if len(values) < 2:
        return values

    pivot = values[0]

    left = []
    right = []
    for i in values[1:]:
        if i < pivot:
            left.append(i)
        else:
            right.append(i)

    left = sort(left)
    right = sort(right)

    return left + [pivot] + right


def main():
    import random
    N = 10
    values = [random.randint(0, N) for _ in range(N)]
    sorted_list = sort(values)
    print(sorted_list)


if __name__ == '__main__':
    main()

Python: コマンドラインパーサの Click が便利すぎた

Python のコマンドラインパーサといえば、標準ライブラリに組み込まれている argparse が基本。 蛇足になるけど、バージョン 2.7 以前で使われていた optparse は将来的に廃止予定なので新たに使うことは避けた方が良い。

そして、今回紹介する Python のサードパーティ製コマンドラインパーサ Click は、既存のパッケージと比較すると最小限のコードで美しくコマンドラインインターフェースを実装できるように作られている。 どれくらい楽になるかといえば、もう argparse を使っている場合じゃないな、と思えるレベル。

Welcome to Click — Click Documentation (8.0.x)

もくじ

Click をインストールする

まずは Click を PyPI からインストールしよう。

$ pip install click
$ pip list | grep click
click (5.1)

基本的な使い方

まずは使い方の雰囲気から見ていこう。

コマンドを定義する

Click を使ってコマンドを定義するには、@click.command() デコレータを使って関数を修飾するだけでいい。 これで Click にコマンドが登録される。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
def cmd():
    click.echo('Hello, World!')


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行すると cmd() 関数の内容が実行される。 とはいえ、これだけだと Click を使っていようと使っていまいと変わらない。

$ python helloworld.py
Hello, World!

オプションを追加する

次に、コマンドに対してオプションを追加してみる。 オプションを追加するには @click.option() デコレータを使ってコマンドになる関数を修飾する。 以下では --name/-n オプションをデフォルト値 'World' で登録している。 オプションが指定された場合にはコマンドの引数 name に、指定された内容が代入される。 尚、デフォルトが無い状態でオプションが指定されないと変数は None になる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--name', '-n', default='World')
def cmd(name):
    msg = 'Hello, {name}!'.format(name=name)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行すると --name/-n オプションが認識されていることがわかる。

$ python option.py
Hello, World!
$ python option.py --name Click
Hello, Click!
$ python option.py -n Sekai
Hello, Sekai!

引数を追加する

先ほど追加したのはオプションなので、ユーザが指定しないことも許容するものだった。 次は必ず指定が必要な引数を追加してみる。 もう Click の API 設計は雰囲気がつかめたと思うけど、ここでもデコレータを使うことになる。 引数を追加するには @click.argument() デコレータを使えばいい。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.argument('name')
def cmd(name):
    msg = 'Hello, {name}!'.format(name=name)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行してみる。 引数はユーザが指定しないとエラーになる。

$ python argument.py
Usage: argument.py [OPTIONS] NAME

Error: Missing argument "name".
$ python argument1.py World
Hello, World!

サブコマンドを作る

ひとつのコマンドで複数の操作をサポートする場合には、コマンドの中に操作別でサブコマンドを追加することがある。 もちろん Click はサブコマンドを作ることもできる。 以下のように、まずは @click.group() デコレータでエントリポイントとなる関数を修飾する。 そして、次は修飾した関数自体がデコレータとしてサブコマンドを登録できるようになる。 ここらへんを見ると API の設計に Flask の影響を受けている感じ。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.group()
def cmd():
    pass


@cmd.command()
def english():
    click.echo('Hello, World!')


@cmd.command()
def japanese():
    click.echo('Konnichiwa, Sekai!')


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行するとサブコマンド english と japanese が作られていることがわかる。

$ python subcommand.py
Usage: subcommand.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  english
  japanese
$ python subcommand.py english
Hello, World!
$ python subcommand.py japanese
Konnichiwa, Sekai!
$ python subcommand.py french
Usage: subcommand.py [OPTIONS] COMMAND [ARGS]...

オプションについて詳しく見ていく

ここからはオプションで指定できるパラメータなどを見ていくことにする。

オプションに説明を追加する

オプションに説明を追加するには help パラメータを指定すればいい。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--prefix', default='Hello', help='Prefix of greetings.')
def cmd(prefix):
    msg = '{prefix}, World!'.format(prefix=prefix)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

これでコマンドに --help オプションを指定した際に説明が出力されるようになる。

$ python help.py --help
Usage: help.py [OPTIONS]

Options:
  --prefix TEXT  Prefix of greetings.
  --help         Show this message and exit.
$ python help.py --prefix Konnichiwa
Konnichiwa, World!

オプションで受け取る型を明示する

ここまでのオプションで受け取っていたのはすべて文字列だけど、type パラメータに型を指定すると引数がその型で受け取れるようになる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--age', type=int, help='Your age.')
def cmd(age):
    msg = 'Your age: {age}'.format(age=age)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行してみる。 オプションの型に int (整数型) を指定したので、そこに文字列を入れようとするとエラーになることがわかる。

$ python type.py --age Hello,World!
Usage: type.py [OPTIONS]

Error: Invalid value for "--age": Hello,World! is not a valid integer
$ python type.py --age 100
Your age: 100

オプションの内容を検証 (バリデーション) する

先ほどの例では型は指定したものの、値の範囲などを検証していなかった。 次のサンプルコードではオプションで受け取った引数を検証している。 検証した結果、不正な値が入っていることがわかった場合には click.BadParameter() 例外を raise すれば良い。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--age', type=int, help='Your age.')
def cmd(age):
    if age < 1:
        raise click.BadParameter('You are liar!')
    if age > 100:
        raise click.BadParameter('Really?!')

    msg = 'Your age: {age}'.format(age=age)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

今度は、同じ整数でもおかしな値を入力するとエラーになる。

$ python badparam.py --age -1
Usage: badparam.py [OPTIONS]

Error: Invalid value: You are liar!
$ python badparam.py --age 101
Usage: badparam.py [OPTIONS]

Error: Invalid value: Really?!

コールバック関数で値を検証する

先ほどのコードではコマンドとなる関数自体に値を検証するコードが含まれていた。 しかし、これは可読性の上であまり好ましいとはいえないだろう。 以下のように、オプションの callback パラメータでコールバック関数を登録しておき、その中で値の検証をするとコードの見通しがよくなるかもしれない。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


def validate_age(ctx, param, value):
    age = value

    if age < 1:
        raise click.BadParameter('You are liar!')
    if age > 100:
        raise click.BadParameter('Really?!')


@click.command()
@click.option('--age', callback=validate_age, default=18)
def cmd(age):
    msg = 'Your age: {age}'.format(age=age)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記も、先ほどと同様におかしな値を入れるとエラーになる。

$ python validate.py --age -1
Usage: validate.py [OPTIONS]

Error: Invalid value for "--age": You are liar!
$ python validate.py --age 101
Usage: validate.py [OPTIONS]

Error: Invalid value for "--age": Really?!

値の範囲を指定する

先ほどは自分で検証用のコードを書いたけど、整数型であれば組み込みで範囲の検証が可能になっている。 type パラメータに click.IntRange() を指定すると、指定した値の範囲で検証してくれる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--age', type=click.IntRange(0, 100))
def cmd(age):
    msg = 'Your age: {age}'.format(age=age)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

エラーメッセージにも具体的な指定できる値の範囲が出力されていて親切な感じ。

$ python intrange.py --age 101
Usage: intrange.py [OPTIONS]

Error: Invalid value for "--age": 101 is not in the valid range of 0 to 100.
$ python intrange.py --age -1
Usage: intrange.py [OPTIONS]

Error: Invalid value for "--age": -1 is not in the valid range of 0 to 100.

ちなみに click.IntRange() は clamp パラメータを True に指定すると、範囲を超えた内容が入力されたときに範囲内の近い値に補正してくれる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--age', type=click.IntRange(0, 100, clamp=True))
def cmd(age):
    msg = 'Your age: {age}'.format(age=age)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

clamp パラメータを指定することで 101 は 100 に、-1 は 0 に補正された。

$ python intrange.py --age 101
Your age: 100
$ python intrange.py --age -1
Your age: 0

オプションを選択肢から選べるようにする

これまでの例ではオプションはユーザの自由入力だった。 それに対して type パラメータに click.Choice() を指定すると、既存の選択肢から選ぶ形になる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--language', type=click.Choice(['Japanese', 'English']))
def cmd(language):
    click.echo(language)


def main():
    cmd()


if __name__ == '__main__':
    main()

実行すると、選択肢の中にある Japanese や English は指定できるが、例えば存在しない French を指定するとエラーになる。

$ python choice.py --help
Usage: choice.py [OPTIONS]

Options:
  --language [Japanese|English]
  --help                         Show this message and exit.
$ python choice.py --language Japanese
Japanese
$ python choice.py --language French
Usage: choice.py [OPTIONS]

Error: Invalid value for "--language": invalid choice: French. (choose from Japanese, English)

タプルで引数を受け取る

ひとつのオプションで複数の引数を受け取りたい場合には、type パラメータにタプルを指定できる。 こうすればタプル内のそれぞれの型でオプションを受け取ることができる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--values', type=(str, int))
def cmd(values):
    msg = '{s} {i}'.format(s=values[0], i=values[1])
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記であれば --values オプションひとつに文字列と整数のふたつを渡すことになる。

$ python tuplevalues.py --values Hello 123
Hello 123

同じ名前で複数のオプションを受け取れるようにする

今度は同じ名前のオプションを複数回使えるようにするパターン。 これには multiple パラメータを True にすれば良い。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--values', multiple=True)
def cmd(values):
    for value in values:
        click.echo(value)


def main():
    cmd()


if __name__ == '__main__':
    main()

これで複数回 --values オプションが指定できるようになる。

$ python multiopts.py --values 1 --values 2
1
2

オプションの数をカウントする

コマンドラインインターフェースでよくあるパターンとして -v/--verbose オプションの個数で出力される内容の詳しさが変化するというものがある。 これを実現するには count パラメータを True に指定する。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('-v', '--verbose', count=True)
def cmd(verbose):
    msg = 'Verbosity: {v}'.format(v=verbose)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

これでオプションが指定された個数が手に入る。

$ python count.py -vvvv
Verbosity: 4
$ python count.py -v -v -v
Verbosity: 3
$ python count.py -v --verbose
Verbosity: 2

オプションで真偽値のフラグを扱う

真偽値を扱う場合には is_flag オプションを True にする。 これで、オプションの有無に応じて引数に真偽値が代入されるようになる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--shout', is_flag=True)
def cmd(shout):
    msg = 'Hello, World!'
    if shout:
        msg = msg.upper()
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行すると --shout オプションを指定した場合のみメッセージが大文字になる。

$ python boolflag.py
Hello, World!
$ python boolflag.py --shout
HELLO, WORLD!

また、'/' 記号を挟んでふたつオプションを指定した上でデフォルト値を真偽値に設定するやり方もある。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--shout/--no-shout', default=False)
def cmd(shout):
    msg = 'Hello, World!'
    if shout:
        msg = msg.upper()
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

この場合は、どちらか片方のオプションのみが有効なものとして動作する。

$ python boolflag.py
Hello, World!
$ python boolflag.py --shout
HELLO, WORLD!
$ python boolflag.py --no-shout
Hello, World!

異なる名前のオプションでひとつの引数を扱う

以下のようにすると、異なる名前のオプションがひとつの引数に対して値を代入するようになる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--upper', 'transformation', flag_value='upper')
@click.option('--lower', 'transformation', flag_value='lower', default=True)
def cmd(transformation):
    click.echo(transformation)


def main():
    cmd()


if __name__ == '__main__':
    main()

--upper オプションを指定した場合には transformation 引数に 'upper' が、反対に --lower オプションを指定した場合には 'lower' が代入される。

$ python switch.py
lower
$ python switch.py --upper
upper
$ python switch.py --lower
lower

動的にオプションのデフォルト値を決定する

オプションのデフォルト値には静的な値以外にも動的な値を指定することもできる。 例えば、以下のように default パラメータに値を返す関数を指定しておくと、動的にデフォルト値を決定できる。 以下では 1 ~ 100 の範囲でデフォルト値がランダムに代入される。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import random

import click


@click.command()
@click.option('--age', default=lambda: random.randint(1, 100))
def cmd(age):
    msg = 'Your age: {age}'.format(age=age)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

実行すると、確かに毎回表示される値が異なる。

$ python dynamicdefault.py
Your age: 98
$ python dynamicdefault.py
Your age: 63
$ python dynamicdefault.py
Your age: 41

オプションのデフォルト値を環境変数から取得する

オプションのデフォルト値を環境変数にするというのも、コマンドラインインターフェースでは多用されるパターンだとおもう。 Click でそれを実現するには envvar パラメータに環境変数名を指定するだけでいい。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--shell', envvar='SHELL')
def cmd(shell):
    click.echo(shell)


def main():
    cmd()


if __name__ == '__main__':
    main()

オプションが指定されていないときは環境変数から、指定されたときはその値が使用される。

$ python env.py
/bin/zsh
$ python env.py --shell /bin/bash
/bin/bash

引数について詳しく見ていく

ここからは引数 (argument) について詳しく見ていくことにする。 ちなみに引数で紹介したパラメータでも、実際にはオプションで使えたりするものは多い。

複数の引数を指定させる

引数の個数は nargs パラメータで指定できる。 その中にはひとつだけ -1 を指定することができて、これはその引数が何個でも指定できることを意味している。 以下では src 引数が幾つでも指定できた上で、dst 引数がひとつだけ指定できる。

#!/usr/bin/intrange python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.argument('src', nargs=-1)
@click.argument('dst', nargs=1)
def cmd(src, dst):
    for i in src:
        msg = 'move from {src} to {dst}'.format(src=i, dst=dst)
        click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行した結果は次の通り。

$ python nargs.py src1 src2 src3 dst
move from src1 to dst
move from src2 to dst
move from src3 to dst

引数でファイルを扱う

type パラメータに click.File() オプションを指定するとオープン済みのファイルオブジェクトが代入されることになる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.argument('file', type=click.File('r'))
def cmd(file):
    with file as f:
        while True:
            line = f.readline()
            if not line:
                break
            click.echo(line, nl=False)


def main():
    cmd()


if __name__ == '__main__':
    main()

試しにテキストファイルを用意して指定すると、その内容が取得できていることがわかる。

$ cat << EOF > greet.txt
Hello,
World!
EOF
$ python file.py greet.txt
Hello,
World!

パス文字列を扱う

先ほどの click.File() ではオープン済みのファイルオブジェクトが取得できた。 ただ、ファイルを開くのはそのファイルについて事前に色々とチェックしてから、ということも多いはず。 そんなときは click.Path() を指定する。 その際 exists パラメータを True に指定すると、ファイルの有無も同時にチェックしてくれる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.argument('path', type=click.Path(exists=True))
def cmd(path):
    click.echo(path)
    filename = click.format_filename(path, shorten=True)
    click.echo(filename)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行すると、指定したパス文字列が存在するファイルであればそのまま得られ、存在しない場合にはエラーになる。

$ python filepath.py /dev/random
/dev/random
random
$ python filepath.py /dev/foo
Usage: filepath.py [OPTIONS] PATH

Error: Invalid value for "path": Path "/dev/foo" does not exist.

インタラクティブな入力を受け付ける方法について詳しく見ていく

Click はコマンドラインのオプションをパースする以外にも、ユーザからインタラクティブに入力を受け付ける方法の提供も行っている。

プロンプトで入力を受け付ける。

オプションを追加する際に prompt パラメータを指定すると、コマンドラインでオプションを指定しなかった際にプロンプトを表示して入力を受け付けることができる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--name', prompt='Name', default='World',
              help='The person to greet.')
def cmd(name):
    msg = 'Hello, {name}!'.format(name=name)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

次のように、オプションの指定がない場合にはプロンプトを表示して入力を受け付け、指定された場合にはそれが使われるようになる。

$ python prompt.py
Name [World]:
Hello, World!
$ python prompt.py
Name [World]: Sekai
Hello, Sekai!
$ python prompt.py --name Click
Hello, Click!

パスワードの入力を受け付ける

オプションを追加する際に prompt=True hide_input=True を指定することで、入力がエコーバックされることがなくなる。 また、confirmation_prompt=True も指定することで、入力を二回促して両者が一致する場合のみ処理を継続するというパスワードを設定させるのに都合のいい挙動になる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--password', prompt=True, hide_input=True,
              confirmation_prompt=True, help='Your password')
def cmd(password):
    msg = 'Your password: {password}'.format(password=password)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行するとエコーバックのない入力が二回促されて、両者が一致する場合だけ処理が継続する。 もちろん、本当のパスワードであったら標準出力に出してはだめ。

$ python password.py
Password:
Repeat for confirmation:
Your password: password123

コマンドの中でプロンプトを指定する

click.prompt() はデコレータ以外にも、通常の関数として呼び出すこともできる。 なので、次のようにしてコマンドの処理の途中に入力を促すことも可能。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
def cmd():
    age = click.prompt('Please enter your age', type=int)
    click.echo(age)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記の実行結果は次の通り。

$ python rawprompt.py
Please enter your age: 100
100

ユーザに確認 (confirm) を出す

副作用のある処理の前などで、継続するかユーザの指示を仰ぐには click.confirm() を使う。 返り値は y/N が True/False で返ってくるため、そのまま条件分岐に突っ込めばいい。 処理を中断した場合には click.Abort() 例外を raise するのもいいとおもう。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
def cmd():
    if not click.confirm('Do you want to continue?'):
        raise click.Abort()

    click.echo('Hello, World!')


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行すると、y を指定したときだけ後続の処理が実行される。

$ python confirm.py
Do you want to continue? [y/N]:
Aborted!
$ python confirm.py
Do you want to continue? [y/N]: y
Hello, World!

サブコマンドについて詳しく見ていく

ここからはサブコマンドを作る場合に指定できるオプションや構造について詳しく見ていくことにする。

サブコマンドからグループコマンドのオプションや引数を参照する

サブコマンドからグループコマンドで指定されたオプションなどを参照する場合には、ちょっと面倒だけど次のようにする。 まず、@click.pass_context デコレータを使ってコンテキストを各コマンドを表した関数のシグネチャに追加する。 コンテキストは各コマンドの間で共有されるオブジェクトなので、そこに辞書型のデータなどを登録しておくことで情報を共有できる。 以下のサンプルコードでいえば、obj という辞書型のデータをコンテキストに追加して、そこの 'DEBUG' というキーでオプションの情報を受け渡ししている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.group()
@click.option('--debug', is_flag=True)
@click.pass_context  # コンテキストをインジェクトする
def cmd(ctx, debug):
    # インジェクトしたコンテキストに情報を詰める
    ctx.obj['DEBUG'] = debug


@cmd.command()
@click.pass_context
def subcmd(ctx):
    if ctx.obj['DEBUG']:
        # サブコマンドではコンテキストから情報を取り出す
        click.echo('DEBUG MODE!')
    click.echo('Hello, World!')


def main():
    # コンテキストから参照するアトリビュートを渡す
    cmd(obj={})


if __name__ == '__main__':
    main()

上記を実行すると、サブコマンドからも --debug オプションの情報が参照できていることがわかる。

$ python passcontext.py subcmd
Hello, World!
$ python passcontext.py --debug subcmd
DEBUG MODE!
Hello, World!

サブコマンドを追加した状態でグループコマンドも実行できるようにする

@click.group() デコレータのパラメータに invoke_without_command=True を指定すると、サブコマンドを追加した状態でもグループコマンドが単体で実行できるようになる。 尚、そのままだとサブコマンドが実行された際にグループコマンドを表す関数の内容も一緒に実行されてしまうので、コンテキストから invoked_subcommand メンバの値が存在しない (= サブコマンドが指定されていない) 場合のみグループコマンドの中身を実行するようにしている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.group(invoke_without_command=True)
@click.pass_context
def cmd(ctx):
    if ctx.invoked_subcommand is None:
        click.echo('This is parent!')


@cmd.command()
def subcmd():
    click.echo('This is child!')


def main():
    # コンテキストから参照するアトリビュートを渡す
    cmd(obj={})


if __name__ == '__main__':
    main()

上記を実行すると、サブコマンドとは別にグループコマンドも実行できるようになっている。

$ python withoutsubcmd.py subcmd
This is child!
$ python withoutsubcmd.py
This is parent!

サブコマンドを複数指定できるようにする

@click.group() デコレータのパラメータ chain に True を指定すると、サブコマンドを複数つなげて (チェイン) 実行できるようにすることができる。 例えば次のサンプルコードではサブコマンド subcmd1 と subcmd2 があるが、どちらも同時に使えるようになる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.group(chain=True)
def cmd():
    pass


@cmd.command()
def subcmd1():
    click.echo('subcmd1')


@cmd.command()
def subcmd2():
    click.echo('subcmd2')


def main():
    cmd()


if __name__ == '__main__':
    main()

上記にふたつのサブコマンドを指定して実行した結果が次の通り。

$ python commandchain.py subcmd1 subcmd2
subcmd1
subcmd2

サブコマンドのチェインを使いやすくする

サブコマンドをチェインさせるパターンでは、あるサブコマンドで実行した内容を後続のサブコマンドで更に処理する、というのがよくある。 これを実現するには、まずサブコマンドでオプションや引数を処理する関数を返すようにした上で、それをグループコマンドの resultcallback() デコレータで修飾した関数でまとめて処理する、というやり方があるようだ。 次のサンプルコードでは、サブコマンドが指定されたテキストファイルから行を読みだして大文字にしたり、後ろに感嘆符をつける関数を返している。 そしてそれを @cmd.resultcallback() デコレータで修飾された pipeline() 関数の中で順次適用している。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.group(chain=True, invoke_without_command=True)
@click.argument('textfile', type=click.File('r'))
def cmd(textfile):
    pass


@cmd.resultcallback()
def pipeline(processors, textfile):
    ite = (line.rstrip('\r\n') for line in textfile)
    for processor in processors:
        ite = processor(ite)
    for line in ite:
        click.echo(line)


@cmd.command()
def shout():
    def processor(ite):
        for line in ite:
            shouted = '{line}!!!'.format(line=line)
            yield shouted
    return processor


@cmd.command()
def upper():
    def processor(ite):
        for line in ite:
            yield line.upper()
    return processor


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行すると次のようになる。

$ python pipeline.py greet.txt upper shout
HELLO,!!!
WORLD!!!!

まとめ

今回は Python のサードパーティ製コマンドラインパーサ Click を使ってみた。 Click には、一般的なコマンドラインインターフェースに必要な機能があらかじめ一通り揃っているようだ。 また、その API に関しても argparse などに比べると洗練されていて扱いやすく読みやすいと感じた。

Python で ROT13 (シーザー暗号) を書いてみる

Golang で ROT13 を書いたので、ついでに Python 版も書いてみる。

blog.amedama.jp

先に断っておくと、Python の場合 ROT13 は言語に組み込みで実装があるので、本来は自分で書く必要がない。

>>> import codecs
>>> codecs.decode('Uryyb, Jbeyq!', 'rot13')
'Hello, World!'

自分で書いたバージョンは次の通り。 ジェネレータ内包表記で文字列に ROT13 を適用しているところがポイントかな。

#!/usr/bin/env python
# -*- coding: utf-8 -*-


def _rot13(c):
    if 'A' <= c and c <= 'Z':
        # 13 文字分ずらす
        return chr((ord(c) - ord('A') + 13) % 26 + ord('A'))

    if 'a' <= c and c <= 'z':
        # 13 文字分ずらす
        return chr((ord(c) - ord('a') + 13) % 26 + ord('a'))

    # その他の文字は対象外
    return c


def rot13(s):
    # ジェネレータ内包表記で文字列に ROT13 を適用する
    g = (_rot13(c) for c in s)
    # 文字列に直す
    return ''.join(g)


def main():
    s = 'Uryyb, Jbeyq!'
    print(rot13(s))


if __name__ == '__main__':
    main()

実行結果は次の通り。

$ python rot13.py
Hello, World!

ばっちり。