見出し画像

Python基礎8:モジュール/パッケージ

概要

 Pythonの基礎としてモジュール及びパッケージを説明します。一般的な用語の説明は下記の通りです。

【用語説明】
モジュール:コードが記載されたPythonファイル
パッケージ:複数モジュールがまとめて管理されたディレクトリ
ライブラリ:プログラムの再利用や配布が可能な形式にまとめたもの。複数のモジュールで構成されており実際はパッケージです。
ビルトインモジュール:Pythonに組み込まれている一部の標準ライブラリのことを呼びます。

1.モジュール

 モジュールとはPythonコードが記載されたファイルです。Pythonコードをモジュール化することで再利用性・保守性が高まります。
 本章での説明用ファイル構成は下記の通りです。

【ファイル構成】
●実行用ファイル:モジュールを動かすためのファイル
●モジュール:ほしい動作(変数・関数)を含むPythonファイル
●ターミナル:実行用ファイルとは別パターンとしてモジュールを使用する

1-1.モジュールの作成

 モジュールの作成はpyファイルにPythonコードを記載するだけです。まず説明用に”sayhello.py”を作成しました。このコードには1-4節の内容を含めていないため、実際の適用品は異なるコードとなります。

[In] sayhello.py
import sys

varhello_JP='変数:こんにちは'

def Hello(Language: str):
    if Language.upper() == 'JP':
        print('こんにちは')
    elif Language.upper() == 'EN':
        print('Hello')
    else:
        print('JP, ENのいずれかを入力してください')
        
def printsys():
    print(sys.argv)
    
def shownameval():
    print(__name__)
    
shownameval()

1-2.モジュールのインポート:import module

 モジュールのインポート方法は下記の通りです。

【モジュールのインポート】
●通常は「import <module>」とする。
●略称を使用してモジュールを使用する場合は「import <module> as <略称>」とする。(例:import pandas as pd, import numpy as np)
●モジュールをインポートする場合はパスが通っている箇所で実施

1-3.モジュールの使用方法

 モジュールの使用方法はインポート後にmodule.属性名で使用できます。使用可能な属性名はdir()で確認できます(下記参照)。

[In] Jupyterで実行
import sayhello

print(type(sayhello))
print(dir(sayhello))

print(sayhello.varhello_JP)
sayhello.Hello('EN')

[Out]
<class 'module'>
['Hello', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'printsys', 'shownameval', 'sys', 'varhello_JP']

変数:こんにちは
Hello

1-4.ターミナルで直接実行

 ターミナルで直接実行する場合はモジュール内に実行したい関数定義だけでなく関数の実行(func())を直接記載することで可能となります。

 1-4-1.ターミナルで実行: python module.py

 ターミナルにおいて「python file名」でモジュールを実行できます。※python2と3が混在している時Macユーザーは注意は「python3 file名」となります。
 上記”sayhello”モジュールでは”shownameval()”が自動で実行されます。

[Terminal]
python sayhello.py

 1-4-2.ターミナルで引数を渡す:sys.argv

 ターミナル経由でsys.argvを使用するとリスト形式で['モジュール名']が渡され、引数を記載すると['モジュール名', '引数']で渡されます。よってターミナルから引数を取得したい場合はsys.argv[1]となります。
 参考としてモジュールを下記に修正します。

[In] sayhello.py
import sys

varhello_JP='変数:こんにちは'

def Hello(Language: str):
    if Language.upper() == 'JP':
        print('こんにちは')
    elif Language.upper() == 'EN':
        print('Hello')
    else:
        print('JP, ENのいずれかを入力してください')
        
def printsys():
    print(sys.argv)
    
def shownameval():
    print(__name__)
    
printsys()

1-5.直接実行時のみ動く:if __name__=='__main__':

 1-1で作成したコードには欠陥がありモジュールを読み込んだ時点で関数が必ず処理されます。参考として下記の通りJupyter側でモジュールをインポートだけしてみるとモジュールに記載した関数が自動で実行されました。

 モジュールのインポート時には関数を実行せずモジュールを直接実行時のみ関数を処理される方法として「if __name __== '__main__'」があります。
 __
name__引数には特殊引数であり下記のような動作になります。

【__name__の特徴】
name= '__main__':①スクリプトとして実行、②対話モードで実行
__name__=モジュール名:モジュールとしてインポートされた

 つまりimport時には実行させずスクリプトとして実行させたい場合は「if __name __== '__main__'」と記述すると期待される実行結果が得られます。

[In]
import sys

varhello_JP='変数:こんにちは'

def Hello(Language: str):
    if Language.upper() == 'JP':
        print('こんにちは')
    elif Language.upper() == 'EN':
        print('Hello')
    else:
        print('JP, ENのいずれかを入力してください')
        
def printsys():
    print(sys.argv)
    
def shownameval():
    print(__name__)

if __name__ == '__main__':
    shownameval()

[Out]
import時にshownameval()が自動で出力されない

2.パッケージ(ライブラリ)

 複数のモジュールが管理されたディレクトリをパッケージと呼びます。ディレクトリ内に用途ごとに作成したモジュールを配置することでまるで一つのモジュールの様に扱うことができます(参考例は下記の通り)。

 今回はKIYOパッケージを作成します。初めに"KIYO"としたフォルダを作成しました。このフォルダ内にモジュールを入れてパッケージを作成します。

2-1.パッケージ作成の初期:__init__.pyの配置

 一般的にパッケージを作成する場合は__init__.pyを配置します。こちらは空ファイルでも問題ありませんが、下記機能があり有効活用ができます。

【__init_.pyの目的・使い方】
●名前空間パッケージ:__init__
.pyがないパッケージは名前空間パッケージと呼ばれる特殊仕様として扱われる。
パッケージの初期化:パッケージのインポート時に一番初めに実行される
公開するAPI管理:配布時に使用してほしいモジュール内の属性を指定することで必要な関数・変数だけを管理できる。

 パッケージ用フォルダに__init__.pyを空ファイルで配置しました。これだけでモジュールとして読み込みが可能です。属性にはモジュールには無かた”__path__”が追加されています。

[In]
import KIYO

print(type(KIYO))
print(dir(KIYO))
print(KIYO.__path__)

[Out]
<class 'module'>

['__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__']

_NamespacePath(['c:\\Users\\KIYO-\\Desktop\\note\\note_モジュール\\KIYO', 'c:\\Users\\KIYO-\\Desktop\\note\\note_モジュール\\KIYO'])

2-2.モジュールの配置

 パッケージ内に__init__.pyとは別に使用したいモジュールを配置します。
今回は適当なモジュールを3つ作成しました。
 ※helloworld.py内の__all__については後述。

[KIYO/helloworld.py]
def sayhello(Language: str):
    if Language.upper() == 'JP':
        print('こんにちは')
    elif Language.upper() == 'EN':
        print('Hello')
    else:
        print('JP, ENのいずれかを入力してください')

def addnums(a: int, b: int) -> int:
    return a + b

def multiplynums(a: int, b: int) -> int:
    return a * b

__all__ = ['sayhello']
        
[KIYO/printsys.py]
import sys

def printsys():
    print(sys.argv)

[KIYO/showname.py]
def shownameval():
    print(__name__)

2-3.パッケージのインポート

 パッケージのインポート方法は読み込む内容で要領が異なります

【パッケージのインポート1※__init__.pyにコード記載不要】
import package.module:モジュールの呼び出し
※使用時もpackage.moduleと記載する必要があるため通常では使用しない
※import packageとしてpackage.moduleとしても使用できない
from package import module:パッケージ内のモジュールを取得
from package import module1, module2:複数モジュールの取得
from package.module import *:指定モジュール内の属性を全て取得

【パッケージのインポート2※__init.pyのコード記載必要】
●import package:
パッケージの呼び出し
 ->パッケージでは__init__.pyのみ読み込まれるため属性の定義をしていないと使用できない※package.moduleの記法はエラー。
from package import attribute:属性だけを取得
※from package import attribute1, attribute2:複数属性を取得

 

 足りていないと思うけど図解すると下記の通りです。

 なおパッケージのインポートは下記順序で実施されます。用語はよくわからなくても「Python 標準ライブラリと同じ名前のモジュールは作成しない」ことを注意すれば大きなトラブルは防げます。

【インポート時のPythonのモジュール検索順序】
1.ビルトインモジュール:Python組み込みの一部のライブラリ
2.カレントディレクトリのモジュール:実行ファイルと同じpath内
3.ビルトインモジュール以外の標準ライブラリ:-

2-4.パッケージの使用方法

 __init__.pyはまだ空ファイルですがモジュールをインポートしてみます。結果として下記のことが確認できました。

【確認事項1:全般】
●パッケージをimportしても、package.moduleでモジュールは使用できない
 ->モジュールを使用する場合は明示が必要
●パッケージ内の属性にはモジュールのみ現れる(モジュール内属性はでない)
●ファイルの属性:dir()ではモジュールの属性も確認できる。

【確認事項2:from package import module】

●パッケージの属性にモジュールが追加された
●モジュールの属性を使用する場合はmodule.属性と記載(属性だけはエラー)

【確認事項3:from package.module import * 】
●こちらもパッケージの属性にモジュールが追加された。
●モジュールの属性を使用する場合は属性名だけで使用可能である。
●モジュール内の全属性は使用できるがパッケージの属性には表示なし。

[IN]
import KIYO
from KIYO import showname
from KIYO.helloworld import *
from KIYO.helloworld import addnums

print(dir(KIYO)) #KIYOモジュールの属性を確認
print(dir()) #現在のモジュールの属性を確認

#モジュールの関数を実行
showname.shownameval() #shonameモジュールのshownameval関数を呼び出す
KIYO.helloworld.sayhello('EN') #モジュール内の属性をワイルドカード(*)で呼び出したため使用可能
addnums(1,2)


#下記はエラー発生コード
# shownameval() #関数だけ記載するのはNameError
# KIYO.printsys.printsys() #この書き方はAttributeError

[Out]
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'helloworld', 'showname']

['In', 'KIYO', 'Out', '_', '_1', '__', '___', '__builtin__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', '__vsc_ipynb_file__', '_dh', '_i', '_i1', '_i2', '_ih', '_ii', '_iii', '_oh', 'addnums', 'exit', 'get_ipython', 'multiplynums', 'os', 'quit', 'sayhello', 'showname', 'site', 'sys']

KIYO.showname
Hello
3

2-5.import * での呼び出し属性の制限:__all__

 import *ですべての属性を呼び出せますが下記問題もあります。

 ①dir()で確認すると不要な属性が出るため見にくい
 ②他モジュールと属性が被ることで期待しない動作が起こる

 上記問題を避けるために呼び出すことができる属性を制限できます。制限としては__all__引数に指定する属性をリスト形式で渡します。
 既にhelloworldモジュールには__all__引数を指定済みのため下記を実行するとモジュール内に記載したはずの関数が使用できないことが確認できます。

[In]
from KIYO.helloworld import *

print(dir())

sayhello('JP')
multiplynums(1,2) #NameError:NameError: name 'multiplynums' is not defined

[Out]
['In', 'Out', '_', '__', '___', '__builtin__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', '__vsc_ipynb_file__', '_dh', '_i', '_i1', '_i2', '_ih', '_ii', '_iii', '_oh', 'exit', 'get_ipython', 'os', 'quit', 'sayhello', 'site', 'sys']
こんにちは
NameError 

3.__init__.pyの活用

 前述の通り__init__.pyは下記用途があります。

【__init_.pyの目的・使い方】
●名前空間パッケージ:__init__
.pyがないパッケージは名前空間パッケージと呼ばれる特殊仕様として扱われる。
パッケージの初期化:パッケージのインポート時に一番初めに実行される
公開するAPI管理:配布時に使用してほしいモジュール内の属性を指定することで必要な関数・変数だけを管理できる。

 パッケージをさらに便利に使用できるように__init__.pyを利用します。

3-1.__init__.pyの動作確認:import時に自動実行

 まずは__init__.pyの動作を確認します。__init__.pyに下記コードを記載して(再起動して)パッケージをimportすると自動で__init__.pyが実行されました。

[__init__.py] 
print(f'__init__.pyが実行されたよ')
print(f'__name__={__name__}')
[In]
import KIYO

[Out]
__init__.pyが実行されたよ
__name__KIYO

3-2.相対インポートによる属性の取得

 前述の通り空__init__.pyではimport packageだけだと何も使用できませんでした。package.属性で参照できるAPIを作りたい場合は__init__.pyに相対インポートでモジュール内の属性を記載することでAPIを実現できます。
 相対インポートとは位置関係を示す記述でありmodule名の前に.ドットを記載すると現在のパッケージを意味します。

[__init__.pyが空ファイル]

[In ※実行ファイル] 
import KIYO
print(dir(KIYO))

[Out]
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__']
[__init__.py]
from .showname import shownameval
from .printsys import printsys

[In]
[In ※実行ファイル] 
import KIYO
print(dir(KIYO))

KIYO.shownameval()
KIYO.printsys()

[Out]
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'printsys', 'showname', 'shownameval']
KIYO.shownameval()
※['C:\\Users\\KIYO-\\anaconda3\\lib\\site-packages\\ipykernel_launcher.py',以下省略]

3-3.__all__+ワイルドカード(*)を同時利用

 ワイルドカード(*)でインポートする属性を指定する"__all__"を設定しておき、"__init__.py"をfrom .module import *とすることで"__init__.py"は更新不要となります。

[__init__.py]
from .showname import shownameval
from .printsys import printsys
from .helloworld import *

[helloworld.py]
def sayhello(Language: str):
    if Language.upper() == 'JP':
        print('こんにちは')
    elif Language.upper() == 'EN':
        print('Hello')
    else:
        print('JP, ENのいずれかを入力してください')

def addnums(a: int, b: int) -> int:
    return a + b

def multiplynums(a: int, b: int) -> int:
    return a * b

__all__ = ['sayhello']
[In 実行ファイル]
import KIYO

print(dir(KIYO))
KIYO.sayhello('EN') #__all__で設定済み
KIYO.addnums(1,2) #__all__で設定していない->AttributeError 

[Out]
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'helloworld', 'printsys', 'sayhello', 'showname', 'shownameval']
Hello
AttributeError

 結果として"helloworld.py"モジュールは3つの関数がありますが"__all__"設定によるワイルドカードで呼び出しても指定した属性しか使用できません。

4.補足説明1:相対インポート

 3章で相対インポートを使用して__init__.pyを利用することでパッケージに属性を取得しましたが相対インポートについてさらに詳しく確認します。

4-1.親フォルダ内モジュールのインポート

  相対参照より".."だと親フォルダを意味するためfrom .. import moduleの様に記載するとエラーが発生します。実施時はsys.pathへの追加が必要です。

[In ※KIYO/child.ipynbで実行 ※parent.pyはKIYOフォルダ内に配置]
from .. import parent

[Out]
ImportError: attempted relative import with no known parent package

4-2.パッケージ内モジュール:from . import module

 同じパッケージ内のモジュールからimportする場合は1章と考え方が異なります。テストでKIYO2パッケージ内にA.py, B.pyを作成してimport Aでモジュールを読み込みましたが「ModuleNotFoundError 」が発生します。

 詳細は下記記事に記載しておりますが「パッケージ内のモジュールをインポートするときはfrom . import modueと記載」します。

[KIYO2/A.py]
varA = 'Aの変数'

class A:
    varA = 'Aのクラス変数'
    def __init__(self):
        self.varA = 'Aのインスタンス変数'
        
A_ins = A()

[KIYO2/B.py]
from . import A #相対インポートでないとエラー

vBrB = 'Bの変数'

class B:
    varB = 'Bのクラス変数'
    def __init__(self):
        self.varB = 'Bのインスタンス変数'
        
def checkAB():
    print(f'A.varA = {A.varA}, B.varB = {B.varB}')
    print(A.A_ins.varA)

[In]
from KIYO2 import A, B

#Aのクラス変数とインスタンス変数の取得確認
B.checkAB() #B.pyがA.pyをインポートしてクラス変数を出力

[Out]
A.varA = Aの変数, B.varB = Bのクラス変数
Aのインスタンス変数

5.まとめ(自分用)

 自分が使用するための箇条書きとなります。

【モジュール】
●モジュールにするなら脳死で if __name__=='__main__': を書け
●標準ライブラリと同じモジュール名は絶対つけるな
●ターミナルからあんまり使わないからsys.argvは気にしなくてもいいかも

【パッケージ】
●パッケージのディレクトリを作ったら即__init__.pyを作れ
●自分が欲しい機能を整理したうえでモジュールを設計・作成開始しよう
●__init__.pyを設計できないとimport package だけで使用できない
->Numpyみたいな感じには使えなくなる
->from package import moduleをモジュールごとに記載させるなら不要
●パッケージ内のモジュールをターミナルで実行するならpackage.moduleで実行できるよ


参考資料

dir()関数の参考

あとがき

 自分でライブラリなんて作ることはないと思ってたけど、特定のライブラリを使用するために最低限モジュールの知識は必要だったのでとりあえずまとめました。

追記1@2022年4月3日:importって使うと楽(に使えるようにしてもらっている)だけど作ろうとするとこんなに難しいんだ・・・・・



この記事が気に入ったらサポートをしてみませんか?