CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: PyPI にないパッケージを依存パッケージにするには

PyPI に無いパッケージを自作パッケージの依存ライブラリにしようとしたら色々と苦労したので、そこで得られた知見を共有しておく。 どうやら現状では setuptools と pip で対応方法が異なっているために、それぞれで微妙に異なるやり方を取る必要があるようだ。

今回使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.11.3
BuildVersion:   15D21
$ python --version
Python 3.5.1
$ pip list
pip (8.0.2)
setuptools (20.1.1)
wheel (0.24.0)

自作パッケージを用意する

まずは動作確認に使う自作パッケージを用意する。 名前は mypackage にしよう。 パッケージは枠だけ用意して中身は空っぽにする。 今回、パッケージ自体は関心の外にあるため。

$ mkdir mypackage
$ touch mypackage/__init__.py

次にパッケージに対してセットアップスクリプトを書く。 これでパッケージ mypackage がインストール可能になる。

$ cat << 'EOF' > setup.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from setuptools import setup


def main():
    setup(
        name='mypackage',
        version='0.0.1',
        zip_safe=False,
        packages=['mypackage'],
    )


if __name__ == '__main__':
    main()
EOF

早速セットアップスクリプトを使って今の環境に mypackage をインストールしよう。

$ python setup.py install

パッケージをインストールすると pip list サブコマンドで mypackage が見えるようになる。

$ pip list | grep mypackage
mypackage (0.0.1)

もちろん Python でインポートして使うこともできる。 まあ、中身は空っぽだけど。

$ python -c "import mypackage"

PyPI にない依存パッケージを追加する

さて、ここまででやっとスタートラインに立てた。 作成した mypackage に PyPI には置かれていない依存パッケージを追加してみよう。 追加するのは自分で作った以下のジョークパッケージにする。

github.com

PyPI にないパッケージを依存パッケージに追加するには次のドキュメントが参考になる。

Specifying Dependencies — Python Packaging Tutorial

通常であれば依存パッケージは setuptools.setup() 関数の install_requires 引数に名前を追加して終わりだ。 しかし、追加したいパッケージが PyPI に無いときは dependency_links の指定も必要になる。 ここにインストールに使うリンクを記述する。

$ cat << 'EOF' > setup.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from setuptools import setup


def main():
   setup(
       name='mypackage',
       version='0.0.1',
       zip_safe=False,
       packages=['mypackage'],
       install_requires=[
           'wehatemtg==0.0.1',
       ],
       dependency_links=[
           'git+https://github.com/momijiame/wehatemtg.git#egg=wehatemtg-0.0.1',
       ]
   )


if __name__ == '__main__':
   main()

EOF

セットアップスクリプトを修正したら、もう一度 mypackage をインストールし直そう。

$ python setup.py install

今度は依存パッケージの wehatemtg が一緒にインストールされるようになった。

$ pip list | grep wehatemtg
wehatemtg (0.0.1)

これで、セットアップスクリプトを使ったインストールで PyPI にないパッケージを依存パッケージに追加できるようになった。

pip で PyPI にない依存パッケージを扱う

めでたしめでたし、と行きたいところだけど残念ながらそうはいかない。 実は問題は pip を使ったときに発生する。

まずは一旦依存パッケージの wehatemtg をアンインストールしておこう。

$ pip uninstall -y wehatemtg

次に mypackage をソース配布物としてビルドする。

$ python setup.py sdist

できあがったソース配布物を pip コマンドでインストールしてみよう。

$ pip install dist/mypackage-0.0.1.tar.gz
Processing ./dist/mypackage-0.0.1.tar.gz
  Requirement already satisfied (use --upgrade to upgrade): mypackage==0.0.1 from file:///Users/amedama/Documents/temporary/package-not-on-pypi in /Users/amedama/.virtualenvs/temporary/lib/python3.5/site-packages
Collecting wehatemtg==0.0.1 (from mypackage==0.0.1)
  Could not find a version that satisfies the requirement wehatemtg==0.0.1 (from mypackage==0.0.1) (from versions: )
No matching distribution found for wehatemtg==0.0.1 (from mypackage==0.0.1)

見事にエラーになった。 wehatemtg をインストールしようとしたけど見つからないらしい。

何故エラーになるのだろうか? pip のソースコードを調べたところ原因が判明した。 次を見てもらいたい。

github.com

ここには次のようなコメントがある。

# We trust every url that the user has given us whether it was given
#   via --index-url or --find-links
# We explicitly do not trust links that came from dependency_links
# We want to filter out any thing which does not have a secure origin.

つまり pip はセットアップスクリプトの dependency_links を信用しないらしい。 PyPI にないパッケージをインストールするときは --index-url か --find-links オプションを明示的に指定する必要があるようだ。

ではそのコメントにしたがってオプションをつけて実行してみよう。

$ pip install dist/mypackage-0.0.1.tar.gz --find-links=git+https://github.com/momijiame/wehatemtg.git#egg=wehatemtg-0.0.1
...(省略)...
Successfully installed wehatemtg-0.0.1

今度は上手くいった。

$ pip list | grep wehatemtg
wehatemtg (0.0.1)

まとめ

今回は PyPI にないパッケージを依存パッケージにする方法について書いた。 基本的には setuptools.setup() 関数の dependency_links 引数にパッケージのインストール元 URL を記述すれば良い。 ただし、どうやら pip はそこに書いてある内容を信用しないのでコマンド実行時にオプションで渡してやる必要がある点に注意が必要だ。

Python: pipe を使った Infix プログラミング

今回紹介する Python の pipe というサードパーティ製のパッケージは、実用性はともかくとして非常に面白い API になっている。 pipe を使ったソースコードを見れば目からウロコが落ちること請け合いだ。 その独特な書き方はインフィックス記法というらしい。

pypi.python.org

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.11.3
BuildVersion:   15D21
$ python --version
Python 2.7.11

インストール

まずは pip で pipe をインストールする。

$ pip install pipe

使ってみる

ここからは Python の REPL で pipe の動作を試していく。

$ python

まずは pipe の提供する API をすべて読み込んでおこう。

>>> from pipe import *

pipe を使うときは、その名の通りパイプ演算子を駆使することになる。 基本は Iterable なオブジェクトに対してパイプ演算子をつなげて、そこに何らかの操作を続ける。

add

例えばリストの内容をすべて足したいときはパイプ演算子に続けて add を使う。

>>> [1, 2, 3] | add
6

where

where を使えば特定の条件にマッチする内容だけが得られる。 結果が複数のときは基本的に Iterable なオブジェクトが返る。

>>> [1, 2, 3] | where(lambda x: x % 2 == 0)
<generator object <genexpr> at 0x100d33dc8>

分かりやすいように結果をリストにしてみよう。 これも as_list をつなげることで変換できる。

>>> [1, 2, 3] | where(lambda x: x % 2 == 0) | as_list
[2]

stdout / lineout

内容を標準出力に書き出すには stdout や lineout が使える。 stdout は末尾に改行文字をつけないやつで lineout はつけるやつ。

>>> 'foo\n' | stdout
foo
>>> 'foo' | lineout
foo
>>>

tee

Unix のコマンドでもおなじみの tee を使えば標準出力に出しつつ、次のパイプにつなげることができる。

>>> [1, 2, 3] | tee | as_list
1
2
3
[1, 2, 3]

as_list

既に登場してるけどオブジェクトをリストに変換するときは as_list を使えば良い。

>>> (1, 2, 3) | as_list
[1, 2, 3]

as_tuple

同様にタプルへの変換なら as_tuple を使う。

>>> [1, 2, 3] | as_tuple
(1, 2, 3)

as_dict

辞書なら as_dict だ。

>>> [('a', 1), ('b', 2), ('c', 3)] | as_dict
{'c': 3, 'b': 2, 'a': 1}

concat

Iterable な内容をつなげて文字列にするときは concat を使う。 つなげるのに使う文字を引数で指定することもできる。

>>> [1, 2, 3] | concat
'1, 2, 3'
>>> [1, 2, 3] | concat('&')
'1&2&3'

average

算術平均を取りたいときは average が使える。

>>> [1, 2, 3] | average
2.0

netcat

netcat を使えば文字列をリモートホストに送ることもできる。 試しに Google に HTTP リクエストを投げてみよう。

>>> 'GET / HTTP/1.0\n\n' | netcat('www.google.jp', 80) | concat
'HTTP/1.0 302 Found\r\nCache-Control: private\r\nContent-Type: text/html; charset=UTF-8\r\nLocation: http://www.google.co.jp/?gfe_rd=cr&ei=KRTDVqXZNYug8wfQmrD4Dw\r\nContent-Length: 261\r\nDate: Tue, 16 Feb 2016 12:20:57 GMT\r\nServer: GFE/2.0\r\n\r\n<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">\n<TITLE>302 Moved</TITLE></HEAD><BODY>\n<H1>302 Moved</H1>\nThe document has moved\n<A HREF="http://www.google.co.jp/?gfe_rd=cr&amp;ei=KRTDVqXZNYug8wfQmrD4Dw">here</A>.\r\n</BODY></HTML>\r\n'

上手くいった。

netwrite

netcat は書き込みと読み込みを両方やる命令だったけど書き込むだけなら netwrite で良い。

まずは、動作確認のために socat を使ってエコーサーバを立てておこう。

$ brew install socat
$ socat -v tcp-listen:8080,fork system:cat

そして REPL で netwrite を使ってエコーサーバに文字列を送り込む。

>>> 'Hello, World!\n' | netwrite('localhost', 8080)

するとエコーサーバのターミナルで文字列が送られたことが確認できる。

$ socat -v tcp-listen:8080,fork system:cat
> 2016/02/16 21:26:13.875238  length=14 from=0 to=13
Hello, World!
< 2016/02/16 21:26:13.879153  length=14 from=0 to=13
Hello, World!

count

要素の数を得るには count が使える。

>>> [1, 2, 3] | count
3

first

最初の要素を得るには first を使う。

>>> [1, 2, 3] | first
1

chain

ネストしたリストをフラットにするには chain が使える。

>>> [[1, 2], [3, 4]] | chain | as_list
[1, 2, 3, 4]

traverse

深くネストしているときは traverse を使う。

>>> [1, [[2], [3]]] | traverse | as_list
[1, 2, 3]

select

各要素に何らかの処理を適用するには select を使う。 これは組み込み関数の map() に相当する。

>>> [1, 2, 3] | select(lambda x: x * x) | as_list
[1, 4, 9]

where

特定の条件にマッチする要素だけ得るときは where を使う。 組み込み関数の filter() に相当するかな。

>>> [1, 2, 3] | where(lambda x: x % 2 == 0) | as_list
[2]

take_while

特定の条件にマッチするまで要素を得るときは take_while を使う。 これは itertools.takewhile() に相当する。

>>> [1, 2, 3] | take_while(lambda x: x < 3) | as_list
[1, 2]

skip_while

同じように特定の条件にマッチする要素を飛ばすには skip_while を使う。 これは itertools.dropwhile() に相当する。

>>> [1, 2, 3] | skip_while(lambda x: x % 2 != 0) | as_list
[2, 3]

chain_with

オブジェクトに要素をつなげたいときは chain_with を使う。

>>> [1, 2, 3] | chain_with([4], [5, 6], [7]) | as_list
[1, 2, 3, 4, 5, 6, 7]

take

オブジェクトの先頭の要素だけ取り出したいときは take を使う。

>>> [1, 2, 3] | take(2) | as_list
[1, 2]

tail

同じように末尾の要素だけほしいときは tail を使う。

>>> [1, 2, 3] | tail(2) | as_list
[2, 3]

skip

先頭 n 個の要素から後ろがほしいときは skip を使う。

>>> [1, 2, 3, 4, 5] | skip(2) | as_list
[3, 4, 5]

islice

要素の中からスライスを取り出したいときは islice を使う。 これは itertools.islice() に相当する。

例えば先頭だけを取り出したいときは引数をひとつだけ指定する。

>>> [1, 2, 3, 4, 5] | islice(2) | as_list
[1, 2]

ふたつ指定すると開始と終了の場所が指定できる。

>>> [1, 2, 3, 4, 5] | islice(2, 4) | as_list
[3, 4]

みっつ指定すれば開始、終了に加えて n 個間隔で要素を取り出せる。

>>> [1, 2, 3, 4, 5] | islice(0, None, 2) | as_list
[1, 3, 5]

izip

ふたつの Iterable な要素をタプルにして取り出せるようにするには izip を使う。 これは itertools.izip() に相当する。

>>> [1, 2, 3] | izip(['a', 'b', 'c']) | as_list
[(1, 'a'), (2, 'b'), (3, 'c')]

aggregate

要素をひとつずつ順番に処理してひとまとめにするときは aggregate を使う。 これは functools.reduce() に相当する。

>>> [1, 2, 3] | aggregate(lambda l, r: l * r)
6

any

特定の条件にヒットする要素が含まれるかを調べるときは any が使える。

>>> [1, 2, 3] | any(lambda x: x > 2)
True

all

同じようにすべての要素が特定の条件にヒットするか調べるには all を使う。

>>> [1, 2, 3] | all(lambda x: x > 2)
False

max

Iterable なオブジェクトの中で最も大きなものを得るには max を使う。

>>> [1, 2, 3] | max
3

min

同じように最小なら min を使う。

>>> [1, 2, 3] | min
1

groupby

条件によってオブジェクトをグループに分けたいときは groupby を使う。 例えば偶数と奇数でグループ分けしてみよう。

>>> [1, 2, 3, 4] | groupby(lambda x: x % 2 == 0) | as_list
[(False, <itertools._grouper object at 0x1095a3390>), (True, <itertools._grouper object at 0x1095a3510>)]

そのままだと中身がジェネレータのままで見づらいのでリストに直す。 うん、かなり読みづらい。

>>> [1, 2, 3, 4] | groupby(lambda x: x % 2 == 0) | select(lambda x: x[1] | as_list) | as_list
[[1, 3], [2, 4]]

sort

内容をソートするには sort が使える。

>>> [1, 5, 2, 4, 3] | sort
[1, 2, 3, 4, 5]

ソートのやり方を指定するには key 引数を指定する。 これは sorted() の key と同じこと。

>>> ['This', 'is', 'a', 'pen'] | sort(key=str.lower)
['a', 'is', 'pen', 'This']

reverse

Iterable な内容をひっくり返すなら reverse を使う。

>>> [1, 2, 3] | reverse | as_list
[3, 2, 1]

permutations

繰り返しを許さない順列を得るには permutations を使う。

>>> 'abc' | permutations(2) | as_list
[('a', 'b'), ('a', 'c'), ('b', 'a'), ('b', 'c'), ('c', 'a'), ('c', 'b')]

自作のパイプを作る

pipe は自分でパイプに使う API を定義することもできる。 今回は試しに要素をランダムにシャッフルして返すものを作ってみよう。

pipe の API を作るには関数を @Pipe デコレータで修飾する。 関数はひとつの引数を取って値を返す。 引数はパイプの左側から渡されるオブジェクトになる。 そして返り値はパイプの右側に渡されるオブジェクトになる。

>>> import random
>>> @Pipe
... def shuffle(x):
...     random.shuffle(x)
...     for i in x:
...         yield i
...

実際に使ってみよう。 リストに対して自作した shuffle を使うとジェネレータが返る。

>>> [1, 2, 3, 4] | shuffle
<generator object shuffle at 0x109732730>

このままだと分かりにくいのでリストに直そう。

>>> [1, 2, 3, 4] | shuffle | as_list
[4, 3, 1, 2]

ばっちり中身がシャッフルされている。

まとめ

ここまで見てきたように pipe を使うととても独創的な記法で処理が記述できる。 あなたはもう pipe の魅力にメロメロのはずだ。 明日からでも本番のコードで使いたくなったに違いない。

いえ、わたしは遠慮しておきます。

補足

ちなみに、こうした API は Python がパイプ演算子の挙動をオーバーライドできるために実現できる。

すごく単純なサンプルを書いてみよう。 具体的にはユーザ定義クラスで特殊メソッド __ror__() を実装する。

>>> class Print(object):
...     def __ror__(self, obj):
...         print(obj)
... 

これで Print クラスのインスタンスにパイプ演算子を使ったときに __ror__() メソッドが呼ばれる。

>>> 'Hello, World!' | Print()
Hello, World!

自身の左側にある演算子に反応するところがミソだね。

Python: concurrent.futures を使った並行・並列処理

Python の concurrent.futures はバージョン 3.2 で追加された並行・並列処理用のパッケージ。 似たようなパッケージにはこれまでにも threading や multiprocessing があったんだけど、これはそれよりも高レベルの API になっている。 デフォルトでスレッド・プロセスプールが使えたり、マルチスレッドとマルチプロセスがほとんどコードを変えずに使い分けられるメリットがある。

下準備

使う Python のバージョンが 3.2 未満のときは PyPI にあるバックポート版のパッケージをインストールする必要がある。

$ pip install futures

ただし、今回使う環境は Python 3.5 なので関係ない。

$ python --version
Python 3.5.1
$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.11.3
BuildVersion:   15D21

シングルスレッドの場合

今回は扱うものが並行・並列処理なので、計算量がたくさん必要になるものを題材とする。 試しに、いくつかの大きな数を素数か判定するプログラムをシングルスレッドで書いてみよう。 アルゴリズムとしては単純な試し割り法を使った。

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

import math


def _is_prime(n):
    """数値が素数か判定する関数"""
    if n < 2:
        return False

    if n == 2:
        return True

    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False

    return True


def _calc():
    # 調査対象の数値
    values = [
        112272535095293,
        112582705942171,
        112272535095293,
        115280095190773,
        115797848077099,
        1099726899285419,
    ]
    for target in values:
        # 素数かチェックして結果を表示する
        result = _is_prime(target)
        msg = '{n}: {prime}'.format(n=target, prime=result)
        print(msg)


def main():
    import time
    start = time.time()
    _calc()
    end = time.time()
    # 計算にかかった時間を表示する
    elapsed_time = end - start
    msg = 'Time: {elapsed_time} sec'.format(elapsed_time=elapsed_time)
    print(msg)


if __name__ == '__main__':
    main()

これを実行してみると、それぞれの数をリストに収まっている順番で判定していくことになる。

$ python singlethread.py
112272535095293: True
112582705942171: True
112272535095293: True
115280095190773: True
115797848077099: True
1099726899285419: False
Time: 3.3549559116363525 sec

シングルスレッド版は 3.35 秒かかった。

マルチスレッドの場合

次は先ほどのサンプルをマルチスレッドにしてみよう。 ソースコードの細かい説明はコメントで行っている。

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

import math
from concurrent import futures


def _is_prime(n):
    """数値が素数か判定する関数"""
    if n < 2:
        return False

    if n == 2:
        return True

    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False

    return True


def _calc():
    # 調査対象の数値
    values = [
        112272535095293,
        112582705942171,
        112272535095293,
        115280095190773,
        115797848077099,
        1099726899285419,
    ]
    with futures.ThreadPoolExecutor() as executor:
        # 並行処理する対象とそれに対応する値 (処理の引数) を辞書で用意する
        mappings = {executor.submit(_is_prime, n): n for n in values}
        # futures.as_completed() は処理がおわったものから結果を返していくジェネレータ
        for future in futures.as_completed(mappings):
            # 完了した処理に対応する引数を辞書から取得する
            target = mappings[future]
            # 処理結果を取得する
            result = future.result()
            # 結果を表示する
            msg = '{n}: {prime}'.format(n=target, prime=result)
            print(msg)


def main():
    import time
    start = time.time()
    _calc()
    end = time.time()
    # 計算にかかった時間を表示する
    elapsed_time = end - start
    msg = 'Time: {elapsed_time} sec'.format(elapsed_time=elapsed_time)
    print(msg)


if __name__ == '__main__':
    main()

次も同じように実行してみよう。

$ python multithread.py
1099726899285419: False
112582705942171: True
112272535095293: True
112272535095293: True
115280095190773: True
115797848077099: True
Time: 3.3795218467712402 sec

先ほどとかかっている時間はほとんど変わっていない。 マルチスレッドにしたのに何故かと不思議に思われるかもしれない。 ただ、これにはちゃんとした原因がある。 CPython には GIL (Global Interpretor Lock) というものがあって、マルチスレッドにしても同時に実行できるスレッドはひとつだけだからだ。 むしろスレッドの生成コストの分だけ遅くなっても何ら不思議ではない。 とはいえ同時に実行できるスレッドがひとつなだけで、処理は「並行」に進められている。 その証拠に、先ほどとは数が出力されている順番が異なることに気づくことだろう。

マルチプロセスの場合

次は先ほどのサンプルをマルチプロセスにした場合。 なんと修正するところはたったひとつ。 先ほど使った ThreadPoolExecutor を ProcessPoolExecutor に変えるだけ。 これでマルチスレッドの実行がマルチプロセスでの実行に変わる。

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

import math
from concurrent import futures


def _is_prime(n):
    """数値が素数か判定する関数"""
    if n < 2:
        return False

    if n == 2:
        return True

    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False

    return True


def _calc():
    # 調査対象の数値
    values = [
        112272535095293,
        112582705942171,
        112272535095293,
        115280095190773,
        115797848077099,
        1099726899285419,
    ]
    with futures.ProcessPoolExecutor() as executor:
        # 並行処理する対象とそれに対応する値 (処理の引数) を辞書で用意する
        mappings = {executor.submit(_is_prime, n): n for n in values}
        # futures.as_completed() は処理がおわったものから結果を返していくジェネレータ
        for future in futures.as_completed(mappings):
            # 完了した処理に対応する引数を辞書から取得する
            target = mappings[future]
            # 処理結果を取得する
            result = future.result()
            # 結果を表示する
            msg = '{n}: {prime}'.format(n=target, prime=result)
            print(msg)


def main():
    import time
    start = time.time()
    _calc()
    end = time.time()
    # 計算にかかった時間を表示する
    elapsed_time = end - start
    msg = 'Time: {elapsed_time} sec'.format(elapsed_time=elapsed_time)
    print(msg)


if __name__ == '__main__':
    main()

同じように実行してみよう。

$ python multiprocess.py
112272535095293: True
112582705942171: True
112272535095293: True
115280095190773: True
1099726899285419: False
115797848077099: True
Time: 1.3142600059509277 sec

今度は複数のプロセスで「並列」に処理が進んだから実行時間が短縮されている。

まとめ

今回は Python の標準ライブラリ concurrent.futures を使った並行・並列処理について紹介した。 このライブラリを使うとマルチスレッドとマルチプロセスでほとんどソースコードを変えずに使い分けることができる。 ただし、マルチプロセスを使うときはひとつだけ注意が必要だ。 マルチプロセスの場合、プロセス間でのオブジェクトのやり取りは Pickle にもとづいて行われる。 そのため、並列化する関数の引数と返り値には Pickle 化できないオブジェクトを使うことができない。 Pickle についてはこのブログで以前に書いたことがあるのであわせて読んでもらいたい。

blog.amedama.jp

Python: TensorFlow のチュートリアルでソフトマックス回帰を試す

最近、囲碁でプロ棋士を負かした AlphaGo も、この TensorFlow で組んだニューラルネットワークを使っているんだとか。 とはいえ、チュートリアルを読みながら使ってみると、これは単にディープラーニングだけをターゲットにしたライブラリではないように感じた。 どちらかといえば機械学習で使われるモデルをプログラムで表現して動かしやすくするためのツールキットのようだ。

今回試すソフトマックス回帰というのは TensorFlow に用意されている最初のチュートリアルで使う機械学習アルゴリズムだ。 モデルとしては一層のニューラルネットワークとソフトマックス関数を組み合わせたシンプルなものになっている。 一層のニューラルネットワークなので、これはディープラーニングではないし、ぶっちゃけ最終的に得られる汎化性能も高くない。

チュートリアルでは MNIST という手書き数字のデータセットを使っている。 このデータセットには 28x28 ピクセルの画像データと、それが数字の何を表すかの正解となるラベルデータが入っている。 手順としては、まずソフトマックス回帰のアルゴリズムを訓練用データを使って学習させる。 そして、学習に使っていないテスト用のデータを識別させて正答率を出す。 この、学習に使っていないテスト用のデータで正答率を出すのが汎化性能 (未知のデータに対処する能力) を測る上で重要となる。

今回使う環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.11.3
BuildVersion:   15D21
$ python --version
Python 3.5.1

元ネタのチュートリアルはこちら。

https://www.tensorflow.org/versions/0.6.0/tutorials/mnist/beginners/index.html

TensorFlow をインストールする

TensorFlow はバージョン 0.6.0 で Python 3 に対応している。 公式で各ターゲット向けの Wheel ファイルが配布されているので、それを使ってインストールする。 今回の環境 Mac OS X w/ Python 3 であれば次の Wheel を使う。

$ pip install -U https://storage.googleapis.com/tensorflow/mac/tensorflow-0.6.0-py3-none-any.whl

データセットを確認する

チュートリアルのソフトマックス回帰ではデータセットとして 28x28 ピクセルの手書き数字を収めた MNIST を使う。 あらかじめ、どういったものなのかを見ておこう。

データセットの内容を視覚化するためにグラフ描画ライブラリの matplotlib をインストールしておく。

$ pip install matplotlib

まず適当な作業ディレクトリで Python の REPL を起動したら MNIST のデータセットをダウンロードする。 これには少し時間がかかるはずだ。

$ python
>>> from tensorflow.examples.tutorials.mnist import input_data
>>> mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
Extracting MNIST_data/train-images-idx3-ubyte.gz
Extracting MNIST_data/train-labels-idx1-ubyte.gz
Extracting MNIST_data/t10k-images-idx3-ubyte.gz
Extracting MNIST_data/t10k-labels-idx1-ubyte.gz

学習用のデータは 55000 点ある。 データの内容は 28x28 ピクセルなので 784 次元だ。 各ピクセルは色の濃淡に対応して 0 ~ 1 の範囲で値を取る。 ちなみにグレースケールだ。

>>> mnist.train.images.shape
(55000, 784)
>>> mnist.train.images[0]
array([ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
        0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
...(省略)...
        0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
        0.        ,  0.        ,  0.01568628,  0.45882356,  0.27058825,
        0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
        0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
        0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
        0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
        0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
        0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
        0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
        0.        ,  0.        ,  0.        ,  0.        ], dtype=float32)

そして、学習用のデータには対応する正解ラベルがある。 こちらは 0 ~ 9 までの数字を表すので 10 次元になっている。

>>> mnist.train.labels.shape
(55000, 10)
>>> mnist.train.labels[0]
array([ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.])

データセットの中からいくつか適当に選んで可視化してみよう。

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

from matplotlib import pyplot as plt
from matplotlib import cm
import numpy as np
from numpy import random

from tensorflow.examples.tutorials.mnist import input_data


def _to_number(label):
    for index, n in enumerate(label):
        if n != 0:
            return index


def main():
    mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
    X = mnist.train.images
    y = mnist.train.labels

    # データの中から 25 点を無作為に選び出す
    p = random.random_integers(0, len(X), 25)

    # 選んだデータとラベルを matplotlib で表示する
    samples = np.array(list(zip(X, y)))[p]
    for index, (data, label) in enumerate(samples):
        # 画像データを 5x5 の格子状に配置する
        plt.subplot(5, 5, index + 1)
        # 軸に関する表示はいらない
        plt.axis('off')
        # データを 8x8 のグレースケール画像として表示する
        plt.imshow(data.reshape(28, 28), cmap=cm.gray_r, interpolation='nearest')
        n = _to_number(label)
        # 画像データのタイトルに正解ラベルを表示する
        plt.title(n, color='red')
    # グラフを表示する
    plt.show()


if __name__ == '__main__':
    main()

上記を実行すると、こんな感じ。

f:id:momijiame:20160208190456p:plain

TensorFlow を使ってモデルを組む

ここからはソフトマックス回帰のアルゴリズムを TensorFlow で組んでいく。

まず TensorFlow で登場する概念のひとつが placeholder だ。 これは「何らかの値が入る」場所を表している。 次のコードは x という変数は float32 型で 784 次元の何らかのデータがここに入る、という意味になる。 第二引数の配列の最初が None になっているのは、長さが可変なことを示している。 ここではデータ点数がいくつあっても良いという意味になる。 x には入力となる画像データが入る。

>>> import tensorflow as tf
>>> x = tf.placeholder(tf.float32, [None, 784])

次に登場するのが Variable で、これはニューラルネットワークに使う変数になっている。 W は入力 (各ピクセルデータ) に対する重みなので 784 次元を受け取って 0 ~ 9 の各数字に対応する 10 次元を返す。 b は各数字に対するバイアスだ。

>>> W = tf.Variable(tf.zeros([784, 10]))
>>> b = tf.Variable(tf.zeros([10]))

ここまでに用意した placeholder と Variable を使って数式を表現する。 ひとつのパーセプトロンは y = xW + b で表現できる。 次のコードは、右辺にソフトマックス関数をかましている以外はその数式そのものを表している。 ソフトマックス関数は活性化関数というもの。 xW + b で得られた出力から、その数字の確率はどの程度かを 0 ~ 1 の間で表現するのに使われるんだとおもう。 y にはアルゴリズムが予測したそれぞれの数字の確率が入る。

>>> y = tf.nn.softmax(tf.matmul(x, W) + b)

次に、どのように学習を進めるかを表現する。 まずは先ほどと同じように placeholder を用意する。 y_ には正解のラベルデータが入る。

>>> y_ = tf.placeholder(tf.float32, [None, 10])

学習には、アルゴリズムの予想内容と正解のラベルデータを使って、その交差エントロピーを使う。 それを表現しているのが次のコードだ。 学習を進めるための指針となるこれをコスト関数と呼ぶ。

>>> cross_entropy = -tf.reduce_sum(y_*tf.log(y))

学習は先ほどの交差エントロピーが小さくなるように進める。 交差エントロピーが最小となるポイントは勾配降下法というアルゴリズムで見つける。 それを表現しているのが次のコードだ。

>>> train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)

これで必要な材料は揃った。 ここからは実際の学習を行うフェーズだ。 まずは各 Variable の値の初期値を用意して、学習のコンテキストとなる Session オブジェクトに渡す。

>>> init = tf.initialize_all_variables()
>>> sess = tf.Session()
>>> sess.run(init)

次に学習データを使って学習させる。 ここではデータを 100 点ずつ 1000 回取り出して学習に使っている。 学習には Session#run() 関数を使う。 feed_dict には placeholder に当てはめる具体的な値を渡す。

>>> for _ in range(1000):
...   batch_xs, batch_ys = mnist.train.next_batch(100)
...   sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})
...

次に汎化性能を数値で得るための準備に入ろう。 ここも TensorFlow でモデルを用意することになる。 まず、予測結果は各数字ごとの確率で得られるので、その確率が最も高いものを採用したい。 次のコードでは予測結果と正解のラベルデータから最も数値が高いものを取り出して比較することを意味している。 つまり予測と正解が一致しているか否かが correct_prediction の中に入る。

>>> correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))

上記で得られる内容は真偽値なので、それを浮動小数点にキャストする。 True は 1.0 で False は 0.0 になる。 つまり、各予測に対して 0.0 か 1.0 が入ることになる。 それらの算術平均を取ればソフトマックス回帰を使った正答率が得られるという寸法だ。

>>> accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))

このモデルにテスト用のデータを当てはめて実行する。 x にテスト用の画像データを、y_ に正解となるラベルを渡せば良い。

>>> print(sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels}))
0.9183

正答率は 91.83% となった。

上記だけだと何だかよく分からないので個別の画像データを識別させてみよう。 今度はモデルが予測した内容だけを表示する。 予測内容の y から最も確率の高いものだけを表示する関数 f を用意する。 そして x を渡して実行すれば予測結果が得られる。 テストデータの最初にあるデータをひとつだけ渡してみよう。

>>> f = tf.argmax(y, 1)
>>> sess.run(f, feed_dict={x: [mnist.test.images[0]]})
array([7])

予測結果は 7 となった。

>>> mnist.test.labels[0]
array([ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.])

正解のラベルデータを見ると 8 番目の要素、つまり 7 に対応するインデックスに 1 が立っている。 どうやら正解のようだ。

どのように予測したのか確率を見たいときは argmax() 関数を使うまでもなく y そのものを見ればいい。

>>> f = y
>>> sess.run(f, feed_dict={x: [mnist.test.images[0]]})
array([[  1.59484807e-05,   8.12810930e-10,   3.76464246e-04,
          1.28450710e-03,   1.43751876e-07,   9.29207999e-06,
          1.45384793e-09,   9.98167157e-01,   7.07713616e-06,
          1.39490847e-04]], dtype=float32)

よくみると 7 に対応するインデックスだけ 9.98167157e-01 と圧倒的に値が大きくなっていることがわかる。

まとめ

今回は TensorFlow のチュートリアルに沿って MNIST のソフトマックス回帰を試した。 ちなみに、今回得られた 91.83% という正答率は MNIST を使った分類の結果としてはかなりひどい数字らしい。 これまでに知られている最も優れたモデルでは 99.2% の正答率が得られるんだとか。

ここまで見てきたように TensorFlow はディープラーニングに特化したライブラリというわけではないようだ。 あくまで機械学習で用いられる数式をプログラムに落とし込みやすいようにしたパーツを提供しているに過ぎない。 ディープラーニングをしたいなら、それらのパーツを使って自分自身でモデルを組み上げていく必要がある。

Python: モジュールを動的にロードする

Python のモジュールは通常であれば import 文を使ってあらかじめ静的にロードする。 では、プログラムの中で動的にロードしたい場合にはどうしたらいいのか?というのが今回の話。 結論から先に言うと今は標準ライブラリの importlib を使うのが推奨される方法のようだ。

使った環境は次の通り。

$ python --version
Python 3.5.1
$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.11.3
BuildVersion:   15D21

下準備

まずは適当に Python のモジュールを作っておく。 Python のモジュールというのは単なる Python ファイルのことだ。

$ cat << 'EOF' > helloworld.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


def greet():
    print('Hello, World!')

EOF

動的にロードしてみる

ファイル名が helloworld.py であれば Python からは helloworld モジュールとしてインポートできる。 まずは通常通り import 文を使って helloworld モジュールをロードしてみよう。 helloworld.py があるのと同じディレクトリで Python の REPL を起動して次を実行する。

$ python
>>> import helloworld
>>> helloworld.greet()
Hello, World!

ちゃんとモジュールがロードできていることが分かる。

次に importlib を使ってロードする方法を試してみよう。 importlib を使ってモジュールをロードするには import_module() 関数を使う。 この関数にはモジュール名を文字列で指定すると返り値としてモジュールが得られる。

>>> import importlib
>>> module = importlib.import_module('helloworld')
>>> module.greet()
Hello, World!
>>> module.__name__
'helloworld'

ばっちりだ。

名前を指定してロードしたい

先ほどの import_module() 関数ではモジュール名の指定ができなかった。 モジュール名を指定してロードしたい場合には importlib の中でもより低レベルな API を使う必要がある。 具体的には importlib.machinery の SourceFileLoader を使う。 このクラスはモジュール名と、その名前で読み込む Python ファイルを指定してインスタンス化することになっている。 得られたインスタンスの load_module() メソッドを呼び出すことでモジュールが手に入るという寸法だ。

>>> from importlib import machinery
>>> loader = machinery.SourceFileLoader('mymodule', 'helloworld.py')
>>> module = loader.load_module()
>>> module.greet()
Hello, World!
>>> module.__name__
'mymodule'

指定した通り helloworld.py が mymodule という名前のモジュールとしてロードされていることがわかる。

注意点

標準ライブラリの importlib は Python 3.3 から導入されたモジュールだ。 以前は同様の機能を提供する標準ライブラリとして imp があったものの、これは 3.x では非推奨となっている。 Python 2.7 にも一応は importlib が存在するものの、限定的な機能を提供するサブセットになっている。 もし Python 2.x でもフルセットの importlib が使いたいときはバックポート版を PyPI からインストールする必要がある。

$ pip install importlib

めでたしめでたし。

Python: doctest を書いてみよう

今回は Python が標準で提供しているテスト機構の一つである doctest について書いてみる。 doctest というのは docstring という Python のドキュメンテーション機構を使って一緒にテストもしちゃおうという発想の代物。 docstring については以前にもこのブログで詳しく書いたことがある。

blog.amedama.jp

docstring のおさらい

ひとまず docstring のおさらいから入ろう。 docstring はモジュールや関数などを解説するための特殊なコメント。

例えば次の FizzBuzz を実装した関数 fizzbuzz() に対して docstring を書いてみることにしよう。

# -*- coding: utf-8 -*-


def fizzbuzz(n):
    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)

docstring は関数の直後に書く。 例えばこんな感じ。 通常は docstring 用のコメントを書くのに """ または ''' を使う。

# -*- coding: utf-8 -*-


def fizzbuzz(n):
    '''
    FizzBuzz を実装した関数です。
    引数の整数に応じて次のような値を返します。
      - 3 で割り切れる: 'Fizz'
      - 5 で割り切れる: 'Buzz'
      - 3 と 5 の両方で割り切れる: '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)

docstring を書くと、それがモジュールや関数のドキュメントになる。 docstring で書かれたドキュメントは組み込み関数 help() を使うことで読める。

例えば、先ほどの fizzbuzz のサンプルコードを fizzbuzz.py という名前で保存しておこう。 そして、同じディレクトリで Python の REPL を起動する。 fizzbuzz() 関数をインポートしたら、それを help() 関数に渡してやろう。

>>> from fizzbuzz import fizzbuzz
>>> help(fizzbuzz)

すると、次のように fizzbuzz() 関数に書いた docstring を確認できる。

Help on function fizzbuzz in module fizzbuzz:

fizzbuzz(n)
    FizzBuzz を実装した関数です。
    引数の整数に応じて次のような値を返します。
      - 3 で割り切れる: 'Fizz'
      - 5 で割り切れる: 'Buzz'
      - 3 と 5 の両方で割り切れる: 'FizzBuzz'
      - いずれの値でも割り切れない: 数値の文字列表現

doctest を書いてみよう

doctest を書くには、まずテスト対象への入力と期待される出力の組が必要になる。 これには先ほど docstring を読んだのと同様に REPL を使うと良い。

例えば fizzbuzz() 関数にいくつかの値を入力して、その出力を確認しておこう。

$ python
>>> from fizzbuzz import fizzbuzz
>>> fizzbuzz(2)
'2'
>>> fizzbuzz(3)
'Fizz'
>>> fizzbuzz(4)
'4'
>>> fizzbuzz(5)
'Buzz'
>>> fizzbuzz(6)
'Fizz'
>>> fizzbuzz(7)
'7'
>>> fizzbuzz(14)
'14'
>>> fizzbuzz(15)
'FizzBuzz'
>>> fizzbuzz(16)
'16'

doctest で必要な作業のほとんどは上記のように REPL で得られた出力をそのまま docstring の中に埋め込むだけで終わる。 先ほどの fizzbuzz() 関数の中に先ほど得られた内容をそのまま埋め込んでみよう。 コードの前に入っているインデントに使われる空白は無視されるので、特に気にする必要はない。 そして、モジュールがスクリプトとして実行された際に doctest を実行するためのイディオムをソースコードの末尾に追加する。

# -*- coding: utf-8 -*-


def fizzbuzz(n):
    '''
    FizzBuzz を実装した関数です。
    引数の整数に応じて次のような値を返します。
      - 3 で割り切れる: 'Fizz'
      - 5 で割り切れる: 'Buzz'
      - 3 と 5 の両方で割り切れる: 'FizzBuzz'
      - いずれの値でも割り切れない: 数値の文字列表現

      >>> from fizzbuzz import fizzbuzz
      >>> fizzbuzz(2)
      '2'
      >>> fizzbuzz(3)
      'Fizz'
      >>> fizzbuzz(4)
      '4'
      >>> fizzbuzz(5)
      'Buzz'
      >>> fizzbuzz(6)
      'Fizz'
      >>> fizzbuzz(7)
      '7'
      >>> fizzbuzz(14)
      '14'
      >>> fizzbuzz(15)
      'FizzBuzz'
      >>> fizzbuzz(16)
      '16'

    '''
    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)


if __name__ == "__main__":
    import doctest
    doctest.testmod()

これで doctest を実行する準備は整った。 早速 fizzbuzz.py をスクリプトとして実行してみよう。

$ python fizzbuzz.py
$

出力結果が何もない状態で実行が終了した。 とはいえ、これで何も問題ない。

テストの実行結果を詳しく見たいときは -v オプションを付けて実行しよう。

$ python fizzbuzz.py -v
Trying:
    from fizzbuzz import fizzbuzz
Expecting nothing
ok
Trying:
    fizzbuzz(2)
Expecting:
    '2'
ok
Trying:
    fizzbuzz(3)
Expecting:
    'Fizz'
ok
Trying:
    fizzbuzz(4)
Expecting:
    '4'
ok
Trying:
    fizzbuzz(5)
Expecting:
    'Buzz'
ok
Trying:
    fizzbuzz(6)
Expecting:
    'Fizz'
ok
Trying:
    fizzbuzz(7)
Expecting:
    '7'
ok
Trying:
    fizzbuzz(14)
Expecting:
    '14'
ok
Trying:
    fizzbuzz(15)
Expecting:
    'FizzBuzz'
ok
Trying:
    fizzbuzz(16)
Expecting:
    '16'
ok
1 items had no tests:
    __main__
1 items passed all tests:
  10 tests in __main__.fizzbuzz
10 tests in 2 items.
10 passed and 0 failed.
Test passed.

今度は様々な入力に対して期待される結果と実際の結果が比較されていることがわかる。

スクリプトとして実行した時に doctest 以外の処理を走らせたい

先ほどの例ではモジュールをスクリプトとして実行した際に doctest が走るようになっていた。 とはいえ、doctest を書いた状態でスクリプトとして実行した時は別の処理を走らせたいというニーズももちろんあるはず。 そんなときはどうするかについても書いておく。

まずは先ほどのソースコードの末尾にあったイディオムを取り除こう。

# -*- coding: utf-8 -*-


def fizzbuzz(n):
    '''
    FizzBuzz を実装した関数です。
    引数の整数に応じて次のような値を返します。
      - 3 で割り切れる: 'Fizz'
      - 5 で割り切れる: 'Buzz'
      - 3 と 5 の両方で割り切れる: 'FizzBuzz'
      - いずれの値でも割り切れない: 数値の文字列表現

      >>> from fizzbuzz import fizzbuzz
      >>> fizzbuzz(2)
      '2'
      >>> fizzbuzz(3)
      'Fizz'
      >>> fizzbuzz(4)
      '4'
      >>> fizzbuzz(5)
      'Buzz'
      >>> fizzbuzz(6)
      'Fizz'
      >>> fizzbuzz(7)
      '7'
      >>> fizzbuzz(14)
      '14'
      >>> fizzbuzz(15)
      'FizzBuzz'
      >>> fizzbuzz(16)
      '16'

    '''
    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)

この状態で doctest を実行する場合には -m オプションに doctest を指定しつつ、テストしたいファイルを渡せば良い。

$ python -m doctest fizzbuzz.py
$

例外を扱う場合

テストの中で例外が上がる際の挙動について記述したい場合もあるはず。

例えば、次のように常に Exception を上げる関数の doctest を書きたい。 まずはこのソースコードを exception.py という名前で保存しておこう。

# -*- coding: utf-8 -*-


def always_raise():
    '''
    常に Exception を上げます。
    '''
    raise Exception('Oops!')

そして、先ほどと同じように REPL で例外が上がる際の出力を手に入れる。 ただ、この状態だと保存されているファイルなどがトレースバックの中に含まれていて邪魔だ。

$ python
>>> from exception import always_raise
>>> always_raise()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/amedama/Documents/workspace/exception.py", line 5, in always_raise
    raise Exception('Oops!')
Exception: Oops!

doctest に組み込む際には Traceback 以降の行は ... (ピリオドを3つ以上連続させる) で省略してしまって構わない。 必要なのは最初の Traceback ... の行と、最後の Exception: ... の行だけだ。

# -*- coding: utf-8 -*-


def always_raise():
    '''
    常に Exception を上げます。

    >>> from exception import always_raise
    >>> always_raise()
    Traceback (most recent call last):
        ...
    Exception: Oops!
    '''
    raise Exception('Oops!')

上記の doctest を実行してみよう。

$ python -m doctest -v exception.py
Trying:
    from exception import always_raise
Expecting nothing
ok
Trying:
    always_raise()
Expecting:
    Traceback (most recent call last):
        ...
    Exception: Oops!
ok
1 items had no tests:
    exception
1 items passed all tests:
   2 tests in exception.always_raise
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

ばっちりだ。

使った感じ doctest はがっつりテストをするというよりも、どちらかというと使い方の例を明示しつつデグレードを防ぐような用途で使うのが良さそうな気がした。

Python: 自作パッケージにデータファイルを含める

Python で自作パッケージを作る際に、ソースコード以外のファイルを含めたくなる場合がある。 例えば Web アプリケーションを作るときの HTML テンプレートなんかはよくあるパターン。 今回は自作パッケージにソースコード以外のファイル (データファイル) を含めるやり方について書いてみる。

使用する環境は次の通り。

$ python --version
Python 3.5.1
$ pip list
pip (7.1.2)
setuptools (18.2)
wheel (0.24.0)

自作パッケージを用意する

データファイル云々を説明する前に、まずはそれを入れるためのパッケージから作っていこう。 今回作成するパッケージは 'mypackage' という名前にする。

最初にプロジェクトのディレクトリを用意する。 作成するパッケージ名とプロジェクトのディレクトリ名は揃える場合が多い。

$ mkdir mypackage
$ cd mypackage

プロジェクトのディレクトリに移動したら、さらに同じ名前でディレクトリを作る。 これが Python パッケージになるディレクトリだ。

$ mkdir mypackage

作成したディレクトリの配下に init.py という名前のファイルを作る。 これで mypackage ディレクトリが Python のパッケージとして認識できるようになる。 ファイルには動作確認用に greet() という名前の関数をひとつだけ用意しておこう。

$ cat << 'EOF' > mypackage/__init__.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


def greet():
    print('Hello, World!')
EOF

次に mypackage のセットアップスクリプト (setup.py) を用意する。 このファイルを用意することで自作の Python パッケージを pip コマンドなどから扱えるようになる。

$ cat << 'EOF' > setup.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from setuptools import setup
from setuptools import find_packages


def main():
    description = 'mypackage'

    setup(
        name='mypackage',
        version='0.0.1',
        author='example',
        author_email='example@example.jp',
        url='www.example.jp',
        description=description,
        long_description=description,
        zip_safe=False,
        include_package_data=True,
        packages=find_packages(),
        install_requires=[],
        tests_require=[],
        setup_requires=[],
    )


if __name__ == '__main__':
    main()
EOF

これで mypackage パッケージが完成した。 ここまででディレクトリ構成は次のようになっている。

$ tree
.
├── mypackage
│   └── __init__.py
└── setup.py

1 directory, 2 files

自作パッケージをインストールする

それでは先ほど作ったセットアップスクリプトを使ってパッケージをインストールしよう。 パッケージのインストールにはスクリプトのサブコマンドとして install を指定する。

$ python setup.py install
...(省略)...
Processing dependencies for mypackage==0.0.1
Finished processing dependencies for mypackage==0.0.1

pip コマンドでインストール済みのパッケージを見ると mypackage がインストールされたことがわかる。

$ pip list | grep -i mypackage
mypackage (0.0.1)

インストールされたパッケージが使えることを確認しておこう。 Python からパッケージをインポートして動作確認用の関数を呼び出してみる。

$ python -c "import mypackage; mypackage.greet()"
Hello, World!

ばっちりだ。

ソースコード配布物を作る

ここからは説明の都合上、ソースコード配布物を使うことにする。 パッケージのソースコード配布物は sdist サブコマンドで作成できる。

$ python setup.py sdist
...(省略)...
Writing mypackage-0.0.1/setup.cfg
Creating tar archive
removing 'mypackage-0.0.1' (and everything under it)

コマンドを実行すると dist ディレクトリにソースコード配布物の tar ball ができている。

$ ls dist | grep tar.gz$
mypackage-0.0.1.tar.gz

できたソースコード配布物を適当な場所に展開しよう。

$ tar xvf dist/mypackage-0.0.1.tar.gz -C /tmp

中身はこんなかんじになっている。 あまり詳しくは説明しないものの、元のセットアップスクリプトとパッケージ以外にもパッケージ情報を格納したファイルなどが入っている。

$ tree /tmp/mypackage-0.0.1
/tmp/mypackage-0.0.1
├── PKG-INFO
├── mypackage
│   └── __init__.py
├── mypackage.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   ├── not-zip-safe
│   └── top_level.txt
├── setup.cfg
└── setup.py

2 directories, 9 files

ソースコード配布物にデータファイルを含める

さて、ここから今回の本題に入っていく。 まずはパッケージというかソースコード配布物に含めたいデータファイルを用意しよう。 例として greeting.txt という名前のテキストファイルを作ることにする。

$ cat << 'EOF' > greeting.txt
Hello, World!
EOF

さて、この状態で改めてソースコード配布物をビルドして展開してみよう。

$ python setup.py sdist
$ tar xvf dist/mypackage-0.0.1.tar.gz -C /tmp

先ほど作ったデータファイルが展開されたディレクトリに含まれるか調べてみよう。

$ find /tmp/mypackage-0.0.1 -name "greeting.txt"

残念ながら見つからない。 何もしない状態ではデータファイルはソースコード配布物に含まれないようだ。

では、どうすればデータファイルがソースコード配布物に含まれるようになるのだろうか。 これには MANIFEST.in という名前をもった設定ファイルが必要になる。 先ほど作った greeting.txt を含めるように include で指定しよう。

$ cat << 'EOF' > MANIFEST.in
include greeting.txt
EOF

改めてソースコード配布物をビルドして展開し直す。

$ python setup.py sdist
$ tar xvf dist/mypackage-0.0.1.tar.gz -C /tmp

すると今度は展開されたディレクトリの中に greeting.txt が見つかった!

$ find /tmp/mypackage-0.0.1 -name "greeting.txt"
/tmp/mypackage-0.0.1/greeting.txt

パッケージ内にデータファイルを含める

先ほどはソースコード配布物の中には含めたものの Python パッケージの中に入れていなかった。 次はデータファイルをパッケージ内に作るパターンを確認しておこう。

パッケージ内に data という名前でディレクトリを作って、そこに先ほどの greeting.txt を移動させよう。

$ mkdir -p mypackage/data
$ mv greeting.txt mypackage/data

次は MANIFEST.in の指定もちょっと工夫してみる。 具体的には recursive-include を使うことで特定のディレクトリ以下の内容を再帰的に検索して含めるようにする。 例えば次の設定では mypackage/data に含まれるすべての内容が含まれるようになる。

$ cat << 'EOF' > MANIFEST.in
recursive-include mypackage/data *
EOF

ディレクトリ構成としては次のようになる。 pycache というディレクトリは先ほどソースコード配布物をビルドした際にパッケージの内容がコンパイルされたためにできた。

$ tree mypackage
mypackage
├── __init__.py
├── __pycache__
│   └── __init__.cpython-35.pyc
└── data
    └── greeting.txt

2 directories, 3 files

この状態で改めてソースコード配布物をビルドして展開し直そう。 先ほど展開した内容を途中で一旦削除している点に注意する。

$ python setup.py sdist
$ rm -rf /tmp/mypackage-0.0.1
$ tar xvf dist/mypackage-0.0.1.tar.gz -C /tmp

ソースコード配布物を展開した内容を調べるとパッケージ内に greeting.txt が含まれていることがわかる。

$ find /tmp/mypackage-0.0.1 -name "greeting.txt"
/tmp/mypackage-0.0.1/mypackage/data/greeting.txt

データファイルがインストールされることを確認する

データファイルがソースコード配布物に含まれることは確認できたので、次はそれが実際にインストールされることを確認しよう。 まずは最初にインストールした内容 (データファイルが含まれていなかった) をアンインストールした上で、もう一度パッケージをインストールし直す。

$ pip uninstall -y mypackage
$ python setup.py install

この状態でサードパーティ製のパッケージがインストールされるディレクトリに移動する。 具体的には Python の site-packages というディレクトリになるんだけど、その場所を取得してそこに移動するには次のようにする。

$ cd $(python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")

インストールされた内容を確認すると、ちゃんと data ディレクトリと greeting.txt ファイルが格納されていることがわかる。

$ tree mypackage-0.0.1-py3.5.egg
mypackage-0.0.1-py3.5.egg
├── EGG-INFO
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   ├── not-zip-safe
│   └── top_level.txt
└── mypackage
    ├── __init__.py
    ├── __pycache__
    │   └── __init__.cpython-35.pyc
    └── data
        └── greeting.txt

4 directories, 8 files

インストールされたデータファイルを扱う

さて、データファイルがインストールされたことは確認できたので、次は実際にそれを使ってみよう。

インストール済みのデータファイルにアクセスするには、標準ライブラリであれば pkgutil というモジュールを使う。 pkgutil.get_data() 関数にパッケージ名とファイル名を指定するとその内容がバイナリで取得できることがわかる。

$ python
>>> import pkgutil
>>> pkgutil.get_data('mypackage', 'data/greeting.txt')
b'Hello, World!\n'

あとは取得したデータを煮るなり焼くなり好きにすれば良い。

ちなみに、これは setuptools の pkg_reources モジュール経由でも取得できる。

>>> import pkg_resources
>>> pkg_resources.resource_string('mypackage', 'data/greeting.txt')
b'Hello, World!\n'

めでたしめでたし。