sys.path.append() を使わないでください
はじめに
まず結論だけ言います。 Poetry を使って sys.path.append()
を封印しましょう。
なにがあったのか
それは新たな開発チームに配属されて、キャッチアップのためにPJのコードを読んでいたときのことでした。
「ん?このモジュールには sys.path.append("/path/to/src")
があるぞ?ってことは……」
「やられた!ありとあらゆるモジュールとテストに sys.path.append("/path/to/src")
がいる!!」
なぜ問題か
ひとことで言えば、モジュールがパスに依存します。では、一体どういう問題が起きるのか?具体例を見ていきましょう。
ケース1: 別名のユーザで実行できない
"/path/to/src"
を絶対パスで書いた場合、 "/home/hoge/src"
のようにユーザ名 hoge
の含まれたパスでホームディレクトリを指定する訳ですが、 もしユーザ fuga
が利用したい場合、どうなるのでしょうか??
普通は権限がないので利用できません。そもそも fuga
さんのローカル環境に hoge
ユーザが存在しないことも多いでしょう。
それでは、 sys.path.append()
を使うひとたちは、どのようにしてこの問題に対処しているのでしょうか。 以下、私が出くわした対処をご紹介します。
対処A. パスのユーザ名 hoge
をすべて fuga
に書き換える
なんということでしょう。ありとあらゆるソースやテストに潜む文字列を全部書き換えるというのです。これでは本来不要な作業に無駄な工数を取ることになり、またソースの変更によってバグが生まれる可能性も作り出しています。これではやっている方も周りも疲れてしまいます。というか単純にめんどくさいと思わないのかな。
対処B. インスタンスを用意し、すべてユーザ名 hoge
で運用することを前提とする
開発環境も本番環境もすべて同一のユーザ名を持つインスタンスを用意し、その中での動作を前提にするアプローチです。当然、別ユーザやローカルで動かす際にはパスが通らなくなります。ローカルで動かさないでね!と主張する効果はありそうですが、そのために負債を抱え込むのは割に合いません。
対処C. パスを設定ファイルに保存する
ソースでは設定ファイルを参照するようにし、設定ファイルの中身にパスを書く方法です。これまでの対処に比べると良いですが、設定ファイルという新たに管理するべきものが増えてしまいました。もちろん、本当に必要なら仕方のないことですが、モジュールのパスを追加するという用途では必要ないでしょう。
ケース2: モジュール名を変えたときの影響が大きい
ここで再び、sys.path.append()
を使った場合に出くわした問題に戻りたいと思います。
モジュール名を頻繁に変えるべきではありませんが、どうしても変えざるを得ない場合はあります。ローカルで開発していたのをPRに出したところ、「やっぱりこっちの名前にしてくれ」というレビューが入ることもあるでしょう。その場合、もし sys.path.append()
を使っていたならどうなるでしょうか?
もちろん、修正したモジュールを呼び出すありとあらゆるソースやテストを修正する必要があります。ケース1の対処Aと同様に、手作業でパスの修正をすることには多くの問題があります。
ケース3: コーディング規約に違反している
よいコードは責任が分割されていて、テスタブルであり、可読性が高いものです。そうでなければ仕様変更の際に多くの負債を溜め込むことになります。では具体的にはどういうコードを指すのか?そのひとつのものさしがコーディング規約です。 Python には PEP8 というコーディング規約があり、これによって世の中の Python コードの品質を担保しています。
この規約を無意味に破って生じる負債について、最初に実装しているときには何も感じないかもしれません。しかし、いつかそのコードを参照することになる誰かが、余分な工数を取られることになるでしょう。その人は未来の自分かもしれませんし、工数は何か月にも及ぶかもしれません。
sys.path.append()
のケースでは、冒頭に置きさえすれば可読性に問題はないと思う方もいるでしょう。しかしこのアンチパターンを受け入れると、リンタにもいちいちそのパターンを無視するよう設定しなければならなくなります。「このチームではこのパターンが許される」という特殊な規約をチーム内に周知する必要もあります。こうした規約は積み重なると膨大なものとなり、エンジニアリングスキルに結びつかない割に多大な労力を強いることになり、不健全な開発現場を生み出すでしょう。
もちろんどうしても必要ならばやらざるを得ないでしょうが、たいていの場合、そもそも sys.path.append()
なんて使わずともモジュールのインポートはできるので、無駄な作業とトラブルの元を無意味に増やしていることになります。
それでは試しに Flake8 をかけてみましょう。sys.path.append()
を使ったコードには Module level import not at top of file (E402)
が表示されてしまうでしょう。こんなものは pre-commit か workflow で弾くように設定しておかないと、リポジトリの負債がどんどん溜まる一方です。
どうすればいいのか
簡単です。 Poetry なり requirements.txt を使って自作モジュールを読み込みましょう。
解決法1: Poetry
Poetry をインストールし、pyproject.toml に追加したいパッケージを加える。それだけです。
これだけだと味気ないので、具体的な手順も残しておきます。
手順1. 自作パッケージを次のツリー構造で配置する
/path/to/work_dir
├── pyproject.toml
└── src
└── my_package
├── __init__.py
└── my_module.py
手順2. pyproject.toml の設定
[tool.poetry]
name = "my_package"
version = "0.1.0"
description = ""
authors = [""]
packages = [
{ include = "my_package", from = "src" }
]
手順3. Poetry の環境に入り、インストールする
poetry shell
poetry install
これで自作のパッケージを使えるようになりました。インタプリタで import my_package
と実行してみてください。パスが通らないときは、大体 poetry install
を忘れている気がします。
解決法2: requirements.txt
setup.py に追加したいパッケージの情報を記述し requirements.txt を使ってインストールする方法もあります。 Poetry が使用できない環境ではこちらを使いましょう。
こちらも使い方は至って単純です。手順を示します。
手順1. 自作パッケージを次のツリー構造で配置する
/path/to/work_dir
├── pyproject.toml
└── src
└── my_package
├── __init__.py
└── my_module.py
└── sub_dir
└── my_module2.py
Poetry のときと同じディレクトリ構造にしてください。ただ、 Poetry のときとは違って sub_dir
というディレクトリとその下にモジュール my_module2.py
を追加してみました。 Poetry ではこの階層を意識せずとも勝手にインストールしてくれるのですが、 requirements.txt
の手法ではひと手間加える必要があります。具体的には次の手順2で紹介します。
手順2. setup.py, requirements.txt の設定
from distutils.core import setup
setup(
name="my_package",
version="0.1.0",
description="",
author="",
packages=["my_package"],
package_dir={
"my_package": "src/my_package",
},
package_data={
"my_package": [
"sub_dir/*.py",
]
},
)
-e .
package_data
の項目には下の階層のディレクトリを指定しておく必要があります。そうでなければ一番上の階層のファイルだけがインストールされてしまいます。
手順3. インストール
pip install -r requirements.txt
これで requirements.txt からでも好きなパッケージを追加することができるようになりました。
おわりに
冒頭でも書きましたが、 Poetry や requirements.txt を使えばパッケージやモジュールのパスを通すことができるようになります。これで問題だらけの sys.path.append()
を使わなくて済むようになるため、コードの品質が向上します。
実にこのアンチパターンは Python 界隈には深く根付いていて、私の周りでも「オライリーの書籍で紹介されたから使っていた」などの声を聞きました。たとえばこちら。
# coding: utf-8
import sys, os
sys.path.append(os.pardir) # 親ディレクトリのファイルをインポートするための設定
import numpy as np
from dataset.mnist import load_mnist
from PIL import Image
出典は『ゼロから作る Deep Learning』という書籍ですが、こちらはあくまで深層学習の実装に関数ハウツー本といったテイストです。そのため細かなコーディング規約違反を気にしても仕方のないことですが、書籍のメインターゲットであるアナリストや研究者にとって、コードの品質は軽視される傾向にあると感じます。たとえば Kaggle の公開カーネルにはアンチパターンをこれでもかと詰め込んだ Notebook が蔓延していますし、それを元に勉強する人もまたアンチパターンの再生産をして、どんどん広げてしまうのでしょう。
アナリストの目的は分析であって、可読性や結合度の改善は後回しになりがちだと思います。しかしその実装は後から誰かが使うかもしれません。実装した時の記憶の薄れた未来の自分が読み直すこともあるでしょう。読むだけならまだしも、機能を修正して再利用する場合だってあると思います。そうしたときに、いちいち sys.path.append()
を何度も書いたり、書き直したりするのはとても大変なことです。
ちょっとだけ意識してみると、みんなが幸せになれると思います。