249
227

More than 5 years have passed since last update.

Python におけるモジュールとパッケージは「名前空間」

Posted at

この記事を書いた動機

  • Python のパッケージシステムを理解せずに他の言語にある、「1クラス1ファイル」の哲学をそのまま持ち込むとつらい
  • 「モジュールやパッケージをつかうとこういうふうに書けるよ」という入門記事はよく見るけど、どう使うことが想定されているのかという議論がなかなか見えてこない (特に __init__.py がなんなのかがわからない)
  • 強制するつもりはなく、ベースとしてはこういうことなんじゃないのかな?という話がしたい

もし「この記事 or ドキュメントにちゃんと書いてあんだろーが」みたいなものがあれば教えていただけると喜びます。

クラス単位にファイルを分けるデメリット

Java や C# などオブジェクト指向を制約とする言語では、基本的に public なクラスは1ファイルに1つ書く習慣があるかと思います。

しかし python に於いて 〜.py はすべてモジュールであり、モジュールは提供する機能のセットです。モジュールには複数のクラスがあってもいいと思います。

たとえば Django の models.py (ORMのエンティティクラスを実装するデフォルトのモジュール) に、すべてのエンティティクラスを書き連ねていくイメージです。

models.py
from django.db import models


class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')


class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

「はじめての Django アプリ作成、その2」 より抜粋

この行数で収まるのは実務ではさすがにまれなケースかもしれませんが、たかだか数行のクラスを別のファイルに分けることにそれほどメリットがあるでしょうか。もちろん、言語が変われば常識も変わります。あくまでも python では1クラス1ファイルを規定のように扱うのは違うのではないかと感じます。

そのように感じる一番大きな理由としては、importの書き方に影響を与えるからです。

1クラス1ファイルにした時のモジュールインポート

例えば次のような構成にした場合

polls/models/
 ├─ __init__.py
 ├─ question.py ... Question のみが定義されている
 └─ choice.py   ... Choice のみが定義されている

それぞれのクラスを呼び出すには、こう書きます。

from polls.models.question import Question
from polls.models.choice import Choice
question = Question()
choice = Choice()

もしくは、こう書くこともできます。

from polls import models
question = models.question.Question()
choice = models.choice.Choice()

クラス一つ一つごとの専用の名前空間ができてしまっていますよね。私はやり過ぎと感じます。

一つのモジュールにすべてのクラスがある場合

チュートリアルと同じ構成です。

polls/models.py

クラスを呼び出すにはこう書きます。

from polls.models import Question, Choice
question = Question()
choice = Choice()

または * を使うことですべてインポートできます。

from polls.models import *
question = Question()
choice = Choice()
# これはどこからなにをインポートしたのか明示的ではなくなるので使うかどうかはまた別の問題

polls.models という名前空間に Question Choice が住んでいるというシンプルな構成だと思います。

パッケージを使う理由

では、なぜパッケージという機能が用意されているのでしょうか。
例えば次のような理由が考えられます

  • モジュール内にクラスや関数が増えてきた
  • モジュールの中でしか使わないようなクラスや関数が出てきた
  • クラスが大きくなってきて、さすがにファイル分割したい

こうなったらパッケージを使わないほうが見通しが悪くなってきますし、整理整頓ができなくなってしまいます。ぜひパッケージを使ってモジュールを分けていきましょう。パッケージ内でどのようにモジュールを切るかは、そのパッケージが何を提供するためにあるのかなどから都度考える必要があると思います。

ただ一つ、重要なのは import する側は1モジュールだったときと同じようにアクセスできるようにする ということではないでしょうか。

そのための __init__.py

たとえば(あまりないかもしれませんが)先のチュートリアルの例をもとに、それぞれのクラスが肥大化し、models だけで使うヘルパー関数などが出てきたためにパッケージにしてファイルを分割したとします。

polls/models/
 ├─ __init__.py
 ├─ question.py ... Question が定義されている
 ├─ choice.py   ... Choice が定義されている
 └─ helper.py   ... 便利関数が定義されている

このままでは、Question を呼び出そうと思ったら from polls.models.question import Question と書かなければなりません。つまり1ファイルだった時と比較して import の書き方が変わってしまうということです。もしもこれがライブラリであればインターフェイスに影響を与える破壊的変更となってしまいます。

そこでいつも空っぽの __init__.py に次のように追記します。
対象のモジュールを . から書くと相対参照となるので便利です。

polls/models/__init__.py
from .question import Question
from .choice import Choice
# (helper は内部でしか使わないので書かない)

これを書いておくことで、「Question Choicepolls.models 名前空間にいますよ!」と明示でき、import の書き方はそのままでよくなります。

from polls.models import Question, Choice
question = Question()
choice = Choice()

つまりモジュールやパッケージは名前空間

  • 名前空間の粒度は適切にしたいし、一つのクラスのためだけの名前空間をいちいち定義するのはちょっと変だよね
  • パッケージにするなら、外部から参照されるものを __init__.py に書いておくといいんじゃない

もちろんライブラリとかで、パッケージのなかのモジュールを直接参照させる設計になっているものもあります。おそらくフレームワークのような巨大なライブラリなのでしょう。それでも、末端のパッケージは利用者のことを考えて整理してあったりします。そういったコードを読むと参考になるかもしれません。

249
227
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
249
227