Pythonを使った開発について

最近はPythonを使うことが多くなった。一時期はGolangという感じだったが、やはりPythonは偉い。仕事でもJavaを追放してPythonに絞っていこうとしている。このブログでもPythonネタをたまにぶち込むと思いますがよろしくお願いします。

Pythonで何かを開発するときに、まあベストプラクティス的なものがいくつかあるんだけど、そのうちの「ディレクトリ構造」に関して、自分としてはこれがいいんじゃないかな、というのが掴めてきたので、紹介します。世の中で言われているものとはちょっと異なりますので、その点はご了承ください。

まず、リポジトリのトップディレクトリに.envrcというファイルを置いて、中身は1行、[ -f bin/activate ] && source bin/activate と記します。direnvとvenvを使うというわけですね。この1行だけで、見る人が見れば分かるしdirenvを使ってる人は必ず気づく。

.gitignoreにはbin, lib, share, __pycache__, pyvenv.cfg などを入れていきます。

次に、requirements.txtを書きます。依存ライブラリの記述は近年はpipenvなんかが優勢な状況ですが、pipenvはアプリの実行にはいいけどライブラリの開発には向いていないので、使いません。作ってみて出来が良ければパッケージをPyPIに登録していくことになるんですけど、そのときの依存ライブラリの記述に使いたい、というのが主目的なんで。requirements.txtならアプリ実行/ライブラリ開発のどちらにも対応できますし、Pythonの標準機能のpipから自然に使えます。requirements.txtにはユーザが使うときに必須となる依存ライブラリを書いていき、自分が使うtwineやwheelやjupyter、coverageなどはrequirements-dev.txtみたいに別ファイルに書きます。

requirements.txtの中身には、基本はパッケージ名のみを書きバージョン指定をしません。開発中は最新バージョンに追随するので問題ないし、リリース後に問題になったらそのとき書けばいい。バージョンの書き方はpkgname<Nで、Nは最初に問題になったバージョン。問題のバージョンの1個前が入るようにします。pip freezeとかは信用しない。requirements.txtの行数はなるべく縮める。依存ライブラリの依存ライブラリとかは勝手に入るので、書かない。pipenvはこの辺の処理は便利なんですけどね。

venvもpipもPythonの標準機能に入っており追加ソフトウェアのインストールが不要というのが大きいです。ユーザはシステムワイドにインストールするかもしれないし、venvやvirtualenvやpipenvを使うかもしれないし、dockerイメージに入れるかもしれない。開発者はインストール方法を強制できないけど、Pythonの標準機能だけは当てになる。ゆえに開発者はできるだけ標準機能を使い、ユーザは自分の使いたいものを使う。

Linuxのディストリビューションとかだとpipは処理系とは別パッケージなのでpipは標準機能ではない! と思っている人もいますが、Python3系のどこか以降からvenvが標準機能として含まれるようになり、システムにpipを入れてなくてもvenvで作ったディレクトリにはpipが入ります。なのでpipも標準機能です。venvを無効にしている? そんな奴は犬の餌にでもしとけ。

パッケージはeggやtar.gzやrpmではなく、wheel形式で作ります。pip install wheelして、python setup.py bdist_wheelする。

この、パッケージングで使うファイルにsetup.pyとsetup.cfgの2つがあります。これはどちらが優れているかはまだ決めかねています。世間的にはsetup.cfgを使って、setup.pyにはsetup()のみを書く、というのが良いとされているようですが、結局setup.cfgを書いてもsetup.pyが必要になるのがなんだかなぁ…と。python -m setuptoolsみたいなコマンドでsetup()を呼んでくれればsetup.pyを捨ててこの話題は終わり、になるんでしょうけど、まだそうなってないんです。setup.pyをファイルごと捨てられる状態になるまでは、setup.cfgを使わなくてもいいかな、と思ってます。setup()の引数は、こんな↓感じ。

  • install_requires→open(“requirements.txt”).readlines()
  • extra_require[“development”]→open(“requirements-dev.txt”).readlines()
  • long_description→open(“README.md”).read()
  • long_description_content_type→”text/markdown”
  • version→(モジュール名).version.VERSION (importしておく)
  • その他は適当に。setup()呼び出し以外の処理は書かない

README.mdはmarkdown(gfm)で書きます。このREADMEは拙さ丸出しの英語で書き、使い方のサンプルを中心に書いていきます。打つコマンドと結果を羅列していくだけでなんとなく使い方が伝わる、というのが理想。

実際、OSSのドキュメンテーションってのはあんまりうまく文章を書こうとしないのがキモだと思ってます。うまく文章を書けてしまうことの弊害ってのがあって、文章の説明だけで押し切れてしまうようになり、肝心のソースがおろそかになってしまうんですね。ややこしい注意点の文章を長々と書かず、ややこしい説明なしで使えるソースにしておく、ということです。私は幸いにもややこしいことを英語で説明できませんから、英語がちょうどいい。英語が得意な人は英語を避けてドイツ語かロシア語あたりで書くようにすればいいと思います。文章でサポートできなくなるため、必然的にコードが使いやすくなります。

サンプルのデータやコード、説明のための.ipynb等はexamplesなどの名前のディレクトリを掘って、そこに置きます。jupyterの.ipynbは作るのも簡単だし、githubでプレビューできるので流通させるのにも便利なフォーマットです。

READMEのCLIの例ではvenvを使うようにします。こんな感じ↓

  • git clone https….
  • cd xxx
  • python -m venv .
  • ./bin/pip install -r requirements.txt
  • ./bin/python -m xxx.cli

ソースの配置ですが、パッケージ名でディレクトリを作り、__init__.pyを配置します。サブパッケージを作るときも毎回__init__.pyを作ります。__init__.pyの中身は決まっていて、

  • 同一階層のソースファイルfilename.pyに関して、from .filename import *
  • パッケージディレクトリdirnameに関して、from . import dirname
  • 必要なら__all__=[“シンボル名”,…]
    • これはなくてもいい。実際は__all__を書いてもシンボルを隠せるわけではなく、気休めにしかならない

このようにすることで、ユーザは import xxxだけでサブパッケージも含めて全ての機能を使えるようになります。__init__.pyをちゃんと書かないとimport xxx.filenameみたいにしないと読み込まれなかったりして、使いにくいんですね。

逆に、__init__.pyを上記のように書くことにより、ファイル名の変更の影響が少なくなるという効果もあります。ファイルの分割やマージは日常的にやりますが、ユーザに影響を与えずにそれができる。Golangみたいな感じになる、と言えばわかりやすいかな??

__init__.pyはいつか自動生成か何かで対応したいと思っていたりします。

ユニットテストはなるべく多くの部分にdoctestを使います。coverageを使ってカバレッジを見つつ、doctestでカバーできないテストはtestsディレクトリを掘って書いていく。実行はnoseを使い、nosetest -v –with-doctestで。まあでも私はユニットテストはかなりサボることが多いです。

CIは、会社とかでは社内にCIのサービスがあるので設定してREADMEにバッジをつけたりパッケージのファイルやカバレッジレポートを生成してgh-pagesに上げたりしてますが、趣味のコードではやらないです。

ログは、各ソースファイルの先頭付近に定型句を書く。

  • from logging import getLogger
  • log = getLogger(__name__)

以前はクラスの中でgetLogger()してself.log.debug()みたいな呼び出し方をしていたが、めんどくさい割に利益がなかった。今はロガーはクラスの外に置いている。ログの設定はmain()の中で、あるところまではbasicConfig(level=DEBUG)で、ちゃんとしたものになっていったらYAMLか何かで設定ファイルを作って、dictConfig()で設定する。こんなイメージ↓

  • logging.dictConfig(yaml.load(“your-config-file.yaml”).get(“log”, {}))

ソースの中で自分のパッケージの別ファイルを読み込むときは、相対importを使います。from ..pkgname import xxx みたいな。

CLIは今はclickというモジュールを使うことがほとんど。標準のargparseを使うことはほぼなくなりました。click, requests, PyYAML, jupyter。この辺はなかなか手放せない。

まとめると、パッケージxxxのリポジトリのディレクトリ構造は、、、

  • .envrc ([ -f bin/activate ] && source bin/activate)
  • .gitignore (自分用に適当に書く)
  • README.md (超カタコト英語、ほぼ実行例のみ)
  • setup.py (setup.cfgをつけるかどうかは、そのときの気分による)
  • requirements.txt (バージョン指定は必要になってから)
  • requirements-dev.txt (wheel, twine, nose, jupyterなど)
  • xxx/
    • __init__.py (from .file1 import *; from .file2 import *; from . import yyy)
    • file1.py
    • file2.py
    • yyy/
      • __init__.py (from .file3 import *)
      • file3.py
  • examples/ (実行例やサンプルデータなど雑多に)
  • tests/ (doctestに書けないユニットテスト)

ライセンスは昔はGPL一択だったけど、最近はMITにすることが多いかな。あんまり深く考えてない。LICENSEファイルをつけるとgithubが検出して正しく表示してくれます。setup()の引数にもライセンスの項目がありますね。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です