🫥

Act 05. Pythonの関数とモジュール

2024/10/22に公開

はじめに

Act 01. AIで外国為替を自動売買するまでの道のりをベースに学習を進めて行く。

今回はPythonの関数とモジュールについて書こうと思う。

以前自分で書いた記事「Act 02. WindowsでPythonの環境構築とバージョン管理を行う
」の環境で試す。

Pythonのバージョンは3.12.7

関数とモジュールの違い

まずは一応Pythonの関数とモジュールの違いについて。

関数

  • 定義: 関数は、特定のタスクを実行するための再利用可能なコードのブロック
  • 特徴:
    • 入力(引数)を受け取り、処理を行い、結果(戻り値)を返す
    • 他の関数やプログラムから呼び出して使用可能
    • 同じ処理を何度も行う必要がある場合に便利

モジュール

  • 定義: モジュールは、関連する関数、クラス、変数などをまとめたファイル(通常は.py拡張子)
  • 特徴:
    • 複数の関数やクラスを含むことができ、特定の機能やライブラリを構造化して管理するために使用される
    • 他のモジュールやプログラムからインポートして使用可能
    • Python標準ライブラリやサードパーティのライブラリとしも利用可能

まとめ

  • 関数は特定の処理を行うためのコードのブロックであり、モジュールはそのような関数を含むファイル。モジュールを使うことで、コードを整理し、再利用性を高めることができる。

関数

関数の定義

Pythonでは、defキーワードを使って関数を定義する。基本的な構文は以下の通り。

def 関数名(引数1, 引数2, ...):
    """関数の説明(オプション)"""
    処理内容
    return 戻り値  # 戻り値はオプション

  1. 引数なし、戻り値なしの関数
    この関数を呼び出すと、"Hello, World!"が出力される。

    def greet():
        print("Hello, World!")
    
    greet()
    
  2. 引数あり、戻り値ありの関数
    この関数は二つの数を受け取り、その合計を返す。

    def add(a, b):
        return a + b
    
    result = add(3, 5)
    print(result)  # 8
    
  3. デフォルト引数を持つ関数
    引数が指定されない場合、"World"が使用される。

    def greet(name="World"):
        print(f"Hello, {name}!")
    

    指定する場合と指定しない場合の出力は以下の通り。

    greet()          # Hello, World!
    greet("Alice")  # Hello, Alice!
    
  4. 可変長引数を持つ関数
    この関数は任意の数の引数を受け取り、その合計を返す。

    def summarize(*args):
      return sum(args)
    

    例えばこんな感じ。

    total = summarize(1, 2, 3, 4, 5)
    print(total)  # 15
    
  5. 可変長のキーワード引数を持つ関数
    この関数は任意の数のキーワード引数を受け取り、その内容を出力する。

    def print_info(**kwargs):
        for key, value in kwargs.items():
            print(f"{key}: {value}")
    

    例えばこんな感じ。

    print_info(name="Alice", age=30, city="Tokyo")
    # name: Alice
    # age: 30
    # city: Tokyo
    

    **kwargsとかって便利だけど、何が来るか分からないの怖すぎない・・?
    関数の最初の方で型チェックとか何かしらすることが出来る。例えば以下のようなイメージ。

    def configure_settings(**kwargs):
     if "theme" not in kwargs or not isinstance(kwargs["theme"], str):
         print("theme does not exist or the type is not str type.")
         return False
     if "language" not in kwargs or not isinstance(kwargs["language"], str):
        print("language does not exist or the type is not str type.")
        return False
     
     # バリデーションチェックに成功したとき
     print("validation check successed!")
    

    実行するとこんな感じ

    configure_settings(theme="dark", language="Japanese") # validation check successed!
    configure_settings(theme=1, language="Japanese")  # theme does not exist or the type is not str type.
    configure_settings(theme="dark")  # language does not exist or the type is not str type.
    

    いやいや、これって数が増えれば増えるほど面倒じゃない?と思ってしまった。

便利なライブラリ紹介

そんな時はdataclassesモジュールやPydanticモジュールが使えるかも。

dataclasses

まずはdataclassesモジュールを使ってみる。

from dataclasses import dataclass
    
@dataclass
class Config:
    theme: str
    language: str
    font_size: int
    
def apply_settings(config: Config):
    print(f"Theme: {config.theme}, Language: {config.language}, Font Size: {config.font_size}")
    
# 使用例
config = Config(theme="dark", language="Japanese", font_size=12)
    apply_settings(config)  # Theme: dark, Language: Japanese, Font Size: 12

ちなみにこんな感じで補完される。(画像の下側に注目)

@dataclassを取ると補完されない。(画像の下側に注目)

これなら結構便利かも。と思ったのもつかの間。
themeをint型にしても正常に動いてしまった。

config = Config(theme=1, language="Japanese", font_size=12)
    apply_settings(config)  # Theme: 1, Language: Japanese, Font Size: 12

あくまで補完機能が追加されるだけでバリデーションチェックはしてないっぽい。

Pydantic

そんな時はPydanticを使う。一般的な書き方は以下の通り。
pip install pydanticでインストールする必要あり

from pydantic import BaseModel
    
class Config(BaseModel):
    theme: str
    language: str
    font_size: int
    
def apply_settings(config: Config):
    print(f"Theme: {config.theme}, Language: {config.language}, Font Size: {config.font_size}")
    
# 使用例
config = Config(theme="dark", language="Japanese", font_size=12)
    apply_settings(config)  # Theme: dark, Language: Japanese, Font Size: 12

使用するConfigと関数は同様で不正なパターンも試してみる。
どちらもエラーが発生するためtry-exceptを使うことになる。

  • 引数が不足しているパターン

    try:
        invalid_config = Config(theme="dark", language="Japanese")  # font_sizeが欠けている
    except Exception as e:
        print(e)
    

    実行結果はエラーが発生。どれが不足しているか分かるしいい感じ。

    1 validation error for Config
    font_size
      Field required [type=missing, input_value={'theme': 'dark', 'language': 'Japanese'}, input_type=dict]
        For further information visit https://errors.pydantic.dev/2.9/v/missing
    
  • 型が異なるパターン

    try:
        invalid_config = Config(theme=1, language="Japanese", font_size=12)  # themeの型が異なる
    except Exception as e:
        print(e)
    

    実行結果はエラーが発生。こちらも問題なし。

    1 validation error for Config
    theme
      Input should be a valid string [type=string_type, input_value=1, input_type=int]
        For further information visit https://errors.pydantic.dev/2.9/v/string_type
    

モジュール

次にモジュールについてだが、ここまで結構長かったので疲れた。
けど後少し頑張ろう。

まずは自作モジュールを作成する方法について。
結論、以下のようなフォルダ構成でOK。

.
├── Act05.py
└── my_package
    ├── __init__.py
    ├── another_module.py
    └── my_module.py

フォルダ名やファイル名はなんでもOKだが、another_module.pymy_module.pydefなどで関数を定義する。
__init__.pyはそのままの名前

モジュールを使用したいファイル内でfrom my_package import my_moduleのような形式でインポートを行う。

__init__.pyは、Pythonのパッケージを示すための特別なファイル。このファイルが存在することで、そのディレクトリがパッケージとして扱われる。

__init__.pyは少し特殊で、以下のような役割がある。
※ついでにパッケージの使用方法も記載しておく

1. パッケージの定義

__init__.pyが存在するディレクトリは、Pythonにとってパッケージとして認識される。
このため、そのディレクトリ内のモジュールをインポートできるようになる。

2. 初期化処理

__init__.pyにコードを記述することで、パッケージがインポートされたときに実行される初期化処理を定義できる。たとえば、共通の設定や定数を初期化するのに使うことができる。

試してみる。
__init__.pyに以下を記述する。

__init__.py
print("my_packageがインポートされた")

my_module.pyに以下を記述する。

my_module.py
def hello():
  print("hello world")

Act05.pyに以下を記述する。

Act05.py
from my_package import my_module
my_module.hello()

以下は出力

my_packageがインポートされた
hello world

3. 名前空間の管理

__init__.pyを使用することで、パッケージ内で公開するモジュールやクラスを制御できる。たとえば、特定のモジュールだけを外部に公開したい場合、__all__リストを使うことができる。

__init__.pyに以下を記述する。

__init__.py
__all__ = ['my_module']  # my_moduleだけを公開

another_module.pyに以下を記述する。

another_module.py
def add(a: int, b: int):
  """
  引数2つを加算する関数
  """
  return a + b

Act05.pyに以下を記述する。

Act05.py
from my_package import my_module, another_module
my_module.hello()
add = another_module.add(10, 20)
print(add)

以下は出力
全然another_moduleのadd関数を使えるやないかい!

hello world
30

本当は使えない想定だった。
少し調べてみたらどうやらfrom my_package import *でインポートしたときは使えなくなるらしい。

Act05.pyを以下の通り修正。

Act05.py
# from my_package import my_module, another_module
from my_package import *
my_module.hello()
add = another_module.add(10, 20)
print(add)

するとanother_moduleが参照できなくなった。めでたしめでたし。

4. 簡易インポート

__init__.pyを使って、パッケージ内のモジュールを簡単にインポートできるようにすることもできる。

my_package.py
from .my_module import hello

これにより、外部からは次のように呼び出せるようになる。

Act05.py
from my_package import hello
hello()

さいごに

個人的には**kwargsが気に入らなかったから、dataclassesPydanticの存在を知れたのはかなりアツかった。

やっぱ知ってるつもりになって勉強しないのはだめだね。反省。

Discussion