Python 入门指南(七)(2)

简介: Python 入门指南(七)

Python 入门指南(七)(1)https://developer.aliyun.com/article/1507456

单例实现

Python 没有私有构造函数,但为了这个目的,我们可以使用__new__类方法来确保只创建一个实例:

class OneOnly: 
 _singleton = None 
    def __new__(cls, *args, **kwargs): 
        if not cls._singleton: 
 cls._singleton = super(OneOnly, cls 
                ).__new__(cls, *args, **kwargs) 
        return cls._singleton 

当调用__new__时,通常会构造该类的一个新实例。当我们重写它时,我们首先检查我们的单例实例是否已经创建;如果没有,我们使用super调用来创建它。因此,每当我们在OneOnly上调用构造函数时,我们总是得到完全相同的实例:

>>> o1 = OneOnly()
    >>> o2 = OneOnly()
    >>> o1 == o2
    True
    >>> o1
    <__main__.OneOnly object at 0xb71c008c>
    >>> o2
    <__main__.OneOnly object at 0xb71c008c>  

这两个对象是相等的,并且位于相同的地址;因此,它们是同一个对象。这个特定的实现并不是很透明,因为很难看出一个单例对象已经被创建。每当我们调用一个构造函数,我们期望得到该对象的一个新实例;在这种情况下,这个约定被违反了。也许,如果我们真的认为需要一个单例,类的良好文档字符串可以缓解这个问题。

但我们并不需要它。Python 程序员不喜欢强迫他们的代码用户进入特定的思维方式。我们可能认为一个类只需要一个实例,但其他程序员可能有不同的想法。单例可能会干扰分布式计算、并行编程和自动化测试,例如。在所有这些情况下,拥有特定对象的多个或替代实例可能非常有用,即使正常操作可能永远不需要一个。

模块变量可以模仿单例

通常,在 Python 中,可以使用模块级变量来充分模拟单例模式。它不像单例那样安全,因为人们随时可以重新分配这些变量,但就像我们在第十六章中讨论的私有变量一样,在 Python 中这是可以接受的。如果有人有充分的理由更改这些变量,我们为什么要阻止他们呢?它也不会阻止人们实例化对象的多个实例,但同样,如果他们有充分的理由这样做,为什么要干涉呢?

理想情况下,我们应该给它们一个机制来访问默认的单例值,同时也允许它们在需要时创建其他实例。虽然从技术上讲根本不是单例,但它提供了最符合 Python 风格的单例行为机制。

为了使用模块级变量而不是单例,我们在定义类之后实例化类的实例。我们可以改进我们的状态模式以使用单例。我们可以创建一个始终可访问的模块级变量,而不是在每次更改状态时创建一个新对象:

class Node:
    def __init__(self, tag_name, parent=None):
        self.parent = parent
        self.tag_name = tag_name
        self.children = []
        self.text = ""
    def __str__(self):
        if self.text:
            return self.tag_name + ": " + self.text
        else:
            return self.tag_name
class FirstTag:
    def process(self, remaining_string, parser):
        i_start_tag = remaining_string.find("<")
        i_end_tag = remaining_string.find(">")
        tag_name = remaining_string[i_start_tag + 1 : i_end_tag]
        root = Node(tag_name)
        parser.root = parser.current_node = root
        parser.state = child_node
        return remaining_string[i_end_tag + 1 :]
class ChildNode:
    def process(self, remaining_string, parser):
        stripped = remaining_string.strip()
        if stripped.startswith("</"):
            parser.state = close_tag
        elif stripped.startswith("<"):
            parser.state = open_tag
        else:
            parser.state = text_node
        return stripped
class OpenTag:
    def process(self, remaining_string, parser):
        i_start_tag = remaining_string.find("<")
        i_end_tag = remaining_string.find(">")
        tag_name = remaining_string[i_start_tag + 1 : i_end_tag]
        node = Node(tag_name, parser.current_node)
        parser.current_node.children.append(node)
        parser.current_node = node
        parser.state = child_node
        return remaining_string[i_end_tag + 1 :]
class TextNode:
    def process(self, remaining_string, parser):
        i_start_tag = remaining_string.find("<")
        text = remaining_string[:i_start_tag]
        parser.current_node.text = text
        parser.state = child_node
        return remaining_string[i_start_tag:]
class CloseTag:
    def process(self, remaining_string, parser):
        i_start_tag = remaining_string.find("<")
        i_end_tag = remaining_string.find(">")
        assert remaining_string[i_start_tag + 1] == "/"
        tag_name = remaining_string[i_start_tag + 2 : i_end_tag]
        assert tag_name == parser.current_node.tag_name
        parser.current_node = parser.current_node.parent
        parser.state = child_node
        return remaining_string[i_end_tag + 1 :].strip()
first_tag = FirstTag()
child_node = ChildNode()
text_node = TextNode()
open_tag = OpenTag()
close_tag = CloseTag()

我们所做的只是创建可以重用的各种状态类的实例。请注意,即使在变量被定义之前,我们也可以在类内部访问这些模块变量?这是因为类内部的代码直到调用方法时才会执行,而到这个时候,整个模块都已经被定义了。

在这个例子中的不同之处在于,我们不是浪费内存创建一堆必须进行垃圾回收的新实例,而是为每个状态重用一个单一的状态对象。即使多个解析器同时运行,只需要使用这些状态类。

当我们最初创建基于状态的解析器时,您可能会想知道为什么我们没有将解析器对象传递给每个单独状态的__init__,而是像我们所做的那样将其传递给process方法。然后状态可以被引用为self.parser。这是状态模式的一个完全有效的实现,但它将不允许利用单例模式。如果状态对象保持对解析器的引用,那么它们就不能同时用于引用其他解析器。

请记住,这是两种不同目的的模式;单例模式的目的可能对实现状态模式有用,但这并不意味着这两种模式有关联。

模板模式

模板模式对于消除重复代码非常有用;它旨在支持我们在第十九章中讨论的“不要重复自己”的原则,何时使用面向对象编程。它设计用于我们需要完成几个不同任务,这些任务有一些但不是全部步骤相同的情况。共同的步骤在基类中实现,不同的步骤在子类中被覆盖以提供自定义行为。在某些方面,它类似于一般化的策略模式,只是使用基类共享算法的相似部分。以下是它的 UML 格式:


一个模板示例

让我们以创建一个汽车销售报告为例。我们可以在 SQLite 数据库表中存储销售记录。SQLite 是一个简单的基于文件的数据库引擎,允许我们使用 SQL 语法存储记录。Python 在其标准库中包含了 SQLite,因此不需要额外的模块。

我们有两个需要执行的共同任务:

  • 选择所有新车销售并以逗号分隔的格式输出到屏幕
  • 输出一个逗号分隔的所有销售人员及其总销售额的列表,并将其保存到可以导入电子表格的文件中

这些看起来是非常不同的任务,但它们有一些共同的特征。在这两种情况下,我们都需要执行以下步骤:

  1. 连接到数据库。
  2. 构造一个新车或总销售的查询。
  3. 发出查询。
  4. 将结果格式化为逗号分隔的字符串。
  5. 将数据输出到文件或电子邮件。

查询构造和输出步骤对于这两个任务是不同的,但其余步骤是相同的。我们可以使用模板模式将共同的步骤放在一个基类中,将不同的步骤放在两个子类中。

在开始之前,让我们创建一个数据库并放入一些示例数据,使用几行 SQL:

import sqlite3
conn = sqlite3.connect("sales.db")
conn.execute(
    "CREATE TABLE Sales (salesperson text, "
    "amt currency, year integer, model text, new boolean)"
)
conn.execute(
    "INSERT INTO Sales values"
    " ('Tim', 16000, 2010, 'Honda Fit', 'true')"
)
conn.execute(
    "INSERT INTO Sales values"
    " ('Tim', 9000, 2006, 'Ford Focus', 'false')"
)
conn.execute(
    "INSERT INTO Sales values"
    " ('Gayle', 8000, 2004, 'Dodge Neon', 'false')"
)
conn.execute(
    "INSERT INTO Sales values"
    " ('Gayle', 28000, 2009, 'Ford Mustang', 'true')"
)
conn.execute(
    "INSERT INTO Sales values"
    " ('Gayle', 50000, 2010, 'Lincoln Navigator', 'true')"
)
conn.execute(
    "INSERT INTO Sales values"
    " ('Don', 20000, 2008, 'Toyota Prius', 'false')"
)
conn.commit()
conn.close()

希望您能看出这里发生了什么,即使您不懂 SQL;我们创建了一个用于保存数据的表,并使用了六个insert语句来添加销售记录。数据存储在名为sales.db的文件中。现在我们有一个示例可以用来开发我们的模板模式。

既然我们已经概述了模板必须执行的步骤,我们可以开始定义包含这些步骤的基类。每个步骤都有自己的方法(这样可以轻松地选择性地覆盖任何一个步骤),而且我们还有一个管理方法依次调用这些步骤。没有任何方法内容的话,它可能会是这样的:

class QueryTemplate:
    def connect(self):
        pass
    def construct_query(self):
        pass
    def do_query(self):
        pass
    def format_results(self):
        pass
    def output_results(self):
        pass
    def process_format(self):
        self.connect()
        self.construct_query()
        self.do_query()
        self.format_results()
        self.output_results()

process_format方法是外部客户端调用的主要方法。它确保每个步骤按顺序执行,但它不关心该步骤是在这个类中实现的还是在子类中实现的。对于我们的示例,我们知道两个类之间会有三个方法是相同的:

import sqlite3 
class QueryTemplate:
    def connect(self):
        self.conn = sqlite3.connect("sales.db")
    def construct_query(self):
        raise NotImplementedError()
    def do_query(self):
        results = self.conn.execute(self.query)
        self.results = results.fetchall()
    def format_results(self):
        output = []
        for row in self.results:
            row = [str(i) for i in row]
            output.append(", ".join(row))
        self.formatted_results = "\n".join(output)
    def output_results(self):
        raise NotImplementedError()

为了帮助实现子类,两个未指定的方法会引发NotImplementedError。这是在 Python 中指定抽象接口的常见方式,当抽象基类看起来太重量级时。这些方法可以有空实现(使用pass),或者可以完全未指定。然而,引发NotImplementedError有助于程序员理解该类是用于派生子类和覆盖这些方法的。空方法或不存在的方法更难以识别需要实现和调试,如果我们忘记实现它们。

现在我们有一个模板类,它处理了繁琐的细节,但足够灵活,可以执行和格式化各种查询。最好的部分是,如果我们想要将数据库引擎从 SQLite 更改为另一个数据库引擎(比如py-postgresql),我们只需要在这个模板类中进行修改,而不需要触及我们可能编写的两个(或两百个)子类。

现在让我们来看看具体的类:

import datetime 
class NewVehiclesQuery(QueryTemplate):
    def construct_query(self):
        self.query = "select * from Sales where new='true'"
    def output_results(self):
        print(self.formatted_results)
class UserGrossQuery(QueryTemplate):
    def construct_query(self):
        self.query = (
            "select salesperson, sum(amt) "
            + " from Sales group by salesperson"
        )
    def output_results(self):
        filename = "gross_sales_{0}".format(
            datetime.date.today().strftime("%Y%m%d")
        )
        with open(filename, "w") as outfile:
            outfile.write(self.formatted_results)

这两个类实际上相当简短,考虑到它们的功能:连接到数据库,执行查询,格式化结果并输出。超类处理了重复的工作,但让我们可以轻松指定在任务之间变化的步骤。此外,我们还可以轻松地更改在基类中提供的步骤。例如,如果我们想要输出除逗号分隔字符串之外的其他内容(例如:要上传到网站的 HTML 报告),我们仍然可以覆盖format_results

练习

在撰写本章的示例时,我发现想出应该使用特定设计模式的好例子可能非常困难,但也非常有教育意义。与其去审查当前或旧项目,看看你可以在哪里应用这些模式,正如我在之前的章节中建议的那样,不如考虑这些模式以及可能出现这些模式的不同情况。试着超越你自己的经验。如果你当前的项目是银行业务,考虑一下在零售或销售点应用这些设计模式。如果你通常编写 Web 应用程序,考虑在编写编译器时使用设计模式。

看看装饰器模式,并想出一些适用它的好例子。专注于模式本身,而不是我们讨论的 Python 语法。它比实际模式要更一般一些。然而,装饰器的特殊语法是你可能想要寻找现有项目中适用的地方。

有哪些适合使用观察者模式的领域?为什么?不仅考虑如何应用模式,还要考虑如何在不使用观察者的情况下实现相同的任务?选择使用它会得到什么,或者失去什么?

考虑策略模式和状态模式之间的区别。在实现上,它们看起来非常相似,但它们有不同的目的。你能想到可以互换使用这些模式的情况吗?重新设计一个基于状态的系统以使用策略,或者反之,是否合理?设计实际上会有多大的不同?

模板模式是继承的一个明显应用,可以减少重复的代码,你可能以前就使用过它,只是不知道它的名字。试着想出至少半打不同的场景,它在哪些情况下会有用。如果你能做到这一点,你将会在日常编码中经常找到它的用武之地。

总结

本章详细讨论了几种常见的设计模式,包括示例、UML 图表,以及 Python 和静态类型面向对象语言之间的差异讨论。装饰器模式通常使用 Python 的更通用的装饰器语法来实现。观察者模式是一种有用的方式,可以将事件与对这些事件采取的行动分离。策略模式允许选择不同的算法来完成相同的任务。状态模式看起来类似,但实际上是用来表示系统可以使用明确定义的操作在不同状态之间移动。单例模式在一些静态类型的语言中很受欢迎,但在 Python 中几乎总是反模式。

在下一章中,我们将结束对设计模式的讨论。

第二十三章:Python 设计模式 II

在本章中,我们将介绍更多的设计模式。我们将再次介绍经典示例以及 Python 中常见的替代实现。我们将讨论以下内容:

  • 适配器模式
  • 外观模式
  • 延迟初始化和享元模式
  • 命令模式
  • 抽象工厂模式
  • 组合模式

适配器模式

与我们在上一章中审查的大多数模式不同,适配器模式旨在与现有代码交互。我们不会设计一组全新的实现适配器模式的对象。适配器用于允许两个现有对象一起工作,即使它们的接口不兼容。就像显示适配器允许您将 Micro USB 充电线插入 USB-C 手机一样,适配器对象位于两个不同接口之间,在其间进行实时翻译。适配器对象的唯一目的是执行这种翻译。适配可能涉及各种任务,例如将参数转换为不同格式,重新排列参数的顺序,调用不同命名的方法或提供默认参数。

在结构上,适配器模式类似于简化的装饰器模式。装饰器通常提供与其替代物相同的接口,而适配器在两个不同的接口之间进行映射。这在以下图表中以 UML 形式表示:


在这里,Interface1期望调用一个名为make_action(some, arguments)的方法。我们已经有了完美的Interface2类,它做了我们想要的一切(为了避免重复,我们不想重写它!),但它提供的方法名为different_action(other, arguments)Adapter类实现了make_action接口,并将参数映射到现有接口。

这里的优势在于,从一个接口映射到另一个接口的代码都在一个地方。另一种选择将会非常丑陋;每当我们需要访问这段代码时,我们都必须在多个地方执行翻译。

例如,假设我们有以下现有类,它接受格式为YYYY-MM-DD的字符串日期并计算该日期时的人的年龄:

class AgeCalculator:
    def __init__(self, birthday):
        self.year, self.month, self.day = (
            int(x) for x in birthday.split("-")
        )
    def calculate_age(self, date):
        year, month, day = (int(x) for x in date.split("-"))
        age = year - self.year
        if (month, day) < (self.month, self.day):
            age -= 1
        return age

这是一个非常简单的类,它完成了它应该完成的工作。但我们不得不思考程序员当时在想什么,为什么要使用特定格式的字符串,而不是使用 Python 中非常有用的内置datetime库。作为尽可能重用代码的负责任的程序员,我们编写的大多数程序将与datetime对象交互,而不是字符串。

我们有几种选择来解决这种情况。我们可以重写类以接受datetime对象,这可能更准确。但如果这个类是由第三方提供的,我们不知道如何或不能改变它的内部结构,我们需要另一种选择。我们可以使用原样的类,每当我们想要计算datetime.date对象上的年龄时,我们可以调用datetime.date.strftime('%Y-%m-%d')将其转换为正确的格式。但这种转换会发生在很多地方,更糟糕的是,如果我们将%m误写为%M,它会给我们当前的分钟而不是输入的月份。想象一下,如果您在十几个不同的地方写了这个,然后当您意识到错误时不得不返回并更改它。这不是可维护的代码,它违反了 DRY 原则。

相反,我们可以编写一个适配器,允许将普通日期插入普通的AgeCalculator类,如下面的代码所示:

import datetime 
class DateAgeAdapter:
    def _str_date(self, date):
        return date.strftime("%Y-%m-%d")
    def __init__(self, birthday):
        birthday = self._str_date(birthday)
        self.calculator = AgeCalculator(birthday)
    def get_age(self, date):
        date = self._str_date(date)
        return self.calculator.calculate_age(date)

这个适配器将datetime.datedatetime.time(它们具有相同的接口到strftime)转换为一个字符串,以便我们原始的AgeCalculator可以使用。现在我们可以使用原始代码与我们的新接口。我将方法签名更改为get_age,以演示调用接口可能也在寻找不同的方法名称,而不仅仅是不同类型的参数。

创建一个类作为适配器是实现这种模式的常规方法,但是,通常情况下,在 Python 中还有其他方法可以实现。继承和多重继承可以用于向类添加功能。例如,我们可以在date类上添加一个适配器,以便它与原始的AgeCalculator类一起使用,如下所示:

import datetime 
class AgeableDate(datetime.date): 
    def split(self, char): 
        return self.year, self.month, self.day 

像这样的代码让人怀疑 Python 是否应该合法。我们已经为我们的子类添加了一个split方法,它接受一个参数(我们忽略),并返回一个年、月和日的元组。这与原始的AgeCalculator类完美配合,因为代码在一个特殊格式的字符串上调用strip,而在这种情况下,strip返回一个年、月和日的元组。AgeCalculator代码只关心strip是否存在并返回可接受的值;它并不关心我们是否真的传入了一个字符串。以下代码确实有效:

>>> bd = AgeableDate(1975, 6, 14)
>>> today = AgeableDate.today()
>>> today
AgeableDate(2015, 8, 4)
>>> a = AgeCalculator(bd)
>>> a.calculate_age(today)
40  

它有效,但这是一个愚蠢的想法。在这种特定情况下,这样的适配器将很难维护。我们很快会忘记为什么需要向date类添加一个strip方法。方法名称是模糊的。这可能是适配器的性质,但是显式创建一个适配器而不是使用继承通常可以澄清其目的。

除了继承,有时我们还可以使用猴子补丁来向现有类添加方法。它不适用于datetime对象,因为它不允许在运行时添加属性。然而,在普通类中,我们可以添加一个新方法,提供调用代码所需的适配接口。或者,我们可以扩展或猴子补丁AgeCalculator本身,以用更符合我们需求的东西替换calculate_age方法。

最后,通常可以将函数用作适配器;这显然不符合适配器模式的实际设计,但是如果我们记得函数本质上是具有__call__方法的对象,那么它就成为一个明显的适配器适应。

外观模式

外观模式旨在为复杂的组件系统提供一个简单的接口。对于复杂的任务,我们可能需要直接与这些对象交互,但通常对于系统的典型使用,这些复杂的交互并不是必要的。外观模式允许我们定义一个新对象,封装系统的典型使用。每当我们想要访问常见功能时,我们可以使用单个对象的简化接口。如果项目的另一部分需要访问更复杂的功能,它仍然可以直接与系统交互。外观模式的 UML 图表实际上取决于子系统,但在模糊的方式下,它看起来像这样:


外观在许多方面类似于适配器。主要区别在于,外观试图从复杂的接口中抽象出一个简单的接口,而适配器只试图将一个现有的接口映射到另一个接口。

让我们为一个电子邮件应用程序编写一个简单的外观。Python 中用于发送电子邮件的低级库,正如我们在第二十章中看到的那样,Python 面向对象的快捷方式,非常复杂。用于接收消息的两个库甚至更糟。

有一个简单的类可以让我们发送单封电子邮件,并列出当前在 IMAP 或 POP3 连接中收件箱中的电子邮件,这将是很好的。为了让我们的例子简短,我们将坚持使用 IMAP 和 SMTP:两个完全不同的子系统,碰巧处理电子邮件。我们的外观只执行两个任务:向特定地址发送电子邮件,并在 IMAP 连接上检查收件箱。它对连接做了一些常见的假设,比如 SMTP 和 IMAP 的主机位于同一个地址,用户名和密码相同,并且它们使用标准端口。这涵盖了许多电子邮件服务器的情况,但如果程序员需要更灵活性,他们总是可以绕过外观直接访问这两个子系统。

该类使用电子邮件服务器的主机名、用户名和密码进行初始化:

import smtplib 
import imaplib 
class EmailFacade: 
    def __init__(self, host, username, password): 
        self.host = host 
        self.username = username 
        self.password = password 

send_email方法格式化电子邮件地址和消息,并使用smtplib发送。这不是一个复杂的任务,但需要相当多的调整来将传递到外观的自然输入参数正确格式化,以使smtplib能够发送消息,如下所示:

def send_email(self, to_email, subject, message):
        if not "@" in self.username:
            from_email = "{0}@{1}".format(self.username, self.host)
        else:
            from_email = self.username
        message = (
            "From: {0}\r\n" "To: {1}\r\n" "Subject: {2}\r\n\r\n{3}"
        ).format(from_email, to_email, subject, message)
        smtp = smtplib.SMTP(self.host)
        smtp.login(self.username, self.password)
        smtp.sendmail(from_email, [to_email], message)

方法开头的if语句捕获了username是否是整个from电子邮件地址,或者只是@符号左边的部分;不同的主机对登录详细信息的处理方式不同。

最后,获取当前收件箱中的消息的代码是一团糟。IMAP 协议过度设计,imaplib标准库只是协议的薄层。但我们可以简化它,如下所示:

def get_inbox(self):
        mailbox = imaplib.IMAP4(self.host)
        mailbox.login(
            bytes(self.username, "utf8"), bytes(self.password, "utf8")
        )
        mailbox.select()
        x, data = mailbox.search(None, "ALL")
        messages = []
        for num in data[0].split():
            x, message = mailbox.fetch(num, "(RFC822)")
            messages.append(message[0][1])
        return messages

现在,如果我们把所有这些加在一起,我们就有了一个简单的外观类,可以以相当直接的方式发送和接收消息;比起直接与这些复杂的库进行交互,要简单得多。

虽然在 Python 社区很少提到它的名字,但外观模式是 Python 生态系统的一个组成部分。因为 Python 强调语言的可读性,语言及其库往往提供了易于理解的接口来处理复杂的任务。例如,for循环,list推导和生成器都是更复杂的迭代器协议的外观。defaultdict实现是一个外观,它在字典中键不存在时抽象出烦人的边缘情况。第三方的requests库是一个强大的外观,可以使 HTTP 请求的库更易读,它们本身是管理基于文本的 HTTP 协议的外观。

轻量级模式

轻量级模式是一种内存优化模式。新手 Python 程序员往往忽视内存优化,认为内置的垃圾收集器会处理它们。这通常是完全可以接受的,但是在开发具有许多相关对象的较大应用程序时,关注内存问题可能会有巨大的回报。

轻量级模式确保共享状态的对象可以使用相同的内存来存储该共享状态。通常只有在程序显示出内存问题后才会实现它。在某些情况下,从一开始设计一个最佳配置是有意义的,但请记住,过早优化是创建一个过于复杂以至于无法维护的程序的最有效方式。

让我们看一下轻量级模式的以下 UML 图表:


每个享元都没有特定的状态。每当它需要对具体状态执行操作时,该状态都需要被调用代码传递给享元。传统上,返回享元的工厂是一个单独的对象;它的目的是为了根据标识该享元的给定键返回一个享元。它的工作原理类似于我们在第二十二章中讨论的单例模式,Python 设计模式 I;如果享元存在,我们就返回它;否则,我们创建一个新的。在许多语言中,工厂被实现为Flyweight类本身上的静态方法,而不是作为一个单独的对象。

想象一下汽车销售的库存系统。每辆汽车都有特定的序列号和特定的颜色。但是对于特定模型的所有汽车来说,大部分关于汽车的细节都是相同的。例如,本田 Fit DX 型号是一辆几乎没有功能的汽车。LX 型号有空调、倾斜、巡航和电动窗户和锁。Sport 型号有时尚的轮毂、USB 充电器和扰流板。如果没有享元模式,每个单独的汽车对象都必须存储一个长长的列表,其中包含它具有或不具有的功能。考虑到本田一年销售的汽车数量,这将导致大量的内存浪费。

使用享元模式,我们可以为与模型相关的功能列表共享对象,然后只需为单个车辆引用该模型,以及序列号和颜色。在 Python 中,享元工厂通常使用那个奇怪的__new__构造函数来实现,类似于我们在单例模式中所做的。

与只需要返回类的一个实例的单例模式不同,我们需要能够根据键返回不同的实例。我们可以将项目存储在字典中,并根据键查找它们。然而,这种解决方案存在问题,因为只要项目在字典中,它就会一直保留在内存中。如果我们卖完了 LX 型号的 Fit,那么 Fit 享元将不再需要,但它仍然会留在字典中。我们可以在卖车时清理这些内容,但这不是垃圾收集器的作用吗?

我们可以利用 Python 的weakref模块来解决这个问题。该模块提供了一个WeakValueDictionary对象,基本上允许我们在字典中存储项目,而垃圾收集器不会关心它们。如果一个值在一个弱引用字典中,并且在应用程序的任何其他地方都没有对该对象的其他引用(也就是说,我们已经卖完了 LX 型号),垃圾收集器最终会为我们清理掉它。

首先让我们构建我们汽车享元的工厂,如下所示:

import weakref
class CarModel:
    _models = weakref.WeakValueDictionary()
    def __new__(cls, model_name, *args, **kwargs):
        model = cls._models.get(model_name)
        if not model:
            model = super().__new__(cls)
            cls._models[model_name] = model
        return model

基本上,每当我们使用给定名称构造一个新的享元时,我们首先在弱引用字典中查找该名称;如果存在,我们就返回该模型;如果不存在,我们就创建一个新的。无论哪种方式,我们都知道__init__方法在每次都会被调用,无论它是一个新的还是现有的对象。因此,我们的__init__方法可以看起来像以下代码片段:

def __init__(
        self,
        model_name,
        air=False,
        tilt=False,
        cruise_control=False,
        power_locks=False,
        alloy_wheels=False,
        usb_charger=False,
    ):
        if not hasattr(self, "initted"):
            self.model_name = model_name
            self.air = air
            self.tilt = tilt
            self.cruise_control = cruise_control
            self.power_locks = power_locks
            self.alloy_wheels = alloy_wheels
            self.usb_charger = usb_charger
            self.initted = True

if语句确保我们只在第一次调用__init__时初始化对象。这意味着我们以后可以只用模型名称调用工厂,并得到相同的享元对象。然而,如果享元没有外部引用存在,它将被垃圾收集,我们必须小心不要意外地创建一个具有空值的新享元。

让我们为我们的享元添加一个假设的方法,该方法查找特定车型的车辆上的序列号,并确定它是否曾经参与过任何事故。这个方法需要访问汽车的序列号,这个序列号因车而异;它不能与享元一起存储。因此,这些数据必须由调用代码传递给方法,如下所示:

def check_serial(self, serial_number):
        print(
            "Sorry, we are unable to check "
            "the serial number {0} on the {1} "
            "at this time".format(serial_number, self.model_name)
        )

我们可以定义一个类,该类存储附加信息,以及对 flyweight 的引用,如下所示:

class Car: 
    def __init__(self, model, color, serial): 
        self.model = model 
        self.color = color 
        self.serial = serial 
    def check_serial(self): 
        return self.model.check_serial(self.serial) 

我们还可以跟踪可用的模型,以及停车场上的各个汽车,如下所示:

>>> dx = CarModel("FIT DX")
>>> lx = CarModel("FIT LX", air=True, cruise_control=True,
... power_locks=True, tilt=True)
>>> car1 = Car(dx, "blue", "12345")
>>> car2 = Car(dx, "black", "12346")
>>> car3 = Car(lx, "red", "12347")  

现在,让我们在以下代码片段中演示弱引用的工作:

>>> id(lx)
3071620300
>>> del lx
>>> del car3
>>> import gc
>>> gc.collect()
0
>>> lx = CarModel("FIT LX", air=True, cruise_control=True,
... power_locks=True, tilt=True)
>>> id(lx)
3071576140
>>> lx = CarModel("FIT LX")
>>> id(lx)
3071576140
>>> lx.air
True  

id函数告诉我们对象的唯一标识符。当我们在删除对 LX 型号的所有引用并强制进行垃圾回收后第二次调用它,我们发现 ID 已经改变。CarModel __new__工厂字典中的值被删除,然后创建了一个新的值。然后,如果我们尝试构建第二个CarModel实例,它会返回相同的对象(ID 相同),即使在第二次调用中没有提供任何参数,air变量仍然设置为True。这意味着对象第二次没有被初始化,就像我们设计的那样。

显然,使用 flyweight 模式比只在单个汽车类上存储特性更复杂。我们应该在什么时候选择使用它?flyweight 模式旨在节省内存;如果我们有成千上万个相似的对象,将相似的属性合并到 flyweight 中对内存消耗会产生巨大影响。

对于优化 CPU、内存或磁盘空间的编程解决方案来说,通常会导致比未经优化的代码更复杂。因此,在决定代码可维护性和优化之间的权衡时,权衡是很重要的。在选择优化时,尝试使用 flyweight 等模式,以确保优化引入的复杂性局限于代码的一个(有良好文档的)部分。

如果一个程序中有很多 Python 对象,通过使用__slots__是节省内存的最快方法之一。__slots__魔术方法超出了本书的范围,但是如果您查看在线信息,会有很多信息可用。如果内存仍然不足,flyweight 可能是一个合理的解决方案。

Python 入门指南(七)(3)https://developer.aliyun.com/article/1507463

相关文章
|
2天前
|
数据采集 运维 API
适合所有编程初学者,豆瓣评分8.6的Python入门手册开放下载!
Python是一种跨平台的计算机程序设计语言,它可以用来完成Web开发、数据科学、网络爬虫、自动化运维、嵌入式应用开发、游戏开发和桌面应用开发。 Python上手很容易,基本有其他语言编程经验的人可以在1周内学会Python最基本的内容(PS:没有基础的人也可以直接学习,速度会慢一点) 今天给小伙伴们分享一份Python语言及其应用的手册,这份手册主要介绍 Python 语言的基础知识及其在各个领域的具体应用,基于最新版本 3.x。
|
5天前
|
数据可视化 API Python
Python零基础“圣经”!300W小白从入门到精通首选!
今天分享的这本书在让你尽快学会 Python基础知识的同时,能够编写并正确的运行程序(游戏、数据可视化、Web应用程序) 最大的特色在于,在为初学者构建完整的 Python 语言知识体系的同时,面向实际应用情境编写代码样例,而且许多样例还是 后续实践项目部分的伏笔。实践项目部分的选题经过精心设计,生动详尽 又面面俱到。相信这本书能够得到更多 Python 初学者的喜爱。
|
6天前
|
Python
小白入门必备!计科教授的Python精要参考PDF开放下载!
随着互联网产业的高速发展,在网络上早已积累了极其丰富的Python学习资料,任何人都可以基于这些资源,自学掌握 Python。 但实际上,网络上充斥的资源太多、太杂且不成体系,在没有足够的编程/工程经验之前,仅靠“看”线上资源自学,的确是一件非常困难的事。
|
6天前
|
数据可视化 API Python
豆瓣评分9.4!堪称经典的Python入门圣经,你还没看过吗?
最理想的新人入门书应该满足两个特点:第一就是内容通俗易懂;第二就是要有实战,能够让读者在学完之后知道具体怎么用。 今天给小伙伴们分享的这份Python入门手册,在为初学者构建完整的Python语言知识体系的同时,面向实际应用情境编写代码样例,而且许多样例还是后续实践项目部分的伏笔。实践项目部分的选题经过精心设计,生动详尽又面面俱到。
|
8天前
|
数据采集 运维 API
适合所有编程初学者,豆瓣评分8.6的Python入门手册开放下载!
Python是一种跨平台的计算机程序设计语言,它可以用来完成Web开发、数据科学、网络爬虫、自动化运维、嵌入式应用开发、游戏开发和桌面应用开发。 Python上手很容易,基本有其他语言编程经验的人可以在1周内学会Python最基本的内容(PS:没有基础的人也可以直接学习,速度会慢一点)
|
9天前
|
数据采集 SQL 数据可视化
使用Python和Pandas库进行数据分析的入门指南
使用Python和Pandas库进行数据分析的入门指南
73 0
|
9天前
|
Linux iOS开发 MacOS
Python入门指南
Python入门指南
33 0
|
10天前
|
数据采集 前端开发 JavaScript
Python爬虫入门
网络爬虫是自动抓取网页数据的程序,通过URL获取网页源代码并用正则表达式提取所需信息。反爬机制是网站为防止爬取数据设置的障碍,而反反爬是对这些机制的对策。`robots.txt`文件规定了网站可爬取的数据。基础爬虫示例使用Python的`urllib.request`模块。HTTP协议涉及请求和响应,包括状态码、头部和主体。`Requests`模块是Python中常用的HTTP库,能方便地进行GET和POST请求。POST请求常用于隐式提交表单数据,适用于需要发送复杂数据的情况。
16 1
|
13天前
|
机器学习/深度学习 人工智能 数据可视化
Python编程入门:从零开始探索编程的奇妙世界
这篇教程引导初学者入门Python编程,从安装Python开始,逐步讲解基本语法,如`print()`、变量、条件判断、循环以及自定义函数。文章强调了Python在数据处理、数据分析、人工智能和机器学习等领域的重要性,并鼓励学习者探索Python的广泛应用,开启编程之旅。
|
14天前
|
数据可视化 API Python
Python零基础“圣经”!300W小白从入门到精通首选!
今天分享的这本书在让你尽快学会 Python基础知识的同时,能够编写并正确的运行程序(游戏、数据可视化、Web应用程序) 最大的特色在于,在为初学者构建完整的 Python 语言知识体系的同时,面向实际应用情境编写代码样例,而且许多样例还是 后续实践项目部分的伏笔。实践项目部分的选题经过精心设计,生动详尽 又面面俱到。相信这本书能够得到更多 Python 初学者的喜爱。