見出し画像

DP.11:共有できるオブジェクトにして省リソース化する。- Flyweight -【Python】

【1】Flyweightパターン概要

Flyweightパターンは、『状態や変動パラメータなどをもたないオブジェクト(※)を用意して、複数個所でそのオブジェクトを共有して使用できるようにする』書き方。(※変動するデータは分離して記述する)

■イメージ図:pygameに「★」の画像を表示する

画像1

【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()
   
画像3

#実行結果
pygame 2.0.1 (SDL 2.0.14, Python 3.9.4)
Hello from the pygame community. https://www.pygame.org/contribute.html
load img
load img
[<__main__.ShapeImage object at 0x00000245799370A0>, <__main__.ShapeImage object at 0x00000245799371F0>]
[[100, 100], [420, 365]]
load img
[<__main__.ShapeImage object at 0x00000245799370A0>, <__main__.ShapeImage object at 0x00000245799371F0>, <__main__.ShapeImage object at 0x0000024579937160>]
[[100, 100], [420, 365], [155, 142]]
load img
[<__main__.ShapeImage object at 0x00000245799370A0>, <__main__.ShapeImage object at 0x00000245799371F0>, <__main__.ShapeImage object at 0x0000024579937160>, <__main__.ShapeImage object at 0x0000024579937190>]
[[100, 100], [420, 365], [155, 142], [427, 483]]

画像3

▲このままでは、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インスタンスを作成する。

obj = ShapeImage("sample.jpg")

こうすると、「__new__()」の「引数:shape_type」に「sample.jpg」という文字列が設定されて「__new__()」がコールされる。
この後、次の処理

obj = cls.pool.get(shape_type, None)

で「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()
画像4

#実行結果
pygame 2.0.1 (SDL 2.0.14, Python 3.9.4)
Hello from the pygame community. https://www.pygame.org/contribute.html
[<__main__.ShapeImage object at 0x00000282437A0580>, <__main__.ShapeImage object at 0x00000282437A0580>]
[[100, 100], [367, 335]]
[<__main__.ShapeImage object at 0x00000282437A0580>, <__main__.ShapeImage object at 0x00000282437A0580>, <__main__.ShapeImage object at 0x00000282437A0580>]
[[100, 100], [367, 335], [496, 263]] 0282437A0580>]
[<__main__.ShapeImage object at 0x00000282437A0580>, <__main__.ShapeImage object at 0x00000282437A0580>, <__main__.ShapeImage object at 0x00000282437A0580>, <__main__.ShapeImage object at 0x00000282437A0580>]
[[100, 100], [367, 335], [496, 263], [669, 71]]
the end

画像5

▲Flyweightを適用して、画像ファイルを1回だけ読み込んでオブジェクトに格納しておく。描画時はそのオブジェクトを共有して使用することでメモリの使用量を抑えることができている。

もっと応援したいなと思っていただけた場合、よろしければサポートをおねがいします。いただいたサポートは活動費に使わせていただきます。