このガイドでは、Pythonのプロトタイプデザインパターンの理論と実装、そしてそれを活用することでどのようなメリットがあるのかについて見ていきたいと思います。
オブジェクト指向プログラミング(OOP)パラダイム
デザインパターンは、オブジェクト指向プログラミング(OOP)アーキテクチャに限らず、一般的に存在する問題に対する解決策である。
OOPは、その直感的な性質と実世界を反映できることから、最も一般的なプログラミングパラダイムの1つである。
OOPを通じて、物理的な世界をソフトウェアに抽象化し、自然に観察してコードを書くことができるようになるのです。
各エンティティはオブジェクトとなり、これらのオブジェクトは他のオブジェクトと関連付けられ、システム内にオブジェクトの階層を形成します。
このアプローチは私たちにとって非常に直感的で自然なものですが、現実の世界と同じように、すぐに慌ただしくなってしまうことがあります。
多くの関係、相互作用、結果がある中で、すべてを首尾一貫した形で維持することは困難です。
それが創造であれ、構造であれ、動作であれ、こうしたシステムのスケーリングは非常に厄介なものになり、一歩間違えば、より深く問題に入り込んでしまうことになる。
これが、今日、デザインパターンが適用され、広く利用されている理由である。
ABCライブラリー
OOPパラダイムでは、一般的に抽象クラスが利用されますが、Pythonには組み込みの機能はありません。
この機能を実現するために、ABC (Abstract Base Classes) ライブラリを使用します。
ABCを使うことで、抽象クラスを定義し、それを元にサブクラスを作ることができるようになり、このパターンを実装することができます。
デザインパターン
繰り返しになりますが、デザインパターンは、OOPアーキテクチャにおいてスケーラブルでクリーンな実装を構築するのに役立つ、標準化されたプラクティスや構造です。
また、このパターンは、コードを書くときに従うべき基本的な構造を提供し、パターンの基本的な概念に従う限り、カスタマイズすることができる。
このように、デザインパターンは3つの主要なカテゴリーに分類される。
- このような場合、「Creational Design Patterns」(創造的デザインパターン)は、オブジェクトの創造ロジックを抽象化/隠蔽しながら、オブジェクトの創造を可能にすることに関係する。
- そのため、このパターンでは、オブジェクトがどのように構成されるかを制御するために、継承に依存する。
- オブジェクト間の通信、オブジェクト間のデータ移動の制御、クラス間の動作の分散に重点を置く。
プロトタイプパターン直観
プロトタイプパターンは、基本的な性質を定義したスーパークラスであるプロトタイプオブジェクトをクローンするためのCreational Design Patternである。
当然ながら、サブクラスも基本的な性質は同じで、いくつかの特殊な性質を持つ。
プロトタイプデザインパターンは、通常、クローンを作成する方が新しいオブジェクトを作成するよりも安価な操作であり、作成が長く高価な呼び出しを必要とする場合に適用されます。
これらの呼び出しは、一般的に高価なデータベース操作と結びついていますが、他の高価な処理である可能性もあります。
>。
これをシミュレートするために – 私たちは、オブジェクトの作成に高価なプロセスコールを、全体の3秒続くモックします。
そして、Prototypeデザインパターンを使って、この制限を回避しながら、新しいオブジェクトを作成します。
この機能を実現するために、2つのクラスを使用する。
- プロトタイプ。プロトタイプ:スーパークラスで、クローンが
Prototype
クラスをコピーしたときに持つ、基本的な必須属性とメソッドをすべて含んでいます。また、プロトタイプは抽象的なclone()
メソッドを持っており、これはすべてのサブクラスで実装されなければなりません。 - 具体的なクラス(es): プロトタイプを作成したら、それを基に具象クラスを定義していきます。具象クラスは独自の属性やメソッドを持つことができますが、常にオリジナルのプロトタイプの属性と
clone()
の上書き版を持つことになります。
Pythonによるプロトタイプパターンの実装
架空のビデオゲームのために、いくつかの NPC タイプを作成することにします。
Shopkeeper
、Warrior
、Mage
です。
それぞれは NPC であり、共通のスーパークラスですが、異なる属性を持つことになります。
店主」は「カリスマ」を持っているので、物々交換がうまくできます。
「魔道士」は「戦士」のように「スタミナ」ではなく「マナ」を持っています。
プロトタイプクラスは一般的なNPCを意味し、そこから具象クラスを実装することができます。
プロトタイプのコンストラクタと具象クラスの両方で遅延を発生させ、コンストラクタの高価な呼び出しをモック化します – コードの実行を数秒遅らせ、新しいオブジェクトの作成を非常に高価な処理にします。
最後に、そうしないとクラスが合理的な方法で使用できなくなるため、Prototypeパターンを活用してこの問題を軽減し、パフォーマンスを回復します。
プロトタイプクラスの定義
まず、NPCのスーパークラスであるプロトタイプクラスから始めましょう。
その clone()
メソッドは空ですが、そのサブクラスはそれを実装します。
当然ながら、サブクラスの基本属性もすべて含まれます。
すべてのサブクラスが clone()
メソッドを必ず実装するようにしたいので、 @abstractmethod
としてマークされています。
このアノテーションは ABC ライブラリに由来しており、抽象メソッドは実装を提供せず、サブクラスによって実装されなければならないのです。
from abc import ABC, abstractmethod
import time
# Class Creation
class Prototype(ABC):
# Constructor:
def __init__(self):
# Mocking an expensive call
time.sleep(3)
# Base attributes
self.height = None
self.age = None
self.defense = None
self.attack = None
# Clone Method:
@abstractmethod
def clone(self):
pass
コンクリートクラス
それでは、Prototype
をベースにした具象クラスを定義してみましょう。
ここでは、 clone()
メソッドをオーバーライドし、実際にその実装を提供します。
オブジェクトをコピーするために、Python に組み込まれている copy
ライブラリを使用します。
このライブラリの copy()
メソッドはオブジェクトのシャローコピーを行い、 deepcopy()
はオブジェクトのディープコピーを作成します。
オブジェクトの構造によっては、どちらか一方を選択することになるでしょう。
浅いコピーでは、辞書、リスト、セット、その他のクラスなど、非プリミティブなフィールドへの参照をコピーするだけです。
ディープコピーは、同じデータで新しいインスタンスを作成します。
つまり、浅いコピーでは、複数のオブジェクトが同じ参照を通じて同じフィールドを共有する可能性があるため、1つのクラスではなく、複数のクラスのフィールドを変更することになるかもしれないのです。
ということです。
浅いコピーは、非プリミティブ型に対して新しいものをインスタンス化しないので、より安価な操作です。
一般的に、これらの型はインスタンス化するのに高価ではないかもしれないので、あまり得るものはないでしょう。
しかし、もしあなたのクラスが高価なフィールドを持つ場合、つまりインスタンス化に時間のかかるフィールドを持つ場合、メモリ内で同じオブジェクトを共有する代償として、浅いコピーは深いコピーよりもはるかに性能が良くなります。
というわけで、サブクラスを定義してみましょう。
コンストラクタから値を受け取る代わりに、これらの具象クラスのすべてのインスタンスに対していくつかの基本値を提供します。
from prototype import Prototype
import copy
import time
class Shopkeeper(Prototype):
def __init__(self, height, age, defense, attack):
super().__init__()
# Mock expensive call
time.sleep(3)
self.height = height
self.age = age
self.defense = defennse
self.attack = attack
# Subclass-specific Attribute
self.charisma = 30
# Overwritting Cloning Method:
def clone(self):
return copy.deepcopy(self)
NPCの Warrior
には、もうひとつ基本値があります。
from prototype import Prototype
import copy
import time
class Warrior(Prototype):
def __init__(self, height, age, defense, attack):
# Call superclass constructor, time.sleep() and assign base values
# Concrete class attribute
self.stamina = 60
# Overwritting Cloning Method
def clone(self):
return copy.deepcopy(self)
そして最後に、Mage
です。
from prototype import Prototype
import copy
import time
class Mage(Prototype):
def __init__(self, height, age, defense, attack):
# Call superclass constructor, time.sleep() and assign base values
self.mana = 100
# Overwritting Cloning Method
def clone(self):
return copy.deepcopy(self)
Pythonのプロトタイプデザインパターンのテスト
それでは、このパターンをテストしてみましょう。
まず、Shopkeeper
のインスタンスをそのまま作成し、かかった時間をメモしておく。
print('Starting to create a Shopkeeper NPC: ', datetime.datetime.now().time())
shopkeeper = Shopkeeper(180, 22, 5, 8)
print('Finished creating a Shopkeeper NPC: ', datetime.datetime.now().time())
print('Attributes: ' + ', '.join("%s: %s" % item for item in vars(shopkeeper).items()))
プロトタイプが3秒、ショップキーパーが3秒待ちますが、最終的には6秒後にオブジェクトが作成されます。
Starting to create a Shopkeeper NPC: 15:57:40.852336
Finished creating a Shopkeeper NPC: 15:57:46.859203
Attributes: height: 180, age: 22, defense: 5, attack: 8, charisma: 30
予想通り、これは非常に遅い処理です。
もし別のshopkeeperが必要だったらどうなるでしょうか?もっといいのは、もう5人の店番が必要になったらどうするか?5人の店員を含むギルドのインスタンスを作成してみましょう。
print('Instantiating trader guild at: ', datetime.datetime.now().time())
for i in range(5):
shopkeeper = Shopkeeper(180, 22, 5, 8)
print(f'Finished creating a Shopkeeper NPC {i} at: ', datetime.datetime.now().time())
print('Finished instantiating trader guild at: ', datetime.datetime.now().time())
Instantiating trader guild at: 16:15:14.353285
Finished creating a Shopkeeper NPC 0 at: 16:15:20.360971
Finished creating a Shopkeeper NPC 1 at: 16:15:26.365997
Finished creating a Shopkeeper NPC 2 at: 16:15:32.370327
Finished creating a Shopkeeper NPC 3 at: 16:15:38.378361
Finished creating a Shopkeeper NPC 4 at: 16:15:44.383375
Finished instantiating trader guild at: 16:15:44.383674
ショップキーパーはすべて同じパターンなので、クローンを作ることができます。
print('Instantiating trader guild at: ', datetime.datetime.now().time())
shopkeeper_template = Shopkeeper(180, 22, 5, 8)
for i in range(5):
shopkeeper_clone = shopkeeper_template.clone()
print(f'Finished creating a Shopkeeper clone {i} at: ', datetime.datetime.now().time())
print('Finished instantiating trader guild at: ', datetime.datetime.now().time())
という結果になる。
Instantiating trader guild at: 16:19:24.965780
Finished creating a Shopkeeper clone 0 at: 16:19:30.975445
Finished creating a Shopkeeper clone 1 at: 16:19:30.975763
Finished creating a Shopkeeper clone 2 at: 16:19:30.975911
Finished creating a Shopkeeper clone 3 at: 16:19:30.976058
Finished creating a Shopkeeper clone 4 at: 16:19:30.976132
Finished instantiating trader guild at: 16:19:30.976529
これで、最初のテンプレート Shopkeeper
がインスタンス化されるだけで、ナノ秒単位でクローンを作成することができるようになりました。
5つのクローンを作成するのに要した時間は、わずか0.001秒です。
これで、異なるNPCの集団全体を問題なく作成できるようになりました。
print('Instantiating 1000 NPCs: ', datetime.datetime.now().time())
shopkeeper_template = Shopkeeper(180, 22, 5, 8)
warrior_template = Warrior(185, 22, 4, 21)
mage_template = Mage(172, 65, 8, 15)
for i in range(333):
shopkeeper_clone = shopkeeper_template.clone()
warrior_clone = warrior_template.clone()
mage_clone = mage_template.clone()
print(f'Finished creating NPC trio clone {i} at: ', datetime.datetime.now().time())
print('Finished instantiating NPC population at: ', datetime.datetime.now().time())
その結果、~1000個のコピーができ、その全てのコピーにかかる時間は、合計で~0.1秒でした。
Instantiating 1000 NPCs: 16:27:14.566635
Finished creating NPC trio clone 0 at: 16:27:32.591992
...
Finished creating NPC trrio clone 331 at: 16:27:32.625681
Finished creating NPC trio clone 332 at: 16:27:32.625764
Finished instantiating NPC population at: 16:27:32.625794
結論
複雑なオブジェクトを作成する場合、特に高価なデータベースを呼び出す必要がある場合は、時間がかかるものです。
このガイドでは、PythonでPrototypeデザインパターンを実装する方法を見て、新しいインスタンスを作成するのではなく、高価なインスタンスをクローンするために使用すると、パフォーマンスが大幅に向上することを実証しました。