(インターン向けに書いた)Pythonパッケージを作る方法

  • 421
    いいね
  • 5
    コメント

この記事は何?

  • Pythonパッケージの作り方を説明する
  • Pythonパッケージを作るときに意識して欲しいことを説明する
  • この記事はポエムです

これまでのあらすじ

この夏にインターンを迎える予定なのですが、彼らはパッケージを作ったことがないそうです。

一方で、企業としては、パッケージ化までしてくれないと、実務に使うまで時間がかかってしまって大変です。

そこで、社内向けに「Pythonパッケージの作り方」という文書を書きました。これをインターンの人に読んでもらっていい感じのパッケージを作ってもらうぜ!という都合の良い目論見です。

しかし、 私もいままでパッケージ化のノウハウをきちんと体系的に勉強したわけではないので、イマイチ不安です。そこで、Qiitaにポエム投稿して、ポエム修正をしてもらおうと思ったわけです。

ところどころ日本語が変なのはご容赦ください。日本語に不自由してます。1

この記事を読む人の前提

つまりはインターン生が持っている前提知識です。

  • Pythonを日常的に研究に利用している。
  • 機械学習とか自然言語処理の前処理作業はできる。(自然言語処理100本ノックはクリアしているそうです)
  • パッケージはまだ作ったことがない。
  • Gitは使える。

↓ ここからポエム

ディレクトリ構成

実は、Python界には公式のディレクトリ構造はないです。いい加減なディレクトリ構成をしていても、コードは動きます。しかし、それでは、他の開発者にはとてもわかりづらいコードになってしまいます。だから、わかりやすいディレクトリ構造が必要なのです。

なので、私はこのブログの方法に従うのをオススメします。このブログでは、Githubのリポジトリをクローンしています。ここでも、このリポジトリをクローンしましょう。

MacBook-Pro% git clone git@github.com:kennethreitz/samplemod.git

パッケージの名付け規則

Pythonのパッケージ命名規則はすべて小文字アンダースコア区切りが推奨されてます。Googleのスゴい人Pythonの中の人たちがそう言っているのだから、従った方が無難でしょう。

パッケージを作る準備をする

今回はhoge_hogeというパッケージを作ることにしましょう。cloneしてきたディレクトリもパッケージ名に合わせるようにすると良いです。
また、cloneしてきたディレクトリは元のgitファイルを持っています。なので、まずはgitファイルを消してしまいましょう。
その後、スクリプトを置くディレクトリもパッケージ名と同じ名前にします。つまりhoge_hogeですね。

MacBook-Pro% mv samplemod hoge_hoge
MacBook-Pro% cd hoge_hoge
MacBook-Pro% rm -rf .git
MacBook-Pro% mv sample hoge_hoge

上のコマンドを実行した後には、ディレクトリ構成はこんな感じになっているはずです。
(@shn さんのご指摘を受けて、変更しました。 requirement.txt を削除しています。)

MacBook-Pro% tree   
.
├── LICENSE
├── Makefile
├── README.rst
├── docs
│   ├── Makefile
│   ├── conf.py
│   ├── index.rst
│   └── make.bat
├── hoge_hoge
│   ├── __init__.py
│   ├── core.py
│   └── helpers.py
├── setup.py
└── tests
    ├── __init__.py
    ├── context.py
    ├── test_advanced.py
    └── test_basic.py

次のあなたのパッケージ用に、新しくgitを初期化しましょう。

MacBook-Pro% git init
MacBook-Pro% git add .
MacBook-Pro% git commit -m 'first commit'

これで準備ができました。

ついでに・・・

pythonパッケージのディレクトリ構成を初期化するようなツールはいくつかあります。cookiecutterとかね。でも、あまりオススメしません。余計なファイルが作られすぎるからです。

setup.pyを書く

setup.pyというのはパッケージに必須のファイルです。このファイルに、これから作るパッケージの依存情報、バージョン情報、パッケージ名を書きます。
このポエムでは、最低限のフィールドだけを説明します。他のフィールドにも興味があれば、Python中の人の経典を見ましょう。

最低限、書いて欲しいフィールド

元のsetup.pyの中身はこのようになっているはずです。

setup(
    name='sample',
    version='0.0.1',
    description='Sample package for Python-Guide.org',
    long_description=readme,
    author='Kenneth Reitz',
    author_email='me@kennethreitz.com',
    url='https://github.com/kennethreitz/samplemod',
    license=license,
    packages=find_packages(exclude=('tests', 'docs'))
)
フィールド名 説明 もっと詳しい説明
name パッケージの名前を書きます
version 現在のバージョン情報を書きます 
description 「このパッケージは何をするか?」を簡潔にかきましょう
install_requires 依存パッケージ情報を書きます これ
dependency_links 依存パッケージがpypiに存在してない時に書きます これ

特に、絶対に書かないといけないのは、install_requiresdependency_linksです。あなたのパッケージを使う開発者は「どのパッケージに依存してるか?」なんてことを知りません。この情報がないと他の開発者は困ってしまいます。
たぶん、あなたもいままでにpython setup.py installをしたことがあるでしょう。このコマンドが動くのはきちんと依存情報が書かれているからです。

Pypiに存在するパッケージへの依存情報を書く

install_requiresのフィールドにリストで依存しているパッケージを記述します。たとえば、numpyが必要なら、numpyと書きます。

setup(
    name='sample',
    version='0.0.1',
    description='Sample package for Python-Guide.org',
    long_description=readme,
    author='Kenneth Reitz',
    author_email='me@kennethreitz.com',
    install_requires=['numpy'],
    url='https://github.com/kennethreitz/samplemod',
    license=license,
    packages=find_packages(exclude=('tests', 'docs'))
)

社内パッケージへの依存情報を記述する

一般企業ならば、社内で独自に開発したパッケージを運用していることでしょう。そいったパッケージはPypiにはないので、setup.pyにパッケージが存在している場所を教えてやらないといけません。dependency_linksはそんな時に必要です。

(ヘイシャの場合)Githubを利用しているので、gitの例だけを書きます。他のバージョン管理システムの場合はStackOverFlowの記事を見ましょう。

基本的な式は

git+ssh://git@URL-TO-THE-PACKAGE#egg=PACKAGE-NAME

です。
例えば、いま、unkoというパッケージの依存情報を書きたいとします。その時は

git+ssh://git@github.com/our-company-internal/unko.git#egg=unko

です。
もし、特定のバージョンで固定しておきたいなら、@の後ろにブランチ名かタグ名を指定できます。

git+ssh://git@github.com/our-company-internal/unko.git@v0.1#egg=unko-0.1

@v0.1 はgithubのブランチ名かタグ名です。-0.1はパッケージのバージョンです。

name, authorなどを書き換える

最後に、name, authorなどの情報を書き換えます。
自分の名前を書くと、たぶん愛着がわきますよ ;)

最終的にできあがったsetup.pyはこうなります。

setup(
    name='hoge_hoge',
    version='0.0.1',
    description='Sample package for Python-Guide.org',
    long_description=readme,
    author='Kensuke Mitsuzawa',
    author_email='me@kennethreitz.com',
    install_requires=['numpy', 'unko'],
    dependency_links=['git+ssh://git@github.com/our-company-internal/unko.git#egg=unko'],
    url='https://github.com/kennethreitz/samplemod',
    license=license,
    packages=find_packages(exclude=('tests', 'docs'))
)

コードを書く 

コードの書き方にもいろいろあるのですが、今回は細かいことは言いません。
いままで通りに書いてみてください。
でも、「覚えておいて欲しいこと」にできるだけ従って欲しいです。

テストを書く

テストは必ず書いてください。
たぶん、これまでにも動作確認のためのコードは書いたことがあるでしょう。でも、動作確認だけでなく、できる限り自分でテストケースを考えて出力の検証はしてください。

ここでは、テストケースの書き方だけを説明します。
Pythonではunittestという標準のテストフレームワークがあります。このフレームワークを使いましょう。詳細は割愛しますが、この記事-JPこの記事-ENを見て、使い方をマスターしましょう。

テストコードを配置する場所はtests/ディレクトリの配下です。

├── setup.py
└── tests
    ├── __init__.py
    ├── context.py
    ├── test_advanced.py
    └── test_basic.py

1つのスクリプト、1つのテストスクリプト

パッケージのモジュールスクリプトを1つ作ったら、テストスクリプトも1つ作りましょう。その方が、対応関係がはっきりとわかって、メンテナンス性がよくなります。いまは、hoge_hoge/core.pyをモジュールスクリプトとして作成したとしましょう。

├── hoge_hoge
│   ├── __init__.py
│   ├── core.py

core.pyのテストスクリプトはtests/配下に置きます。test_というprefixをつけて、test_core.pyとすると、わかりやすいでしょう。

└── tests
    ├── __init__.py
    ├── test_core.py

test_core.pyの例

テストスクリプトを書くときに覚えておいて欲しいことは2点です。

  • テストクラス名はキャメルケース
  • test_というprefixがメソッド名の前に必要。このprefixがないとtestが無視されてしまいます。

テストがかけたら、スクリプトを実行してみましょう。python test_core.py.
またはpycharmであれば、通常の実行ボタンを押せば、テストが走ります。

class TestCore(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        # procedures before tests are started. This code block is executed only once

    @classmethod
    def tearDownClass(cls):
        # procedures after tests are finished. This code block is executed only once

    def setUp(self):
        # procedures before every tests are started. This code block is executed every time

    def tearDown(self):
        # procedures after every tests are finished. This code block is executed every time

    def test_core(self):
        # one test case. here. 
        # You must “test_” prefix always. Unless, unittest ignores

if __name__ == '__main__':
    unittest.main()

すべてのテストケースをtest_allに書く

@podhmo氏のコメントを受けて更新しました。感謝!)

いくつかのテストスクリプトが書けたら、python setup.py testを実行可能なようにしましょう。

大きな変更をした場合はかならずこのコマンドを実行するクセをつけてください。ついついて頭から抜けていた問題を発見することが多いです。パッケージの改良作業などをしていると、「あ、ここも変更しとこ」と変更して、テスト実行を忘れてしまっていることはよくあります。

また、他の開発者も自分の環境で簡単にテストできるので、非常に助かります。

1: setup.pyを編集する

いまのテストディレクトリはこのような構成になっています。

└── tests
    ├── __init__.py
    ├── test_all.py
    ├── test_advanced.py
    └── test_basic.py

setup.pyにテストディレクトリが存在する場所を指定します。
setup.pyは自動的にテストクラスを読みこんでテストを実施してくれます。

setup(
    name='hoge_hoge',
    version='0.0.1',
    description='Sample package for Python-Guide.org',
    long_description=readme,
    author='Kensuke Mitsuzawa',
    author_email='me@kennethreitz.com',
    install_requires=['numpy', 'unko'],
    dependency_links=['git+ssh://git@github.com/our-company-internal/unko.git#egg=unko'],
    url='https://github.com/kennethreitz/samplemod',
    license=license,
    packages=find_packages(exclude=('tests', 'docs')),
    test_suite='tests'
)

2: 全テスト実行

python setup.py testを実行します。すべてのテストが実行されている様子がわかります。
エラーがあれば、最後に表示されます。

インターフェースとサンプルコードを書く

あなたのパッケージの中ではめちゃんこスゴいアルゴリズムが実装されているのかもしれません。でも、他の開発者は、いちいち中身をチェックしているヒマがないです。(残念ながら)

インターフェースはあなたのめちゃんこスゴいアルゴリズムを、猿開発者でもすぐに使えるようにしてあげるために必要なのです。

簡単なインターフェースがあっても、猿開発者は「使い方がわからん」と文句を言います。サンプルコードを書いて、使い方を教えてあげましょう。

インターフェース

インターフェースを書く場所は好みによると思います。
オブジェクト志向でコードを書いていれば、クラスの中にインターフェースメソッドを書いてあげればいいでしょう。Pythonライクにスクリプトファイルの中に関数オブジェクトを書いていれば、インターフェース用のスクリプトファイルを用意してあげればいいでしょう。(ちなみに私は後者です。)

どちらにせよ、入出力を明確にする ということは徹底してください。
requestパッケージのインターフェースを例に挙げておきます。2

サンプルコード

サンプルコードでも大切なことは入出力を明確にする ことです。
猿開発者はよくサンプルコードをコピペします。コピペ利用でも入出力がすぐにわかるサンプルコードがベストです。
一例としてjaconvのusageを挙げておきます。

READMEを書く

もし、所属チームにREADMEの書き方の作法があれば必ずその作法に従ってください。(残念ながらヘイシャにはそんな作法はない)

READMEを書くときの原則は「他の開発者がREADMEを読んだだけで、インストール法を理解できて、実行方法も理解できること」です。

だいたいの場合は以下の情報があればいいでしょう。

  • インストール方法(だいたいはpython setup.py installでいいはずです)
  • テストの実行方法(だいたいはpython setup.py testでいいはずです)
  • どうやって使うか?(だいたいは「サンプルコードを見てください」でいいはずです)

もし、事前に準備が必要な項目があれば、必ずREADMEに書かなければいけません。
例えば、

  • Mecabを事前にインストールしておかなければならない
  • RedisDBのセットアップをしておかなかればならない

こういう場合は、必ずREADMEにその事を書かなければいけません。
余裕があったら、Makeファイルを書きましょう。他の開発者が泣いて喜びます。

覚えておいてほしいこと

入出力を明確にすること

頭に入れて置いて欲しいことは「他の開発者はコードを見ただけでは入出力を理解することができない」ということです。だからこそ、サンプルコードが必要だったり、docコメントをしっかりかかなきゃいけなかったりします。
でも、すべてのメソッドや関数に丁寧なコメントを書いていては大変です(インターンに使える時間は有限だし)。せめて、インターフェース関数の入出力は丁寧な説明つきで書くようにしてください。

Stable code と developing code

コードの管理にはgithubを使います。(少なくとも弊社では)なので、あなたの作ったパッケージはすぐにでも他の開発者が使いはじめるかもしれません。

だからこそ、branchの持つ意味を大切にしてください。大体の場合、masterのブランチは「安定版」です。開発版は別のbranchにpushしなければいけません。

一例ですが、私の開発スタイルです。

  • 最初のアルファバージョンまではmasterにpushする(かならずREADMEの冒頭に「これは開発中だ」と明記します。)
  • アルファバーション以降は、別の開発ブランチを作成します。ブランチの扱い方はgit-flow3に従います。

Git-flowに従うようにする

git-flowはブランチの扱い方を決めたルールです。このルールがあるおかげでチームで開発を柔軟にできます。
たった1人で開発していたとしても、git-flowに従いましょう。なぜなら、他の開発者がスムーズに開発に参加できるからです。git-flowの流れはこの良記事を見ましょう。

再現性を意識する

他の開発者も同じように実行して、同じ結果を得られるようにしましょう。あなたの環境でしか実行できないような情報をハードコードしてはダメです。
よくありがちなハードコードされる情報は

  • unixコマンドの実行パス
  • 外部ファイルへの絶対パス

こういう情報がハードコードされていると、他の開発者は実行ができません。困ります。
例えば、悪い例がこちら

def do_something():
    path_to_external_unix_command = '/usr/local/bin/command'
    path_to_input_file = '/Users/kensuke-mi/Desktop/codes/hoge_hoge/input.txt'
    return call_some_function(path_to_input_file)

"input.txt"の中身は誰にもわからないままです。
この場合はこうすると良いです。

  • 外部ファイルが必要なら同じリポジトリにpushしておく。スクリプトからの相対パスを使う
  • 関数には引数として情報を渡せるようにしておく。
  • unixコマンドが必要なら、設定ファイルに記述する。

上の悪い例はこうなります。

def do_something(path_to_external_unix_command, path_to_input_file):
    return call_some_function(path_to_input_file)

path_to_input_file = './input.txt'
path_to_external_unix_command = load_from_setting_file()
do_something(path_to_external_unix_command, path_to_input_file)

その他

type hintを使おう!

「入出力を明確に」と何回か書きましたが、正直いって面倒です。しかも、変更したときに忘れがちです。こうなると、せっかく明確にコメントを書いても意味がありません。
そこで、type hintです。Javaの様にオブジェクトの型を記述できる仕組みです。ただ、Javaとは違う点は、Pythonの「type hintは実行になんら影響しない」という点です。4

主な利点

  • 他の開発者が入出力の型を理解しやすい
  • Pycharm(他のIDEも)が入出力で不一致があった時に警告だしてくれるので、間違いに気が付きやすい
  • Pycharm(他のIDEも)が型情報からメソッドのsuggestしてくれるので、開発が早くなる

良くない点

  • ちょっとだけ文字をタイプする量が増える

せっかくなんで、例を出しておきます。
この実装では、names, personsは一体何者なのか?よくわかりません。それにgreetingは何を返してくれる関数なのか?も不明です。

def greeting(names, persons):
    target = decide_target_person(persons)
    greet = generate(target, names)
    return greet

さて、ここでtype hintをつけてみましょう。

def greeting(names: List[str], persons: Dict[str, str])->str:
    target = decide_target_person(persons)
    greet = generate(target, names)
    return greet

これで、入出力は明らかになりました!

pycharmのサポートについて

Pycharmは完全にtype hintをサポートしてくれてます。

  • オブジェクトが持つメソッドのsuggest
  • 型の不一致があった場合の警告

下の画像はPycharmの開発画面です。targetの入出力を間違えているので、警告が出ています。

スクリーンショット 2016-07-29 2.49.55.png

python-3だけのパッケージの場合

この辺りが良記事です。

python-2 と python-3の両方対応のパッケージの場合

Python2ではPython3式のtype hintが解釈できずエラーになります。でも、「コメントとしてtype hintを書く」という素晴らしい方法がPEPで提示されています。
まだPycharmは部分的な対応しかできていませんが、近いうちにFullサポートになるはずです。


↑ ここまでポエム

ポエムを書いてみて振り返り

雑っぽい感じだけど、いいのかな?こんな情報だけでパッケージつくれるのかな?と不安であります。ツッコミ大歓迎です。


  1. 実は元の文書自体を英語で書いています。インターンに来る人は留学生もいるのです。 

  2. requestパッケージは「とても読みやすいコード」と評判が高いです。 

  3. 本当はgit-flowはツールの名前なのですが、開発スタイルを示すことも多いです(私の周りだけ??) 

  4. それってPythonの哲学に反してるんじゃない?という声をちらほら聞きますが、Python中の人達が活動してるPEPでAcceptedになっている項目です。従っておいた方がいいことありますよ。(そもそもこの提案を出したメンバーにGuido van Rossumいるし)