【Python】ファクトリーメソッドと分岐の無くし方【static・classメソッド】

Python
スポンサーリンク

本稿では、クラスのインスタンスを生成する処理をまとめた「ファクトリーメソッド」の実装方法について考えていきます。

スポンサーリンク

1. クラスのインスタンスを生成する

まず、最も単純な方法を見てみましょう。

次のように、図形を表す3つのクラスがあるとします。

class Triangle():
    def __init__(self, width=0, height=0):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height / 2
    
class Rectangle():
    def __init__(self, width=0, height=0):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height
    
class Diamond():
    def __init__(self, vertical_diagonal=0, horizontal_diagonal=0):
        self.vertical_diagonal = vertical_diagonal
        self.horizontal_diagonal = horizontal_diagonal

    def area(self):
        return self.vertical_diagonal * self.horizontal_diagonal / 2

これらのクラスのインスタンスを生成するためには、次のようにすると良いでしょう。

まず、どのクラスを選ぶのか?その判断材料となるものが必要です。

そのために、列挙体ShapeTypeを用意して、各クラスの「型」に対応する値を定義します。

from enum import Enum

class ShapeType(Enum):
    TRIANGLE = 1
    RECTANGLE = 2
    DIAMOND = 3

列挙体で定義した値毎に、対応するクラスのインスタンスを生成します。

今回、生成したオブジェクトは、リストに格納しておきます。

if __name__ == '__main__':
    shape_type_list  =[ShapeType.TRIANGLE, ShapeType.RECTANGLE, ShapeType.DIAMOND]
    shape_object_list = []
    for shape_type in shape_type_list:
        if shape_type == ShapeType.TRIANGLE:
            shape_object_list.append(Triangle())
        if shape_type == ShapeType.RECTANGLE:
            shape_object_list.append(Rectangle())
        if shape_type == ShapeType.DIAMOND:
            shape_object_list.append(Diamond())
スポンサーリンク

2. ファクトリーメソッドを使う

先ほどの例では、クラスを生成するロジックは、main部分に書きました。しかし、「ShapeTypeで定義した値を受け取り、図形を表すクラスのインスタンスを生成する」という1つの機能が成り立っているので、関数化したいところです。そこで登場するのが、ファクトリーメソッドです。

# Factoryクラス
class ShapeCreator:
    # Factoryメソッド
    @staticmethod
    def create_shape(shape_type : ShapeType):
        if shape_type == ShapeType.TRIANGLE:
            created_shape = Triangle()
        if shape_type == ShapeType.RECTANGLE:
            created_shape = Rectangle()
        if shape_type == ShapeType.DIAMOND:
            created_shape = Diamond()
        return created_shape

if __name__ == '__main__':
    shape_type_list  =[ShapeType.TRIANGLE, ShapeType.RECTANGLE, ShapeType.DIAMOND]
    shape_object_list = []
    for shape_type in shape_type_list:
        created_shape = ShapeCreator.create_shape(shape_type)
        shape_object_list.append(created_shape)

上のメソッドは、@staticmethodというデコレータで修飾しています。修飾されたメソッド、staticメソッドは、引数にクラス自身を示すselfやclsを必要としません。

これで、オブジェクト生成を一手に担うファクトリーメソッドが完成しました。しかし、もう一つ問題が残っています。それは、main部分に合った分岐が、ファクトリーメソッドに移動しただけであることです。そこで、分岐を無くす工夫をしてみます。

スポンサーリンク

3. 辞書型で処理の切り替えをシンプルにする

ファクトリーメソッドを持つクラスには、データを持たせることができます。そこで、予め生成したいオブジェクトを用意してみます。ここでは、辞書型のキーにShapeTypeを、値に生成したいオブジェクトをセットします。

これにより、ファクトリー内では、生成するオブジェクトを選択するだけでよいです。

先ほどのファクトリーメソッドと異なり、処理毎にわざわざ分岐を書く必要がありません。

また、今回はクラスの持つデータを参照する必要があるので、デコレータは@classmethodを付けます。

class ShapeCreator:
    shape_dictionary = {ShapeType.TRIANGLE:Triangle(), ShapeType.RECTANGLE:Rectangle(), ShapeType.DIAMOND:Diamond()}
    @classmethod
    def create_shape(cls, shape_type : ShapeType):
        created_shape = cls.shape_dictionary[shape_type]
        return created_shape

ここで注意点があります。このままでは、辞書型で用意しておいたオブジェクトと、ファクトリーメソッドが返すオブジェクトは同一のものとなってしまいます。そこで、copyモジュールをインポートして、コピーを返すように修正します。

ここで注意したいのが、今回コピーしたいのはクラスインスタンスです。この場合、浅いコピーと深いコピーのどちらを使うかが問題です。Pythonのリファレンスでは、2つのコピーの違いについて、次のように述べています。

浅い (shallow) コピーと深い (deep) コピーの違いが関係するのは、複合オブジェクト (リストやクラスインスタンスのような他のオブジェクトを含むオブジェクト) だけです:

  • 浅いコピー (shallow copy) は新たな複合オブジェクトを作成し、その後 (可能な限り) 元のオブジェクト中に見つかったオブジェクトに対する 参照 を挿入します。
  • 深いコピー (deep copy) は新たな複合オブジェクトを作成し、その後元のオブジェクト中に見つかったオブジェクトの コピー を挿入します。

メモリ領域をどれだけ消費するか、ということを考えなければ、とりあえずdeep copyを使っておけば良さそうです。文字通り、メソッドが返すオブジェクトは、辞書型で用意しておいたオブジェクトとは完全に異なる(オブジェクトのidと、オブジェクトの持つデータのidが異なる)オブジェクトになるということです。

# Factoryクラス
class ShapeCreator:
    shape_dictionary = {ShapeType.TRIANGLE:Triangle(5, 10), ShapeType.RECTANGLE:Rectangle(5, 10), ShapeType.DIAMOND:Diamond(5, 10)}
    # Factoryメソッド
    @classmethod
    def create_shape(self, shape_type : ShapeType):
        # コピーを作って返す
        created_shape = copy.deepcopy(self.shape_dictionary[shape_type])
        return created_shape
    
if __name__ == '__main__':
    shape_type_list  =[ShapeType.TRIANGLE, ShapeType.RECTANGLE, ShapeType.DIAMOND]
    shape_object_list = []       

    # クラスは1つだけあれば良いので、オブジェクトとして生成する
    shape_creator = ShapeCreator
    for shape_type in shape_type_list:
        created_shape = shape_creator.create_shape(shape_type)
        shape_object_list.append(created_shape)    

まとめ

  • ファクトリーメソッドでオブジェクトの生成を一手に担う。
  • ファクトリーメソッドは、クラスの持つ定数を参照しない場合は@staticmethodを、定数を参照する場合は@classmethodを付ける。
  • 予めオブジェクトを生成して、辞書型で管理し、そのコピーを返すことで分岐を無くせる。

コメント