Python 设计模式实践教程(二)
五、构建器模式
他能修好它吗?是的,他可以!——《建筑者鲍勃》主题曲
如果你从事软件工作一段时间,你将不得不处理来自用户的输入——从让玩家在游戏中输入他们的角色名字到在电子表格的单元格中添加数据。表单通常是应用的核心。实际上,大多数应用只是信息的输入、转换和反馈流的特例。有许多输入小部件和界面,但最常见的仍然是以下这些:
- 正文框
- 下拉和选择列表
- 复选框
- 单选按钮
- 文件上传字段
- 小跟班
这些可以以许多不同的方式混合和匹配,以从用户那里获取不同类型的数据作为输入。本章我们感兴趣的是如何编写一个脚本来简化生成这些表单的工作。对于我们的用例,我们将生成 HTML webforms,但是同样的技术也可以用来生成移动应用接口、表单的 JSON 或 XML 表示,或者任何你能想到的东西。让我们从编写一个简单的函数开始,它将为我们生成一个小表单。
basic_form_generator.py
def generate_webform(field_list):
generated_fields = "\n".join(
map(
lambda x: '{0}:<br><input type="text" name="{0}"><br>'.format(x),
field_list
)
)
return "<form>{fields}</form>".format(fields=generated_fields)
if __name__ == "__main__":
fields = ["name", "age", "email", "telephone"]
print(generate_webform(fields))
在这个简单的例子中,代码假设字段列表只包含文本字段。字段名作为字符串包含在列表中。列表中的字符串也是返回的表单中使用的标签。对于列表中的每个元素,都会向 webform 添加一个字段。然后,webform 作为包含生成的 webform 的 HTML 代码的字符串返回。
如果我们更进一步,我们可以让一个脚本获取生成的响应,并从它构建一个常规的 HTML 文件,一个可以在 web 浏览器中打开的文件。
html_ 表单 _ 生成器. py
def generate_webform(field_list):
generated_fields = "\n".join(
map(
lambda x: '{0}:<br><input type="text" name="{0}"><br>'.format(x),
field_list
)
)
return "<form>{fields}</form>".format(fields=generated_fields)
def build_html_form(fields):
with open("form_file.html", w) as f:
f.write(
"<html><body>{}</body></html>".format(generate_webform(fields))
)
if __name__ == "__main__":
fields = ["name", "age", "email", "telephone"]
build_html_form(fields)
正如我在本章开始时提到的,webforms(以及一般的表单)可以有比简单的文本字段更多的字段类型。我们可以使用命名参数向表单添加更多的字段类型。请看下面的代码,我们将在其中添加复选框。
html_ 表单 _ 生成器. py
def generate_webform(text_field_list=[], checkbox_field_list=[]):
generated_fields = "\n".join(
map(
lambda x: '{0}:<br><input type="text" name="{0}"><br>'.format(x),
text_field_list
)
)
generated_fields += "\n".join(
map(
lambda x: '<label><input type="checkbox" id="{0}" value="{0}"> {0}<br>'.format(x),
checkbox_field_list
)
)
return "<form>{fields}</form>".format(fields=generated_fields)
def build_html_form(text_field_list=[], checkbox_field_list=[]):
with open("form_file.html", 'w') as f:
f.write(
"<html><body>{}</body></html>".format(
generate_webform(
text_field_list=text_field_list,
checkbox_field_list=checkbox_field_list
)
)
)
if __name__ == "__main__":
text_fields = ["name", "age", "email", "telephone"]
checkbox_fields = ["awesome", "bad"]
build_html_form(text_field_list=text_fields, checkbox_field_list=checkbox_fields)
这种方法有明显的问题,第一个问题是,我们不能处理具有不同默认值或选项的字段,或者事实上除了简单的标签或字段名之外的任何信息。我们甚至不能考虑表单中使用的字段名和标签之间的差异。我们现在要扩展表单生成器函数的功能,这样我们就可以满足大量的字段类型,每种类型都有自己的设置。我们还有一个问题,那就是没有办法交错不同类型的字段。为了向您展示这是如何工作的,我们去掉了命名参数,并用一个字典列表替换它们。该函数将在列表中的每个字典中查找,然后使用包含的信息生成字典中定义的字段。
html _ 字典 _ 表单 _ 生成器. py
def generate_webform(field_dict_list):
generated_field_list = []
for field_dict in field_dict_list:
if field_dict["type"] == "text_field":
generated_field_list.append(
'{0}:<br><input type="text" name="{1}"><br>'.format(
field_dict["label"],
field_dict["name"]
)
)
elif field_dict["type"] == "checkbox":
generated_field_list.append(
'<label><input type="checkbox" id="{0}" value="{1}"> {2}<br>'.format(
field_dict["id"],
field_dict["value"],
field_dict["label"]
)
)
generated_fields = "\n".join(generated_field_list)
return "<form>{fields}</form>".format(fields=generated_fields)
def build_html_form(field_list):
with open("form_file.html", 'w') as f:
f.write(
"<html><body>{}</body></html>".format(
generate_webform(field_list)
)
)
if __name__ == "__main__":
field_list = [
{
"type": "text_field",
"label": "Best text you have ever written",
"name": "best_text"
},
{
"type": "checkbox",
"id": "check_it",
"value": "1",
"label": "Check for one",
},
{
"type": "text_field",
"label": "Another Text field",
"name": "text_field2"
}
]
build_html_form(field_list)
字典包含一个type字段,用于选择要添加的字段类型。然后,字典中有一些可选元素,如标签、名称和选项(在选择列表的情况下)。您可以以此为基础创建一个表单生成器,它可以生成您能想到的任何表单。现在你应该闻到一股臭味了。将循环和条件语句堆叠在一起会很快变得不可读和不可维护。让我们稍微清理一下代码。
我们将提取每个字段的生成代码,并将其放入一个单独的函数中,该函数获取字典并返回表示该字段的 HTML 代码片段。这一步的关键是在不改变主函数的任何功能、输入或输出的情况下清理代码。
cleaned _ html _ dictionary _ form _ generator . py
def generate_webform(field_dict_list):
generated_field_list = []
for field_dict in field_dict_list:
if field_dict["type"] == "text_field":
field_html = generate_text_field(field_dict)
elif field_dict["type"] == "checkbox":
field_html = generate_checkbox(field_dict)
generated_field_list.append(field_html)
generated_fields = "\n".join(generated_field_list)
return "<form>{fields}</form>".format(fields=generated_fields)
def generate_text_field(text_field_dict):
return '{0}:<br><input type="text" name="{1}"><br>'.format(
text_field_dict["label"],
text_field_dict["name"]
)
def generate_checkbox(checbox_dict):
return '<label><input type="checkbox" id="{0}" value="{1}"> {2}<br>'.format(
checkbox_dict["id"],
checkbox_dict["value"],
checkbox_dict["label"]
)
def build_html_form(field_list):
with open("form_file.html", 'w') as f:
f.write(
"<html><body>{}</body></html>".format(
generate_webform(field_list)
)
)
if __name__ == "__main__":
field_list = [
{
"type": "text_field",
"label": "Best text you have ever written",
"name": "best_text"
},
{
"type": "checkbox",
"id": "check_it",
"value": "1",
"label": "Check for one",
},
{
"type": "text_field",
"label": "Another Text field",
"name": "text_field2"
}
]
build_html_form(field_list)
语句还在,但至少现在代码更干净了。随着我们向表单生成器添加更多的字段类型,这些杂乱无章的 if 语句将会越来越多。让我们通过使用面向对象的方法来改善这种情况。我们可以使用多态性来处理一些特定的字段以及在生成这些字段时遇到的问题。
oop_html_ form_ generator.py
class HtmlField(object):
def __init__(self, **kwargs):
self.html = ""
if kwargs['field_type'] == "text_field":
self.html = self.construct_text_field(kwargs["label"], kwargs["field_name"])
elif kwargs['field_type'] == "checkbox":
self.html = self.construct_checkbox(kwargs["field_id"], kwargs["value"], kwargs["label"])
def construct_text_field(self, label, field_name):
return '{0}:<br><input type="text" name="{1}"><br>'.format(
label,
field_name
)
def construct_checkbox(self, field_id, value, label):
return '<label><input type="checkbox" id="{0}" value="{1}"> {2}<br>'.format(
field_id,
value,
label
)
def __str__(self):
return self.html
def generate_webform(field_dict_list):
generated_field_list = []
for field in field_dict_list:
try:
generated_field_list.append(str(HtmlField(**field)))
except Exception as e:
print("error: {}".format(e))
generated_fields = "\n".join(generated_field_list)
return "<form>{fields}</form>".format(fields=generated_fields)
def build_html_form(field_list):
with open("form_file.html", 'w') as f:
f.write(
"<html><body>{}</body></html>".format(
generate_webform(field_list)
)
)
if __name__ == "__main__":
field_list = [
{
"field_type": "text_field",
"label": "Best text you have ever written",
"field_name": "Field One"
},
{
"field_type": "checkbox",
"field_id": "check_it",
"value": "1",
"label": "Check for on",
},
{
"field_type": "text_field",
"label": "Another Text field",
"field_name": "Field One"
}
]
build_html_form(field_list)
对于每一组可选的参数,我们都需要另一个构造函数,所以我们的代码很快就会崩溃,变成一堆构造函数,这就是通常所说的伸缩构造函数反模式。像设计模式一样,由于现实世界中软件设计和开发过程的性质,反模式是您经常会看到的错误。对于一些语言,比如 Java,您可以根据接受的参数和随后构造的内容,用许多不同的选项来重载构造函数。另外,请注意,构造函数__init__()中的if条件可以定义得更清楚。
我相信你对这个实现有很多反对意见,你应该反对。在我们进一步清理之前,我们将快速查看一下反模式。
反模式
顾名思义,反模式是软件模式的对外观。也是软件开发过程中定期出现的东西。反模式通常是解决特定类型问题的通用方法,但也是解决问题的错误方法。这并不意味着设计模式总是正确的。这意味着我们的目标是编写易于调试、易于更新和易于扩展的干净代码。反模式会导致与这些既定目标完全相反的代码。它们会产生糟糕的代码。
伸缩构造函数反模式的问题是,它会产生几个构造函数,每个构造函数都有特定数量的参数,然后所有这些构造函数都委托给一个默认的构造函数(如果该类编写正确的话)。
构建器模式不使用大量的构造函数;它使用一个生成器对象。这个 builder 对象逐步接收每个初始化参数,然后将生成的构造对象作为单个结果返回。在 webform 示例中,我们希望添加不同的字段,并获得生成的 webform 作为结果。构建器模式的另一个好处是,它将对象的构造与对象的表示方式分开,因此我们可以改变对象的表示方式,而不改变构造它的过程。
在构建器模式中,两个主要参与者是Builder和Director。
Builder是一个抽象类,它知道如何构建最终对象的所有组件。在我们的例子中,Builder类将知道如何构建每个字段类型。它可以将各个部分组装成一个完整的表单对象。
Director控制建造的过程。有一个Builder的实例(或多个实例)供Director用来构建 webform。Director的输出是一个完全初始化的对象——在我们的例子中是 webform。Director实现了从它包含的字段建立一个 webform 的指令集。这组指令独立于传递给控制器的各个字段的类型。
Python 中构建器模式的一般实现如下所示:
form_builder.py
from abc import ABCMeta, abstractmethod
class Director(object, metaclass=ABCMeta):
def __init__(self):
self._builder = None
@abstractmethod
def construct(self):
pass
def get_constructed_object(self):
return self._builder.constructed_object
class Builder(object, metaclass=ABCMeta):
def __init__(self, constructed_object):
self.constructed_object = constructed_object
class Product(object):
def __init__(self):
pass
def __repr__(self):
pass
class ConcreteBuilder(Builder):
pass
class ConcreteDirector(Director):
pass
你看我们有一个抽象类,Builder,它形成了创建对象(产品)的接口。然后,ConcreteBuilder为Builder提供了一个实现。产生的对象能够构造其他对象。
使用 builder 模式,我们现在可以重做 webform 生成器。
from abc import ABCMeta, abstractmethod
class Director(object, metaclass=ABCMeta):
def __init__(self):
self._builder = None
def set_builder(self, builder):
self._builder = builder
@abstractmethod
def construct(self, field_list):
pass
def get_constructed_object(self):
return self._builder.constructed_object
class AbstractFormBuilder(object, metaclass=ABCMeta):
def __init__(self):
self.constructed_object = None
@abstractmethod
def add_text_field(self, field_dict):
pass
@abstractmethod
def add_checkbox(self, checkbox_dict):
pass
@abstractmethod
def add_button(self, button_dict):
pass
class HtmlForm(object):
def __init__(self):
self.field_list = []
def __repr__(self):
return "<form>{}</form>".format("".join(self.field_list))
class HtmlFormBuilder(AbstractFormBuilder):
def __init__(self):
self.constructed_object = HtmlForm()
def add_text_field(self, field_dict):
self.constructed_object.field_list.append(
'{0}:<br><input type="text" name="{1}"><br>'.format(
field_dict['label'],
field_dict['field_name']
)
)
def add_checkbox(self, checkbox_dict):
self.constructed_object.field_list.append(
'<label><input type="checkbox" id="{0}" value="{1}"> {2}<br>'.format(
checkbox_dict['field_id'],
checkbox_dict['value'],
checkbox_dict['label']
)
)
def add_button(self, button_dict):
self.constructed_object.field_list.append(
'<button type="button">{}</button>'.format(
button_dict['text']
)
)
class FormDirector(Director):
def __init__(self):
Director.__init__(self)
def construct(self, field_list):
for field in field_list:
if field["field_type"] == "text_field":
self._builder.add_text_field(field)
elif field["field_type"] == "checkbox":
self._builder.add_checkbox(field)
elif field["field_type"] == "button":
self._builder.add_button(field)
if __name__ == "__main__":
director = FormDirector()
html_form_builder = HtmlFormBuilder()
director.set_builder(html_form_builder)
field_list = [
{
"field_type": "text_field",
"label": "Best text you have ever written",
"field_name": "Field One"
},
{
"field_type": "checkbox",
"field_id": "check_it",
"value": "1",
"label": "Check for on",
},
{
"field_type": "text_field",
"label": "Another Text field",
"field_name": "Field One"
},
{
"field_type": "button",
"text": "DONE"
}
]
director.construct(field_list)
with open("form_file.html", 'w') as f:
f.write(
"<html><body>{0!r}</body></html>".format(
director.get_constructed_object()
)
)
我们的新脚本只能构建带有文本字段、复选框和基本按钮的表单。作为练习,您可以添加更多字段类型。
其中一个ConcreteBuilder类具有创建您想要的 webform 所需的逻辑。Director调用了create()方法。这样,创建不同种类产品的逻辑就被抽象出来了。注意构建器模式是如何一步一步地构建表单的。这与抽象工厂形成了鲜明的对比,在抽象工厂中,可以使用多态性创建并立即返回一系列产品。
由于构建器模式将复杂对象的构造与其表示分离,因此您现在可以使用相同的构造过程来创建不同表示形式的表单,从本机应用接口代码到简单的文本字段。
这种分离的另一个好处是减少了对象的大小,从而使代码更加简洁。我们对构造过程有更好的控制,模块化的本质允许我们容易地对对象的内部表示进行修改。
最大的缺点是,这种模式要求我们为您想要创建的每种类型的产品创建ConcreteBuilder构建器。
那么,什么时候不应该使用构建器模式呢?当您构造可变对象时,这些对象提供了一种在构造完对象后修改配置值的方法。如果构造函数很少,构造函数很简单,或者任何构造函数参数都没有合理的默认值,并且所有这些参数都必须显式指定,那么也应该避免这种模式。
关于抽象的一个注记
当我们谈论抽象时,最基本的术语是指将名称与事物联系起来的过程。在 C 语言中,机器代码是使用机器指令的标签来抽象的,这些指令是一组十六进制指令,而十六进制指令又是机器中实际二进制数的抽象。使用 Python,我们在对象和函数方面有了更高层次的抽象(尽管 C 也有函数和库)。
练习
- 实现单选按钮组生成功能,作为表单生成器的一部分。
- 扩展表单构建器以处理提交后的数据,并将收到的信息保存在字典中。
- 实现表单生成器的一个版本,该版本使用数据库表模式来构建到数据库表的表单接口。
- 编写一个管理界面生成器,自动生成表单来处理数据库中的信息。您可以将一个模式作为字符串传递给表单生成器,然后让它返回表单的 HTML 以及一些提交 URL。
- 扩展表单构建器,这样就有了两个新对象,一个创建表单的 XML 版本,另一个生成包含表单信息的 JSON 对象。
六、适配器模式
幸存下来的既不是最强壮的物种,也不是最聪明的物种。它是最能适应变化的。—查尔斯·达尔文
有时您没有想要连接的接口。这可能有许多原因,但在这一章中,我们将关心我们能做些什么来改变我们被给予的东西,使之更接近我们想要的东西。
假设你想开发自己的营销软件。首先,编写一些发送电子邮件的代码。在更大的系统中,您可以这样调用:
send_email.py
import smtplib
from email.mime.text import MIMEText
def send_email(sender, recipients, subject, message):
msg = MIMEText(message)
msg['Subject'] = subject
msg['From'] = sender
msg['To'] = ",".join(recipients)
mail_sender = smtplib.SMTP('localhost')
mail_sender.send_message(msg)
mail_sender.quit()
if __name__ == "__main__":
response = send_email(
'me@example.com',
["peter@example.com", "paul@example.com", "john@example.com"],
"This is your message", "Have a good day"
)
接下来,你实现一个 email sender 类,因为到现在为止,你怀疑如果你要构建一个更大的系统,你将需要以面向对象的方式工作。EmailSender类处理向用户发送消息的细节。用户存储在逗号分隔值(CSV)文件中,您可以根据需要加载该文件。
users.csv
name, surname, email
peter, potter, peter@example.comma
...
下面是您的电子邮件发送类第一次运行时的样子:
mail_sender.py
import csv
import smtplib
from email.mime.text import MIMEText
class Mailer(object):
def send(sender, recipients, subject, message):
msg = MIMEText(message)
msg['Subject'] = subject
msg['From'] = sender
msg['To'] = [recipients]
s = smtplib.SMTP('localhost')
s.send_message(recipient)
s.quit()
if __name__ == "__main__":
with open('users.csv', 'r') as csv_file:
reader = csv.DictReader(csv_file)
users = [x for row in reader]
mailer = Mailer()
mailer.send(
'me@example.com',
[x["email"] for x in users],
"This is your message", "Have a good day"
)
为此,您可能需要在您的操作系统上设置某种电子邮件服务。如果你需要的话,Postfix 是一个不错的免费选择。在我们继续这个例子之前,我想介绍两个你会在本书中多次看到的设计原则。这些原则为编写更好的代码提供了清晰的指导。
不要重复自己(干)
像 DRY 这样的设计原则是你在任何软件项目中需要克服的问题和挑战的森林中的指路明灯。它们是你大脑深处的细小声音,指引你走向一个干净、清晰、可维护的解决方案。你会忍不住跳过它们,但这样做会让你感到不舒服。当你的代码中有没有人喜欢去的黑暗角落时,很可能是因为最初的开发人员没有遵循这些准则。我不会对你撒谎;开始的时候,很难。用正确的方法做事情往往会多花一点时间,但从长远来看,这是值得的。
在“四人帮”的书(关于面向对象设计模式的原始大部头)中,作者提出的解决常见问题的两个主要原则如下:
- 编程到接口,而不是实现
- 优先选择对象组合而不是继承
记住第一个原则,我们想改变获取用户数据的方式,这样我们就不再处理在EmailSender类中获取数据的实现。我们希望将这部分代码移到一个单独的类中,这样我们就不再关心如何检索数据了。这将允许我们将文件系统替换为某种数据库,甚至是远程微服务,如果将来需要的话。
关注点分离
对接口编程允许我们在获取用户详细信息的代码和发送消息的代码之间创建更好的分离。你越清晰地将你的系统分成不同的单元,每个单元都有自己的关注点或任务,这些部分彼此越独立,维护和扩展系统就越容易。只要你保持界面不变,你就可以切换到更新更好的做事方式,而不需要对你现有的系统进行全面的改造。在现实世界中,这可能意味着一个被 COBOL 库和磁带卡住的系统和一个与时俱进的系统之间的差别。它还有一个额外的好处,那就是如果你发现你早期做出的一个或多个设计决策是次优的,你可以更容易地进行修正。
这个设计原则被称为关注点分离(SoC ),迟早你会非常感激你遵守了它,或者非常遗憾你没有遵守。你已经被警告了。
让我们按照自己的建议,将用户信息的检索从EmailSender类中分离出来。
user_ fetcher.py
class UserFetcher(object):
def __init__(source):
self.source = source
def fetch_users():
with open(self.source, 'r') as csv_file:
reader = csv.DictReader(csv_file)
users = [x for row in reader]
return rows
mail_sender.py
import csv
import smtplib
from email.mime.text import MIMEText
class Mailer(object):
def send(sender, recipients, subject, message):
msg = MIMEText(message)
msg['Subject'] = subject
msg['From'] = sender
msg['To'] = [recipients]
s = smtplib.SMTP('localhost')
s.send_message(recipient)
s.quit()
if __name__ == "__main__":
user_fetcher = UserFetcher('users.csv')
mailer = Mailer()
mailer.send(
'me@example.com',
[x["email"] for x in user_fetcher.fetch_users()],
"This is your message", "Have a good day"
)
你能看出要做好这件事可能需要一点努力,但是当我们想在不改变实际发送电子邮件的代码的情况下加入 Postgres、Redis 或一些外部 API 时,会容易得多吗?
现在您已经有了这段向用户发送电子邮件的漂亮代码,您可能会收到一个请求,要求您的代码能够向脸书或 Twitter 这样的社交媒体平台发送状态更新。您不想更改您的工作邮件发送代码,并且您在网上找到了一个可以为您处理集成的另一方面的包或程序。您如何创建某种消息传递接口,使您能够向许多不同的平台发送消息,而无需更改发送代码来适应每个目标,或者重新发明轮子来适应您的需求?
我们要解决的更普遍的问题是:我有的界面不是我想要的界面!
对于大多数常见的软件问题,都有一个设计模式。让我们先处理一个示例问题,这样在我们为手头的问题实现它之前,我们可以对解决方案有一点直觉。
让我们从把问题过于简单化开始。我们想将一段代码插入另一段代码,但插头与插座不匹配。当你去另一个国家旅行时,想要给你的笔记本电脑充电,你需要找一个适配器来把你的充电器插头插到墙上的插座上。在我们的日常生活中,我们有用于在两叉插头和三叉插座之间切换的适配器,或者用于使用带有 HDMI 输出的 VGA 屏幕或其他物理设备。这些都是物理解决问题的接口,我有不是我想要的接口!
当你从头开始构建一切的时候,你将不会面临这个问题,因为你要决定你将在你的系统中使用的抽象层次,以及系统的不同部分将如何相互作用。在现实世界中,你很少能从头开始构建一切。你必须使用可用的插件包和工具。你需要以一种你从未考虑过的方式来扩展你一年前写的代码,没有足够的时间回去重做你以前做过的一切。每隔几周或几个月,你会意识到你不再是以前的程序员了——你变得更好了——但你仍然需要支持你以前做过的项目(和错误)。你将需要你没有的接口。你需要适配器。
样本问题
我们说我们想要一个适配器,但实际上我们想要一个我们没有的特定接口。
class WhatIHave(object):
def provided_function_1(self): pass
def provided_function_2(self): pass
class WhatIWant(object):
def required_function(): pass
我想到的第一个解决方案就是简单地改变我们想要的东西,使之适合我们现有的东西。当您只需要与单个对象进行交互时,您能够做到这一点,但是让我们想象我们还想使用第二个服务,现在我们有了第二个接口。我们可以从使用一个if语句将执行指向正确的接口开始:
class Client(object)
def __init__(some_object):
self.some_object = some_object
def do_something():
if self.some_object.__class__ == WhatIHave:
self.some_object.provided_function_2()
self.some_object.provided_function_1()
else if self.some_object.__class__ == WhatIWant:
self.some_object.required_function()
else:
print("Class of self.some_object not recognized")
在一个只有两个接口的系统中,这是一个不错的解决方案,但是到现在为止,你知道它很少会只存在于两个系统中。同样容易发生的是,程序的不同部分可能会使用相同的服务。突然之间,您需要修改多个文件来适应您引入的每个新界面。在前一章中,您意识到通过到处创建多个、通常是可伸缩的if语句来添加到一个系统中很少是可行的方法。
您希望创建一段代码,它位于您拥有的库或接口之间,并使它看起来像您想要的接口。添加另一个类似的服务只需要向该服务添加另一个接口,这样您的调用对象就与所提供的服务分离了。
类别适配器
解决这个问题的一个方法是创建几个接口,这些接口使用多态来继承预期的和提供的接口。因此,目标接口可以创建为一个纯接口类。
在下面的例子中,我们导入了一个第三方库,其中包含一个名为WhatIHave的类。这个类包含两个方法,provided_function_1和provided_function_ 2,我们用它们来构建客户端对象所需的函数。
from third_party import WhatIHave
class WhatIWant:
def required_function(self): pass
class MyAdapter(WhatIHave, WhatIWant):
def __init__(self, what_i_have):
self.what_i_have = what_i_have
def provided_function_1(self):
self.what_i_have.provided_function_1
def provided_function_2(self):
self.what_i_have.profided_function_2
def required_function(self):
self.provided_function_1()
self.provided_function_2()
class ClientObject():
def __init__(self, what_i_want):
self.what_i_want = what_i_want
def do_something():
self.what_i_want.required_function()
if __name__ == "__main__":
adaptee = WhatIHave()
adapter = MyAdapter(adaptee)
client = ClientObject(adapter)
client.do_something()
适配器存在于各种复杂程度。在 Python 中,适配器的概念超越了类及其实例。通常,可调用程序是通过 decorators、闭包和 functools 来适应的。
还有另一种方法可以使一个接口适应另一个接口,这种方法的核心甚至更加 Pythonic 化。
对象适配器模式
我们可以利用组合来代替使用继承来包装一个类。然后,适配器可以包含它包装的类,并调用包装对象的实例。这种方法进一步降低了实现的复杂性。
class ObjectAdapter(InterfaceSuperClass):
def __init__(self, what_i_have):
self.what_i_have = what_i_have
def required_function(self):
return self.what_i_have.provided_function_1()
def __getattr__(self, attr):
# Everything else is handled by the wrapped object
return getattr(self.what_i_have, attr)
我们的适配器现在只从InterfaceSuperClass继承,并在其构造函数中接受一个实例作为参数。然后实例被存储在self.what_i_have中。它实现了一个required_function方法,该方法返回调用其包装的WhatIHave对象的provided_function_1()方法的结果。对该类的所有其他调用都通过___ getattr__()方法传递给它的what_i_have实例。
您仍然只需要实现您正在适应的接口。ObjectAdapter不需要继承其余的WhatIHave接口;相反,它提供了__getattr__()来提供与WhatIHave类相同的接口,除了它自己实现的方法。
__getattr__()方法很像__init__()方法,是 Python 中的一个特殊方法。通常称为魔术方法,这类方法由前导和尾随双下划线表示,有时也称为双下划线或 dunder。当 Python 解释器对对象进行属性查找但找不到它时,调用__getattr__()方法,并将属性查找传递给self.what_i_have对象。
由于 Python 使用了一个叫做 duck typing 的概念,我们可以进一步简化我们的实现。
鸭子打字
鸭子打字说,如果一个动物走路像鸭子,叫起来像鸭子,那它就是鸭子。
用我们能理解的术语来说,在 Python 中,我们只关心一个对象是否提供了我们需要的接口元素。如果是的话,我们可以像使用接口实例一样使用它,而不需要声明一个公共父类。InterfaceSuperClass只是失去了它的用处,可以去掉。
现在,考虑一下ObjectAdapter,因为它充分利用了 duck 类型来摆脱InterfaceSuperClass继承。
from third_party import WhatIHave
class ObjectAdapter(object):
def __init__(self, what_i_have):
self.what_i_have = what_i_have
def required_function(self):
return self.what_i_have.provided_function_1()
def __getattr__(self, attr):
# Everything else is handeled by the wrapped object
return getattr(self.what_i_have, attr)
代码看起来没有任何不同,除了用默认对象代替InterfaceSuperClass作为ObjectAdapter类的父类。这意味着我们永远不必定义一些超类来使一个接口适应另一个接口。我们遵循了适配器模式的意图和精神,没有不太动态的语言所带来的所有规避。
记住,你不是我们领域经典文本中事物定义方式的奴隶。随着技术的发展和变化,我们这些技术的实现者也必须如此。
我们新版本的适配器模式使用组合而不是继承来交付这种结构设计模式的更 Pythonic 化的版本。因此,我们不需要访问接口类的源代码。这反过来允许我们不违反 Eiffel 编程语言的创造者 Bertrand Meyer 提出的开放/封闭原则,以及契约式设计的思想。
开放/封闭原则规定:软件实体(类、模块、函数等。)应该对扩展开放,但对修改关闭。
您希望在不修改源代码的情况下扩展实体的行为。
当你想学习新的东西时,特别是当它不是琐碎的信息时,你需要在深层次上理解这些信息。加深理解的一个有效策略是将大图解构为小部分,然后将它们简化为可消化的块。然后你用这些简单的组块来发展对问题及其解决方案的直觉。一旦你对自己的能力有了信心,你就加入了更多的复杂性。一旦你对部分有了深刻的理解,你继续把它们串起来,你会发现你对整体有了清晰得多的感觉。每当你想开始探索一个新的想法时,你可以使用我们刚刚遵循的过程。
在现实世界中实现适配器模式
既然您现在已经对适配器模式的清晰的 pythonic 式实现有了感觉,那么让我们将这种直觉应用到我们在本章开始时看到的例子中。我们将创建一个适配器,它将允许我们向命令行写入文本,以便向您展示适配器模式的实际效果。作为练习的一部分,您将被要求为 Python 创建一个社会网络库的适配器,并将其与下面的代码集成。
import csv
import smtplib
from email.mime.text import MIMEText
class Mailer(object):
def send(sender, recipients, subject, message):
msg = MIMEText(message)
msg['Subject'] = subject
msg['From'] = sender
msg['To'] = [recipients]
s = smtplib.SMTP('localhost')
s.send_message(recipient)
s.quit()
class Logger(object):
def output(message):
print("[Logger]".format(message))
class LoggerAdapter(object):
def __init__(self, what_i_have):
self.what_i_have = what_i_have
def send(sender, recipients, subject, message):
log_message = "From: {}\nTo: {}\nSubject: {}\nMessage: {}".format(
sender,
recipients,
subject,
message
)
self.what_i_have.output(log_message)
def __getattr__(self, attr):
return getattr(self.what_i_have, attr)
if __name__ == "__main__":
user_fetcher = UserFetcher('users.csv')
mailer = Mailer() mailer.send(
'me@example.com',
[x["email"] for x in user_fetcher.fetch_users()],
"This is your message", "Have a good day"
)
你也可以使用你在第一章开发的Logger类。在前面的例子中,Logger类只是一个例子。
对于您想要适应的每个新接口,您需要编写一个新的AdapterObject类型类。其中每一个都在其构造函数中用两个参数构造,其中一个是 adaptee 的实例。每个适配器还需要用所需的参数实现send_message(self, topic, message_body)方法,这样就可以从代码中的任何地方使用所需的接口调用它。
如果我们传递给每个provided_function的参数与传递给required_function的参数保持不变,我们就可以创建一个更通用的适配器,它不再需要知道关于适配器的任何事情;我们只需给它提供一个对象和provided_function,就大功告成了。
这是这个想法的一般实现:
class ObjectAdapter(object):
def __init__(self, what_i_have, provided_function):
self.what_i_have = what_i_have
self.required_function = provided_function
def __getattr__(self, attr):
return getattr(self.what_i_have, attr)
以这种方式实现消息发送适配器也是本章末尾的练习之一。到现在为止,您可能已经意识到您必须自己完成越来越多的实现工作。这是故意的。目的是在学习过程中指导你。您应该实现这些代码,以便对它们所涉及的概念有更多的直觉。一旦你有了一个工作版本,摆弄它,破坏它,然后改进它。
临别赠言
您已经看到,当您需要在设计好适配器之后让它们工作时,适配器是非常有用的。这是一种向被提供接口的主体提供不同于所提供接口的接口的方式。
适配器模式具有以下元素:
- 目标——定义客户端使用的特定于域的接口
- 客户端-使用符合目标接口的对象
- adaptee——因为对象不符合目标而必须改变的接口
- 适配器——将适配器中的内容更改为客户机中的内容的代码
要实现适配器模式,请遵循以下简单过程:
- 定义您想要容纳的组件。
- 确定客户需要的接口。
- 设计和实现适配器,以将客户机所需的接口映射到适配器提供的接口。
客户端从适配器解耦,并耦合到接口。这为您提供了可扩展性和可维护性。
练习
- 扩展 user-fetcher 类,使其能够从 CSV 文件或 SQLite 数据库中获取数据。
- 选择任何一个现代的社交网络,它提供一个 API 来与网络进行交互并发布某种消息。现在,为该社交网络安装 Python 库,并为该库创建一个类和一个对象适配器,这样你在本章中开发的代码就可以用来向该社交网络发送消息。一旦完成,你可以祝贺自己建立了许多提供预定社交媒体信息的大企业的基础。
- 重写上一个练习中的代码,将通用适配器用于您实现的所有接口。
七、装饰器模式
对那些利用时间的人来说,时间停留得够久了。—莱昂纳多·达芬奇
随着您变得越来越有经验,您会发现自己已经超越了可以用最通用的编程结构轻松解决的问题类型。这时,你会意识到你需要一套不同的工具。本章将重点介绍一个这样的工具。
当您试图从您的机器中挤出最后一点性能时,您需要确切地知道一段特定的代码执行需要多长时间。在启动要分析的代码之前,必须节省系统时间,然后执行代码;一旦结束,您必须保存第二个系统时间戳。最后,从第二个时间中减去第一个时间,以获得执行过程中经过的时间。看看这个计算斐波纳契数的例子。
纤维。巴拉圭
import time
n = 77
start_time = time.time()
fibPrev = 1
fib = 1
for num in range(2, n):
fibPrev, fib = fib, fib + fibPrev
end_time = time.time()
print("[Time elapsed for n = {}] {}".format(n, end_time - start_time))
print("Fibonacci number for n = {}: {}".format(n, fibIter(n)))
每个系统都是不同的,但是您应该会得到如下所示的结果:
[Time elapsed for n = 77] 8.344650268554688e-06
Fibonacci number for n = 77: 5527939700884757
我们现在扩展我们的 Fibonacci 代码,这样我们就可以请求我们想要检索的 Fibonacci 序列中的元素数。我们通过将斐波那契计算封装在一个函数中来实现这一点。
fib_ 函数. py
import time
def fib(n):
start_time = time.time()
if n < 2:
return
fibPrev = 1
fib = 1
for num in range(2, n):
fibPrev, fib = fib, fib + fibPrev
end_time = time.time()
print("[Time elapsed for n = {}] {}".format(n, end_time - start_time))
return fib
if __name__ == "__main__":
n = 77
print("Fibonacci number for n = {}: {}".format(n, fib(n)))
在前面的代码中,分析发生在函数本身之外。当您只想查看单个函数时,这是没问题的,但当您优化程序时,情况通常不是这样。幸运的是,函数是 Python 中的一等公民,因此我们可以将函数作为参数传递给另一个函数。因此,让我们创建一个函数来计时另一个函数的执行。
纤维 _ 功能 _ 轮廓 _ 我. py
import time
def fib(n):
if n < 2:
return
fibPrev = 1
fib = 1
for num in range(2, n):
fibPrev, fib = fib, fib + fibPrev
return fib
def profile_me(f, n):
start_time = time.time()
result = f(n)
end_time = time.time()
print("[Time elapsed for n = {}] {}".format(n, end_time - start_time))
return result
if __name__ == "__main__":
n = 77
print("Fibonacci number for n = {}: {}".format(n, profile_me(fib, n)))
每当我们想要获得执行一个函数所花费的时间,我们可以简单地将该函数传递给分析函数,然后像往常一样运行它。这种方法确实有一些限制,因为您必须预先定义要应用于定时函数的参数。Python 又一次帮助了我们,它允许我们返回函数。我们现在返回添加了概要分析的函数,而不是调用函数。
base_profiled_ fib.py
import time
def fib(n):
if n < 2:
return
fibPrev = 1
fib = 1
for num in range(2, n):
fibPrev, fib = fib, fib + fibPrev
return fib
def profile_me(f, n):
start_time = time.time()
result = f(n)
end_time = time.time()
print("[Time elapsed for n = {}] {}".format(n, end_time - start_time))
return result
def get_profiled_function(f):
return lambda n: profile_me(f, n)
if __name__ == "__main__":
n = 77
fib_profiled = get_profiled_function(fib)
print("Fibonacci number for n = {}: {}".format(n, fib_profiled(n)))
这种方法工作得更好,但是当试图分析几个函数时仍然需要一些努力,因为这会干扰执行函数的代码。一定有更好的方法。理想情况下,我们希望有一种方法将特定的函数标记为要分析的函数,然后不用担心启动函数调用的调用。我们将使用装饰器模式来实现这一点。
装饰器模式
要修饰一个函数,我们需要返回一个可以作为函数使用的对象。decorator 模式的经典实现利用了 Python 实现常规过程函数的方式,这些函数可以被视为具有某种执行方法的类。在 Python 中,一切都是对象;函数是具有特殊__call__()方法的对象。如果我们的 decorator 用一个__call__()方法返回一个对象,结果可以作为一个函数使用。
装饰器模式的经典实现并不使用 Python 中可用的装饰器。同样,我们将选择更 Python 化的 decorator 模式实现,因此我们将利用 Python 中 decorator 的内置语法,使用@符号。
class _ decorated _ profiled _ fib . py
import time
class ProfilingDecorator(object):
def __init__(self, f):
print("Profiling decorator initiated")
self.f = f
def __call__(self, *args):
start_time = time.time()
result = self.f(*args)
end_time = time.time()
print("[Time elapsed for n = {}] {}".format(n, end_time - start_time))
return result
@ProfilingDecorator
def fib(n):
print("Inside fib")
if n < 2:
return
fibPrev = 1
fib = 1
for num in range(2, n):
fibPrev, fib = fib, fib + fibPrev
return fib
if __name__ == "__main__":
n = 77
print("Fibonacci number for n = {}: {}".format(n, fib(n)))
fib函数中的print语句只是用来显示执行相对于分析装饰的位置。一旦您看到它在运行,就删除它,以免影响计算所用的实际时间。
Profiling decorator initiated
Inside fib
[Time elapsed for n = 77] 1.1682510375976562e-05
Fibonacci number for n = 77: 5527939700884757
在类定义中,我们看到修饰函数在初始化过程中被保存为对象的一个属性。然后,在call函数中,被修饰的函数的实际运行被包装在时间请求中。就在返回之前,配置文件值被打印到控制台。当编译器遇到_@ProfilingDecorator_时,它会启动一个对象,并将被包装的函数作为参数传递给构造函数。返回的对象具有作为方法的__ call__()函数,因此鸭子类型将允许该对象作为函数使用。另外,注意我们如何在__call__()方法的参数中使用*args来传递参数。允许我们处理多个传入的参数。这被称为打包,因为所有传入的参数都被打包到args变量中。然后,当我们调用装饰对象的f属性中的存储函数时,我们再次使用*args。这被称为解包,它将args中集合的所有元素转化为传递给相关函数的独立参数。
装饰器是一个一元函数(接受单个参数的函数),它接受要装饰的函数作为其参数。正如您在前面看到的,它返回一个与原始函数相同的函数,只是增加了一些功能。这意味着所有与被修饰的函数交互的代码都可以保持与函数未被修饰时相同。这允许我们堆叠装饰器。因此,在一个愚蠢的例子中,我们可以分析我们的 Fibonacci 函数,然后将结果字符串输出为 HTML。
stacked_ fib.py
import time
class ProfilingDecorator(object):
def __init__(self, f):
print("Profiling decorator initiated")
self.f = f
def __call__(self, *args):
print("ProfilingDecorator called")
start_time = time.time()
result = self.f(*args)
end_time = time.time()
print("[Time elapsed for n = {}] {}".format(n, end_time - start_time))
return result
class ToHTMLDecorator(object):
def __init__(self, f):
print("HTML wrapper initiated")
self.f = f
def __call__(self, *args):
print("ToHTMLDecorator called")
return "<html><body>{}</body></html>".format(self.f(*args))
@ToHTMLDecorator
@ProfilingDecorator
def fib(n):
print("Inside fib")
if n < 2:
return
fibPrev = 1
fib = 1
for num in range(2, n):
fibPrev, fib = fib, fib + fibPrev
return fib
if __name__ == "__main__":
n = 77
print("Fibonacci number for n = {}: {}".format(n, fib(n)))
这将产生以下输出。当装饰一个函数时,要注意你包装的函数的输出类型是什么,以及装饰器的结果的输出类型是什么。通常,您希望保持类型一致,这样使用未修饰函数的函数就不需要更改。在本例中,我们将类型从数字更改为 HTML 字符串,但这不是您通常想要做的事情。
Profiling decorator initiated
HTML wrapper initiated
ToHTMLDecorator called
ProfilingDecorator called
Inside fib
[Time elapsed for n = 77] 1.52587890625e-05
Fibonacci number for n = 77: <html><body>5527939700884757</body></html>
请记住,我们能够使用一个类来修饰一个函数,因为在 Python 中,只要对象有一个__call__()方法,一切都是对象,所以它可以像函数一样使用。现在,让我们来探索如何反向使用 Python 的这个属性。与其用类来修饰函数,不如直接用函数来修饰我们的fib函数。
函数 _ 装饰 _ 纤维. py
import time
def profiling_decorator(f):
def wrapped_f(n):
start_time = time.time()
result = f(n)
end_time = time.time()
print("[Time elapsed for n = {}] {}".format(n, end_time - start_time))
return result
return wrapped_f
@profiling_decorator
def fib(n):
print("Inside fib")
if n < 2:
return
fibPrev = 1
fib = 1
for num in range(2, n):
fibPrev, fib = fib, fib + fibPrev
return fib
if __name__ == "__main__":
n = 77
print("Fibonacci number for n = {}: {}".format(n, fib(n)))
准备好接受一点疯狂吧。decorator 函数必须返回一个在被修饰的函数被调用时使用的函数。返回的函数用于包装被修饰的函数。调用函数时会创建wrapped_f()函数,因此它可以访问传递给profiling_decorator()函数的所有参数。这种将一些数据(在本例中,传递给profiling_decorator()函数的函数)附加到代码上的技术在 Python 中被称为闭包。
关闭
让我们看看访问非局部变量f的wrapped_f()函数,该变量作为参数传递给profiling_decorator()函数。当wrapped_f()函数被创建时,非局部变量f的值被存储并作为函数的一部分返回,即使最初的变量移出了作用域,并在程序退出profiling_decorator()函数时从名称空间中删除。
简而言之,要有一个闭包,你必须让一个函数返回嵌套在它自己内部的另一个函数,嵌套函数引用封闭函数范围内的一个变量。
保留函数 name 和 doc 属性
如前所述,理想情况下,您不希望使用 decorator 的任何函数以任何方式被更改,但是我们在上一节中实现包装函数的方式导致函数的__name__和__doc__属性更改为wrapped_f()函数的属性。查看以下脚本的输出,了解__name__属性是如何变化的。
func_attrs.py
def dummy_decorator(f):
def wrap_f():
print("Function to be decorated: ", f.__name__)
print("Nested wrapping function: ", wrap_f.__name__)
return f()
return wrap_f
@dummy_decorator
def do_nothing():
print("Inside do_nothing")
if __name__ == "__main__":
print("Wrapped function: ",do_nothing.__name__)
do_nothing()
检查以下结果;被包装的函数采用了wrap函数的名称。
Wrapped function: wrap_f
Function to be decorated: do_nothing
Nested wrapping function: wrap_f
Inside do_nothing
为了保持被包装的函数的__name__和__doc__属性,我们必须在离开装饰函数的范围之前将它们设置为等于传入的函数。
def dummy_decorator(f):
def wrap_f():
print("Function to be decorated: ", f.__name__)
print("Nested wrapping function: ", wrap_f.__name__)
return f()
wrap_f.__name__ = f.__name__
wrap_f.__doc__ = wrap_f.__doc__
return wrap_f
@dummy_decorator
def do_nothing():
print("Inside do_nothing")
if __name__ == "__main__":
print("Wrapped function: ",do_nothing.__name__)
do_nothing()
现在,do_nothing()函数和修饰过的do_nothing()函数之间不再有区别。
Python 标准库包括一个模块,该模块允许保留__name__和__doc__属性,而无需我们自己进行设置。在本章的上下文中,更好的是模块通过对包装函数应用装饰器来实现这一点。
from functools import wraps
def dummy_decorator(f):
@wraps(f)
def wrap_f():
print("Function to be decorated: ", f.__name__)
print("Nested wrapping function: ", wrap_f.__name__)
return f()
return wrap_f
@dummy_decorator
def do_nothing():
print("Inside do_nothing")
if __name__ == "__main__":
print("Wrapped function: ",do_nothing.__name__)
do_nothing()
如果我们想选择打印时间的单位,比如秒而不是毫秒,会怎么样呢?我们必须找到一种方法将参数传递给装饰器。为此,我们将使用一个装饰工厂。首先,我们创建一个装饰函数。然后,我们将它扩展成一个装饰工厂,修改或替换包装器。然后工厂返回更新后的装饰器。
让我们看看包含了functools包装器的斐波那契代码。
import time
from functools import wraps
def profiling_decorator(f):
@wraps(f)
def wrap_f(n):
start_time = time.time()
result = f(n)
end_time = time.time()
elapsed_time = (end_time - start_time)
print("[Time elapsed for n = {}] {}".format(n, elapsed_time))
return result
return wrap_f
@profiling_decorator
def fib(n):
print("Inside fib")
if n < 2:
return
fibPrev = 1
fib = 1
for num in range(2, n):
fibPrev, fib = fib, fib + fibPrev
return fib
if __name__ == "__main__":
n = 77
print("Fibonacci number for n = {}: {}".format(n, fib(n)))
现在,让我们扩展代码,这样我们可以传入一个选项,以毫秒或秒为单位显示经过的时间。
import time
from functools import wraps
def profiling_decorator_with_unit(unit):
def profiling_decorator(f):
@wraps(f)
def wrap_f(n):
start_time = time.time()
result = f(n)
end_time = time.time()
if unit == "seconds":
elapsed_time = (end_time - start_time) / 1000
else:
elapsed_time = (end_time - start_time)
print("[Time elapsed for n = {}] {}".format(n, elapsed_time))
return result
return wrap_f
return profiling_decorator
@profiling_decorator_with_unit("seconds")
def fib(n):
print("Inside fib")
if n < 2:
return
fibPrev = 1
fib = 1
for num in range(2, n):
fibPrev, fib = fib, fib + fibPrev
return fib
if __name__ == "__main__":
n = 77
print("Fibonacci number for n = {}: {}".format(n, fib(n)))
和以前一样,当编译器到达\@profiling_decorator_with_unit时,它调用函数,函数返回装饰器,然后装饰器被应用到被装饰的函数。您还应该注意到,我们再次使用闭包概念来处理传递给装饰器的参数。
装饰课
如果我们想分析特定类中的每个方法调用,我们的代码应该是这样的:
class DoMathStuff(object):
@profiling_decorator
def fib(self):
...
@profiling_decorator
def factorial(self):
...
对类中的每个方法应用相同的方法会非常好,但是它违反了 DRY 原则(不要重复自己)。如果我们对性能感到满意,并且需要从大量杂乱的类中取出分析代码,会发生什么呢?如果我们在类中添加了方法,却忘了修饰这些新添加的方法,那该怎么办?一定有更好的方法,而且确实有。
我们希望能够做的是修饰类,并让 Python 知道将修饰应用于类中的每个方法。
我们想要的代码应该是这样的:
@profile_all_class_methods
class DoMathStuff(object):
def fib(self):
...
@profiling_decorator
def factorial(self):
...
本质上,我们想要的是一个从外部看起来与DoMathStuff类一模一样的类,唯一的区别是每个方法调用都应该被分析。
我们在上一节中编写的分析代码应该仍然有用,所以让我们利用它,使包装函数更加通用,以便它可以为传递给它的任何函数工作。
import time
def profiling_wrapper(f):
@wraps(f)
def wrap_f(*args, **kwargs):
start_time = time.time()
result = f(*args, **kwargs)
end_time = time.time()
elapsed_time = end_time - start_time
print("[Time elapsed for n = {}] {}".format(n, elapsed_time))
return result
return wrap_f
我们收到了两个打包的参数:一个常规参数列表和一个映射到关键字的参数列表。在它们之间,这两个参数将捕获可以传递给 Python 函数的任何形式的参数。当我们将它们传递给要包装的函数时,我们也对它们进行解包。
现在,我们想要创建一个类,它将包装一个类,并将装饰函数应用于该类中的每个方法,并返回一个看起来与它接收到的类没有什么不同的类。这是通过__ getattribute__()的魔法方法实现的。Python 使用这个方法来检索对象的方法和属性,通过覆盖这个方法,我们可以根据需要添加装饰器。由于__getattribute__()返回方法和值,我们还需要检查请求的属性是方法。
class_profiler.py
import time
def profiling_wrapper(f):
@wraps(f)
def wrap_f(*args, **kwargs):
start_time = time.time()
result = f(*args, **kwargs)
end_time = time.time()
elapsed_time = end_time - start_time
print("[Time elapsed for n = {}] {}".format(n, elapsed_time))
return result
return wrap_f
def profile_all_class_methods(Cls):
class ProfiledClass(object):
def __init__(self, *args, **kwargs):
self.inst = Cls(*args, **kwargs)
def __getattribute__(self, s):
try:
x = super(ProfiledClass, self).__getattribute__(s)
except AttributeError:
pass
else:
x = self.inst.__getattribute__(s)
if type(x) == type(self.__init__):
return profiling_wrapper(x)
else:
return x
return ProfiledClass
@profile_all_class_methods
class DoMathStuff(object):
def fib(self):
...
@profiling_decorator
def factorial(self):
...
你有它!现在可以修饰类和函数了。如果您下载并阅读 Flask 源代码(从官方网站: http://flask.pocoo.org/ )会对您有所帮助,特别注意他们是如何利用 decorators 来处理路由的。另一个你可能会感兴趣的包是 Django Rest 框架(来自官方网站: http://www.django-rest-framework.org/ ),它使用 decorators 来改变返回值。
临别赠言
装饰器模式与适配器和外观模式的不同之处在于,接口不会改变,但会以某种方式添加新的功能。所以,如果你有一个特定的功能,你想把它附加到一些函数上,在不改变它们的接口的情况下改变这些函数,装饰器可能是个不错的选择。
练习
- 扩展 Fibonacci 函数包装器,使其也返回以分钟和小时为单位的时间,打印为“hrs:minutes:seconds .毫秒”。
- 列出你能想到的可以实现 decorators 的十种常见情况。
- 阅读 Flask 源代码,写下你遇到的每一个有趣的 decorators 用法。
八、外观模式
歌剧魅影就在那里,在我的脑海里。—克里斯汀
在这一章中,我们将会看到另一种向系统用户隐藏系统复杂性的方法。对外界来说,所有移动的部分应该看起来像一个单一的实体。
所有系统都比乍看起来更复杂。以销售点(POS)系统为例。大多数人只是作为收银台另一边的客户与这样的系统进行交互。在一个简单的例子中,发生的每一笔交易都需要一些事情发生。需要记录销售情况,需要调整交易中每件商品的库存水平,甚至可能需要为老顾客申请某种忠诚度积分。这些系统中的大多数还允许考虑交易中的项目的特价,如买两个并免费获得最便宜的一个。
销售点示例
在这个交易过程中涉及到许多东西,比如忠诚度系统、股票系统、特价或促销系统、任何支付系统,以及使交易过程正常进行所需的任何东西。对于这些交互中的每一个,我们将有不同的类来提供所讨论的功能。
不难想象流程事务的实现会是这样的:
import datetime
import random
class Invoice(object):
def __init__(self, customer):
self.timestamp = datetime.datetime.now()
self.number = self.generate_number()
self.lines = []
self.total = 0
self.tax = 0
self.customer = customer
def save(self):
# Save the invoice to some sort of persistent storage
pass
def send_to_printer(self):
# Send invoice representation to external printer
pass
def add_line(self, invoice_line):
self.lines.append(invoice_line)
self.calculate()
def remove_line(self, line_item):
try:
self.lines.remove(line_item)
except ValueError as e:
print("could not remove {} because there is no such item in the invoice".format(line_item))
def calculate(self):
self.total = sum(x.total * x.amount for x in self.lines)
self.tax = sum(x.total * x.tax_rate for x in self.lines)
def generate_number(self):
rand = random.randint(1, 1000)
return "{}{}".fomat(self.timestamp, rand)
class InvoiceLine(object):
def __init__(self, line_item):
# turn line item into an invoice line containing the current price etc
pass
def save(self):
# save invoice line to persistent storage
pass
class Receipt(object):
def __init__(self, invoice, payment_type):
self.invoice = invoice
self.customer = invoice.customer
self.payment_type = payment_type
pass
def save(self):
# Save receipt to persistent storage
pass
class Item(object):
def __init__(self):
pass
@classmethod
def fetch(cls, item_barcode):
# fetch item from persistent storage
pass
def save(self):
# Save item to persistent storage
pass
class Customer(object):
def __init__(self):
pass
@classmethod
def fetch(cls, customer_code):
# fetch customer from persistent storage
pass
def save(self):
# save customer to persistent storage
pass
class LoyaltyAccount(object):
def __init__(self):
pass
@classmethod
def fetch(cls, customer):
# fetch loyaltyAccount from persistent storage
pass
def calculate(self, invoice):
# Calculate the loyalty points received for this purchase
pass
def save(self):
# save loyalty account back to persistent storage
pass
如果您想要处理一笔销售,您必须与所有这些类进行交互。
def complex_sales_processor(customer_code, item_dict_list, payment_type):
customer = Customer.fetch_customer(customer_code)
invoice = Invoice()
for item_dict in item_dict_list:
item = Item.fetch(item_dict["barcode"])
item.amount_in_stock - item_dict["amount_purchased"]
item.save()
invoice_line = InvoiceLine(item)
invoice.add_line(invoice_line)
invoice.calculate()
invoice.save()
loyalt_account = LoyaltyAccount.fetch(customer)
loyalty_account.calculate(invoice)
loyalty_account.save()
receipt = Receipt(invoice, payment_type)
receipt.save()
如您所见,无论哪个系统负责处理销售,都需要与特定于销售点子系统的大量类进行交互。
系统进化
系统会进化——这是生活的现实——随着系统的成长,它们会变得非常复杂。为了保持整个系统的简单,我们希望对客户端隐藏所有的功能。我知道你一看到前面的片段就闻到了。一切都是紧密耦合的,我们有大量的类要与之交互。当我们想要用一些新的技术或功能来扩展我们的 POS 时,我们将需要深入系统的内部。我们可能会忘记更新一些隐藏的小代码,这样我们的整个系统就会崩溃。我们希望在不损失任何系统功能的情况下简化该系统。
理想情况下,我们希望我们的事务处理器看起来像这样:
def nice_sales_processor(customer_code, item_dict_list, payment_type):
invoice = SomeInvoiceClass(customer_code)
for item_dict in item_dict_list:
invoice.add_item(item_dict["barcode"], item_dict_list["amount_purchased"])
invoice.finalize()
invoice.generate_receipt(payment_type)
这使得代码更加清晰易读。面向对象编程中一个简单的经验法则是,每当你遇到一个丑陋或混乱的系统,把它藏在一个对象中。
您可以想象,复杂或丑陋系统的问题是程序员生活中经常遇到的问题,因此您可以肯定,对于如何最好地解决这个问题,有几种观点。一个这样的解决方案是使用一种叫做外观模式的设计模式。这种模式专门用于创建子系统或子系统集合的复杂性限制接口。请看下面这段代码,以便更好地理解这种通过 facade 模式的抽象应该如何发生。
simple_ facade.py
class Invoice:
def __init__(self, customer): pass
class Customer:
# Altered customer class to try to fetch a customer on init or creates a new one
def __init__(self, customer_id): pass
class Item:
# Altered item class to try to fetch an item on init or creates a new one
def __init__(self, item_barcode): pass
class Facade:
@staticmethod
def make_invoice(customer): return Invoice(customer)
@staticmethod
def make_customer(customer_id): return Customer(customer_id)
@staticmethod
def make_item(item_barcode): return Item(item_barcode)
# ...
在继续阅读之前,请尝试自己实现 POS 系统的外观模式。
是什么让门面模式与众不同
facade 模式的一个关键区别是,facade 不仅用于封装单个对象,还用于提供一个包装器,该包装器向一组复杂的子系统提供一个简化的接口,该接口易于使用,没有不必要的功能或复杂性。只要有可能,您希望限制向系统外的世界暴露的知识量和/或复杂性。您越是允许访问一个系统并与之交互,这个系统就变得越紧密。允许更深入地访问您的系统的最终结果是一个可以像黑匣子一样使用的系统,具有一组清晰的预期输入和定义良好的输出。facade 应该决定使用哪个内部类和表示。也是在 facade 中,当时机成熟时,您将对分配给外部或更新的内部系统的功能进行更改。
facade 经常在软件库中使用,因为 facade 提供的方便的方法使库更容易理解和使用。因为与库的交互是通过外观进行的,所以使用外观也减少了库外部对其内部工作的依赖性。对于任何其他系统,这允许在开发库时有更大的灵活性。
并非所有的 API 都是生来平等的,您将不得不处理设计糟糕的 API 集合。然而,使用 facade 模式,您可以将这些 API 包装在一个设计良好的 API 中,这是一种乐趣。
你可以养成的最有用的习惯之一就是为自己的问题创造解决方案。如果你有一个不太理想的系统,为它建立一个门面,让它在你的环境中工作。如果您的 IDE 缺少您喜欢的工具,编写您自己的扩展来提供这个功能。不要等待其他人来提供解决方案。每天,当你采取行动来改变你的世界以适应你的需求时,你会感到更有力量。
让我们将 POS 后端实现为这样一个门面。因为我们不想构建整个系统,所以定义的几个函数将是存根,如果您愿意,可以对其进行扩展。
class Sale(object):
def __init__(self):
pass
@staticmethod
def make_invoice(customer_id):
return Invoice(Customer.fetch(customer_id))
@staticmethod
def make_customer():
return Customer()
@staticmethod
def make_item(item_barcode):
return Item(item_barcode)
@staticmethod
def make_invoice_line(item):
return InvoiceLine(item)
@staticmethod
def make_receipt(invoice, payment_type):
return Receipt(invoice, payment_type)
@staticmethod
def make_loyalty_account(customer):
return LoyaltyAccount(customer)
@staticmethod
def fetch_invoice(invoice_id):
return Invoice(customer)
@staticmethod
def fetch_customer(customer_id):
return Customer(customer_id)
@staticmethod
def fetch_item(item_barcode):
return Item(item_barcode)
@staticmethod
def fetch_invoice_line(line_item_id):
return InvoiceLine(item)
@staticmethod
def fetch_receipts(invoice_id):
return Receipt(invoice, payment_type)
@staticmethod
def fetch_loyalty_account(customer_id):
return LoyaltyAccount(customer)
@staticmethod
def add_item(invoice, item_barcode, amount_purchased):
item = Item.fetch(item_barcode)
item.amount_in_stock - amount_purchased
item.save()
invoice_line = InvoiceLine.make(item)
invoice.add_line(invoice_line)
@staticmethod
def finalize(invoice):
invoice.calculate()
invoice.save()
loyalt_account = LoyaltyAccount.fetch(invoice.customer)
loyalty_account.calculate(invoice)
loyalty_account.save()
@staticmethod
def generate_receipt(invoice, payment_type):
receipt = Receipt(invoice, payment_type)
receipt.save()
使用这个新的Sales类,它作为系统其余部分的门面,我们前面看到的理想函数现在看起来像这样:
def nice_sales_processor(customer_id, item_dict_list, payment_type):
invoice = Sale.make_invoice(customer_id)
for item_dict in item_dict_list:
Sale.add_item(invoice, item_dict["barcode"], item_dict_list["amount_purchased"])
Sale.finalize(invoice)
Sale.generate_receipt(invoice, payment_type)
如你所见,没什么变化。
Sales的代码导致了更复杂的业务系统的单一入口点。我们现在有一组有限的函数可以通过 facade 调用,它允许我们与需要访问的系统部分进行交互,而不会让我们在系统的所有内部事务上负担过重。我们还有一个单独的类可以交互,所以我们不再需要在整个类图中寻找正确的地方来连接系统。
在子系统中,第三方股票管理系统或忠诚度程序可以取代内部系统,而无需使用 facade 类的客户修改一行代码。因此,我们的代码更加松散耦合,也更容易扩展。POS 客户不关心我们是否决定将库存和会计工作交给第三方提供商,或者构建和运行一个内部系统。简化的界面仍然幸福地不知道引擎盖下的复杂性,而不降低子系统固有的功率。
临别赠言
门面格局没什么。一开始,你会对一个子系统或一组子系统没有按照你想要的方式工作感到沮丧,或者你发现自己不得不在许多课程中查找交互。到目前为止,您已经知道有些事情不太对劲,所以您决定清理这些乱七八糟的东西,或者至少将它们隐藏在一个单一的、优雅的界面后面。您设计您的包装器类来将丑陋的子系统变成一个黑盒,为您处理交互和复杂性。您的代码的其余部分,以及其他潜在的客户端代码,只处理这个外观。因为你的努力,这个世界现在变得更好了一点点——干得好。
练习
- 当外观不需要跟踪状态时,它们通常被实现为单件。在最后一段代码中修改 facade 实现,使其使用第二章中的单例模式。
- 扩展本章概述的销售点系统,直到你有一个可以在现实世界中使用的系统。
- 选择你最喜欢的社交媒体平台,创建你自己的门面,让你与他们的 API 互动。
九、代理模式
先生,你对我们咬拇指吗?—亚伯兰、罗密欧与朱丽叶
随着程序的成长,你经常会发现有一些你经常调用的函数。当这些计算繁重或缓慢时,您的整个程序都会受到影响。
请考虑以下计算数字 n 的斐波那契数列的示例:
def fib(n):
if n < 2:
return 1
return fib(n - 2) + fib(n - 1)
这个相当简单的递归函数有一个严重的缺陷,特别是当 n 变得非常大的时候。你能找出问题可能是什么吗?
如果你认为当你想计算一个大于 2 的 n 的斐波那契数时,某个f(x)的值必须被计算多次,那你就对了。
有很多方法可以解决这个问题,但是我想用一个非常特别的方法,叫做记忆化。
记忆化
每当我们有一个函数被多次调用,其值被重复时,存储计算的响应将是有用的,这样我们就不必再次经历计算值的过程;我们宁愿只获取已经计算过的值并返回。这种保存函数调用结果以备后用的行为叫做记忆化。
现在,让我们看看记忆化对我们的简单例子会有什么影响。
import time
def fib(n):
if n < 2:
return 1
return fib(n - 2) + fib(n - 1)
if __name__ == "__main__":
start_time = time.time()
fib_sequence = [fib(x) for x in range(1,80)]
end_time = time.time()
print(
"Calculating the list of {} Fibonacci numbers took {} seconds".format(
len(fib_sequence),
end_time - start_time
)
)
让这个运行一段时间;查看计算从 0 到 40 的斐波纳契数所花费的时间。
在我的电脑上,我得到了以下结果:
Calculating the list of 80 Fibonacci numbers took 64.29540348052979 seconds
一些新系统嘲笑仅仅 80 个斐波那契数。如果你在那种情况下,你可以试着增加数量级——80,800,8000——直到你偶然发现一个显示负载的数字,而不用花很长时间来完成。在本章的其余部分使用这个数字。
如果我们缓存每次调用的结果,会对计算产生什么影响?
让我们实现和以前一样的函数,但是这次我们将利用一个额外的字典来跟踪我们以前已经计算过的请求。
import time
def fib_cached1(n, cache):
if n < 2:
return 1
if n in cache:
return cache[n]
cache[n] = fib_cached1(n - 2, cache) + fib_cached1(n - 1, cache)
return cache[n]
if __name__ == "__main__":
cache = {}
start_time = time.time()
fib_sequence = [fib_cached1(x, cache) for x in range(0, 80)]
end_time = time.time()
print(
"Calculating the list of {} Fibonacci numbers took {} seconds".format(
len(fib_sequence),
end_time - start_time
)
)
运行这段代码。在我的机器上,同样的一系列计算现在给出了以下输出:
Calculating the list of 80 Fibonacci numbers took 4.7206878662109375e-05 seconds
这是一个非常好的结果,以至于我们想要创建一个calculator对象来进行一些数学级数计算,包括计算斐波那契数列。看这里:
class Calculator(object):
def fib_cached(self, n, cache):
if n < 2:
return 1
try:
result = cache[n]
except:
cache[n] = fib_cached(n - 2, cache) + fib_cached(n - 1, cache)
result = cache[n]
return result
你知道你的对象的大多数用户不知道他们应该如何处理cache变量,或者它意味着什么。在calculator方法的方法定义中包含这个变量可能会导致混乱。相反,您想要的是一个方法定义,它看起来像我们看到的第一段代码,但是具有缓存的性能优势。
在理想的情况下,我们应该有一个类作为calculator类的接口。客户端不应该知道这个类,因为客户端只向原始类的接口编码,代理提供与原始类相同的功能和结果。
代理模式
代理提供与原始对象相同的接口,但它控制对原始对象的访问。作为该控件的一部分,它可以在访问原始对象之前和之后执行其他任务。当我们想实现像记忆化这样的东西,而又不想让客户端承担任何理解缓存的责任时,这一点尤其有用。通过屏蔽客户端对fib方法的调用,代理允许我们返回计算结果。
Duck typing 允许我们通过复制对象接口,然后使用代理类而不是原始类来创建这样的代理。
import time
class RawCalculator(object):
def fib(self, n):
if n < 2:
return 1
return self.fib(n - 2) + self.fib(n - 1)
def memoize(fn):
__cache = {}
def memoized(*args):
key = (fn.__name__, args)
if key in __cache:
return __cache[key]
__cache[key] = fn(*args)
return __cache[key]
return memoized
class CalculatorProxy(object):
def __init__(self, target):
self.target = target
fib = getattr(self.target, 'fib')
setattr(self.target, 'fib', memoize(fib))
def __getattr__(self, name):
return getattr(self.target, name)
if __name__ == "__main__":
calculator = CalculatorProxy(RawCalculator())
start_time = time.time()
fib_sequence = [calculator.fib(x) for x in range(0, 80)]
end_time = time.time()
print(
"Calculating the list of {} Fibonacci numbers took {} seconds".format(
len(fib_sequence),
end_time - start_time
)
)
我必须承认,这不是一段无足轻重的代码,但是让我们一步一步来看看发生了什么。
首先,我们有RawCalculator类,这是我们想象的计算对象。目前,它只包含 Fibonacci 计算,但您可以想象它包含许多其他递归定义的系列和序列。和以前一样,这个方法使用递归调用。
接下来,我们定义一个封装函数调用的闭包,但是让我们把它留到以后。
最后,我们有一个CalculatorProxy类,它在初始化时将目标对象作为参数,在代理对象上设置一个属性,然后用fib方法的记忆版本覆盖目标对象的fib方法。每当目标对象调用它的fib()方法时,内存化的版本就会被调用。
现在,memoize函数将一个函数作为参数。接下来,它初始化一个空字典。然后我们定义一个函数,它接受一个参数列表,获取传递给它的函数名,并创建一个包含函数名和接收到的参数的元组。然后这个元组形成了__cache字典的键。该值表示函数调用返回的值。
memoized 函数首先检查该键是否已经在缓存字典中。如果是,则不需要重新计算该值,并返回该值。如果找不到键,则调用原始函数,并在返回之前将值添加到字典中。
memoize函数将一个常规的旧函数作为参数,然后记录调用接收到的函数的结果。如果需要,它调用函数并接收新计算的值,然后返回一个新函数,如果将来需要该值,则保存结果。
最终结果如下所示:
Calculating the list of 80 Fibonacci numbers took 8.20159912109375e-05 seconds
我非常喜欢这个事实,即memoize函数可以与传递给它的任何函数一起使用。拥有一段这样的通用代码是有帮助的,因为它允许您在不修改代码的情况下记忆许多不同的功能。这是面向对象编程的主要目标之一。随着经验的积累,您还应该建立一个有趣且有用的代码库,以便重用。理想情况下,您希望将这些打包到您自己的库中,这样您就可以在更短的时间内做更多的事情。这也有助于你不断进步。
现在您已经理解了代理模式,让我们来看看可供我们使用的不同类型的代理。我们已经详细了解了缓存代理,所以剩下的内容如下:
- 远程代理
- 虚拟代理
- 保护代理
远程代理
当我们想要抽象一个对象的位置时,我们可以使用远程代理。远程对象对客户端来说似乎是本地资源。当您调用代理上的方法时,它会将调用转移到远程对象。一旦返回结果,代理就使这个结果对本地对象可用。
虚拟代理
有时创建对象的成本可能很高,并且可能直到程序执行的后期才需要它们。当延迟对象创建有帮助时,可以使用代理。只有在需要时,才能创建目标对象。
保护代理
许多程序有不同的用户角色和不同的访问级别。通过在目标对象和客户机对象之间放置代理,可以限制对目标对象上的信息和方法的访问。
临别赠言
您看到了代理可以采取多种形式,从我们在本章中讨论的缓存代理到网络连接。每当我们希望以一种对客户端透明的方式控制对象或资源的访问时,就应该考虑代理模式。在本章中,您看到了代理模式如何包装其他对象,并以这些函数的用户不可见的方式改变它们的执行方式,因为它们仍然将相同的参数作为输入,并将相同的结果作为输出返回。
代理模式通常有三个部分:
- 需要访问某个对象的客户端
- 客户端想要访问的对象
- 控制对对象的访问的代理
客户端实例化代理并调用代理,就好像它是对象本身一样。
因为 Python 是动态类型的,所以我们不关心定义一些公共接口或抽象类,因为我们只需要提供与目标对象相同的属性,以便客户端代码像对待目标对象一样对待代理。
用我们已经熟悉的术语来思考代理是有帮助的,比如 web 代理。一旦连接到代理,你,客户端,不知道代理;您可以像没有代理一样访问互联网和其他网络资源。这是代理模式和适配器模式之间的关键区别。使用代理模式,您希望界面保持不变,一些操作在后台发生。相反,适配器模式的目标是改变接口。
练习
- 向
RawCalculator类添加更多的函数,这样它就可以创建其他序列。 - 扩展
CalculatorProxy类来处理您添加的新系列生成函数。他们也使用开箱即用的memoize功能吗? - 修改
__init__()函数来迭代目标对象的属性,并自动记忆可调用的属性。 - 计算每个
RawCalculator方法在一个程序中被调用的次数;使用它来计算对fib()方法进行了多少次调用。 - 查看 Python 标准库,了解记忆函数的其他方法。