通俗 Python 設計模式——代理模式

平田發表於2017-09-16

今天來說一說代理模式。

代理模式顧名思義,是對資源進行代理訪問的一種模式,這裡的資源是泛指在程式中會使用到的資料、操作、過程、物件等等。當然,針對不同的資源,代理進行的操作不盡相同,根據前人的總結,有四種常見且知名的代理模式:

  1. 遠端代理,即在物理上的遠端資源(如伺服器)在本地的代理
  2. 虛擬代理,即不是真實的代理,是對一些可以不在第一時間執行的操作進行的代理,他可以將比如複雜耗時的操作推遲到真正需要的時候再進行,所謂的惰性載入即是典型的虛擬代理模式
  3. 保護代理,即對敏感資源進行保護,在實際訪問前,進行相應安全性控制的代理
  4. 智慧代理,即引用代理,在資源被訪問時,執行一些額外的預操作,如檢查引用計數或執行緒安全之類的

書中提供了一個惰性載入的例項,來講解虛擬代理,這裡我們摘錄於此。

首先我們編寫一個名為 LazyProperty 的裝飾器,他的作用在於,將他裝飾的物件的執行時機從宣告之時推後到被呼叫之時LazyProperty 裝飾器程式碼如下:

class LazyProperty(object):
    def __init__(self, method):
        self.method = method
        self.method_name = method.__name__
        print('function overriden: {}'.format(self.method))
        print("function's name: {}".format(self.method_name))

    def __get__(self, obj, cls):
        if not obj:
            return None
        value = self.method(obj)
        print('value {}'.format(value))
        setattr(obj, self.method_name, value)
        return value

這裡我們為 LazyProperty 重寫了 __get__ 方法,從程式碼中可以看出,__get__ 方法其實本質上是代理了被 LazyProperty 所修飾的物件的訪問操作。也就是說,要訪問被 LazyProperty 所修飾的物件,在實際返回其值之前,會先執行 LazyProperty.__get__ 方法。下面我們來驗證一下。

編寫一個 Test 類,假設其中 resource 操作會花費較長時間,程式碼如下:

class Test:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self._resource = None

    def resource(self):
        print('initializing self._resource which is: {}'.format(self._resource))
        self._resource = tuple(range(x, y))    # 假設這一行的計算成本比較大
        return self._resource

如果我們只是這樣編寫程式碼,那麼每次在遇到需要使用 self._resource 時,呼叫 Test.resource 方法,都會需要重新執行一遍其中複雜的操作。如下程式碼所示:

def main():
    t1 = Test(1,5)
    t2 = Test(10,15)
    print(t1.x)
    print(t1.y)
    print(t1._resource)
    print(t2.x)
    print(t2.y)
    print(t2._resource)
    # 做更多的事情……
    print(t1.resource())
    print(t2.resource())
    print(t1._resource)
    print(t2._resource)
    print(t1.resource())
    print(t2.resource())

main()

這段程式碼的輸出是:

1
5
None
10
15
None
initializing self._resource which is: None
(1, 2, 3, 4)
initializing self._resource which is: None
(10, 11, 12, 13, 14)
(1, 2, 3, 4)
(10, 11, 12, 13, 14)
initializing self._resource which is: (1, 2, 3, 4)
(1, 2, 3, 4)
initializing self._resource which is: (10, 11, 12, 13, 14)
(10, 11, 12, 13, 14)

請注意其中兩次出現的 initializing self._resource which is: 內容,這表明,每次呼叫 t.resource(),都重新執行了一次賦值操作。

然而當我們使用 LazyProperty 裝飾器 & 描述符來修飾 Test.resource 方法時,修改 Test.resource 方法程式碼如下:

@LazyProperty
def resource(self):
    print('initializing self._resource which is: {}'.format(self._resource))
    self._resource = tuple(range(self.x, self.y))    # 假設這一行的計算成本比較大
    return self._resource

如此一來,我們再將 main() 方法中,各處呼叫 t.resource() 改為 t.resource (因為這裡 LazyProperty 已經充當了描述符,使得 t.resource 可以像訪問屬性一樣直接訪問),會發現輸出內容有所改變,具體如下:

function overriden: <function Test.resource at 0x01969E40>
function's name: resource
1
5
None
10
15
None
initializing self._resource which is: None
value (1, 2, 3, 4)
(1, 2, 3, 4)
initializing self._resource which is: None
value (10, 11, 12, 13, 14)
(10, 11, 12, 13, 14)
(1, 2, 3, 4)
(10, 11, 12, 13, 14)
(1, 2, 3, 4)
(10, 11, 12, 13, 14)

除開最上面部分有一些之前沒有見過的內容,我們可以明顯的看到,初始化操作只執行了一次,之後的每次呼叫,都是直接獲取已經初始化好的 Test._resource 屬性。也就是說,本例中的虛擬代理 LazyProperty,一方面幫我們完成了惰性載入的操作,另一方面也充當了資源的描述符,方便其之後的獲取其值。當然,根據需要,也可以在 LazyProperty 中實現 __set__ 等其他相關描述符操作。

本例完整程式碼如下:

class LazyProperty(object):
    def __init__(self, method):
        self.method = method
        self.method_name = method.__name__
        print('function overriden: {}'.format(self.method))
        print("function's name: {}".format(self.method_name))

    def __get__(self, obj, cls):
        if not obj:
            return None
        value = self.method(obj)
        print('value {}'.format(value))
        setattr(obj, self.method_name, value)
        return value

class Test(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self._resource = None

    @LazyProperty
    def resource(self):
        print('initializing self._resource which is: {}'.format(self._resource))
        self._resource = tuple(range(self.x, self.y))    # 假設這一行的計算成本比較大
        return self._resource

def main():
    t1 = Test(1,5)
    t2 = Test(10,15)
    print(t1.x)
    print(t1.y)
    print(t1._resource)
    print(t2.x)
    print(t2.y)
    print(t2._resource)
    # 做更多的事情……
    print(t1.resource)
    print(t2.resource)
    print(t1._resource)
    print(t2._resource)
    print(t1.resource)
    print(t2.resource)

main()

下面再將書中一個關於保護代理的例項摘錄於此。

我們首先有一個需要訪問的類,SensitiveInfo,其中包含了列表資訊,一個讀取列表資訊的方法以及一個修改列表內容的方法:

class SensitiveInfo(object):
    def __init__(self):
        self.users = ['nick', 'tom', 'ben', 'mike']

    def read(self):
        print('There are {} users: {}'.format(len(self.users), ' '.join(self.users)))

    def add(self, user):
        self.users.append(user)
        print('Added user {}'.format(user))

這裡我們假設他的列表資訊是需要保密訪問的,只有獲取密碼後才能訪問相應內容,那麼我們在不修改這個類本身的情況下,要實現訪問控制,就需要通過代理的方式來進行。增加一個 Info 類作為保護代理,他包含有與被保護物件 SensitiveInfo 相同的方法。程式碼如下:

class Info(object):
    def __init__(self):
        self.protected = SensitiveInfo()
        self.secret = '0xdeadbeef'

    def read(self):
        self.protected.read()

    def add(self, user):
        sec = input('what is the secret? ')
        self.protected.add(user) if sec == self.secret else print("That's wrong!")

這裡的 Info 中,將被保護物件作為一個屬性代理了起來,在要進行敏感操作(這裡是修改被保護物件列表值)時,設定一系列驗證等檢測,來確保對被訪問物件的操作時安全或者符合要求的。

我們依舊編寫一個 main() 函式進行測試:

def main():
    info = Info()

    while True:
        print('1. read list |==| 2. add user |==| 3. quit')
        key = input('choose option: ')
        if key == '1':
            info.read()
        elif key == '2':
            name = input('choose username: ')
            info.add(name)
        elif key == '3':
            exit()
        else:
            print('unknown option: {}'.format(key))

通過執行這個例項,可以看到保護代理是怎樣實現保護這一核心操作的。

同時,書上還留了幾道思考題,我摘錄最有價值的一題於此。其實現程式碼將在本文最下方給出。

該示例有一個非常大的安全缺陷。沒有什麼能阻止客戶端程式碼通過直接建立一個SensitveInfo例項來繞過應用的安全設定。優化示例來阻止這種情況。一種方式是使用abc模組來禁止直接例項化SensitiveInfo。在這種情況下,會要求進行其他哪些程式碼變更呢?


答案:

from abc import ABCMeta, abstractmethod

# 將類宣告為抽象類,併為用 @abstractmethod 修飾相應的需要成為抽象方法的方法
# 如此一來,即無法直接將此類例項化,避免開發中的失誤導致繞過代理,出現不安全的情況
class SensitiveInfo(metaclass=ABCMeta):
    def __init__(self):
        self.users = ['nick', 'tom', 'ben', 'mike']

    @abstractmethod
    def read(self):
        '''read'''
        pass

    @abstractmethod
    def add(self, user):
        '''add'''
        pass

class Info(SensitiveInfo):
    '''SensitiveInfo的保護代理'''

    def __init__(self):
        # 通過這種方式,呼叫 SensitiveInfo.__init__() 獲得 users 列表
        super().__init__()
        self.secret = '0xdeadbeef'

    def read(self):
        print('There are {} users: {}'.format(len(self.users), ' '.join(self.users)))

    def add(self, user):
        sec = input('what is the secret? ')
        # 此時的操作全部基於從 SensitiveInfo 繼承來的 users 進行
        self.users.append(user) if sec == self.secret else print("That's wrong!") 

def main():
    info = Info()
    while True:
        print('1. read list |==| 2. add user |==| 3. quit')
        key = input('choose option: ')
        if key == '1':
            info.read()
        elif key == '2':
            name = input('choose username: ')
            info.add(name)
        elif key == '3':
            exit()
        else:
            print('unknown option: {}'.format(key))

if __name__ == '__main__':
    main()

相關文章