Python
PythonDay 4

簡単なPythonのパッケージを作る方法

追記: 2017/11/16
よく見られているっぽいので、名前変えました。

Pythonのファイル分割とパッケージとかまでをすごいざっくりまとめてみる。

「パッケージの最小サンプル」だけ見たい人は多分目次から飛んだほうが早いのでオススメ。

Pythonでファイルを分割するということ

「ファイル分割 = 名前空間の作成」が気持ちいいぞ

個人的に気に入ってるのがファイル分割。
何かしらいろいろと作ってくるとコード数が増えて、「昨日書いたコードは今日の敵」のようなことになってしまうので、なるべく機能ごとにまとめて管理していきたい。

だいたいこんな理由ではじめてファイル分割を検索するんだけど、「 Python ファイル分割 」みたいな感じでググっても、記事が古かったり、2系で書いてあるから不安になったり、リファレンスのモジュールの項を見ても欲しい情報に速攻にたどり着けなかったりする。

参考:

なので、さっさとコードを書いて後から説明していく。

ファイル構成は次の通り

advent2016        # ディレクトリ
├── another.py # 分割したファイル
└── main.py    # インポートする側
another.py
year = 2016

def hello(name):
    print("Hello {0}".format(name))

def goodbye(name):
    print("Goodbye {0} !".format(name))
main.py
import another # .pyなどはいらない。

another.hello("Advent Calender")

another.goodbye( another.year )

ここまで書いて、main.py を実行すると次のようになる。

$ python main.py

Hello Advent Calender
Goodbye 2016 !

さらにインポートの方法も複数あって、それを自分で選択できるのが便利。
例えば、インポートするときの名前を変更したいなら、

main2.py
import another as an

an.hello("Advent Calender")

an.goodbye( an.year )

のように、 as を使って anotheran に置き変えることが可能だ。
名前空間が必要ではない場合、は次のように from を使って書くこともできる。

from another import *

hello("Advent Calender")
goodbye( year )

* (アスタリスク) は another.py で記述されているものを全てを引用することが可能だ。ただし、ファイルをインポートした時に同じ名前のメソッドがあった場合は危険であるから、必要なメソッドや変数のみに絞ってインポートしたい時がある。その場合は次のようにすれば良い。

main3.py
from another import hello

hello("Advent Calender")

goodbye( year )           # エラー

さっきの * アスタリスクの部分に必要なものを明示的に書いてあげればよいだけである。

さて、少しだけ注意しておきたいことが有る。
インポートするファイルに直接実行コードが書いてある時だ。

another.py
year = 2016

def hello(name):
    print("Hello {0}".format(name))

def goodbye(name):
    print("Goodbye {0} !".format(name))

print("Hello from another.py") # 追加

この場合、どのようになるか。
仮に、 main.py で実行した時、次の様になる。

$ python main.py

Hello from athoer.py
Hello Advent Calender
Goodbye 2016 !

another.py がインポートされた段階で実行されていることがわかる。
これを避けるためには次のようにすると良い。

another.py
year = 2016

def hello(name):
    print("Hello {0}".format(name))

def goodbye(name):
    print("Goddbye {0} !".format(name))

if __name__ == '__main__': # 追加

    print("Hello from another.py") # 追加

if __name__ == '__main__': の内側に書いてしまえば、直接実行された時以外は内側のコードは実行されなくなる。
インポートされる可能性のあるファイル単体で実行するときは、書いておくほうが無難であろう。

ディレクトリ単位での分割も良いぞ

ファイル分割を個々まで説明してきたが、せっかくなのでディレクトリを利用した場合も説明しておこう。
例えば次のようなファイル構成を作っておく。

advent2016-part2        # ディレクトリ
├── Special          # ディレクトリ
│   └── sunday.py   # ファイル (追加)
└── main.py          # ファイル

早速 sunday.pymain.py にインポートするコードを書いてみる。

sunday.py
filename = "sunday.py"

def hello(name):
    print("Hello {0}. From sunday.py".format(name))
main.py
import Special.sunday

print( Special.sunday.filename )
Special.sunday.hello("HELLO")

見ての通り、 import Special.sunday でディレクトリ内にあるコードも読み込みが可能だ。
ただし、上記のようにインポートした時は Special.sunday をいちいち書く必要がある。
が、ここまで読めば分かる通り、 asfrom を利用すれば、長々と記述する必要がなくなるわけである。

import Special.sunday as sunday

print( sunday.filename )
sunday.hello("HELLO")

すばらしい。好きなようにインポートするのがよいだろう。また、どこからインポートされているのかを確認したければ、

import Special
print( Special.__path__ )

とすると絶対パスが確認ができる。

細かなエラーなどについては Pythonのimportについてまとめる を参照することをおすすめしたい。

次に

いやはや、ファイル分割はコードの見通しを良くする上でとても大事なことだけれども、
ここまでの内容だけだと、一つのディレクトリの中でたくさんコードを管理してしまうことになってしまう。

1つのディレクトリ内でたくさんのコードを用意していればそれだけ引っ張り出せるのだけれど、
人間は忘れてしまう生き物なので、どこに何があるのかいづれわからなくなってしまう。

決して忘れることに罪はないので、忘れないうちに忘れたときの手を打っておくのが健全だろう。

なので、パッケージ化してしまって、どこからでも引っ張りだせれば至極便利。

パッケージ化するって、つまりそういうこと

頻度の高くないものや、小規模なプロジェクトにおいてはそのまま使うのが効率が良いとは思う。
だが、この逆はパッケージ化したほうが便利ではなかろうか。そのはずだ。

Pythonのパッケージ化、でググってだいたい引っかかるのがこの辺。

ざっと見た感じだと長いので、ここではさくさく終わらせていこう。

パッケージ化してしまえ

setup.py さえ作ってしまえパッケージ化は簡単。

from setuptools import setup, find_packages

setup(
    name='パッケージ名',
    version="0.0.1",                 # X.Y.Z 形式
    description="短めの説明",
    long_description="長めの説明",
    url='必要ならばURL',
    author='作者名',
    author_email='メールアドレス',
    license='ライセンス',
    classifiers=[
        # パッケージのカテゴリー
        # https://pypi.python.org/pypi?:action=list_classifiers
        # からあったものをコピペ。
    ],
    keywords='キーワード',
    install_requires=["依存関係のあるパッケージ"],
)

これが終わったら、コマンドでインストールしてしまえばいい。

ローカルでインストール(開発モード)

pip install -e
python setup.py develop

これで、ローカル環境ではどこからでもインポートできる。

パッケージの最小サンプル

試しに最小サンプルを用意しておこう。ただ単純に「"Farewell 2016 !"」と出力するだけのパッケージだ。

Farewell2016
├── Farewell2016
│   └── Greet.py
└── setup.py
Greet.py
def farewell():
    print("Farewell 2016 !")
setup.py
from setuptools import setup, find_packages

setup(
    name='Farewell2016',
    version="0.0.1",                 
    description="さよなら2016年",
    long_description="2016年にお別れを言うだけのパッケージ",
    author='k.himeno',
    license='MIT',
    classifiers=[
        "Development Status :: 1 - Planning"
    ],
    keywords='farewell'
)

書き終わったら setup.py のあるディレクトリで次のコマンドを実行すれば良い。

pip install -e
python setup.py develop

これだけだ!

試しに、全く関係のないディレクトリで次のコードを実行してみるとよい。

sample.py
from Farewell2016.Greet import farewell
farewell()

これだけでも随分と楽になる。

せっかくなのでPyPIに登録しちゃう?

せっかく作ったらアップロードしちゃいましょう。

(とかいって、アドベントカレンダー用に何かパッケージ作ろうとしてたら、まぁまぁ凝ったものを作ってしまった。)

参考程度にソースコード

こやつの構成は次の通り。

CountDownApp
├── CountDownApp       # パッケージの中身
│   ├── __init__,py 
│   └── app.py        
├── MANIFEST.in        # 配布ソースコードに何を含めるか書いたファイル
├── README.rst         # 長い説明とかをまとめているファイル
├── VERSION            # 勝手に作ったバージョンファイル
├── image.png          # ただの画像
├── requirements.txt   # 依存している他のパッケージをまとめるやつ
├── setup.cfg          # オプションをまとめる便利なファイル
└── setup.py           # パッケージの情報が詰まったファイル

このパッケージのファイルをもとに簡単に説明してみる。

配布用ソースコードの作成

以下のコマンドで出来上がる。

python setup.py sdist

MANIFEST.in に従ってパッケージ化される。

MANIFEST.in
include README.rst
include setup.cfg

配布用のバイナリの作成

wheel パッケージを利用するのでインストールしておく。

pip install wheel

wheelのドキュメント

実行方法は

python setup.py bdist_wheel

とすればよい。

Defining the Python versionでは、
--universal--python-tag XXX が説明されてあるが、どの環境に対してパッケージ化するかが選択できる。

setup.cfg ファイルにこれらのオプションをまとめておくことができる。

[bdist_wheel]
universal=1

こうすれば、わざわざオプションをつけなくてよい。

setup,cfgについて

PyPIへアップロード

配布用のソースコードとバイナリを作成してアップロードするなら

python setup.py sdist bdist_wheel upload

でよい。

さいごに

この記事は、箱入り娘状態のコードがあって、公開できるコードがあるのに、公開の方法を知らないという人に届いてホスィ。

コードを共有してくれると僕と僕以外の全て、つまり「全て」が嬉しいので、パッケージを公開するなり、記事を公開するなりやってみませぬか?

おわり。

2016/12/5 追記

※ 編集リクエストくださってる方ありがとうございます!