装饰器模式可以将一个函数或者类进行“装饰”,使其行为进行一致化的修订,目的是为了增强对象的响应能力,或者为了增加对象的多种不同行为的能力。
装饰器模式有点类似于继承,关于使用装饰器还是继承的选择,原则是当需要更多的行为时候,装饰器通常会更简练,而在需要根据情况动态修改对象的行为时,就必须使用装饰器。装饰器具有相同的接口名,因此可以为目标对象创建多个装饰器。本文通过一个简单的socket通信程序,来介绍通过装饰器增强socket的send()的行为。下面是服务器核心功能代码:
import socket
def respond(client):
response = input("Enter a value: ")
client.send(bytes(response, 'utf8'))
client.close()
if __name__ == "__main__":
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('localhost', 2401))
server.listen(1)
try:
while True:
client, addr = server.accept()
respond(LogSocket(client))
finally:
server.close()
从上面的server代码可以看出,核心respond()函数只需要调用客户端接口的两个方法:send和close,下面是客户端代码:
import socket
if __name__ == "__main__":
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('localhost', 2401))
print("Received: {0}".format(client.recv(1024)))
client.close()
整个服务端程序一直在循环监听2401端口,一旦有客户端接入,服务器窗口显示Enter a value:输入通信的消息后,客户端接受消息,并打印输出。随着程序的演进,我们发现需要增强socket的两个功能:增加服务端日志记录功能
对传输数据进行压缩,提高发送效率
下面的UML图中,装饰器通过组合的方式访问需要wrap的接口函数,实现了两个装饰器LogSocket以及GzipSocket,来对socket中的send()函数进行功能增强。装饰器对核心函数send装饰的UML图
装饰器1,增强日志功能
LogSocket装饰器目标是在发送之前输出数据到服务器的控制台,具体代码如下:
class LogSocket:
def __init__(self, socket):
self.socket = socket
def send(self, data):
print("Sending {0} to {1}".format(
data, self.socket.getpeername()[0]))
self.socket.send(data)
def close(self):
self.socket.close()
在send被调用之前,装饰器将信息输出到屏幕上。而使用此装饰器非常方便,只需要修改server端中的一行代码即可:
respond(LogSocket(client))
装饰器2,压缩send数据
下面是第二个装饰器来压缩send的数据:
import gzip
from io import BytesIO
class GzipSocket:
def __init__(self, socket):
self.socket = socket
def send(self, data):
buf = BytesIO()
zipfile = gzip.GzipFile(fileobj=buf, mode="w")
zipfile.write(data)
zipfile.close()
self.socket.send(buf.getvalue())
def close(self):
self.socket.close()
这一版的send将输入数据进行压缩,然后发送给客户端。当我们就有多个装饰器时,可以动态的切换使用。
client, addr = server.accept()
if log_send:
client = LoggingSocket(client)
if client.getpeername()[0] in compress_hosts:
client = GzipSocket(client)
respond(client)
上面的代码,通过一个配置变量log_send来决定是时候装饰socket。类似的检查客户端时候接收压缩数据来决定是否增加压缩装饰器。
Python中的装饰器
装饰器在python中非常有用,但也要其他的替代选项。例如,我们可以使用monkey-patching获得类似的效果;单继承,可以将操作放入一个操作中。
在Python中,对函数进行装饰非常常用,因为函数也是对象。事实上,对函数的装饰非常常用,以至于Python提供一个特殊的语法使其很容易应用装饰器到函数中。
例如,我们可以寻找一种更加通用的方式来记日志, 不用logging,只需要在sockets上发送调用即可。下面的例子实现了一个装饰器:
import time
def log_calls(func):
def wrapper(*args, **kwargs):
now = time.time()
print("Calling {0} with {1} and {2}".format(
func.__name__, args, kwargs))
return_value = func(*args, **kwargs)
print("Executed {0} in {1}ms".format(
func.__name__, time.time() - now))
return return_value
return wrapper
def test1(a, b, c):
print("\ttest1 called")
def test2(a, b):
print("\ttest2 called")
def test3(a, b):
print("\ttest3 called")
time.sleep(1)
test1 = log_calls(test1)
test2 = log_calls(test2)
test3 = log_calls(test3)
test1(1, 2, 3)
test2(4, b=5)
test3(6, 7)
装饰器函数非常类似于前面的例子,在前面的例子中,装饰器讲一个socket对象装饰成另外一个socket对象,这次,装饰器将一个函数对象并且返回一个函数对象,代码有三个不同的任务组成:log_calls函数,接受另外一个函数
这个函数内部定义了一个新的函数,会增加新的工作,在调用原来的函数之前
新函数会返回
以上通过三个函数来展示装饰器的使用,我们把每个函数都放入装饰器当中,并返回一个新的函数,返回的函数与原来的函数名相同,有效替换了原函数。
这个语法允许动态构建函数对象,就像前面的socket例子;如果我们不替换名称,我们可以保留两个版本的函数对象。通常的情况下,需要永久性修改不同的函数。在这种情况下,Python支持一种特殊的语法,给函数定义的同时进行装饰。
@log_calls
def test1(a, b, c):
print("\ttest1 called")
这种方式的好处是,可以很容易看到函数在被定义的同时被装饰了;坏处是只能给自己定义的的函数进行装饰,如果给第三方库进行装饰,我们必须使用之前的语法。