Scott's Blog

学则不固, 知则不惑

0%

Python 设计模式-装饰器与观察者模式

设计模式就像建筑师决定建造一座桥、一座塔、一栋楼时,他们会遵循的原则。

这是设计模式系列文章的一部分,点击查看该系列的其他文章。

装饰器模式

装饰器可以将一个提供核心功能的对象,和其他可以改变这个功能的对象“包裹”在一起使用。

它主要有两种用途:

  • 增强一个组件给另一个组件发送数据时的响应能力
  • 支持多种可选的行为(适当的代替多重继承)

装饰器

如果你不知道什么是装饰器,可以看下这篇 文章,其中有一段代码可以让你很容易理解它的原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def a_new_decorator(a_func):
def wrapTheFunction():
print("我在 a_func() 执行之前做一些无聊的工作")

a_func()

print("我在 a_func() 执行之后做一些无聊的工作")
return wrapTheFunction

@a_new_decorator
def a_function_requiring_decoration():
"""就是你! 来包装我吧!"""
print("我是一个需要被包装的家伙"
"快来拯救我!")

a_function_requiring_decoration()

网络编程装饰器实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import socket

# 关于 socket,socket.AF_APPLETALK 等内容请参考:
# https://docs.oracle.com/cd/E19120-01/open.solaris/817-4415/sockets-18552/index.html


def respond(client):
"""使用传入对象 client 的发送方法,作出“回应”,它只关心 client 的
send 和 close 方法,即不管你传进来的啥东西,只要有 send 和 close
方法即可。
"""
response_str = input("有连接请求,输出你的回应:")
client.send(bytes(response_str, 'utf-8'))
client.close()

# AF_INET 是协议族的规定,本质上上是一个常量数字
# SOCK_STREAM 指定为 TCP,另外还有 UDP(这属于计算机网络的知识)
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('localhost', 2401))
server.listen(1)

try:
while True:
client, addr = server.accept()
respond(client)
finally:
server.close()

上面的 respond 方法只关注接受的对象有没有 send, close 方法。

我们甚至可以传入一个自定义的对象,只要它有 send, close 方法,respond 方法可以继续工作。

让我们来实现一个自己的对象,它拥有 send,close 方法,这两个方法是对 client 的 send, close 方法的包装,这样我们就可以在调用 client 的 send,close 方法之前或者之后做一些事情。

下面的例子是一个网络编程实例的实现,它有一个服务端和客户端,服务端会一直处于待命状态,只要有客户端连接,服务端就会做出回应.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import socket

# 关于 socket,socket.AF_APPLETALK 等内容请参考:
# https://docs.oracle.com/cd/E19120-01/open.solaris/817-4415/sockets-18552/index.html


def respond(client):
"""使用 client 的发送方法,作出“回应”,它只关心 client 的
send 和 close 方法。
"""
response_str = input("有连接请求,输出你的回应:")
client.send(bytes(response_str, 'utf-8'))
client.close()


class LogSocket:
"""
上面的 respond 方法只关注接受的对象有没有 send, close 方法。

我们可以传入一个自定义的对象,只要它有 send, close 方法,respond 方法就还是可以继续工作。

所以,我们可以写一个自己的对象,它拥有 send,close 方法,这两个方法是对 client 的 send, close 方法的包装,这样我们就可以在调用 client 的 send,close 方法之前或者之后做一些事情。

下面举一个例子是在 send,close 调用的时候执行答应。
"""
def __init__(self, socket):
self.socket = socket

def send(self, data):
print(f"Sending {data} to {self.socket.getpeername()[0]}")
self.socket.send(data)

def close(self):
self.socket.close()



# AF_INET 是协议族的规定,本质上上是一个数字
# SOCK_STREAM 指定为 TCP
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)) # client 被包装(装饰),或者说被替换了
finally:
server.close()

这里使用装饰器的好处是,你可以灵活的切换,比如你可以另外写一个装饰器,用于对发送的数据压缩。 然后你就可以实现类似这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import gzip
from io import BytesIO

class GzipSocket:
def __init__(self, socket):
self.socket = socket

def send(self, data):
buf = BytesIO(data)
zipfile = gzip.GzipFile(fileobj=buf, mode='w')
zipfile.write(data)
zipfile.close()

self.socket.send(buf.getvalues())

def close(self, data):
self.socket.close()


# ...
client, addr = server.accept()

if log_send:
client = LogSocket(client)
if gzip_send:
client = GzipSocket(client)

自定义打印的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time

def log_calls(func):
# 创建一个新的函数,替换原来传入的函数
def wrapper(*args, **kwargs):
now = time.time()
print("调用:{0}, 携带参数 {1}, {2}".format(
func.__name__, args, kwargs
))

return_value = func(*args, **kwargs)

print("函数 {} 用时 {} ".format(
func.__name__,
time.time() - now
))
return return_value
return wrapper
1
2
3
4
5
6
7
# 用方法实现的装饰器的使用

def test(x, y):
return x + y

test = log_calls(test)
result = test(1, 2)
1
2
3
4
5
6
7
# python 特殊的方法使用装饰器

@log_calls
def test1(x, y):
return x + y

test(2, 3)

输出如下

1
2
3
调用:test, 携带参数 (2, 3), {}
函数 test 用时 2.002716064453125e-05
5

装饰器技巧

想象一下,如果你需要给一个类中的所有方法添加一个装饰器,你会怎么办?也许你不会有这样的需求,但有时候,你可能需要对某几个函数添加装饰器,但你又不想在原来的类旁边添加任何代码。

这可以通过 metaclass 来实现,或者通过循环类的方法使用 setattr 方法修改,有兴趣的可以研究一下。

这里放一个 StackOverflow 上的 讨论

观察者模式

观察者模式适用于状态监测事件处理

它有一个核心对象,以及观察者。核心对象由一组未知,并可能正在扩展的 “观察者” 对象来监控。一旦核心对象的值发生了变化,便会通过 update 方法告诉每一个观察者。观察者收到更新后,可能会做不一样的事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Inventory:
def __init__(self):
self.observers = []
self._product = None
self._quantity = 0

def attach(self, observer):
self.observers.append(observer)

@property
def product(self):
return self._product

@product.setter
def product(self, value):
self._product = value
self._update_observers()

@property
def quantity(self):
return self._quantity

@quantity.setter
def quantity(self, value):
self._quantity = value
self._update_observers()

def _update_observers(self):
for observer in self.observers:
# 调用 observer 对象,为了让一个对象可以被调用,它需要实现
# __call__ 方法
observer()

class ConsoleObserver:
def __init__(self, inventory):
self.inventory = inventory

def __call__(self, *args, **kwds):
"""观察者模式可以用于备份数据至不同的地方,比如文件、数据库或互联网应用。
它将正在被观察的代码,和执行的代码分离。
如果不使用这种模式,则必须在每个属性中处理可能出现的情况,这意味着任务代码和
被观察的对象耦合在一起,维护起来会很麻烦。
"""
print(self.inventory.product)
print(self.inventory.quantity)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
invt = Inventory()

# 可以很容易的添加观察者
console_1 = ConsoleObserver(invt)
console_2 = ConsoleObserver(invt)

invt.attach(console_1)
invt.attach(console_2)


# 可以看到每次对 product 或者 quantity 的修改
# 都会产生两次打印,这是因为两个观察者都做出了响应
invt.product = '元气森林'
"""
元气森林
0
元气森林
0
"""

invt.quantity = 100,000

"""
元气森林
(100, 0)
元气森林
(100, 0)
"""