Design Patterns @ Python – Flyweight Pattern 享元(蠅量)模式筆記

設計模式–享元(蠅量)模式(Flyweight Pattern)

前言:

※ 什麼是享元模式?

  • Flyweight Pattern 在中文維基百科是譯為享元模式
  • 如程式中有大量的固定類的相似實例(Instance)物件,而該類物件中有不變的及可變的部分,就可把 可變的部分抽離出來,將不變的部分則共享
  • Flyweight Pattern 就是透過這樣共享物件的設計,來 節省程式執行時的記憶體開銷
  • Flyweight Pattern 適用於 程式中有大量類似的 Instance 物件,其物件中又具備很多相似或不變的值或屬性,當程式有這樣特徵,就可藉由 Flyweight Factory 來設計物件的共享,並以此方式來節省程式執行時的記憶體開銷

※ Example - 1

import random
import string
import sys

class User:
    def __init__(self, name):
        self.name = name

class User2:
    # 創建一個共享的字串列表容器
    strings = []

    def __init__(self, full_name):
        def get_or_add(s):
            '''檢查及取用名稱字串中的文字區塊,如不存在則加入字串列表容器中,如存在則返回該文字區塊於名稱字串的位置 Index'''
            if s in self.strings:
                return self.strings.index(s)
            else:
                self.strings.append(s)
                return len(self.strings)-1
        # 將 Initial 的名稱字串分割成文字區塊,並透過 get_or_add() 方法取得文字區塊於名稱字串的位置 Index 的集合
        self.names = [get_or_add(x) for x in full_name.split(' ')]

    def __str__(self):
        '''從文字區塊於名稱字串的位置 Index 的集合還原回原先 Initial 的名稱字串'''
        return ' '.join([self.strings[x] for x in self.names])

def random_string():
    chars = string.ascii_lowercase
    # 產生 8 個隨機字元的字串
    return ''.join([random.choice(chars) for x in range(8)])


if __name__ == '__main__':
    # 使用 Flyweight Pattern
    u2 = User2('Jim Jones')
    u3 = User2('Frank Jones')
    print(u2.names) # [0, 1]
    print(u3.names) # [2, 1]
    print(User2.strings) # ['Jim', 'Jones', 'Frank'] 其中 'Jones' 重複,所以後面的就直接共享使用了前面創建的,從而減少儲存物件的記憶位址數量
# Execute Result:
[0, 1]
[2, 1]
['Jim', 'Jones', 'Frank']

※ Example - 2

class FormattedText:
    '''格式化字串,字串中如有要設定成大寫的字元,就用 capitalize function 將要設成大寫的字元
       範圍的字元其大寫狀態的布林值設成 True,而非落於該大寫字元範圍的字元的大寫狀態的布林值就
       吃預設值 False
    '''
    def __init__(self, plain_text):
        self.plain_text = plain_text
        # 初始化時先將所有大寫的布林值設成 False
        self.caps = [False] * len(plain_text)

    def capitalize(self, start, end):
        '''將指定範圍的字元設成大寫'''
        for i in range(start, end):
            self.caps[i] = True

    def __str__(self):
        '''複寫 __str__ method,將字串中的字元依照其大寫狀態的布林值來決定是否要設成大寫'''
        result = []
        for i in range(len(self.plain_text)):
            c = self.plain_text[i]
            result.append(c.upper() if self.caps[i] else c)
        return ''.join(result)


class BetterFormattedText:
    def __init__(self, plain_text):
        self.plain_text = plain_text
        self.formatting = []

    class TextRange:
        '''利用內建的TextRange class來儲存所有字元的狀態,包含是否要設成大寫、是否要設成粗體等等 <-- 共享狀態'''
        def __init__(self, start, end, capitalize=False, bold=False, italic=False):
            self.end = end
            self.bold = bold
            self.capitalize = capitalize
            self.italic = italic
            self.start = start

        def covers(self, position):
            return self.start <= position <= self.end

    def get_range(self, start, end):
        '''get_range 就相當於 Flyweight Factory,用來產生 TextRange Instance Object,並將其加入到 formatting list 中'''
        range = self.TextRange(start, end)
        self.formatting.append(range)
        return range

    def __str__(self):
        result = []
        for i in range(len(self.plain_text)):
            c = self.plain_text[i]
            for r in self.formatting:
                if r.covers(i) and r.capitalize:
                    c = c.upper()
            result.append(c)
        return ''.join(result)


if __name__ == '__main__':
    ft = FormattedText('This is a brave new world')
    ft.capitalize(10, 15)
    print(ft)

    bft = BetterFormattedText('This is a brave new world')
    # Flyweight Pattern 讓架構變得複雜,以這例子而言,User 很難知道要這樣來設定大寫字元的範圍
    bft.get_range(16, 19).capitalize = True
    print(bft)
# Execute Result:
This is a BRAVE new world
This is a brave NEW world

※ Example - 3

import unittest

# 這個範例和老師講的第一個範例相似,只是從字元大寫變成單詞大寫
class Sentence:
    def __init__(self, plain_text):
        self.words = plain_text.split(' ')
        self.tokens = {}

    def __getitem__(self, item):
        wt = self.WordToken()
        self.tokens[item] = wt
        return self.tokens[item]

    class WordToken:
        '''Flyweight Factory'''
        def __init__(self, capitalize=False):
            self.capitalize = capitalize

    def __str__(self):
        result = []
        for i in range(len(self.words)):
            w = self.words[i]
            if i in self.tokens and self.tokens[i].capitalize:
                w = w.upper()
            result.append(w)
        return ' '.join(result)


# 單元測試
class Evaluate(unittest.TestCase):
    def test_exercise(self):
        s = Sentence('alpha beta gamma')
        s[1].capitalize = True
        self.assertEqual(str(s), 'alpha BETA gamma')

# unittest.main() 如果在 jupyter notebook 中沒有加 argv=[''], exit=False 會出現錯誤 --> https://tinyurl.com/yn3bfrln
unittest.main(argv=[''], verbosity=2, exit=False)
# Execute Result:
test_exercise (__main__.Evaluate.test_exercise) ... C:\Users\chriswei\AppData\Roaming\Python\Python312\site-packages\jupyter_client\session.py:200: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
  return datetime.utcnow().replace(tzinfo=utc)  # noqa
ok

----------------------------------------------------------------------
Ran 1 test in 0.002s

OK

※ 享元模式的優缺點及應用場景

(1). 優點
  1. 減少程式執行時的記憶體消耗,從而提高程式的執行效能
  2. 使得程式中的物件,藉由 Flyweight Factory 而變得更具擴展性
(2). 缺點
  1. 程式中物件被切分得更細,而使得程式變得更複雜,更不 pythonic
  2. 在多線程環境上應用困難
(3). 應用場景
  1. 大量的繁複的文字處裡,例如戶籍資料的處理
  2. 圖形應用,例如:大量圖形的元素渲染
  3. 遊戲開發,例如:多人扮演遊戲中的角色
1個讚