DP.11:共有できるオブジェクトにして省リソース化する。- Flyweight -【Python】
【1】Flyweightパターン概要
Flyweightパターンは、『状態や変動パラメータなどをもたないオブジェクト(※)を用意して、複数個所でそのオブジェクトを共有して使用できるようにする』書き方。(※変動するデータは分離して記述する)
■イメージ図:pygameに「★」の画像を表示する
【2】例:ウィンドウ上に画像を表示する(pygame使用)
例としてpygameで作成したウィンドウ上に画像を表示するプログラムを考えてみる。(※インストールなどのセットアップは以下参照)
■「a」キーを押すと表示している画像が増える(Flyweight未適用版)
import pygame
import random
class ShapeImage:
def __init__(self,shape_type):
print("load img")
# 画像ロードとサイズ調整と透過処理
self.img = pygame.image.load(shape_type).convert()
self.img = pygame.transform.scale(self.img,(40,40))
colorkey = self.img.get_at((0,0))
self.img.set_colorkey(colorkey, pygame.RLEACCEL)
# 画像を表示
def draw(self,screen,x,y):
screen.blit(self.img,(x,y))
if __name__ == '__main__':
rnd = random.Random()
img_file = "small_star7_yellow.png"
pygame.init()
clock = pygame.time.Clock()
FPS = 60
window_size = (800, 600)
screen = pygame.display.set_mode(window_size)
obj = ShapeImage(img_file) # オブジェクトを作成
objs = []
objs.append(obj)
position_map=[[100,100]] #オブジェクトの描画位置
# 以下フレーム単位でのループ
quits_flg = False
while not quits_flg:
# フレームの最初にで発火されているイベントをチェック
for event in pygame.event.get():
# ウィンドウの[x]で閉じたときのイベント
if event.type == pygame.QUIT:
quits_flg = True
if event.type == pygame.KEYDOWN:
# キーイベント
pressed = pygame.key.get_pressed()
if pressed[pygame.K_a]:
objs.append(ShapeImage(img_file)) # オブジェクト生成
position_map.append([rnd.randint(0,800),rnd.randint(0,600)]) # 表示位置を適当に設定
print(objs)
print(position_map)
# このあたりで描画物の状態更新計算
# screenオブジェクトに書き込み
screen.fill((0,0,0))
for obj,position in zip (objs,position_map):
obj.draw(screen,position[0],position[1])
# 表示(ディスプレイ表示更新)
pygame.display.flip()
# フレーム調整
clock.tick(FPS)
# ループ抜け
print("the end")
pygame.quit()
▲このままでは、aキーを押してオブジェクトを生成するたびに画像ファイルを読み込んで、メモリに積んでしまう。
【3】特殊メソッド「__new__()」で新しいインスタンスを作る
Flyweightパターンを実現するために、特殊メソッド「__new__()」を利用してインスタンスオブジェクトを生成する。
「__new__()」は「__init__()」の前にコールされるもので、自己参照(オブジェクト名:cls)を使うことができる。
これを利用して、次のような感じでオブジェクトの生成状況をチェックして戻り値を制御できる。
class ShapeImage:
pool = dict() # 生成インスタンスを管理するdictオブジェクト
def __new__(cls, shape_type):
obj = cls.pool.get(shape_type,None)
# オブジェクトが登録されてないなら、生成する
if not obj:
obj = object.__new__(cls) # リターンさせるインスタンスを生成する
cls.pool[shape_type] = obj # 生成したインスタンスを登録
obj.shape_type = shape_type # メンバ変数にも値を設定
#以下画像読み込み処理
... ...
return obj # オブジェクトインスタンスが戻り値
ざっくりいうと、「__new__()の挙動を上書きしている」ということ。
※補足:動作概要:
「cls」というのは、「ShapeImage クラス」のことを指すようになっている。まずShapeImageインスタンスを作成する。
こうすると、「__new__()」の「引数:shape_type」に「sample.jpg」という文字列が設定されて「__new__()」がコールされる。
この後、次の処理
で「ShapeImage.poolオブジェクト」にオブジェクトが登録されているかをチェックしている。
■dict.get
あとはdict.getの戻り値を使って新たにオブジェクトを生成するかどうかを判断することになる。
↓ これらを踏まえて、元のコードにFlyweightを適用してみる。
■Flyweightを適用した書き方
class ShapeImage:
pool = dict()
def __new__(cls, shape_type):
obj = cls.pool.get(shape_type,None)
if not obj:
obj = object.__new__(cls)
cls.pool[shape_type] = obj
obj.shape_type = shape_type
obj.img = pygame.image.load(shape_type).convert()
obj.img = pygame.transform.scale(obj.img,(40,40))
colorkey = obj.img.get_at((0,0))
obj.img.set_colorkey(colorkey, pygame.RLEACCEL)
return obj
def draw(self,screen,x,y):
screen.blit(self.img,(x,y))
▲指定の画像ファイルが読み込み済み(オブジェクト生成済み)ならオブジェクト生成や読み込みをしないですむ。
【4】全体コード
import pygame
import random
class ShapeImage:
pool = dict()
def __new__(cls, shape_type):
obj = cls.pool.get(shape_type,None)
if not obj:
obj = object.__new__(cls)
cls.pool[shape_type] = obj
obj.shape_type = shape_type
obj.img = pygame.image.load(shape_type).convert()
obj.img = pygame.transform.scale(obj.img,(40,40))
colorkey = obj.img.get_at((0,0))
obj.img.set_colorkey(colorkey, pygame.RLEACCEL)
return obj
def draw(self,screen,x,y):
screen.blit(self.img,(x,y))
if __name__ == '__main__':
rnd = random.Random()
img_file = "small_star7_yellow.png"
pygame.init()
clock = pygame.time.Clock()
FPS = 60
window_size = (800, 600)
screen = pygame.display.set_mode(window_size)
obj = ShapeImage(img_file)
objs = []
objs.append(obj)
position_map=[[100,100]] #オブジェクトの描画位置
# 以下フレーム単位でのループ
quits_flg = False
while not quits_flg:
# フレームの最初にで発火されているイベントをチェック
for event in pygame.event.get():
# ウィンドウの[x]で閉じたときのイベント
if event.type == pygame.QUIT:
quits_flg = True
if event.type == pygame.KEYDOWN:
# キーイベント
pressed = pygame.key.get_pressed()
if pressed[pygame.K_a]:
objs.append(ShapeImage(img_file))
position_map.append([rnd.randint(0,800),rnd.randint(0,600)])
print(objs)
print(position_map)
# このあたりで描画物の状態更新計算
# screenオブジェクトに書き込み
screen.fill((0,0,0))
for obj,position in zip (objs,position_map):
obj.draw(screen,position[0],position[1])
# 表示(ディスプレイ表示更新)
pygame.display.flip()
# フレーム調整
clock.tick(FPS)
# ループ抜け
print("the end")
pygame.quit()
▲Flyweightを適用して、画像ファイルを1回だけ読み込んでオブジェクトに格納しておく。描画時はそのオブジェクトを共有して使用することでメモリの使用量を抑えることができている。