Pythonを使ったプロジェクトのディレクトリ構造

Pythonプロジェクトの2つのパターン

  1. 使い捨てのスクリプトを書く
  2. パッケージとして整備しつつ外部のプログラムに使われるのを意識しつつ書く(メンテナンス性高い)

2.の用途として、自分が直面しているのはPyramidでWEBアプリを作るときです。

Pyramidは設定ファイルとして、PasteDeployを利用しています。

この設定ファイルから自パッケージ内のエントリポイントとなる関数名を取り出し、WSGIサーバーであるwaitressgunicornが利用することになります。

今回は2.のような場合について、どのようなディレクトリ構造がベストプラクティスなのかを調査しました。

ディレクトリ構造の研究

ディレクトリ構造を研究するために以下を参考にしました。

Pipenvは比較的新しいツールあまり言及している記事がありません。

実際、①と②にはPipenvは登場しません。

③には①と②に即したディレクトリ構造でありながら、かつPipenvを使って構成されていたので参考にしました。

基本的な構造

Pipenvを使わない場合

①を見るとディレクトリ構造は以下のようになっています。

* README.rst
* LICENSE
* setup.py
* requirements.txt
* sample/__init__.py
* sample/core.py
* sample/helpers.py
* docs/conf.py
* docs/index.rst
* tests/test_basic.py
* tests/test_advanced.py

sampleモジュールパッケージが自分が書くソースコードの中心となり、自分で書いたコードはこのディレクトリ以下に配置していくことになります。

setup.pysampleモジュールパッケージの依存関係を処理するためのもので、sampleサードパーティのライブラリが必要な場合などはここで処理していきます。

編集可能モード

(②を順に見ていくと登場しますが)

pipにはモジュールパッケージを編集可能モードとしてインストールできる機能があります。

例えば、上記のsampleモジュールパッケージでは

$ git clone git@github.com:kennethreitz/samplemod.git
$ cd samplemod/
$ pip install -e .

とすることでsampleモジュールパッケージを編集可能モードでインストールできます。

編集可能モードでのインストールのメリットとしては:

  1. インストールで必要な工程を踏んでいるので、sampleが必要としているサードパーティのライブラリを一遍にインストールできる
  2. 編集可能モードの名前の通りいつでもモジュールパッケージに修正を加えることができる
  3. このモジュールパッケージを使うときは標準ライブラリのように扱える(どこでもimport sampleでインポートできる)

Pipenvも使う場合

「依存関係の処理はsetup.pyで行われるが、Pipenvを使うメリットは何だろう?」と考えると、2つあると思います。

  1. 仮想環境(virtualenv)を作ってくれるので開発環境をシステム環境から隔離する
  2. sampleモジュールパッケージが直接的に依存していないライブラリを管理する(例えば、デバッグツールとテストツールなど)

1.に関して、Pythonの標準ライブラリvenvを使えばできてしまえますが、モジュールパッケージを使う準備するときについでに仮想環境を作ってくれるので便利です。

2.に関して、実際にrequestsライブラリ(③)を見てみると、Pipfileにはほとんどテストツールやリンカ用のツールが記述されています。

Pipenvで編集可能モード

また、requestsライブラリのPipfileには以下のような記述があります。

[packages]
"e1839a8" = {path = ".", editable = true, extras = ["socks"]}

これは、requestsライブラリを編集可能モードとしてインストールした痕跡ですね。

Pipenvではpipと同様に編集可能モードとしてモジュールパッケージをインストールすることができます。

$ git clone git@github.com:requests/requests.git
$ cd requests/
$ pipenv install -e .

pipと同様の使用感ですね。

一度、編集可能モジュールとしてインストールしてしまえばPipfileに記述されるので、別なシステムに移動した際でもPipenvを使っていれば勝手に処理してくれるようになります。

実際、requestsMakefileには仮想環境(virtualenv)初期化用のコマンドが記述されています。

上記のpipenv install -e .の代わりに

$ git clone git@github.com:requests/requests.git
$ cd requests/
$ make init  # pip install --upgrade pipenv && pipenv install --dev --skip-lock と同じ

とすれば、

  1. 仮想環境(virtualenv)を作る
  2. requestsモジュールパッケージを編集可能モードでインストールする(その依存関係にあるものも)
  3. テストツールやリンカ用のツールをインストールする

というのを一挙に終わらせてくれます。

あとは、pipenv shellとして仮想環境に入り開発に移ったり、pipenv run ほにゃららとコマンドを叩くだけです。楽ですね。

Docker

PipenvはPipfileの依存パッケージをシステムのPythonにインストールすることも容易です。

例えば、Pipenvが作る仮想環境の代わりにDockerで包んでしまう場合でも、

$ pipenv install --system

とすれば、Docker内ではmyprojectパッケージを標準ライブラリのように自由に使うことができそうです。

(やったことないけど)

感想

Pipfilesetup.pyrequirements.txtなど同じことを目的にしてるように見えるファイルがたくさんあるので混乱しますが、

目的を適切に分けて考えてツールを使えば便利に使えそうでした。

困ったときはrequestsライブラリのディクレトリ構造を参考にしていこうと思います。

Fabric2を使ったデプロイ

お仕事でデータ分析をすることになって1ヶ月ほど。

データ分析をする上でPythonはとても便利です。

モジュールも整備されており、ドキュメントも充実しています(ほとんど英語だけど)。

データサイエンスを利用したサーバーを組むなら、同じくPythonでやってしまった方が今までの方法を利用できて便利です。

今回は、PythonでWEBサーバーを組んだのでそれをデプロイする方法を整備しました。

Fabricとは?

FabricはSSH越しでリモートPCのコマンドを実行できるモジュールです。

Railsで良く使われるデプロイツールのCapistranoに対して、PythonではFabricな感じみたいですね。

Capistranoよりもお手軽な感じらしく、タスクなどを覚える必要がないということでした。

逆にいうと必要最低限のものしか準備されておらず、デプロイツールというよりは公式ドキュメントが言うとおり、

本当にリモートPCのコマンドを叩くためのライブラリのようですね。

PythonCapistranoを使うのもなんかあれだし、Ansibleを使うのも面倒臭いし手軽にリモートPCの操作できないかな?」って時に便利そうです。

Fabricのバージョン?

注意すべきなのは、Fabric 1.x、Fabric2、Fabric3とあって全部別物レベルに違うことです。

結論として使うべきはFabric2です。

Fabric 1.xはFabric2の旧バージョンであり、公式ドキュメントではFabric2のインストールを強く推奨しています。互換性もありません。

Fabric3はFabric 1.xがPython3に対応していない頃にforkされたもので現在はあまり活発に開発されていないようです。

現行の開発が進んでいる、Fabric2ではきちんとPython3にも対応しており、色々と改善されているようです。

Googleで検索して出てくる日本語化された公式ドキュメントはFabric 1.xのドキュメントなので注意が必要です。

コード

やることはfabfile.pyを置いてfabコマンドを叩くだけです。

fabfile.py

from fabric import task


PROJECT_ROOT = '/path/to/project'
REPO_URL = 'Gitレポジトリ のURL'
PIDFILE = 'run/gunicorn.pid' 


@task
def init(c):
    c.run(f'[ ! -e {PROJECT_ROOT} ] && git clone {REPO_URL} {PROJECT_ROOT}', hide=True)


@task
def deploy(c, version):
    c.run(f'cd {d.project_root} && git fetch origin && git checkout {version}', hide=True)
    c.run(f'cd {d.project_root} && PYENV_VERSION={PYENV_VERSION} pyenv exec pipenv clean', hide=True)
    c.run(f'cd {d.project_root} && PYENV_VERSION={PYENV_VERSION} pyenv exec pipenv install --dev --skip-lock', hide=True)
    c.run(f'cd {d.project_root} && kill -HUP $(cat {PIDFILE})', hide=True)

あとは、リモートPCにSSHできる環境から、

$ pipenv run fab -i 秘密鍵 -H ユーザー名@IPアドレス init deploy --version=Gitのタグ

とするだけです。

色々試行錯誤の末に、シンプルにgit checkoutして、Gunicornをリロードするというものになりました。

今回はPyenvとPipenv両方を使っているのそれを考慮したものになりました。

どっちも使っていなければ、上記のコードから関連する記述を消せば動くと思います。

またWSGIサーバーとしてGunicornを使っているので、更新分をリロードするためにHUPシグナルを送っています。

Gitの理由

最初はCapistranoと同じくreleasesディレクトリを作って、currentへシンボリックリンクを貼るみたいなのをやっていたのですが、

Pipenvとの共存を考えると難しそうだなとなって辞めました。

RubyのBundlerとPythonのPipenvは似ているようで、Pipenvはインタプリタまで隔離環境としてコピーするのが異なりますね。

感想

データ分析のために整備していたコードをそのままサーバー化しちゃえ、というのは自然な発想な気がします。

2018年、Javaを抜いてPythonが人気の理由もなんとなくわかった気がします。

SQLAlchemyのSessionとsession

Sessionとsession

SQLAlchemyでscoped_sessionを使った方法でわからなかったことの一つに、

>>> Session.query(ほにゃらら)

>>> session = Session()
>>> session.query(ほにゃらら)

にどんな違いがあるのか気になっていました。

Session.removeを行うまでずっと同じセッションを返し続けるという仕様ならなんで2通りあるのだろうと疑問でした。

答えは、公式ドキュメントにある通り、

>>> from sqlalchemy.orm import scoped_session
>>> from sqlalchemy.orm import sessionmaker

>>> session_factory = sessionmaker(bind=some_engine)
>>> Session = scoped_session(session_factory)

>>> # session = Session()
>>> # print session.query(MyClass).all()
>>> print Session.query(MyClass).all()    # このprint文は上のコードと等価

と違いはありませんでした。

SQLAlchemyのセッション

SQLAlchemyを使っていてセッションとコネクションがわからなかったのでまとめました。

(記事中に間違いなどあれば是非コメントでお知らせ願えたらありがたいです)

定義の再確認

セッション(Session)
データベースとの論理的な接続
データベースとの通信のための情報(ログイン情報や現在のトランザクションレベル)を保持している
コネクション(Connection)
データベースとの低レベルプロトコルを用いた物理的な接続

備考

  • コネクションは複数のセッションを持つことができる
  • セッションは複数のトランザクションにまたいで寿命を存続できる

普通はコネクションで一つのセッションを持つようにするらしい。

セッションをいつ開始・終了するか

セッションをいつ開始・終了するかについてはいくつかのパターンがある。

WEBアプリでの場合

典型的なWEBアプリでは、WEBクライアントからのリクエストごとにセッションの開始・終了を行う。

つまり、リクエストの寿命 = セッションの寿命となる。

いくつかのPythonのWAFではセッションの寿命を制御してくれる統合ライブラリを提供していて、可能ならばそれらの使用を推奨するとのこと。

(FlaskならばFlask-SQLAlchemy、PyramidならばZope-SQLAlchemyなど)

それ以外の場合

Pythonを利用するのはWEBアプリを作る場合だけではないと思う。

その場合は、上記の統合ライブラリを使えない。

SQLAlchemyはそのような場合に備えて自前でヘルパー関数を持っている。

それがscoped_sessionだ。

自分のユースケースでは、データ分析するためにデータをMySQLから取り出したいので、scoped_sessionを使う必要がありそうだ。

scoped_session

今まで、『scoped_sessionって何だろう?』という状態で使っていたので簡単にまとめておく。

scoped_sessionは以下のように使ってSessionオブジェクトを生成することができる。

>>> from sqlalchemy.orm import scoped_session
>>> from sqlalchemy.orm import sessionmaker

>>> session_factory = sessionmaker(bind=some_engine)
>>> Session = scoped_session(session_factory)

生成されたSessionオブジェクトは以下のようにして使うことができ、このsome_sessionを使えばデータベースと通信ができる。

some_session = Session()

Sessionは何回呼び出しても同じオブジェクトを返し続ける。

>>> some_other_session = Session()
>>> some_session is some_other_session
True

これは同じセッションを異なった場所で簡単に使えるようにするためにあるものらしい。

一見して、クラスをインスタンス化しているように見えるので新しいセッションが生成されるのかと思いきや違うらしい。

とても紛らわしい。 ← 勉強不足なだけ。

新しいセッションを開始するには、以下のコードのように、

現在のSessionを削除し、もう一度Sessionを呼び出せばDBとの新しいセッションが開始される。

>>> Session.remove()
>>> new_session = Session()
>>> new_session is some_session
False

WEBアプリの場合はこの一連の流れを統合ライブラリが行ってくれるが、

それ以外の場合はセッションの寿命を管理したいならば、自前でこのコードを書く必要がある。

感想

今までなんとなく使っていたscoped_sessionについて知ることが出来て良かったです。

今回は、プログラムで重いクエリを叩くとプログラムがハングすることがあり、セッション周りが原因で起きているのではないかと思い調査しました。

調査後にMySQLのセッションタイムアウト時間を見てみると8時間に設定されており、

結局のところハングの原因はセッション周りではなく、単に自分のPythonコードが重くて止まっているように見えていただけという。。

参考

セッションとコネクションの定義について
sql server - What is the difference between a connection and a session? - Database Administrators Stack Exchange
SQLAlchemyのSession全般について
Session Basics — SQLAlchemy 0.9 Documentation
scoped_sessionヘルパーについて
Contextual/Thread-local Sessions — SQLAlchemy 0.9 Documentation

統計検定の問題を解く

最近はニューラルネットワークの改良版アルゴリズムであるカプセルネットワークが発表されたり、ソフトバンクの自動運転車が走ったりとますます人工知能業界が盛り上がっていますね。

この記事でも紹介されていますが、人工知能のベースは統計学のようです。

統計解析手法と機械学習手法を数学的に記述するやり方は、細かい慣例などの違いこそあれ「基本的に全く同じ」というわけです

記事中で、

アメリカの数学者アーサー・ベンジャミンはTEDトークなどの場で、「高校までの数学教育では微積分などより統計学を教えるべきである」と主張しています。

ともあるように今後ますます統計学という学問の理解が大事になって行くように思います。

また、How Google Worksでも同様に統計学は学んでおいて損はない学問であると書いてあった気がします。

「How Google works」を読んで | albatrus.com

統計学は21世紀を生き抜く武器

いいですね。いいですね統計学。

統計学・データサイエンスをやっていく上で、実践的に使えるようになるためを考えると、実際に問題で練習してみるのが良い気がしました。

統計検定という統計の資格試験では、過去問が公開されています。

今回は、試験の4級(2017年6月)の過去問をやってみました。

統計検定4級

統計検定4級の内容は以下の通りです。

基本的なグラフ(棒グラフ・折れ線グラフ・円グラフなど)の見方・読み方 データの種類 度数分布表 ヒストグラム(柱状グラフ) 代表値(平均値・中央値・最頻値) 分布の散らばりの尺度(範囲) クロス集計表(2 次元の度数分布表:行比率,列比率) 時系列データの基本的な見方(指数・増減率) 確率の基礎

結果的にケアレスミスなども含めて、26/30(≒ 86%)でした。

統計検定4級は言葉の概念を問われているようなものが多く、度数や相対度数、ヒストグラムの階級値などを知っているかどうかで点数が変わってきそうです。(なんとなく予想はできそうだけど)

復習

ヒストグラムからの平均値・中央値・最頻値

やっている途中で迷ったのは、ヒストグラムからの平均値の導出というのが、階級値を元にしていることです。

つまり、Σ階級値×相対度数=平均値・・・① という式です。

ヒストグラムの作り方を忘れてしまっており、①の値が生データの平均値と一致するのかぐるぐると考え出してしまっていました。

完全独習 統計学入門に寄れば、

度数分布表というのは、(中略)、生データの中の情報の一部を捨ててしまっています。

ということでした。

ヒストグラムの作り上、ヒストグラムから生データなしで本当の平均値を導出することはできなさそうですね。

中央値・最頻値も恐らくそうでしょう。

メモ

円グラフについて

円グラフは相対度数をわかりやすくしただけのものと考えるとしっくりきそう。

まとめ

最初は計算機使っていいのわからずに手計算していましたが、統計検定は基本的な計算機の使用はOKのようです。

次は3級に挑戦したいです。

Pythonの開発環境を整える(Neovim + Pyenv + Pipenv)

ShougoさんのNeovim補完用プラグインDeopleteはneovimがpython3を使える状態にしなければいけない。

今まで、面倒臭くてWarningやErrorを無視していたが、最近Pythonに触れる機会が多くなって来たのでこれを機にちゃんと補完が効くようにする。

Neovimのインストール

$ brew install neovim

Pyenv + Pipenvのインストール

$ brew install pyenv pipenv

あとは、.zshrcに以下を書き込んでexec $SHELL -lすれば設定が完了した。

export PYENV_ROOT=$HOME/.pyenv
export PATH=$PYENV_ROOT/bin:$PATH
eval "$(pyenv init -)"
alias pyenv="CFLAGS='-I$(brew --prefix openssl)/include' LDFLAGS='-L$(brew --prefix openssl)/lib' pyenv"

Pipenvを使う

$ pyenv install 3.6.3
$ pipenv --python 3.6.3

あとは、pipenvがvirtualenvを作ってくれるので必要なモジュールをインストールすれば開発環境を構築できた。

$ pipenv install -d neovim ipython  # -d, --dev で開発環境用にインストールできる
$ pipenv install numpy

pipenvはvirtualenvの環境変数などを反映した状態にするためのコマンドがある。

$ pipenv shell

これを叩くと、環境を反映した子プロセスのシェルが立ち上がる。

これをやらないと毎回、pipenv run ipython とか pipenv run python hoge.py と、pipenv run ...と先頭につける必要がありそう。

Ruby Bundlerのbundle exec ...みたいな感じかな?

子プロセスを起動した状態で、Neovimを起動するとdeopleteが使える状態になっている。

Neovimの状態チェック

Neovimで以下のコマンドを実行すれば、Neovimの状態とアドバイスが得られる。

:CheckHealth

きちんとpython3がNeovimから使える状態になっていると以下のように表示される。

f:id:watanany:20171209014022p:plain

無事、has("python3")が成功しておりNeovimからPython3を使えるようになっているようだ。

下の方に目を通すと、pyenvが最適にセットされていないと出てくる。

f:id:watanany:20171209014039p:plain

Neovim用にvirtualenvを使って仮想環境を作れということらしい。

ただ、それをしてしまうと補完してほしいモジュールをNeovim用の仮想環境と開発用の仮想環境、どちらにも入れなければならなさそうなので、pipenvでneovimパッケージをインストールすることにした。

これのデメリットはpipenvで仮想環境を作るごとにneovimパッケージをインストールしなければいけないことだが、今回はそれで良しとした。