Python-设计模式实践教程-三-

47 阅读44分钟

Python 设计模式实践教程(三)

原文:Practical Python Design Patterns

协议:CC BY-NC-SA 4.0

十、责任链模式

那不是我。—沙吉,《不是我》

网络应用是复杂的野兽。他们需要解释传入的请求,并决定应该如何处理它们。当您考虑创建一个框架来帮助您更快地构建 web 应用时,您很有可能会淹没在这样一个项目所涉及的复杂性和选项中。Python 是许多 web 框架的家园,因为这种语言在云应用的开发人员中很受欢迎。在这一章中,我们将探索 web 框架的一个组件,叫做中间件层。

中间件位于发出应用请求的客户端和实际应用之间。来自客户端的请求和来自主要应用功能的响应都需要通过这一层。在中间件中,您可以做一些事情,比如记录传入的请求以进行故障排除。您还可以添加一些代码来捕获和保存应用返回给用户的响应。通常,您会找到代码来检查用户的访问,以及是否应该允许他们执行他们在系统上发出的请求。您可能希望将传入的请求体转换成易于处理的数据格式,或者对所有输入值进行转义,以防止黑客将代码注入应用。您也可以将应用的路由机制视为中间件的一部分。路由机制确定应该使用什么代码来响应指向特定端点的请求。

大多数 web 框架创建一个在整个系统中使用的请求对象。一个简单的Request对象可能如下所示:

class Request(object):
  def __init__(self, headers, url, body, GET, POST):
    self.headers
    self.url
    self.body
    self.GET
    self.POST

目前,您还没有与请求相关联的额外功能,但是这个有限的对象应该是一个良好的开端,让您对中间件在处理客户端对 web 应用的请求时的使用方式有所了解。

Web 框架需要向客户端返回某种响应。让我们定义一个基本的Response对象:

class Response(object):
  def __init__(self, headers, status_code, body):
    self.headers
    self.status_code
    self.body

如果你看一下其中一个比较流行的框架的文档,比如 Django 或者 Flask,你会发现这些对象比刚刚定义的简单对象要复杂得多,但是这些都符合我们的目的。

我们希望我们的中间件做的事情之一是检查Request对象头中的令牌,然后将User对象添加到该对象中。这将允许主应用访问当前登录的用户,根据需要改变界面和显示的信息。

让我们编写一个基本函数,它将获取用户令牌并从数据库中加载一个User对象。然后,该函数将返回一个可以附加到请求对象的User对象。

import User

def get_user_object(username, password):
  return User.find_by_token(username, password)

您可以轻松地创建一个存储在数据库中的User类。作为一个练习,查看 SQLAlchemy 项目( https://www.sqlalchemy.org/ )并创建一个 SQLite 数据库的接口,在该数据库中存储基本的用户数据。确保User类有一个基于user token字段查找用户的class函数,这是唯一的。

设置 WSGI 服务器

让我们把这个变得更实际一点。

因为您的系统上已经运行了pip,所以在您的虚拟环境中安装 uWSGI:

pip install uwsgi

Windows 用户可能会收到一条错误消息,AttributeError:对于本章,模块“os”没有属性“uname”,在第十二章中,您需要安装 Cygwin。确保在 Cygwin 安装过程中从选项中选择 python3 解释器、pip3、gcc-core、python3-devel(检查您是否选择了 Windows 版本— 32 位/64 位)、libcrypt-devel 和 wget。可以在 http://cygwin.com/install.html 获得。Cygwin 是一个类似 Linux 的 Windows 终端。

您可以使用默认的 Linux 命令进行导航,比如pip3 install uwsgi命令。您也可以按照第一章中的 virtualenv 设置说明,在 Cygwin 中设置一个虚拟环境(activate命令位于YOURENVNAME/Scripts)。

如果这一切看起来太费力,你最好安装 virtualbox 并在机器上运行 Ubuntu。然后,您可以完全遵循 Linux 的道路,而无需对您的日常环境进行任何更改。

让我们测试一下服务器是否正常工作,是否可以在您的机器上提供网站服务。以下脚本在正文中返回一个非常简单的 status 200 success 和 Hello World 消息(来自位于 http://uwsgi-docs.readthedocs.io/en/latest/WSGIquickstart.html 的 uWSGI 文档)。

hello_world_server.py

def application(env, start_response):
    start_response('200 OK', [('Content-Type','text/html')])
    return [b"Hello World"]

在命令行中,在虚拟环境处于活动状态的情况下,启动服务器,将hello_world_server.py设置为处理请求的应用。

uwsgi --http :9090 --wsgi-file hello_world_server.py

如果您将浏览器指向http://localhost:9090,您应该会在窗口中看到消息Hello World

恭喜你!你有自己的网络服务器在运行;你甚至可以把它部署在一个服务器上,指向一个 URL,让世界向它发送请求。我们接下来要做的是,通过从头开始构建中间件,看看中间件如何适应从请求到响应的流程。

由于服务器已经启动并运行,让我们添加一个函数来检查用户令牌是否设置在头中。我们将遵循你可能在网上找到的许多更大的 API 所使用的惯例。

认证标题

其思想是客户端包含一个Authorization头,头的值设置为单词Basic,后跟一个空格,再后跟一个 base64 编码的字符串。被编码的字符串具有下面的形式,USERNAME:PASSWORD,它将用于识别和验证系统上的用户。

在我们继续构建功能之前,重要的是要注意,通过在调用应用时包含Authentication头,并让应用检查Authorization头,应用就不再需要跟踪哪个用户正在进行调用,而是在请求发生时将用户附加到请求上。

测试 URL 端点最简单的方法是在您的系统上安装一个类似 postman 的工具。它允许你从应用请求网址并解释结果。免费的邮递员 app 可以在这里下载: https://www.getpostman.com/

一旦邮差安装结束,打开应用。

首先,在 postman 地址栏中输入您在浏览器中输入的 URL,然后单击 Send 或按 enter。您应该会在屏幕底部的框中看到Hello World。这告诉你,postman 在工作,你的 app 还在运行。

在地址栏的正下方,你会看到一个带有几个标签的框。其中一个选项卡是授权。单击它,并从下拉框中选择基本身份验证。现在,用户名和密码字段被添加到界面中。Postman 允许您在这些框中输入用户名和密码,然后它会为您编码字符串。

要查看从 uWSGI 接收到的Request对象是什么样子,需要修改用于向浏览器返回Hello World消息的脚本。

hello_world_request_display.py

import pprint

pp = pprint.PrettyPrinter(indent=4)

def application(env, start_response):
    pp.pprint(env)

    start_response('200 OK', [('Content-Type','text/html')])
    return [b"Hello World "]

Python 标准库中包含的pprint模块打印字典和列表等数据类型,是一种易于阅读的格式。当您调试复杂的数据类型时,例如从 web 服务器收到的请求,这是一个很有价值的工具。

当您重启 uWSGI 服务器并用 postman 发送包含Authorization头的请求时,您将看到控制台中打印出一个字典。字典中的一个键是HTTP_AUTHORIZATION,它是包含单词Basic的字段,后跟一个空格,再跟一个 base64 编码的字符串。

要从数据库中检索User对象,需要对这个字符串进行解码,然后分别拆分成用户名和密码。我们可以通过更新服务器应用来完成所有这些工作。我们将从获取HTTP_AUTHORIZATION头的值开始。然后,我们将在一个空格上分割这个值,得到单词Basic和 base64 编码的字符串。然后我们将继续对字符串进行解码,并在:上进行分割,这将产生一个数组,用户名在索引 0 处,密码在索引 1 处。然后,我们将把它发送给一个简单的函数,该函数返回一个User对象,为了这个例子,我们只是内联地构造了这个对象,但是欢迎您插入从本地 SQLite 数据库获取用户的函数。

user_aware_server.py

import base64

class User(object):
    def __init__(self, username, password, name, email):
        self.username = username
        self.password = password
        self.name = name
        self.email = email

    @classmethod
    def get_verified_user(cls, username, password):
        return User(
            username,
            password,
            username,
            "{}@demo.com".format(username)
        )

def application(env, start_response):
    authorization_header = env["HTTP_AUTHORIZATION"]
    header_array = authorization_header.split()
    encoded_string = header_array[1]
    decoded_string = base64.b64decode(encoded_string).decode('utf-8')
    username, password = decoded_string.split(":")

    user = User.get_verified_user(username, password)

    start_response('200 OK', [('Content-Type', 'text/html')])
    response = "Hello {}!".format(user.name)
    return [response.encode('utf-8')]

在我们的例子中,我们根据请求创建了一个用户。这显然是不正确的,但是我们将它分离到自己的类中的事实允许我们获取代码并更改get_verified_user方法以基于用户名获取用户。然后,我们验证该用户的密码是否正确。然后,您可能希望添加一些代码来处理数据库中不存在用户的情况。

此时,我们已经看到了许多只与在主服务器应用中检查用户凭证的过程相关的代码。这感觉不对。让我们通过添加最基本的路由代码来强调它是多么的错误。我们的路由器将寻找一个报头,它告诉我们客户端请求的是哪个端点。基于这些信息,我们将为所有情况打印一条 hello 消息,除非路径包含子串goodbye,在这种情况下,服务器将返回一条 goodbye 消息。

当我们打印出请求内容时,我们看到路径信息被捕获在PATH_INFO头中。标题以“/”开头,后面是路径的其余部分。

可以在“/”上拆分路径,然后检查路径是否包含单词goodbye

import base64

from pprint import PrettyPrinter
pp = PrettyPrinter(indent=4)

class User(object):
    def __init__(self, username, password, name, email):
        self.username = username
        self.password = password
        self.name = name
        self.email = email

    @classmethod
    def get_verified_user(cls, username, password):
        return User(
            username,
            password,
            username,
            "{}@demo.com".format(username)
        )

def application(env, start_response):
    authorization_header = env["HTTP_AUTHORIZATION"]
    header_array = authorization_header.split()
    encoded_string = header_array[1]
    decoded_string = base64.b64decode(encoded_string).decode('utf-8')
    username, password = decoded_string.split(":")

    user = User.get_verified_user(username, password)

    start_response('200 OK', [('Content-Type', 'text/html')])

    path = env["PATH_INFO"].split("/")
    if "goodbye" in path:
        response = "Goodbye {}!".format(user.name)
    else:
        response = "Hello {}!".format(user.name)

    return [response.encode('utf-8')]

我相信你的代码意识现在正在制造很多噪音。application函数现在显然在做三件不同的事情。它开始获取User对象。然后,它决定客户端请求什么端点,并基于该路径编写相关消息。最后,它将编码后的消息返回给发出请求的客户机。

每当用户的初始请求和实际响应之间需要更多的功能时,application函数就会变得更加复杂。简而言之,如果你继续这样下去,你的代码一定会变得一团糟。

理想情况下,我们希望每个函数只做一件事。

责任链模式

每段代码做一件事情并且只做一件事情的想法被称为单一责任原则,这是当你致力于编写更好的代码时的另一个方便的指导方针。

如果我们想遵守前面示例中的单一责任原则,我们需要一段代码来处理用户检索,另一段代码来验证用户是否与密码匹配。还有一段代码会根据请求的路径生成一条消息,最后,某段代码会将响应返回给浏览器。

在我们尝试为服务器编写代码之前,我们将使用一个示例应用建立一些关于解决方案的直觉。

在我们的示例应用中,我们有四个函数,每个函数都简单地打印出一条消息。

def function_1():
  print("function_1")

def function_2():
  print("function_2")

def function_3():
  print("function_3")

def function_4():
  print("function_4")

def main_function():
  function_1()
  function_2()
  function_3()
  function_4()

if __name__ == "__main__":
  main_function()

从本节开始的讨论中,我们知道让main_function按顺序调用每个函数并不理想,因为在现实世界中,这会导致我们在上一节中遇到的混乱代码。

我们想要的是创建某种方式,让我们进行一次调用,然后动态调用函数。

class CatchAll(object):
    def __init__(self):
        self.next_to_execute = None

    def execute(self):
        print("end reached.")

class Function1Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self):
        print("function_1")
        self.next_to_execute.execute()

class Function2Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self):
        print("function_2")
        self.next_to_execute.execute()

class Function3Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self):
        print("function_3")
        self.next_to_execute.execute()

class Function4Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self):
        print("function_4")
        self.next_to_execute.execute()   

def main_function(head):
  head.execute()

if __name__ == "__main__":
    hd = Function1Class()

    current = hd
    current.next_to_execute = Function2Class()

    current = current.next_to_execute
    current.next_to_execute = Function3Class()

    current = current.next_to_execute
    current.next_to_execute = Function4Class()

    main_function(hd)

尽管编写的代码量比第一个实例多得多,但您应该开始看到这个新程序是如何清晰地将每个函数的执行与其他函数分开的。

为了进一步说明这一点,我们现在扩展主 execute 函数以包含一个请求字符串。该字符串是一系列数字,如果数字字符串中包含函数名称中的数字,则该函数会将其名称与请求字符串一起打印出来,然后从请求字符串中删除该数字的所有实例,并将请求传递给代码的其余部分进行处理。

在我们最初的上下文中,我们可以按如下方式解决这个新需求:

def function_1(in_string):
    print(in_string)
    return "".join([x for x in in_string if x != '1'])

def function_2(in_string):
    print(in_string)
    return "".join([x for x in in_string if x != '2'])

def function_3(in_string):
    print(in_string)
    return "".join([x for x in in_string if x != '3'])

def function_4(in_string):
    print(in_string)
    return "".join([x for x in in_string if x != '4'])

def main_function(input_string):
    if '1' in input_string:
        input_string = function_1(input_string)
    if '2' in input_string:
        input_string = function_2(input_string)
    if '3' in input_string:
        input_string = function_3(input_string)
    if '4' in input_string:
        input_string = function_4(input_string)

    print(input_string)

if __name__ == "__main__":
    main_function("1221345439")

是的,你是对的——所有的函数都做完全相同的事情,但这只是因为示例问题的设置方式。如果你参考最初的问题,你会发现情况并不总是这样。

还应该感觉到的是一系列的if语句,随着更多的函数被添加到执行队列中,这些语句的长度只会增加。

最后,您可能已经注意到了main_function与正在执行的每个函数之间的紧密耦合。

在我们的项目中实施责任链

现在,让我们用之前开始构建的想法来解决同一个问题。

class CatchAll(object):
    def __init__(self):
        self.next_to_execute = None

    def execute(self, request):
        print(request)

class Function1Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self, request):
        print(request)
        request = "".join([x for x in request if x != '1'])
        self.next_to_execute.execute(request)

class Function2Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self, request):
        print(request)
        request = "".join([x for x in request if x != '2'])
        self.next_to_execute.execute(request)

class Function3Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self, request):
        print(request)
        request = "".join([x for x in request if x != '3'])
        self.next_to_execute.execute(request)

class Function4Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self, request):
        print(request)
        request = "".join([x for x in request if x != '4'])
        self.next_to_execute.execute(request)

def main_function(head, request):
  head.execute(request)

if __name__ == "__main__":
    hd = Function1Class()

    current = hd
    current.next_to_execute = Function2Class()

    current = current.next_to_execute
    current.next_to_execute = Function3Class()

    current = current.next_to_execute
    current.next_to_execute = Function4Class()

    main_function(hd, "1221345439")

很明显,我们手头已经有了一个更清洁的解决方案。我们不需要对每个类与下一个类的连接方式做任何改变,除了传递请求之外,我们不需要对单个执行函数或主函数做任何改变来适应每个类执行请求的方式。我们已经清楚地将每个函数分成它自己的代码单元,这些代码单元可以插入到处理请求的类链中,或者从处理请求的类链中删除。

每个处理程序只关心自己的执行,而忽略另一个处理程序由于查询而执行时会发生什么。处理程序根据收到的请求立即决定是否应该执行任何操作,这增加了确定哪些处理程序应该作为查询结果执行的灵活性。这个实现最好的部分是可以在运行时添加和删除处理程序,当顺序对于执行队列不重要时,您甚至可以打乱处理程序的顺序。这再次表明,松耦合代码比紧耦合代码更灵活。

为了进一步说明这种灵活性,我们扩展了前面的程序,这样每个类只有在看到与类名相关的数字在请求字符串中时才会做一些事情。

class CatchAll(object):
    def __init__(self):
        self.next_to_execute = None

    def execute(self, request):
        print(request)

class Function1Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self, request):
        if '1' in request:
            print("Executing Type Function1Class with request [{}]".format(request))
            request = "".join([x for x in request if x != '1'])

        self.next_to_execute.execute(request)

class Function2Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self, request):
        if '2' in request:
            print("Executing Type Function2Class with request [{}]".format(request))
            request = "".join([x for x in request if x != '2'])

        self.next_to_execute.execute(request)

class Function3Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self, request):
        if '3' in request:
            print("Executing Type Function3Class with request [{}]".format(request))
            request = "".join([x for x in request if x != '3'])

        self.next_to_execute.execute(request)

class Function4Class(object):
    def __init__(self):
        self.next_to_execute = CatchAll()

    def execute(self, request):
        if '4' in request:
            print("Executing Type Function4Class with request [{}]".format(request))
            request = "".join([x for x in request if x != '4'])

        self.next_to_execute.execute(request)

def main_function(head, request):
  head.execute(request)

if __name__ == "__main__":
    hd = Function1Class()

    current = hd
    current.next_to_execute = Function2Class()

    current = current.next_to_execute
    current.next_to_execute = Function3Class()

    current = current.next_to_execute
    current.next_to_execute = Function4Class()

    main_function(hd, "12214549")

注意,这次我们在请求中没有任何3数字,因此在请求被传递给Function4Class的实例之前,Function3Class实例的代码没有被执行。此外,请注意,我们在设置执行链或main_function的代码上没有任何变化。

这个处理者链是责任链模式的本质。

更 Pythonic 化的实现

在该模式的经典实现中,您将定义某种通用处理程序类型,该类型将定义next_to_execute对象和execute函数的概念。正如我们之前看到的,Python 中的 duck-typing 系统使我们不必增加额外的开销。只要我们为链中要使用的每个处理程序定义了相关部分,我们就不需要从某个共享库继承。这不仅节省了大量编写样板代码的时间,而且使您(开发人员)能够根据应用或系统的需要,轻松地发展这些类型的想法。

在更受约束的语言中,您没有在运行中发展设计的奢侈。像这样改变方向通常需要大量的重新设计,很多代码需要重写。为了避免重新设计整个系统或重建大部分应用的痛苦,工程师和开发人员试图迎合他们能想到的每一种可能性。这种方法在两个关键方面存在缺陷。首先,人类对未来的知识是不完善的,所以即使你要为今天设计完美的系统(几乎不可能),也不可能考虑到客户可能使用你的系统的每一种方式,或者市场可能在哪里驱动你的应用。第二,在大多数情况下,你不会需要它(YAGNI),或者,换句话说,在这样一个系统的设计中涉及的大多数潜在的未来将永远不会被需要,因此这是浪费时间和精力。Python 再一次提供了从简单问题的简单解决方案开始,然后随着用例的变化和发展来发展解决方案的能力。Python 与您一起发展,随着您的发展而变化和成长。就像围棋一样,Python 很容易上手,但是你可以用一生的时间来掌握它的多种可能性。

下面是责任链模式的一个简单实现,去掉了所有多余的东西。剩下的就是我们将在 web 应用上实现的一般结构。

class EndHandler(object):
    def __init__(self):
        pass

    def handle_request(self, request):
        pass

class Handler1(object):
    def __init__(self):
        self.next_handler = EndHandler()

    def handle_request(self, request):

        self.next_handler.handle_request(request)

def main(request):
    concrete_handler = Handler1()

    concrete_handler.handle_request(request)

if __name__ == "__main__":
    # from the command line we can define some request

    main(request)

从本章开始,我们将使用这个非常简单的框架来显著改进我们的 web 应用。新的解决方案将更加严格地遵守单一责任原则。您还会注意到,您可以向流程中添加更多的功能和扩展。

import base64

class User(object):
    def __init__(self, username, password, name, email):
        self.username = username
        self.password = password
        self.name = name
        self.email = email

    @classmethod
    def get_verified_user(cls, username, password):
        return User(
            username,
            password,
            username,
            "{}@demo.com".format(username)
        )

class EndHandler(object):
    def __init__(self):
        pass

    def handle_request(self, request, response=None):
        return response.encode('utf-8')

class AuthorizationHandler(object):
    def __init__(self):
        self.next_handler = EndHandler()

    def handle_request(self, request, response=None):
        authorization_header = request["HTTP_AUTHORIZATION"]
        header_array = authorization_header.split()
        encoded_string = header_array[1]
        decoded_string = base64.b64decode(encoded_string).decode('utf-8')
        username, password = decoded_string.split(":")
        request['username'] = username
        request['password'] = password

        return self.next_handler.handle_request(request, response)

class UserHandler(object):
    def __init__(self):
        self.next_handler = EndHandler()

    def handle_request(self, request, response=None):
        user = User.get_verified_user(request['username'], request['password'])
        request['user'] = user

        return self.next_handler.handle_request(request, response)

class PathHandler(object):
    def __init__(self):
        self.next_handler = EndHandler()

    def handle_request(self, request, response=None):
        path = request["PATH_INFO"].split("/")
        if "goodbye" in path:
            response = "Goodbye {}!".format(request['user'].name)
        else:
            response = "Hello {}!".format(request['user'].name)

        return self.next_handler.handle_request(request, response)

def application(env, start_response):
    head = AuthorizationHandler()

    current = head
    current.next_handler = UserHandler()

    current = current.next_handler
    current.next_handler = PathHandler()

    start_response('200 OK', [('Content-Type', 'text/html')])    
    return [head.handle_request(env)]

责任链模式的另一个可选实现是实例化一个主对象,以便在实例化时从传递给调度程序的列表中调度处理程序。

class Dispatcher(object):

  def __init__(self, handlers=[]):
    self.handlers = handlers

    def handle_request(self, request):
      for handler in self.handlers:
        request = handler(request)

      return request

Dispatcher类利用了 Python 中一切都是对象的事实,包括函数。由于函数是一等公民(这意味着它们可以传递给函数,也可以从函数返回),我们可以实例化 dispatcher 并传入一个函数列表,该列表可以像其他列表一样保存。当需要执行函数时,我们遍历列表并依次执行每个函数。

使用Dispatcher实现我们在本节中使用的示例代码如下所示:

class Dispatcher(object):

    def __init__(self, handlers=[]):
        self.handlers = handlers

    def handle_request(self, request):
        for handle in self.handlers:
            request = handle(request)

        return request

def function_1(in_string):
    print(in_string)
    return "".join([x for x in in_string if x != '1'])

def function_2(in_string):
    print(in_string)
    return "".join([x for x in in_string if x != '2'])

def function_3(in_string):
    print(in_string)
    return "".join([x for x in in_string if x != '3'])

def function_4(in_string):
    print(in_string)
    return "".join([x for x in in_string if x != '4'])

def main(request):
    dispatcher = Dispatcher([
        function_1,
        function_2,
        function_3,
        function_4
    ])

    dispatcher.handle_request(request)

if __name__ == "__main__":
    main("1221345439")

作为一个练习,我建议您使用这个结构来实现 web 应用的流程。

在我们结束这一章之前,我想让你思考一下我们开始的关于中间件的讨论。您能看到如何使用责任链模式通过向队列添加处理程序来为您的 web 应用实现中间件吗?

临别赠言

正如我们在本章中所看到的,请求是构建模式的中心对象,因此当请求对象从一个处理程序传递到下一个处理程序时,在构建和修改请求对象时应该小心。

由于责任链模式为我们提供了调整和改变运行时使用的处理程序的机会,我们可以将一些增强性能的想法应用到责任链中。我们可以在每次使用处理程序时将它移动到链的前面,导致更经常使用的处理程序占据链中的早期位置,然后是不经常使用的处理程序(仅当处理程序的执行顺序无关紧要时才使用)。当处理程序的顺序有某种意义时,使用前一章的思想并缓存某些处理的请求的结果,可以提高性能。

我们从来不想完全忽略一个请求,所以您必须确保有某种 end 处理程序作为链的总括,至少提醒您链中的任何处理程序都没有处理该请求。

在本章的例子中,我们让链一直执行,直到所有的处理程序都查看了请求。情况并不一定如此;你可以在任何时候断开这个链并返回一个结果。

责任链模式可以用来将元素的处理封装到管道中(就像数据管道一样)。

最后,一个好的经验法则是,当一个请求有不止一个潜在的处理程序时,使用责任链模式,这样就不知道哪个或哪些处理程序最适合处理您将收到的请求。

练习

  • 查看 SQLAlchemy 并创建一个到 SQLite 数据库的接口,在该数据库中存储基本的用户数据。
  • 扩展检索用户的User类函数,以便它使用 SQLite 数据库,就像在前面的练习中一样。
  • 将您在第二章中构建的日志记录器添加到您的 web 应用的责任链中,这样您就可以记录传入的请求和发送回客户端的响应。
  • 使用责任链的Dispatcher实现你的 web 应用。

十一、命令模式

我会回来的。—终结符

机器人来了。

在这一章中,我们将看看如何使用 Python 代码来控制机器人。出于我们的目的,我们将使用 Python 标准库中包含的turtle模块。这将使我们能够处理前进、左转和右转等命令,而不需要构建一个实际的机器人,尽管您应该能够使用本章中开发的代码,结合 Raspberry Pi 等板和一些执行器,来实际构建一个您可以用代码控制的机器人。

控制海龟

让我们从做一些简单的事情开始,比如用 turtle 模块在屏幕上画一个正方形。

turtle_basic.py

import turtle

turtle.color('blue', 'red')

turtle.begin_fill()

for _ in range(4):
    turtle.forward(100)
    turtle.left(90)

turtle.end_fill()
turtle.done()

海龟是一个简单的线条和填充工具,有助于可视化机器人正在做什么。在前面的代码中,我们导入了turtle库。接下来,我们将线条颜色设置为蓝色,背景颜色设置为红色。然后,我们告诉海龟将当前点记录为稍后将被填充的多边形的起点。然后,循环绘制正方形的四条边。最后,end _fill()方法用红色填充多边形。done()方法防止程序在进程完成后终止和关闭窗口。

如果你使用的是 Ubuntu 系统,并且得到一个关于python3-tk,的错误信息,你需要安装这个包。

sudo apt-get install python3-tk

运行代码,你应该看到一个小三角形(乌龟)在屏幕上移动。

当我们试图在屏幕上绘制形状和线条时,这种方法是可行的,但是因为乌龟只是我们机器人的一个隐喻,所以绘制漂亮的形状并没有太大的用处。我们将扩展我们的机器人控制脚本以接受四个命令(开始、停止、向左转 10 度和向右转 10 度),我们将把它们映射到键盘按键。

turtle_control.py

import turtle

turtle.setup(400, 400)

screen = turtle.Screen()
screen.title("Keyboard drawing!")

t = turtle.Turtle()
distance = 10

def advance():
   t.forward(distance)

def turn_left():
    t.left(10)

def turn_right():
    t.right(10)

def retreat():
    t.backward(10)

def quit():
    screen.bye()

screen.onkey(advance, "w")
screen.onkey(turn_left, "a")
screen.onkey(turn_right, "d")
screen.onkey(retreat, "s")
screen.onkey(quit, "Escape")

screen.listen()
screen.mainloop()

你本质上做的是创建一个界面来控制海龟。在现实世界中,这将类似于一辆遥控汽车,它接受一个信号表示左转,另一个信号表示右转,第三个信号表示加速,第四个信号表示减速。如果您要构建一个 RC 发送器,它将通常来自遥控器的信号转换为现在使用一些电子设备和 USB 端口从键盘输入的信号,您将能够使用类似于我们这里的代码来控制遥控车。同样,你可以通过遥控来控制机器人。

本质上,我们是从系统的一部分向另一部分发送命令。

命令模式

每当您想将一条指令或一组指令从一个对象发送到另一个对象,同时保持这些对象松散耦合时,明智的做法是将执行指令所需的一切都封装在某种数据结构中。

发起执行的客户端不必知道指令将被执行的方式;它所需要做的就是设置所有需要的信息,并传递系统下一步需要发生的任何事情。

因为我们正在处理一个面向对象的范例,用于封装指令的数据结构将是一个对象。用于封装其他方法执行所需信息的对象类称为命令。客户机对象实例化一个命令,该命令包含它希望执行的方法、执行该方法所需的所有参数以及拥有该方法的某个目标对象。

这个目标对象称为接收器。接收者是一个类的实例,它可以在给定封装信息的情况下执行该方法。

所有这些都依赖于一个名为 invoker 的对象,它决定接收方的方法何时执行。认识到方法的调用类似于类的实例可能会有所帮助。

在没有任何实际功能的情况下,命令模式可以这样实现:

class Command:
    def __init__(self, receiver, text):
        self.receiver = receiver
        self.text = text

    def execute(self):
        self.receiver.print_message(self.text)

class Receiver(object):
    def print_message(self, text):
      print("Message received: {}".format(text))

class Invoker(object):
    def __init__(self):
        self.commands = []

    def add_command(self, command):
        self.commands.append(command)

    def run(self):
        for command in self.commands:
            command.execute()

if __name__ == "__main__":
    receiver = Receiver()

    command1 = Command(receiver, "Execute Command 1")
    command2 = Command(receiver, "Execute Command 2")

    invoker = Invoker()
    invoker.add_command(command1)
    invoker.add_command(command2)
    invoker.run()

现在,即使接收者正忙于执行另一个命令,您也可以对要执行的命令进行排队。在分布式系统中,这是一个非常有用的概念,在分布式系统中,需要捕获和处理传入的命令,但是系统可能正忙于另一个动作。将一个对象中的所有信息放在队列中允许系统处理所有传入的命令,而不会在执行其他命令时丢失重要的命令。实际上,您可以在运行时动态地创建新的行为。当您希望一次设置一个方法的执行,然后稍后再执行它时,这个过程特别有用。

想想看,如果操作人员只能发送一个要执行的命令,然后必须等待该命令执行完毕才能发送下一个命令,这对像火星漫游车这样的机器人意味着什么。这将使得在火星上做任何事情都极其耗时,几乎是不可能的。取而代之的是向漫游者发送一系列动作。然后,漫游者可以依次调用每个命令,允许它在单位时间内完成更多的工作,并可能消除漫游者和地球之间传输和接收命令之间的时间延迟的影响。这一串命令称为宏。

在运行自动化测试时,当您想要模拟用户与系统的交互,或者当您想要自动化您重复执行的某些任务时,宏也很有用。您可以为流程中的每一步创建一个命令,然后在让某个调用程序处理执行之前将它们串起来。解耦允许你通过键盘将直接的人工干预换成生成的计算机输入。

一旦我们开始将命令串在一起,我们还可以考虑撤销已经执行的命令。这是命令模式特别有用的另一个领域。通过为每个被调用的命令将撤销命令压入堆栈,我们可以免费获得多级撤销,就像你在从现代文字处理器到视频编辑软件的所有东西中看到的一样。

为了说明如何使用命令模式构建多级撤销堆栈,我们将构建一个非常简单的计算器,它可以做加法、减法、乘法和除法。为了保持例子的简单,我们不考虑与零相乘的情况。

class AddCommand(object):
    def __init__(self, receiver, value):
        self.receiver = receiver
        self.value = value

    def execute(self):
        self.receiver.add(self.value)

    def undo(self):
        self.receiver.subtract(self.value)

class SubtractCommand(object):
    def __init__(self, receiver, value):
        self.receiver = receiver
        self.value = value

    def execute(self):
        self.receiver.subtract(self.value)

    def undo(self):
        self.receiver.add(self.value)

class MultiplyCommand(object):
    def __init__(self, receiver, value):
        self.receiver = receiver
        self.value = value

    def execute(self):
        self.receiver.multiply(self.value)

    def undo(self):
        self.receiver.divide(self.value)

class DivideCommand(object):
    def __init__(self, receiver, value):
        self.receiver = receiver
        self.value = value

    def execute(self):
        self.receiver.divide(self.value)

    def undo(self):
        self.receiver.multiply(self.value)

class CalculationInvoker(object):
    def __init__(self):
        self.commands = []
        self.undo_stack = []

    def add_new_command(self, command):
        self.commands.append(command)

    def run(self):
        for command in self.commands:
            command.execute()
            self.undo_stack.append(command)

    def undo(self):
        undo_command = self.undo_stack.pop()
        undo_command.undo()

class Accumulator(object):
    def __init__(self, value):
        self._value = value

    def add(self, value):
        self._value += value

    def subtract(self, value):
        self._value -= value

    def multiply(self, value):
        self._value *= value

    def divide(self, value):
        self._value /= value

    def __str__(self):
        return "Current Value: {}".format(self._value)

if __name__ == "__main__":
    acc = Accumulator(10.0)

    invoker = CalculationInvoker()
    invoker.add_new_command(AddCommand(acc, 11))
    invoker.add_new_command(SubtractCommand(acc, 12))
    invoker.add_new_command(MultiplyCommand(acc, 13))
    invoker.add_new_command(DivideCommand(acc, 14))

    invoker.run()

    print(acc)

    invoker.undo()
    invoker.undo()
    invoker.undo()
    invoker.undo()

    print(acc)

请注意,当 Python 在打印时将accumulator对象转换为字符串时,使用了__ str()__方法。

正如我们已经多次看到的,Python 中的 duck 类型允许我们跳过定义一个总体基类来继承。只要我们的命令有execute()undo()方法,并接受一个接收器和值作为构造的参数,我们就可以使用任何我们喜欢的命令。

在前面的代码中,我们将调用者执行的每个命令都推送到一个堆栈上。然后,当我们想要撤销一个命令时,从堆栈中弹出最后一个命令,并执行该命令的undo()方法。在计算器的情况下,执行反向计算以将累加器的值返回到命令执行前的值。

命令模式中的Command类的所有实现都有一个方法,比如由调用者调用来执行命令的execute()Command类的实例保存了对接收方的引用、要执行的方法的名称以及在执行中用作参数的值。接收者是唯一知道如何执行命令中保存的方法的对象,它独立于系统中的其他对象管理自己的内部状态。客户端不知道命令的实现细节,命令和调用者也不知道。明确地说,command对象不执行任何东西;它只是一个容器。通过实现这种模式,我们在要执行的动作和调用该动作的对象之间添加了一个抽象层。增加的抽象导致系统中不同对象之间更好的交互,以及它们之间更松散的耦合,使得系统更容易维护和更新。

这种设计模式的核心是将方法调用转换成数据,这些数据可以保存在变量中,作为参数传递给方法或函数,并作为结果从函数返回。应用这种模式的结果是函数或方法成为一等公民。当函数是一级公民时,变量可以指向函数,函数可以作为参数传递给其他函数,它们可以作为执行函数的结果返回。

command 模式的有趣之处,特别是这种将行为转变为一等公民的能力,在于我们可以使用这种模式来实现一种带有惰性求值的函数式编程风格,这意味着函数可以传递给其他函数,也可以由函数返回。所有函数都传递了执行所需的所有信息,没有全局状态,只有在需要返回实际结果时才执行函数。这是对函数式编程的一个相当肤浅的描述,但其含义是深刻的。

我们故事中有趣的转折是,在 Python 中一切都是函数,因此我们也可以去掉封装函数调用的类,只需将函数和相关参数传递给调用者。

def text_writer(string1, string2):
    print("Writing {} - {}".format(string1, string2))

class Invoker(object):
    def __init__(self):
        self.commands = []

    def add_command(self, command):
        self.commands.append(command)

    def run(self):
        for command in self.commands:
            command"function"

if __name__ == "__main__":
    invoker = Invoker()
    invoker.add_command({
        "function": text_writer,
        "params": ("Command 1", "String 1")
    })
    invoker.add_command({
        "function": text_writer,
        "params": ("Command 2", "String 2")
    })
    invoker.run()

对于这样一个简单的例子,你可以只定义一个 lambda 函数,甚至不需要写函数定义。

class Invoker(object):
    def __init__(self):
        self.commands = []

    def add_command(self, command):
        self.commands.append(command)

    def run(self):
        for command in self.commands:
            command"function"

if __name__ == "__main__":
    f = lambda string1, string2: print("Writing {} - {}".format(string1, string2))

    invoker = Invoker()
    invoker.add_command({
        "function": f,
        "params": ("Command 1", "String 1")
    })
    invoker.add_command({
        "function": f,
        "params": ("Command 2", "String 2")
    })
    invoker.run()

一旦你想执行的函数变得更复杂,除了我们在前一个程序中使用的方法之外,你还有两个选择。您可以将命令的execute()函数包装在 lambda 中,从而避免创建类。尽管这是一个巧妙的技巧,但是如果你只是为复杂的情况创建一个类,你的代码可能会更清晰,这是第二种选择。所以,一个好的经验法则是,当你需要为比一段代码更复杂的东西创建命令时,你可以用一行 lambda 函数来包装,为它创建一个类。

临别赠言

无论是建造自动驾驶汽车还是向外太空发送消息,将执行请求与实际执行解耦,不仅可以缓冲需要执行的动作,还可以交换输入的模式。

重要的是,命令模式将调用方与接收方隔离开来。它还将执行的建立时间与处理时间分开。

练习

  • 使用 command 模式创建一个命令行单词替换脚本,该脚本可以用其他单词替换给定文本文件中给定单词的每个实例。然后,添加一个命令来撤消上一次替换操作。

十二、解释器模式

西尔维亚·布鲁姆:当你睡不着的时候你会做什么?托宾·凯勒:我保持清醒。—解释器

有时,不使用 Python 来描述问题或此类问题的解决方案是有意义的。当我们需要一种面向特定问题领域的语言时,我们求助于一种叫做特定领域语言的东西。

特定领域语言

有些语言,如 Python,是为解决任何类型的问题而创建的。Python 属于被称为通用语言的整个语言家族,旨在用于解决任何问题。在极少数情况下,创建一种只做一件事,但做得非常好的语言更有意义。这些语言属于特定领域语言(DSL)家族,对那些在某个特定领域是专家但不是程序员的人很有帮助。

在您的编程生涯中,您可能已经遇到过 DSL。如果你曾经修修补补过一个网站,你知道 CSS,它是一个 DSL,用于定义 HTML 应该如何在浏览器中呈现。

style.css

body {

  color: #00ff00;

}

这一小段 CSS 代码告诉浏览器将使用该 CSS 文件的网页的 body 标记内的所有文本呈现为绿色。浏览器完成这项工作所需的代码比这段简单的代码要复杂得多,但 CSS 对大多数人来说足够简单,几个小时就能学会。HTML 本身是一个 DSL,由浏览器解释,并显示为一个具有相当复杂布局的格式化文档。

或者,您可能在行为驱动开发中使用过 Aloe,在行为驱动开发中,您用人类可读的句子定义了项目的验收标准。

Install the aloe framework using pip
$ pip install aloe

现在,我们可以写一些测试。


zero.feature

Feature: Compute factorial
  In order to play with aloe
  As beginners
  We'll implement factorial

  Scenario: Factorial of 0
    Given I have the number 0
    When I compute its factorial
    Then I see the number 1

接下来,您将这些步骤映射到一些 Python 代码,以便 Aloe 框架能够理解所描述的行为。


steps.py

from aloe import *

@step(r'I have the number (\d+)')

def have_the_number(step, number):
  world.number = int(number)

@step('I compute its factorial')

def compute_its_factorial(step):
  world.number = factorial(world.number)

@step(r'I see the number (\d+)')

def check_number(step, expected):
  expected = int(expected)
  assert world.number == expected, \
    "Got %d" % world.number

def factorial(number):
  return -1

此代码允许您用非技术人员可以理解的术语定义程序的结果,然后将这些术语映射到实际代码,这确保它们按照特性文件中的描述执行。.feature文件中的描述是 DSL,steps.py文件中的 Python 代码用于将 DSL 中的单词和短语转换成计算机可以理解的东西。

当您决定创建一个 DSL(实践领域工程)时,您就能够在领域专家和开发人员之间进行清晰的交流。不仅专家能够更清楚地表达自己,而且开发人员会明白他们需要什么,计算机也是如此。您通常会发现,领域专家会从他们的领域向开发人员描述问题或问题的解决方案,然后开发人员会立即用特定于领域的语言写下描述。领域专家能够理解开发人员对问题或解决方案的描述,并且能够对描述提出修改或扩展的建议。因此,在会议过程中,或者最多几天的时间,描述可以转化为有效的解决方案,从而大大提高每个参与者的工作效率。

让我们来做点实际的。

你已经签约创建一个系统,餐馆可以用它来定义他们的特色菜。这些特价商品有时有简单的规则,如买一送一,但其他时候它们更复杂,如如果你在周二买了三个披萨,最便宜的一个是免费的。挑战在于这些规则经常变化,因此为每次变化重新部署代码是没有意义的。如果您不在乎当您的客户决定更改特价规则时,每周被迫更改代码,那么这将导致的if语句的潜在幅度应该足以吓退您。

既然我们已经讨论了 DSL,您可能会怀疑为餐馆创建 DSL 是解决方案。然后,对特殊标准的任何更改或添加都可以独立于主应用进行存储,并且只在调用时进行解释。

在一次内容丰富的演讲中,ICE 的技术主管 Neil Green 建议采用以下流程来开发 DSL:

  1. 了解你的领域。
  2. 为你的领域建模。
  3. 实现您的域。

如果我们按照尼尔的指导,我们会去和餐馆老板谈谈。我们会尝试学习他或她如何表达特色菜的规则,然后尝试用一种对我们和店主都有意义的语言来捕捉这些规则。然后我们会根据我们的谈话画出一些图表。如果所有者同意我们抓住了他或她所说的本质,我们就可以编写一些代码来实现我们的模型。

DSL 有两种类型:内部和外部。外部 DSL 将代码写在外部文件中或作为字符串。该字符串或文件在使用前由应用读取和解析。CSS 是外部 DSL 的一个例子,其中浏览器的任务是读取、解析和解释表示 HTML 元素应该如何显示的文本数据。另一方面,内部 DSL 使用语言的特性——在这种情况下是 Python——使人们能够编写类似领域语法的代码,一个例子是 numpy 如何使用 Python 来允许数学家描述线性代数问题。

解释器模式特别适合内部 DSL。

这个想法是创建一种语法,它远不如通用语言全面,但却更具表现力,特别是当它与所讨论的领域相关时。

看看下面的代码,它定义了我们想象中的餐馆经营的特色菜的规则,并用 Python 写成了条件语句。

pizzas = [item for tab.items if item.type == "pizza"]

if len(pizzas) > 2 and day_of_week == 4:
    cheapest_pizza_price = min([pizza.price for pizza in pizzas])
    tab.add_discount(cheapest_pizza_price)

drinks = [item for tab.items if item.type == "drink"]

if 17 < hour_now < 19:
    for item in tab.items:
        if item.type == "drink":
            item.price = item.price * 0.90   

if tab.customer.is_member():
    for item in tab.items:
        item.price = item.price * 0.85

现在,看看前面的代码片段和在一个简单的用于定义特价商品的 DSL 中相同规则的定义之间的鲜明对比。

If tab contains 2 pizzas on Wednesdays cheapest one is free
Every day from 17:00 to 19:00 drinks are less 10%
All items are less 15% for members

我们现在还不看解释,它将在稍后出现。我想让你注意的是,DSL 更清楚地传达了如何应用特价规则。它以一种对企业主有意义的方式书写,并允许他们确认它确实符合他们的意图,或者是犯了一个错误,需要更正。模糊性减少了,添加新规则或特色菜变得更加简单——这是两方面的双赢。

DSL 的优势

对于 DSL 所创建的领域的理解和交流水平大大提高了。领域专家可以在没有软件工程背景的情况下对问题和解决方案的表达进行推理。商业信息系统的开发因此可以从软件开发人员转移到领域专家。这导致更丰富、更准确的系统,用系统帮助的专家可以理解的术语来表达。

DSL 的缺点

在您开始为您能想到的每个问题领域创建 DSL 之前,请记住学习 DSL 是有成本的,即使它对领域专家来说是有意义的。新来的人可能理解你的 DSL 在表达什么,但是他们仍然需要学习语言实现以便能够使用它。你的 DSL 越深入和丰富,领域专家操作该语言所需的知识就越多,这可能会影响到能够使用和维护该系统的人数。请记住,DSL 的使用应该有助于业务,而不是阻碍业务,否则从长远来看,如果一开始就采用这种语言,它将会抛弃这种语言。

我们已经考虑了使用 DSL 的利弊,并决定使用 DSL 来指定餐馆特色菜的规则。

在宏观层面上,您希望完成两项任务。首先,你要定义语言。具体来说,您希望定义语言的语义(含义)和语法(结构)。接下来,你要编写代码,能够把语言作为输入,并把它翻译成机器可以理解和行动的形式。

根据您与客户的对话,提取流程中涉及的内容以及每个相关实体采取的行动。在此之后,分离出在所考虑的领域中具有特定含义的常用单词或短语。使用这三个元素构建一个语法,在实现之前,您将与领域专家讨论这个语法。

在我们看一个关于餐馆特色菜的例子之前,我想让你把自己想象成一个工具制造者。每当你遇到挫折或问题时,你应该开始思考你将如何着手解决这个问题,以及你将编写什么工具来使这个过程十倍地更容易或更快。这是将最好的软件开发人员和工程师与该领域其他人区分开来的心态。

在我们的示例中,我们首先与餐馆老板(领域专家)进行了一系列对话,并确定了以下特色菜规则:

Members always get 15% off their total tab
During happy hour, which happens from 17:00 to 19:00 weekdays, all drinks are less 10%
Mondays are buy one get one free burger nights
Thursday are 'eat all you can' ribs
Sunday nights after 18:00 buy three pizzas and get the cheapest one free when you buy 6 you get the two cheapest ones free and so on for every three additional pizzas

然后,我们确定了被提及的内容:

members
tabs
happy hour
drinks
weekdays
Mondays
burgers
Thursday
ribs
Sunday
pizzas

我们还确定了可以采取的行动:

get 15% discount
get 10% discount
get one free
eat all you can

最后,我们认识到了与餐厅特色菜相关的关键理念:

If a customer is of a certain type they always get a fixed % off their total tab
At certain times all items of a specific type gets discounted with a fixed percentage
At certain times you get a second item of a certain type free if you were to buy one item of that type

概括关键理念中的元素会产生以下特价规则:

If certain conditions are met certain items get a % discount

一种正式表达语法的方式是用扩展的巴科斯诺尔形式(EBNF 关于 EBNF 的更多信息参见: https://www.cs.umd.edu/class/fall2002/cmsc214/Tutorial/ebnf.html )。我们在 ENBF 中的餐馆示例如下所示:

rule: "If ", conditions, " then ", item_type, " get ", discount
conditions: condition | conditions " and " conditions | conditions " or " conditions
condition: time_condition | item_condition | customer_condition
discount: number, "% discount" | "cheapest ", number, item_type " free"

time_condition: "today is ", day_of_week | "time is between ", time, " and ", time | "today is a week day" | "today not a week day"
day_of_week: "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday"
time: hours, ":", minutes
hours: hour_tens, hour_ones
hour_tens: "0" | "1"
hour_ones: digit
minutes: minute_tens, minute_ones
minute_tens: "0" | "1" | "2" | "3" | "4" | "5"
minute_ones: digit

item_condition: "item is a ", item_type | "there are ", number, " of ", item_type
item_type: "pizza" | "burger" | "drink" | "chips"
number: {digit}
digit: "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"

customer_condition: "customer is a ", customer_type
customer_type: "member" | "non-member"

您有一组不能用任何模式替换的终结符,以及一组非终结符产生规则,这些规则可用于用一系列终结符或其他非终结符模式的某种配方替换非终结符(占位符)。非终结符的不同替换方式由管道字符“|”分隔。

当您创建一个外部 DSL 时,可以使用 PyParsing 之类的包来把用刚才看到的语法编写的字符串翻译成 Python 代码可以处理的数据类型。

现在,您已经看到了一个示例,说明了如何通过与领域专家的对话来理解领域,识别领域的关键元素,将这种理解整理成某种可以被领域专家验证的领域模型,最后将 DSL 实现为过程的最终产品,从而完成领域建模的过程。

从现在开始,我们将处理内部 DSL。

如果我们要将特殊规则作为内部 DSL 来实现,我们基本上需要为每个符号创建一个类。

我们首先根据前面定义的语法创建存根:

class Tab(object):
    pass

class Item(object):
    pass

class Customer(object):
    pass

class Discount(object):
    pass

class Rule(object):
    pass

class CustomerType(object):
    pass

class ItemType(object):
    pass

class Conditions(object):
    pass

class Condition(object):
    pass

class TimeCondition(object):
    pass

class DayOfWeek(object):
    pass

class Time(object):
    pass

class Hours(object):
    pass

class HourTens(object):
    pass

class HourOnes(object):
    pass

class Minutes(object):
    pass

class MinuteTens(object):
    pass

class MinuteOnes(object):
    pass

class ItemCondition(object):
    pass

class Number(object):
    pass

class Digit(object):
    pass

class CustomerCondition(object):
    pass

在最终的设计中并不需要所有这些类,因此你应该注意 YAGNI 原则;基本上,这个想法是你不应该建造你不需要的东西。在这种情况下,我把它们都写了出来,所以你可以看到我们在定义语法的过程中提到的每一件事,并为每一件事创建了一个类。

在我们减少类并创建最终的内部 DSL 之前,我们将涉及复合模式,这在我们开始构建解释器时会很有用。

复合模式

当一个容器中的元素本身也可以是容器时,就很适合使用复合模式。看看我们为餐馆开发的 EBNF 版本的语法;项目可以是包含项目的组合。

复合模式定义了复合(即非终端)类和叶(即终端)类,它们可用于构建复合组件,如特殊规则。

class Leaf(object):
  def __init__(self, *args, **kwargs):
    pass

  def component_function(self):
    print("Leaf")

class Composite(object):
  def __init__(self, *args, **kwargs):
    self.children = []

  def component_function(self):
    for child in children:
      child.component_function()

  def add(self, child):
    self.children.append(child)

  def remove(self, child):
    self.children.remove(child)

在动态性较低的语言中,您还必须定义一个由CompositeLeaf类继承的超类,但是由于 Python 使用了 duck 类型,我们再次避免了创建不必要的样板代码。

使用复合模式的内部 DSL 实现

让我们考虑一下本章开始时餐馆折扣的第一个规则的实现,作为一些基本的内部 DSL。

class Tab(object):
    def __init__(self, customer):
        self.items = []
        self.discounts = []
        self.customer = customer

    def calculate_cost(self):
        return sum(x.cost for x in self.items)

    def calculate_discount(self):
        return sum(x for x in self.discounts)

class Item(object):
    def __init__(self, name, item_type, cost):
        self.name = name
        self.item_type = item_type
        self.cost = cost

class ItemType(object):
    def __init__(self, name):
        self.name = name

class Customer(object):
    def __init__(self, customer_type, name):
        self.customer_type = customer_type
        self.name = name

    def is_a(self, customer_type):
        return self.customer_type == customer_type

class Discount(object):
    def __init__(self, amount):
        self.amount = amount

class CustomerType(object):
    def __init__(self, customer_type):
        self.customer_type = customer_type

class Rule(object):
    def __init__(self, tab):
        self.tab = tab
        self.conditions = []
        self.discounts = []

    def add_condition(self, test_value):
        self.conditions.append(test_value)

    def add_percentage_discount(self, item_type, percent):
        if item_type == "any item":
            f = lambda x: True
        else:
            f = lambda x: x.item_type == item_type

        items_to_discount = [item for item in self.tab.items if f(item)]
        for item in items_to_discount:
            discount = Discount(item.cost * (percent/100.0))
            self.discounts.append(discount)

    def apply(self):
        if all(self.conditions):
            return sum(x.amount for x in self.discounts)

        return 0

if __name__ == "__main__":
    member = CustomerType("Member")
    member_customer = Customer(member, "John")
    tab = Tab(member_customer)

    pizza = ItemType("pizza")
    burger = ItemType("Burger")
    drink = ItemType("Drink")

    tab.items.append(Item("Margarita", pizza, 15))
    tab.items.append(Item("Cheddar Melt", burger, 6))
    tab.items.append(Item("Latte", drink, 4))

    rule = Rule(tab)
    rule.add_condition(tab.customer.is_a(member))
    rule.add_percentage_discount("any item", 15)

    tab.discounts.append(
        rule.apply()
    )

    print(
        "Calculated cost: {}\nDiscount applied: {}\n{}% Discount applied".format(
            tab.calculate_cost(),
            tab.calculate_discount(),
            100 * tab.calculate_discount() / tab.calculate_cost()
        )
    )

现在我们有了一个工作的规则,使用产生某种形式可读代码的对象,让我们回到使用复合模式的 DSL 实现。条件可以是一组合取条件、一组析取条件或单个布尔表达式。

class AndConditions(object):
    def __init__(self):
        self.conditions = []

    def evaluate(self, tab):
        return all(x.evaluate(tab) for x in self.conditions)

    def add(self, condition):
        self.conditions.append(condition)

    def remove(self, condition):
        self.conditions.remove(condition)

class OrConditions(object):
    def __init__(self):
        self.conditions = []

    def evaluate(self, tab):
        return any(x.evaluate(tab) for x in self.conditions)

    def add(self, condition):
        self.conditions.append(condition)

    def remove(self, condition):
        self.conditions.remove(condition)

class Condition(object):
    def __init__(self, condition_function):
        self.test = condition_function

    def evaluate(self, tab):
        return self.test(tab)

class Discounts(object):
    def __init__(self):
        self.children = []

    def calculate(self, tab):
        return sum(x.calculate(tab) for x in self.children)

    def add(self, child):
        self.children.append(child)

    def remove(self, child):
        self.children.remove(child)

class Discount(object):
    def __init__(self, test_function, discount_function):
        self.test = test_function
        self.discount = discount_function

    def calculate(self, tab):
        return sum(self.discount(item) for item in tab.items if self.test(item))

class Rule(object):
    def __init__(self, tab):
        self.tab = tab
        self.conditions = AndConditions()
        self.discounts = Discounts()

    def add_conditions(self, conditions):
        self.conditions.add(conditions)

    def add_discount(self, test_function, discount_function):
        discount = Discount(test_function, discount_function)
        self.discounts.add(discount)

    def apply(self):
        if self.conditions.evaluate(self.tab):
            return self.discounts.calculate(self.tab)

        return 0

实现解释器模式

两种类型的人使用软件:一种是对现成的产品感到满意的人,另一种是愿意修改和调整软件以更好地满足他们需求的人。解释器模式只对第二组人感兴趣,因为他们愿意花时间学习如何使用 DSL 来使软件满足他们的需求。

回到餐馆,一个老板会对一些特价菜的基本模板感到满意,比如买一送一的优惠,而另一个餐馆老板会希望调整和扩大他的优惠。

我们现在可以概括我们以前所做的 DSL 的基本实现,以创建一种将来解决这些问题的方法。

每个表达式类型都有一个类(和以前一样),每个类都有一个interpret方法。还需要一个类和一个对象来存储全局上下文。这个上下文被传递给解释流中下一个对象的interpret函数。假设解析已经发生。

解释器递归地遍历容器对象,直到找到问题的答案。

class NonTerminal(object):
    def __init__(self, expression):
        self.expression = expression

    def interpret(self):
        self.expression.interpret()

class Terminal(object):
    def interpret(self):
        pass

最后,解释器模式可以用来决定某个选项卡是否符合任何特殊条件。首先,定义 tab 和 item 类,然后是语法所需的类。然后,使用测试标签实现并测试语法中的一些句子。注意,我们对这个例子中的类型进行了硬编码,这样代码就可以运行了;通常,这些是你想存储在一些文件或数据库中的东西。

import datetime

class Rule(object):
    def __init__(self, conditions, discounts):
        self.conditions = conditions
        self.discounts = discounts

    def evaluate(self, tab):
        if self.conditions.evaluate(tab):
            return self.discounts.calculate(tab)

        return 0

class Conditions(object):
    def __init__(self, expression):
        self.expression = expression

    def evaluate(self, tab):
        return self.expression.evaluate(tab)

class And(object):
    def __init__(self, expression1, expression2):
        self.expression1 = expression1
        self.expression2 = expression2

    def evaluate(self, tab):
        return self.expression1.evaluate(tab) and self.expression2.evaluate(tab)

class Or(object):
    def __init__(self, expression1, expression2):
        self.expression1 = expression1
        self.expression2 = expression2

    def evaluate(self, tab):
        return self.expression1.evaluate(tab) or self.expression2.evaluate(tab)

class PercentageDiscount(object):
    def __init__(self, item_type, percentage):
        self.item_type = item_type
        self.percentage = percentage

    def calculate(self, tab):
        return (sum([x.cost for x in tab.items if x.item_type == self.item_type]) * self.percentage) / 100

class CheapestFree(object):
    def __init__(self, item_type):
        self.item_type = item_type

    def calculate(self, tab):
        try:
            return min([x.cost for x in tab.items if x.item_type == self.item_type])
        except:
            return 0

class TodayIs(object):
    def __init__(self, day_of_week):
        self.day_of_week = day_of_week

    def evaluate(self, tab):
        return datetime.datetime.today().weekday() == self.day_of_week.name

class TimeIsBetween(object):
    def __init__(self, from_time, to_time):
        self.from_time = from_time
        self.to_time = to_time

    def evaluate(self, tab):
        hour_now = datetime.datetime.today().hour
        minute_now = datetime.datetime.today().minute

        from_hour, from_minute = [int(x) for x in self.from_time.split(":")]
        to_hour, to_minute = [int(x) for x in self.to_time.split(":")]

        hour_in_range = from_hour <= hour_now < to_hour
        begin_edge = hour_now == from_hour and minute_now > from_minute
        end_edge = hour_now == to_hour and minute_now < to_minute

        return any(hour_in_range, begin_edge, end_edge)

class TodayIsAWeekDay(object):
    def __init__(self):
        pass

    def evaluate(self, tab):
        week_days = [
            "Monday",
            "Tuesday",
            "Wednesday",
            "Thursday",
            "Friday",
        ]
        return datetime.datetime.today().weekday() in week_days

class TodayIsAWeekedDay(object):
    def __init__(self):
        pass

    def evaluate(self, tab):
        weekend_days = [
            "Saturday",
            "Sunday",
        ]
        return datetime.datetime.today().weekday() in weekend_days

class DayOfTheWeek(object):
    def __init__(self, name):
        self.name = name

class ItemIsA(object):
    def __init__(self, item_type):
        self.item_type = item_type

    def evaluate(self, item):
        return self.item_type == item.item_type

class NumberOfItemsOfType(object):
    def __init__(self, number_of_items, item_type):
        self.number = number_of_items
        self.item_type = item_type

    def evaluate(self, tab):
        return len([x for x in tab.items if x.item_type == self.item_type]) == self.number

class CustomerIsA(object):
    def __init__(self, customer_type):
        self.customer_type = customer_type

    def evaluate(self, tab):
        return tab.customer.customer_type == self.customer_type

class Tab(object):
    def __init__(self, customer):
        self.items = []
        self.discounts = []
        self.customer = customer

    def calculate_cost(self):
        return sum(x.cost for x in self.items)

    def calculate_discount(self):
        return sum(x for x in self.discounts)

class Item(object):
    def __init__(self, name, item_type, cost):
        self.name = name
        self.item_type = item_type
        self.cost = cost

class ItemType(object):
    def __init__(self, name):
        self.name = name

class Customer(object):
    def __init__(self, customer_type, name):
        self.customer_type = customer_type
        self.name = name

class CustomerType(object):
    def __init__(self, customer_type):
        self.customer_type = customer_type

member = CustomerType("Member")
pizza = ItemType("pizza")
burger = ItemType("Burger")
drink = ItemType("Drink")

monday = DayOfTheWeek("Monday")

def setup_demo_tab():
    member_customer = Customer(member, "John")
    tab = Tab(member_customer)

    tab.items.append(Item("Margarita", pizza, 15))
    tab.items.append(Item("Cheddar Melt", burger, 6))
    tab.items.append(Item("Hawaian", pizza, 12))
    tab.items.append(Item("Latte", drink, 4))
    tab.items.append(Item("Club", pizza, 17))

    return tab

if __name__ == "__main__":
    tab = setup_demo_tab()

    rules = []

    # Members always get 15% off their total tab

    rules.append(
        Rule(
            CustomerIsA(member),
            PercentageDiscount("any_item", 15)
        )
    )

    # During happy hour, which happens from 17:00 to 19:00 weekdays, all drinks are less 10%

    rules.append(
        Rule(
            And(TimeIsBetween("17:00", "19:00"), TodayIsAWeekDay()),
            PercentageDiscount(drink, 10)
        )
    )

    # Mondays are buy one get one free burger nights

    rules.append(
        Rule(
            And(TodayIs(monday), NumberOfItemsOfType(burger, 2)),
            CheapestFree(burger)
        )
    )

    for rule in rules:
        tab.discounts.append(rule.evaluate(tab))

    print(
        "Calculated cost: {}\nDiscount applied: {}\n".format(
            tab.calculate_cost(),
            tab.calculate_discount()
        )
    )

在这一章中,你看到了解释语法和内部 DSL 的两种方法。我们查看了复合模式,然后用它来解释一家餐馆的特色菜规则。然后,我们基于这个想法,结合通用解释器模式,开发了一个更完整的解释器来测试条件并计算适用的折扣。本质上,您经历了从获取业务需求到根据 DSL 定义问题,然后用代码实现 DSL 的整个过程。

临别赠言

无论你在你的编程之旅的哪个地方,现在就决定你将努力工作以变得更好。你通过从事编程项目和进行编程挑战来做到这一点。

Jeff Bay 的一个这样的挑战可以在务实程序员出版的思想作品选集中找到。

尝试一个不平凡的项目(需要 1000 多行代码来解决的项目),遵循以下规则:

  1. 每个方法只允许一级缩进,因此循环或嵌套中没有if语句。
  2. 不允许使用关键字else
  3. 所有基本类型和字符串都必须包装在对象中——专门用于它们的用途。
  4. 集合是第一类,因此需要它们自己的对象。
  5. 不要缩写名字。如果名字太长,你可能在一个方法或类中做了不止一件事——不要这样做。
  6. 每行只允许一个对象操作符,所以object.method()可以,但是object.attribute.method()不行。
  7. 保持你的实体小(包小于 15 个对象,类小于 50 行,方法小于 5 行)。
  8. 任何类都不能有两个以上的实例变量。
  9. 不允许使用 getters、setters 或直接访问属性。

练习

  • 按照我们在为餐馆定义 DSL 时使用的相同的思维过程来定义 DSL,您可以在自己的 to-do list 应用中实现它。这个 DSL 应该允许你的程序获取像“每周三”这样的提示。。."并将它们转化为周期性的任务。还要加上日期意识和其他一些相对于其他日期或时间的时间指示。
  • 用 Python 实现前面练习中的 DSL,这样它就可以解释待办事项并提取必要的特征。
  • 使用流行的 Python web 框架之一构建一个基本的待办事项列表应用,并让它使用 DSL 解释器来解释待办事项。
  • 找到一些有趣的改进或扩展,使待办事项列表应用成为你可以每天使用的东西。

十三、迭代器模式

公共汽车的轮子转啊转,转啊转,转啊转

数据结构和算法是软件设计和开发过程中不可或缺的一部分。通常,不同的数据结构有不同的用途,对于特定的问题使用正确的数据结构可能意味着更少的工作和更高的效率。选择正确的数据结构,并将其与相关算法相匹配,将会改进您的解决方案。

由于数据结构有不同的实现,我们倾向于实现对它们进行不同操作的算法。例如,如果我们考虑一个看起来像这样的二叉树:

class Node(object):
    def __init__(self, data):
        self.data = data
        self.left = None
        self.right = None

遍历树的元素以确保您访问了每一个元素,可能如下所示:

class Node(object):
    def __init__(self, data):
        self.data = data
        self.left = None
        self.right = None

def tree_traverse(node):
    if node.left is not None:
        tree_traverse(node.left)

    print(node.data)

    if node.right is not None:
        tree_traverse(node.right)

if __name__ == "__main__":
    root = Node("i am the root")

    root.left = Node("first left child")
    root.right = Node("first right child")

    root.left.right = Node("Right child of first left child of root")

    tree_traverse(root)

相比之下,当您想要遍历一个简单的列表时:

lst = [1, 2, 3, 4, 5]

for i in range(len(lst)):
    print(lst[i])

我知道我以一种不被认为是 pythonic 的方式使用了for循环,但是如果你来自 C/C++或 Java 背景,这看起来会很熟悉。我这样做是因为我想说明如何遍历链表和二叉树,以及它们是如何产生相似结果的截然不同的过程。

作为泛型编程的倡导者和 C++标准模板库的主要设计者和实现者,亚历山大·斯捷潘诺夫花了很多时间思考泛型编程的技术,在泛型编程中,代码是根据算法编写的,这些算法对将来要定义的数据类型进行操作。他将纯数学和代数的思想与计算机科学结合起来,并得出结论,大多数算法都可以用一种称为容器的代数数据类型来定义。

通过将算法与容器的特定类型和实现分离,您可以自由地描述算法,而无需关注特定类型容器的实际实现细节。这种分离的结果是更通用、更可重用的代码。

我们希望能够遍历一个集合,而不用担心我们是在处理一个列表、一个二叉树还是其他集合。

为此,我们希望创建一个集合数据类型可以继承的接口,这将允许它一般化遍历集合内容的动作。我们通过使用以下组件来实现这一点。

首先,我们定义了一个接口,该接口定义了一个获取集合中下一个项目的函数,以及另一个提醒外部函数集合中没有元素可返回的函数。

第二,我们定义了某种可以使用接口来遍历集合的对象。这个对象叫做迭代器。

按照传统的方法,我们可以如下实现这个想法:


classic_iter.py

import abc

class Iterator(metaclass=abc.ABCMeta):

    @abc.abstractmethod
    def has_next(self): pass

    @abc.abstractmethod
    def next(self): pass

class Container(metaclass=abc.ABCMeta):

    @abc.abstractmethod
    def getIterator(self): pass

class MyListIterator(Iterator):
        def __init__(self, my_list):
            self.index = 0
            self.list = my_list.list

        def has_next(self):
            return self.index < len(self.list)

        def next(self):
            self.index += 1
            return self.list[self.index - 1]

class MyList(Container):    
    def __init__(self, *args):
        self.list = list(args)

    def getIterator(self):
        return MyListIterator(self)

if __name__ == "__main__":
    my_list = MyList(1, 2, 3, 4, 5, 6)
    my_iterator = my_list.getIterator()

    while my_iterator.has_next():
        print(my_iterator.next())

这会打印出以下结果:

1
2
3
4
5
6

这种遍历集合的想法如此普遍,以至于它有了一个名字——迭代器模式。

迭代器模式的 Python 内部实现

您可能已经猜到了,Python 使得迭代器模式的实现变得极其简单。事实上,你已经在本书中使用了迭代器。Python 中实现for循环的方式使用迭代器模式。

以这个for循环为例:

for i in range(1, 7):
  print(i)

这类似于我们在上一节中看到的迭代器模式实现。

Python 中的这种便利是通过定义迭代器协议来实现的,迭代器协议用于创建可迭代的对象,然后返回一些知道如何迭代这些可迭代对象的对象。

Python 使用两个特定的方法调用和一个引发的异常来提供整个语言的迭代器功能。

iterable 的第一个方法是__ iter__()方法,它返回一个 iterator 对象。接下来,迭代器对象必须提供一个__next__()方法;它用于返回 iterable 中的下一个元素。最后,当迭代完所有元素后,迭代器会引发一个StopIteration异常。

我们经常看到 Python 中的 iterable 集合返回自身的一个实例,因为这个类也实现了__ next__()方法并引发了StopIteration异常。类似于我们之前看到的经典实现的迭代器可能看起来像这样:

class MyList(object):    
    def __init__(self, *args):
        self.list = list(args)
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        try:
            self.index += 1
            return self.list[self.index - 1]
        except IndexError:
            raise StopIteration()

if __name__ == "__main__":
    my_list = MyList(1, 2, 3, 4, 5, 6)

    for i in my_list:
        print(i)

这一次,您会注意到我们使用 Python for循环来迭代列表中的元素,这是唯一可能的,因为我们的MyList类具有 Python 迭代器协议所需的函数。

如果我们要将本章开头看到的二叉树实现为 Python iterable,那么它将如下实现:


bin_tree_iterator.py

class Node(object):
    def __init__(self, data):
        self.data = data
        self.left = None
        self.right = None

class MyTree(object):
    def __init__(self, root):
        self.root = root

    def add_node(self, node):
        current = self.root

        while True:
            if node.data <= current.data:
                if current.left is None:
                    current.left = node
                    return

                else:
                    current = current.left
            else:
                if current.right is None:
                    current.right = node
                    return

                else:
                    current = current.right

    def __iter__(self):
        if self.root is None:
            self.stack = []
        else:
            self.stack = [self.root]
            current = self.root
            while current.left is not None:
                current = current.left
                self.stack.append(current)

        return self

    def __next__(self):
        if len(self.stack) <= 0:
            raise StopIteration

        while len(self.stack) > 0:
            current = self.stack.pop()
            data = current.data

            if current.right is not None:
                current = current.right
                self.stack.append(current)

                while current.left is not None:
                    current = current.left
                    self.stack.append(current)

            return data

        raise StopIteration

if __name__ == "__main__":
    tree = MyTree(Node(16))
    tree.add_node(Node(8))
    tree.add_node(Node(1))
    tree.add_node(Node(17))
    tree.add_node(Node(13))
    tree.add_node(Node(14))
    tree.add_node(Node(9))
    tree.add_node(Node(10))
    tree.add_node(Node(11))

    for i in tree:
        print(i)

我们保留了之前定义的Node类,只是现在我们添加了一个Container类,即MyTree。这个类实现了迭代器协议,因此可以在普通的 Python for循环中使用,正如我们在列表迭代器中看到的。我们还包含了一个方便的函数,允许我们通过简单地调用MyTree的树实例上的add_node方法来构建一个二叉树。这让我们忘记了树是如何实现的。向树中插入新节点的过程只是查看当前节点,如果要添加的节点的数据值小于或等于树中当前节点的数据值,则向左移动。我们一直这样做,直到找到一个空白点,新的节点被放置在这个空白点上。

为了遍历树,我们保存一个节点堆栈,然后我们开始重复推送,向左移动并将当前节点推送到堆栈上。这种情况一直持续到不再有留守儿童。然后,我们从堆栈中弹出顶部节点,并检查它是否有一个正确的子节点。如果是这样的话,右边的子节点将被推送到堆栈上,同时还有从该节点开始的树的最左边的分支。然后,我们返回从堆栈中弹出的节点的值。堆栈作为树对象的一个实例变量来维护,这样我们就能够在每次调用__next__()方法时从停止的地方继续。

这将导致打印有序序列,如下所示:

1
8
9
10
11
13
14
16
17

我们可以使用现有的 Python 函数和结构与二叉树接口,比如for循环和maxsum函数。只是为了好玩,看看对完全自定义的数据类型使用这些构造有多容易。

您已经看到for循环按预期工作;现在,我们将使用与在bin_tree_iterator.py中相同的代码,但是我们将在最后一个if语句中添加一个maxsum调用。因此,代码片段中只包含了if语句。

if __name__ == "__main__":
    tree = MyTree(Node(16))
    tree.add_node(Node(8))
    tree.add_node(Node(1))
    tree.add_node(Node(17))
    tree.add_node(Node(13))
    tree.add_node(Node(14))
    tree.add_node(Node(9))
    tree.add_node(Node(10))
    tree.add_node(Node(11))

    for i in tree:
        print(i)

    print("maximum value: {}".format(max(tree)))

    print("total of values: {}".format(sum(tree)))

如您所料,结果如下:

1
8
9
10
11
13
14
16
17
maximum value: 17
total of values: 99

这些结构中的每一个的算法的实现都与以后使用它们的数据类型完全分离。非常有趣的是,您可以使用 list comprehension 创建一个可迭代的列表对象(这几乎就像创建可迭代对象的简写),如上所述,只有if语句被更改,所以我将再次只包括if的代码和将树转换成列表所需的代码。

if __name__ == "__main__":
    tree = MyTree(Node(16))
    tree.add_node(Node(8))
    tree.add_node(Node(1))
    tree.add_node(Node(17))
    tree.add_node(Node(13))
    tree.add_node(Node(14))
    tree.add_node(Node(9))
    tree.add_node(Node(10))
    tree.add_node(Node(11))

    print([x for x in tree])

从而产生有序的值列表

[1, 8, 9, 10, 11, 13, 14, 16, 17]

列表理解很容易理解。他们接受一个函数或操作,并将其映射到一个 iterable。您还可以在迭代之后添加某种条件语句来排除某些元素。

让我们改变我们刚刚看到的理解,现在忽略所有 3 的倍数。

再一次用 if x % 6!= 0

if __name__ == "__main__":
    tree = MyTree(Node(16))
    tree.add_node(Node(8))
    tree.add_node(Node(1))
    tree.add_node(Node(17))
    tree.add_node(Node(13))
    tree.add_node(Node(14))
    tree.add_node(Node(9))
    tree.add_node(Node(10))
    tree.add_node(Node(11))

    print([x for x in tree if x % 3 != 0])

正如你所希望的,9 不在最终名单中:

[1, 8, 10, 11, 13, 14, 16, 17]

迭代工具

我相信你现在已经确信迭代器的用处了。因此,我将快速提及标准库中包含的 Itertools 包。它包含许多函数,允许您以一些有趣的方式组合和操作迭代器。这些构件取自函数式编程和 Haskell 等语言的世界,并以 pythonic 的方式重新构思。

要获得这些工具的详细列表,请阅读 Itertools 库的文档。给大家介绍库,我只想提三个功能。

第一个是chain(),它允许你一个接一个地链接多个迭代器。

import itertools

print(list(itertools.chain([1, 2, 3, 4], range(5,9), "the quick and the slow")))

这将遍历三个 iterables 中的每个值,并将它们全部转换为一个打印的列表,如下所示:

[1, 2, 3, 4, 5, 6, 7, 8, 't', 'h', 'e', ' ', 'q', 'u', 'i', 'c', 'k', ' ', 'a', 'n', 'd', ' ', 't', 'h', 'e', ' ', 's', 'l', 'o', 'w']

接下来,我们有了无限迭代器cycle(),它允许你创建一个迭代器,这个迭代器将无限循环传递给它的元素。

import itertools

cycler = itertools.cycle([1, 2, 3])
print(cycler.__next__())
print(cycler.__next__())
print(cycler.__next__())
print(cycler.__next__())
print(cycler.__next__())
print(cycler.__next__())
print(cycler.__next__())
print(cycler.__next__())

每当循环程序到达最后一个元素时,它就从头开始。

1
2
3
1
2
3
1
2

第三个也是最后一个函数是zip_longest(),它组合了一组 iterables,并在每次迭代中返回它们匹配的元素。

zip_longest example

import itertools

list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']

zipped = itertools.zip_longest(list1, list2)

print(list(zipped))

结果是一组对,其中第一对包含两个列表的第一个元素,第二对包含两个列表的第二个元素,依此类推。

[(1, 'a'), (2, 'b'), (3, 'c')]

使用迭代器还可以做许多其他有趣的事情,其中许多将帮助您找到复杂问题的简单解决方案。就像 Python 标准库中包含的大多数库一样,探索和掌握 Itertools 包是值得的。

发电机功能

在我们开始研究生成器表达式之前,让我们写一个简单的脚本来演示它们是做什么的。

gen_  func.py

def gen_squares(n):
    i = 0
    while i < n:
        yield i*i
        print("next i")
        i += 1

if __name__ == "__main__":
    g = gen_squares(4)
    print(g.__next__())
    print(g.__next__())
    print(g.__next__())
    print(g.__next__())
    print(g.__next__())

请求next得到的结果是下一个方块,正如你在这里看到的:

0
next i
1
next i
4
next i
9
next i
Traceback (most recent call last):
  File "gen_func.py", line 15, in <module>
    print(g.__next__())
StopIteration

您将从输出中观察到一些有趣的事情,第一个是每次您调用函数时,输出都会发生变化。您应该注意的第二件事是,函数从刚好在yield语句下面开始执行,并继续执行,直到它再次到达yield为止。

当解释器遇到yield语句时,它记录下函数的当前状态,并返回产生的值。一旦通过__next__()方法请求了下一个值,就加载内部状态,函数从停止的地方继续运行。

生成器是简化迭代器创建的一个很好的方法,因为生成器是一个产生结果序列的函数,它遵循集合的接口,可以按照迭代器模式的 Python 实现进行迭代。对于生成器函数,Python 内部会为您处理迭代器协议。

当调用生成器函数时,它返回一个生成器对象,但是直到第一次调用__next__()方法时才开始执行。此时,函数开始执行并运行,直到达到产量。一旦生成器到达末尾,它会像迭代器一样引发同样的异常。生成器函数返回的生成器也是一个迭代器。

生成器表达式

就像我们使用列表理解作为创建迭代器的速记一样,对于生成器有一种特殊的速记,叫做生成器表达式。

请看下面的生成器表达式示例。它只是我们之前定义的生成器函数的一个替代。

g = (x*x for x in range(4))

生成器表达式可以用作某些使用迭代器的函数的参数,比如max()函数。

print(max((x*x for x in range(4))))

这印出了数字 9。

使用 Python,如果调用函数中只有一个参数,我们可以去掉生成器两边的括号。

print(max(x*x for x in range(4)))

这个更整洁一点。

临别赠言

迭代器和生成器将帮助您在探索 Python 世界时完成大量繁重的工作,并且熟悉使用它们将帮助您更快地编码。它们还将您的代码扩展到函数式编程领域,在这里您更关注于定义程序必须做什么,而不是必须如何做。

说到更快地编码,这里有一些提高开发速度的技巧。

  • 学会触摸打字。这就像能读会写一样。如果你每天都要做某件事,学会尽可能有效地去做是一个重要的力量倍增器。
  • 研究并掌握你选择的编辑器或 IDE,无论是 Vim、Emacs、Pycharm,还是你使用的任何其他工具,确保学会所有的快捷键。
  • 排除杂念,因为每次你需要切换上下文时,你将需要 30 分钟来重新找到你的最佳状态。不值得。
  • 确保你的测试运行得很快。你的测试越慢,跳过它们的诱惑就越大。
  • 花在网上搜索答案或例子的每一刻都是编码和思考时间的浪费,所以学习这个领域,掌握这门语言,减少你在论坛上的时间。
  • 偿还技术债务,就像你的生活依赖于它一样——你的幸福确实如此。

练习

  • 使用生成器和常规迭代器实现斐波那契数列的可迭代。
  • 使用生成器函数创建一个二叉树迭代器。