LoginSignup
24

More than 3 years have passed since last update.

Pythonのパッケージとモジュールを理解してみる

Last updated at Posted at 2019-10-31

はじめに

Pythonを触っていると、モジュールとパッケージの違いがよく分からなくなります。
特に、PyCharmというIDEを使っていると、変に気を利かして挙動がおかしくなります。

そのため、パッケージとモジュールとの格闘記をあとから見返せるようにまとめておこうと思います。

モジュールに触れてみる

まずは、モジュールに触れてみます。

モジュールとは?

モジュールは、Pythonではファイルを表します。
つまり、add.pyというファイルを作ったらaddというモジュールになります。

モジュールごとに機能を分けることで、シングルファイルへの機能の肥大化を避けることができます。

ディレクトリ

現在、このようなディレクトリ構成を利用します。

Python_Tutorialというディレクトリの中に
以下が入っています。

.
├── __pycache__
├── calc
│   ├── add.py
│   ├── calc_main.py
│   ├── mul.py
│   └── sub.py
└── main.py

calcディレクトリにモジュールを書き、
その後、calc_mainから他の計算モジュールを呼び出してみます。
また、その後で、Python_Tutorial直下のmain.pyを利用してみます。

下準備

add.py, mul.py, sub.py
を用意します。

print('add.py')
print(__name__)


def add(x, y):
    return x + y


if __name__ == '__main__':
    print(add(10, 20))
print('sub.py')
print(__name__)


def sub(x, y):
    return x - y


if __name__ == '__main__':
    print(sub(10, 20))

print('mul.py')
print(__name__)


def mul(x, y):
    return x * y


if __name__ == '__main__':
    print(mul(10, 20))

nameとは

add.pyなどで

if __name__ == '__main__':
    print(mul(10, 20))

がついていますがこれは何でしょうか?

これは、「ファイル$f$.pyを実行したときの名前」が入っています。

例えば、add.pyを実行してみます。
そのとき、

add.py
__main__
30

と出力されます。

これは、ファイル$f$ = addを起点として、実行し、実行ファイルのnameにはmainが設定されるためです。

実行起点ファイルのnameは必ずmainとなることを意識します。

モジュール呼び出ししてみる

calc_main.pyから同ディレクトリのadd, sub, mulを実行してみます。

calc_main.py

import add

'''
add.py
add
30
'''
print(add.add(10, 20))

from sub import sub

'''
sub.py
sub
-10
'''
print(sub(20, 30))


from mul import *

'''
mul.py
mul
1500
'''
print(mul(30, 50))

どうやら

import 同じ階層のファイル名

で、他ファイルをモジュールとして読み込めるようです。

addの読み込み

import add

を行うと、add.pyの2つのprintが実行されています。
よって、どうやらモジュールを読み込んだ時点でprintなどのベタ書き処理が実行されるようです。
また、nameにはaddが入っています。
このように、実行起点ではなく、モジュールとして呼び出されるときはファイル名がそのまま入るようです。

import ファイル名
で読み込んでいるため、ファイル名の名前空間であるadd.addで関数を利用できます。

この記法によって、add関数が他のファイルにあってもお互いの名前空間が守られるため安全そうです!

subの読み込み

from sub import sub

を行うと、sub.pyのsub関数を直接読み込んでいるようです。
モジュールから取り出してしまうことも可能なようです。

ただ、sub関数を直接取り出すことはできないようです。
add.pyと同様に

'''
sub.py
sub
'''

が出力されるため、from subの時点でsubファイルのすべての中身が実行されるようです。

mulの読み込み

from mul import *

を行うと、mulファイルのすべての内容を一括で強制的にcalc_main.pyの名前空間に持ってくることができます。

基本的に使用しないほうが良いようです。
理由としては、他のモジュールと同じ名前を使用していた時上書きされてしまうなど、予期しない挙動を起こしてしまうためです。

同ディレクトリ内の読み込みまとめ

import ファイル名

が一番便利かも。

多重読み込みを試す

次に、多重読み込みを試してみます。
同じファイルを何回も読み込むとどういう挙動を示すのか、私、木になります

ディレクトリ構成

多重読み込みを試すには
a
b
cのようなシンプルな構成のほうが楽そうです。

以下のようにしてみます。

.
├── __pycache__
├── abc
│   ├── a.py
│   ├── ab.py
│   ├── abc_main.py
│   ├── b.py
│   ├── bc.py
│   ├── c.py
│   └── ca.py
├── calc
│   ├── __pycache__
│   │   ├── add.cpython-37.pyc
│   │   ├── mul.cpython-37.pyc
│   │   └── sub.cpython-37.pyc
│   ├── add.py
│   ├── calc_main.py
│   ├── mul.py
│   └── sub.py
├── main.py

abcディレクトリを追加しました。
ab.pyはaファイルとbファイルをモジュールとして読み込みます。

下準備

print('a file')

aa = 1
print('b file')

bb = 2
print('c file')

cc = 3

a, b, c.pyを以上のようにして実験スタートです。

下準備2

ab.pyファイルなどからそれぞれ対応するファイルを読み込んでみます。

ab.py
import a
import b


print('ab file')
bc.py
import b
import c

print('bc file')
ca.py
import c
import a

print('ca file')
abc_main.py
import ab
import bc
import ca

print('abc main file')

abc_main.py

abc_main.pyを実行すると以下のようになりました。

a file
b file
ab file
c file
bc file
ca file
abc main file

最初にa, bファイルを読み込んでいるようです。
その後、b, cファイルを読み込むため、b fileと出力されるはずですが、飛ばされてcfileと出力されています。

どうやら一回読み込むと読み込まれないようです。

caファイルの読み込みはすでにc,aファイルは読み込まれているため何も怒らないようです。
a fileなども一回読み込まれればあとは読み込まれず、一回しか出力されないのも以外ですね。

どうやら、ファイルは基本的に1回しか読み込まれないようです。

一個下の階層のモジュールを使ってみる

現在、abc_mainやcalc_mainなど同じディレクトリ下のモジュールを使用していました。

それでは、main.pyのように、一個下のディレクトリのモジュールを使うことはできるのでしょうか?

main.py

main.py
from calc import add

'''
add.py
calc.add
5
'''
print(add.add(2, 3))

以上のように、calcディレクトリからaddモジュールをインポートしています。
これは実際に動くコードです。

つまり、Python3.3以降はディレクトリ以下のモジュールを利用することができます。
また、nameに着目すると

calc.add

と出力されています。
よって、calcスペースのaddモジュールと読み込まれているようです。
これはPython3.3以降のネームスペースインポートと呼ばれる機能らしいです。

ただし、以下のようにするとうまく動きません。

main.py

'''
    from calc import add.add
                        ^
SyntaxError: invalid syntax
'''
from calc import add.add
print(add(2, 3))



'''
AttributeError: module 'calc' has no attribute 'add'
'''
import calc
print(calc.add.add(2, 3))

どうやら、import以降に書く内容で、「.」を使ってさらに絞り込むことは許されていないようです。

また、ディレクトリをモジュールとして読み込むことは許されていないようです。

パッケージ

パッケージとは?

パッケージとは、「モジュールを総管理したモジュール」になります。
例えば先程calcディレクトリを作りました。

.
├── calc
│   ├── __pycache__
│   │   ├── add.cpython-37.pyc
│   │   ├── mul.cpython-37.pyc
│   │   └── sub.cpython-37.pyc
│   ├── add.py
│   ├── calc_main.py
│   ├── mul.py
│   └── sub.py

このcalcディレクトリに__init__.pyを置くことで、
calcディレクトリ自体を大きなモジュールとしてみなすことができます。

当然ですが、__init__.pyがないと、calcディレクトリはただの「ディレクトリ」です。
そこで、__init__.pyを置くことで、calcディレクトリ自体をモジュールと見なすようにします。

パッケージを使うことでより大きな単位でファイルを扱うことができ、構造化を行えます。

__init__.pyは?

__init__.pyは、そのパッケージモジュールを呼び出したとき、一番最初に実行されるものです。

__init__.py
print(__name__)

などとすると、ディレクトリ名が出力されます。

これは、ディレクトリが「モジュール」として__init__.pyによって、扱われているためです。

calcを書き換えてみる

calcディレクトリをパッケージ用に書き換えてみます。

calc_main.py
print(__name__)

from . import add
from . import sub
from . import mul

print(add.add(2, 3))

としてみます。

このとき、通常のpython実行ではうまくいきません。

python calc/calc_main.py
python calc_main.py

などとすると

  File "calc_main.py", line 3, in <module>
    from . import add
ImportError: cannot import name 'add'

のエラーが発生します。

python -m calc.calc_main.py

とすると

calcをパッケージとして、calc_main.pyを実行することができます。

このように、パッケージは基本的に「相対パス」で現在は指定するようです。

python calc/calc_main.py
python calc_main.py

でうまくいかない原因ですが、
以下のようなコードに書き換えます。

print(__name__)
print(__package__)

from . import add
from . import sub
from . import mul

print(add.add(2, 3))
python -m calc.calc_main.py

calc
__main__
calc
add.py
calc.add
sub.py
calc.sub
mul.py
calc.mul
5

python calc/calc_main.py

__main__
None
Traceback (most recent call last):
  File "calc/calc_main.py", line 4, in <module>
    from . import add
ImportError: cannot import name 'add'

となります。

ここで、それぞれの違いとして
packageは、そのファイルがどのパッケージに属しているか?を表しますが(親モジュール)
うまく行かないほうでは、package属性がNoneとなっています。
そのため、どのパッケージを参照すればよいか分かっておらず、エラーが出ています。
つまり、「.」などを指定しても「どこのパッケージからの相対パスだよ?」となるのです。

そのため、
大本のディレクトリからcalcパッケージを使用すると

main.py

from calc import calc_main

print(10)
calc
calc.calc_main
calc
add.py
calc.add
sub.py
calc.sub
mul.py
calc.mul
5
10

のように、きちんと実行されます。

そのため、どうやら__package__属性がきちんと指定されるような状況・かつ相対インポートの場合はうまく動くようです。

参考サイト
[Python] importの躓きどころ
Pythonのモジュールについてまとめてみたよ
Python にまつわるアイデア: Python のパッケージとモジュールの違い

きちんと-mオプションをつけて、モジュール・パッケージを明示してあげます。

PyCharmでは?

PyCharmの利用時にはどうすればよいのでしょうか?
どうやらPyCharmは通常のPythonとは異なる挙動をします。
IDEくんがいい感じに推論をして異なる動作をするようです。

1. モジュール読み込みでは相対パスを使う

パッケージ内のモジュール読み込みでは、相対パスを使用します。

のように、同パッケージ内では、相対パスを利用します。

calc_main.py
print(__name__)
print(__package__)

from . import add

print(add.add(2, 3))

2. パッケージの利用時は通常のように書く

パッケージを上から叩くときはそのままで良いです。
通常のモジュールのように呼び出します。

main.py
from calc import calc_main

print(10)

3. パッケージ内だとファイル読み込みできないので、コマンドからやる

PyCharmでは起動構成を実行などがありますが、
パッケージ内のファイルを実行しようとすると

calc_main.py
print(__name__)
print(__package__)

from . import add

print(add.add(2, 3))

packageでNoneが出力され、パッケージ判定されないため、エラーがでます。
そのようなときは

コマンドラインから

python -m calc.calc_main

としてあげればよいです。

PyCharmの起動構成を実行ではうまくいきません。
シングルファイルの実行テストが難しいのが面倒ですね...
うまくやる方法はあるんでしょうか...

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
24