05月14, 2017

通俗 Python 设计模式——代理模式

今天来说一说代理模式。

代理模式顾名思义,是对资源进行代理访问的一种模式,这里的资源是泛指在程序中会使用到的数据、操作、过程、对象等等。当然,针对不同的资源,代理进行的操作不尽相同,根据前人的总结,有四种常见且知名的代理模式:

  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()

本文链接:http://pycode.cc/post/easy-python-design-pattern-proxy-design-pattern.html

-- EOF --

Comments