本文的目录地址
本文的代码地址
在某些应用中,我们需要在访问某个对象之前执行一个或多个重要的操作,例如,访问敏感信息——在允许用户访问敏感信息之前,我们希望确保用户具备足够的权限。操作系统也存在类似的情况,用户必须具有管理员权限才能在系统中安装新程序。
上面提到的重要操作不一定与安全问题相关。延迟初始化是另一个案例:我们想要把一个计算成本较高的对象的创建过程延迟到用户首次真正使用它时才进行。
这类操作通常使用代理设计模式(Proxy design pattern)来实现。该模式因使用代理(又名替代,surrogate)对象在访问实际对象之前执行重要操作而得其名。以下是四种不同的知名代理类型。
应用案例
因为存在至少四种常见的代理类型,所以代理设计模式有很多应用案例,如下所示.
实现一
我发现虚拟代理非常有用,所以现在通过一个例子来看看可以如何实现它。
使用Python来创建虚拟代理存在很多方式,但我始终喜欢地道的符合Python风格的实现。这里展示的代码源自网站stackoverflow.com用户Cyclone的一个超赞回答。我们先创建一个LazyProperty类,用作一个修饰器。当它修饰某个特性时,LazyProperty惰性地(首次使用时)加载特性,而不是立即进行。__init__方法创建两个变量,用作初始化待修饰特性的方法的别名。method变量是一个实际方法的别名,method_name变量则是该方法名称的别名。
class LazyProperty:
def __init__(self,method):
self.method=method
self.method_name=method.__name__
LazyProperty类实际上是一个描述符.描述符(descriptor)是Python中重写类属性访问方法(__get__()、__set__()和__delete__())的默认行为要使用的一种推荐机制。LazyProperty类仅重写了__get__(),因为这是其需要重写的唯一访问方法。换句话说,我们无需重写所有访问方法。__get__()方法所访问的特性值,正是下层方法想要赋的值,并使用setattr()来手动赋值。__get__()实际做的事情非常简单,就是使用值来替代方法!这意味着不仅特性是惰性加载的,而且仅可以设置一次。我们马上就能看到这意味着什么。取消注释,可以得到一些额外信息。
def __get__(self, instance, owner):
if not instance:
return None
value=self.method(instance)
#print(('value {}'.format(value)))
setattr(instance,self.method_name,value)
return value
Test类演示了我们可以如何使用LazyProperty类。其中有三个属性,x、y和_resoure。我们想懒加载_resource变量,因此将其初始化为None,resource()方法是使用LazyProperty类修饰的。因演示目的,LazyProperty类将_resource属性初始化为一个tuple,通常来说这是一个缓慢/代价大的初始化过程(初始化数据库、图形等)。
class Test:
def __init__(self):
self.x='foo'
self.y='bar'
self._resouce=None
@LazyProperty
def resource(self):
print('initializing self._resource which is: {}'.format(self._resouce))
self._resouce=tuple(range(5)) # 代价大的
return self._resouce
main()函数展示了懒初始化是如何进行的。注意,__get__()访问方法的重写使得可以将resouce()方法当做一个变量(可以使用t.resource代替t.resource())。
def main():
t=Test()
print(t.x)
print(t.y)
# 做更多的事情。。。
print(t.resource)
print(t.resource)
运行结果:
foo
bar
initializing self._resource which is: None
(0, 1, 2, 3, 4)
(0, 1, 2, 3, 4)
从这个例子(文件lazy.py)的执行输出中,可以看出:
在OOP中有两种基本的、不同类型的懒初始化,如下:
实现二
我们将实现一个简单的保护代理来查看和添加用户。该服务提供两个选项:
查看用户列表:这一操作不需要特殊权限
添加新用户:这一操作要求客户端提供一个特殊的密码。
SensitiveInfo类包含我们希望保护的信息。users变量是已有用户的列表。read()方法输出用户列表。add()方法将一个新用户添加到列表中。
class SensitiveInfo:
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的一个保护代理。secret变量值是客户端代码在添加新用户时被要求告知/提供的密码。注意,这只是一个例子。现实中,永远不要执行以下操作。
read()方法是SensitiveInfo.read()的一个包装。add()方法确保仅当客户端代码知道密码时才能添加新用户。
class Info:
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!")
main()函数展示了客户端代码可以如何使用代理模式。客户端代码创建一个Info类的实例,并使用菜单让用户选择来读取列表、添加新用户或退出应用。
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))
点击获取完整代码proxy.py
运行结果:
Q1:该示例有一个安全缺陷。即可以直接创建SensitiveInfo实例来绕过安全设置。如何优化实例来阻止这种情况。一种方式是使用abc模块来禁止直接实例化SensitiveInfo。动手试试吧。
proxy_q1.py