HOWTO · Python

Python クラス ファクトリ

このチュートリアルでは、Python のクラス ファクトリについて説明します。

このページの内容

クラス ファクトリは、クラス オブジェクトを作成して返す呼び出し可能オブジェクトです。ファクトリを呼び出してもインスタンスは作成されず、後からインスタンス化できるクラスが得られます。この違いは、実行中にのみ利用できる設定によってクラス定義が決まる場合に役立ちます。

Python プログラムの多くでは、モジュール レベルで通常のクラスを定義するのが適切です。クラスが固定され、予測可能な一つの方法で使われる場合は、その方が明快です。選択した既定値、生成した名前、小さな設定から組み立てたメソッドなど、呼び出し元ごとに調整したクラスが必要なときにファクトリを使います。ファクトリの役割は絞り込みましょう。無関係な形のクラスを多数返すと、型の理解とテストが難しくなります。

このチュートリアルでは、入れ子の class 定義と Python の三引数形式の type() を示します。どちらの方法もクラス オブジェクトを返します。通常は入れ子のクラスの方が読みやすく、クラス名、基底クラス、名前空間を動的に指定する必要がある場合は type() が便利です。

Python でクラス ファクトリを作成する方法

クラス ファクトリの通常の使い方では、呼び出しが二段階になります。まずファクトリを呼び出し、返されたクラスを保存します。次に、その返されたクラスを呼び出してオブジェクトを作成します。たとえば、Ball = create_ball_class() はクラスを Ball に代入し、Ball("green") は Ball のインスタンスを生成します。

class キーワードは、Python が関数本体を実行するときにクラスを作成します。type(name, bases, namespace) 形式もその時点でクラスを作成します。name は文字列、bases は親クラスのタプル、namespace は属性名を値または関数に対応付けるものです。名前空間に置いた関数は、インスタンスを通じて参照されるとインスタンス メソッドになります。

返されるクラスが意外なものにならないよう、名前とコンストラクタ引数を選びます。ファクトリを呼ぶたびに別のクラスを作る場合、定義が同じに見えても、別々の呼び出しから得たインスタンスは同じクラス識別子を共有しません。これは isinstance() チェック、シリアライズ、デバッグに影響することがあります。安定した公開型が一つ必要なら、通常のクラス構文で一度だけ定義し、設定はそのインスタンスに渡します。

class キーワードを使用して Python でクラス ファクトリを作成する

関数の中にクラス定義を置き、クラス名を返します。入れ子のクラスはファクトリのスコープにある値を使えるため、小さな設定を選ぶ用途で実用的です。この例では、ファクトリが Ball を返し、インスタンスはコンストラクタに渡された色を保持します。

def create_ball_class():
    class Ball:
        def __init__(self, color):
            self.color = color

        def get_color(self):
            return self.color

    return Ball


Ball = create_ball_class()
ball = Ball("green")
print(ball.get_color())

返されたクラスは他のクラスと同様に振る舞うため、そのメソッドと属性をインスタンスで使用できます。

green

設定を受け取るファクトリでは、クラスを返す前、または生成したコンストラクタ内で設定を検証します。検証により、使えないオブジェクトが流通するのではなく、不正な入力を文書化された境界で失敗させられます。次の境界ケースでは空の色を拒否し、例外を処理してプログラムの出力を決定的にしています。

def create_ball_class(default_color):
    class Ball:
        def __init__(self, color=default_color):
            if not color:
                raise ValueError("color must not be empty")
            self.color = color

    return Ball


Ball = create_ball_class("")
try:
    Ball()
except ValueError as error:
    print(error)

上記のコードは、次の出力を提供します。

color must not be empty

このファクトリは default_color をクロージャとして保持するため、返される各クラスは、その呼び出しで渡された値を記憶します。小さく不変な設定には便利です。共有状態を意図していない限り、可変の共有オブジェクトをクロージャに閉じ込めるのは避けましょう。そうしないと、あるインスタンスが別のインスタンスに予期せず影響する可能性があります。設定がすべてのインスタンスに属し、確認しやすくしたい場合は、クラス属性として代入する方法もあります。

三引数の組み込み関数 type() を使用して Python でクラス ファクトリを作成する

組み込み関数 type() は、その三引数形式で実行時に直接クラスを構築できます。形式は type(name, bases, namespace) です。名前空間の辞書にはクラス属性を入れます。たとえば "get_color" に格納した関数は、インスタンス メソッドとして使われます。これらの要素がすでにデータになっている場合、直接呼び出す方法は簡潔ですが、その横で定義したヘルパー関数はモジュール レベルの名前として残ります。

def init(self, color):
    self.color = color


def get_color(self):
    return self.color


Ball = type(
    "Ball",
    (object,),
    {"__init__": init, "get_color": get_color},
)
ball = Ball("green")
print(ball.get_color())
green

ヘルパー関数が実装詳細であり、ファクトリが設定済みの実行時クラスを返す必要がある場合は、それらをファクトリ内に閉じ込めます。次のコードは同じ観測可能な動作をしますが、init と get_color はモジュール名前空間に追加されず、ファクトリのスコープに限定されます。

def create_ball_class(class_name):
    def init(self, color):
        self.color = color

    def get_color(self):
        return self.color

    return type(
        class_name,
        (object,),
        {"__init__": init, "get_color": get_color},
    )


Ball = create_ball_class("Ball")
ball = Ball("green")
print(Ball.__name__)
print(ball.get_color())

上記のコードは、次の出力を提供します。

Ball
green

この方法は、名前、継承タプル、属性の集合が本当にデータ駆動である場合に使います。静的なクラスには、通常の class 構文の方が読みやすく、型チェッカーの支援とトレースバックも優れています。どちらの方法を選んでも、ファクトリが値を返したことだけを確認するのではなく、インスタンスを作成し、呼び出し元が必要とする動作を実行して、返されたクラスをテストしてください。