Python-元编程实用指南-全-

Python 元编程实用指南(全)

原文:zh.annas-archive.org/md5/21190aefe9e941fa59ddc14a66d938c3

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

高效且可重用的代码使您的应用程序开发过程无缝且易于维护。使用 Python,您可以访问高级元编程功能,这些功能可以帮助您构建高性能的应用程序。

本书首先向您介绍元编程的需求和应用,然后介绍面向对象编程的基础。随着您的进步,您将学习简单的装饰器,然后与元类一起工作,并随后专注于内省和反射。

在定义算法模板之前,您还将深入了解泛型和类型。

之后,您将使用抽象语法树来理解您的代码,并探索方法解析顺序。本书还向您展示如何创建自己的动态对象,在通过设计模式结构化对象之前。最后,您将了解简单的代码生成技术,以及最佳实践,并最终构建您自己的应用程序。

在这次学习之旅结束时,您将拥有设计和构建可重用高性能应用程序所需的技能和信心,这些应用程序可以解决现实世界的问题。

这本书面向谁

如果您是一位希望通过开发可重用和高级框架来提高编码技能的 Python 中级程序员,这本书适合您。对 Python 编程的基本了解将帮助您充分利用这次学习之旅。

这本书涵盖的内容

第一章元编程的需求和应用,解释了 Python 中最先进功能之一的需求及其实际应用。

第二章Python 中 OOP 概念的复习,概述了现有的 OOP 概念,如类、方法和对象,以及示例。

第三章理解装饰器和它们的用途,介绍了函数和类上的装饰器概念,旨在为您提供装饰器的详细概述,包括如何编写它们以及在哪里使用它们。本章还详细介绍了示例代码的代码遍历。

第四章与元类一起工作,介绍了基类和元类的概念,旨在为您提供元类的详细概述,包括如何编写它们以及在哪里使用它们。本章还详细介绍了示例代码的代码遍历。

第五章理解内省,介绍了 Python 中的内省概念,旨在为您提供内省的详细概述,包括如何编写它以及在哪里使用它。本章还详细介绍了示例代码的代码遍历。

第六章, Python 对象的反射实现,介绍了 Python 中反射的概念,旨在为您提供关于反射的详细概述,包括如何编码它以及在哪里使用它。本章还详细介绍了示例代码的执行过程。

第七章, 理解泛型和类型,介绍了 Python 中的泛型概念,旨在为您提供关于泛型的详细概述,包括如何编码它们以及在哪里使用它们。本章还详细介绍了示例代码的执行过程。

第八章, 定义算法模板,介绍了 Python 中的模板概念,旨在为您提供关于模板的详细概述,包括如何编码它们以及在哪里使用它们。本章还详细介绍了示例代码的执行过程。

第九章, 通过抽象语法树理解代码,介绍了 Python 中的抽象语法树的概念,旨在为您提供关于抽象语法树的详细概述,包括如何编码它们以及在哪里使用它们。本章还详细介绍了示例代码的执行过程。

第十章, 理解继承的方法解析顺序,介绍了 Python 中方法解析顺序的概念,旨在为您提供关于方法解析顺序的详细概述,包括如何编码它以及在哪里使用它。本章还详细介绍了示例代码的执行过程。

第十一章, 创建动态对象,介绍了 Python 中的动态对象概念,旨在为您提供关于动态对象的详细概述,包括如何编码它们以及在哪里使用它们。本章还详细介绍了示例代码的执行过程。

第十二章, 应用 GOF 设计模式 – 第一部分,介绍了 Python 中的行为型设计模式的概念,旨在为您提供关于行为型设计模式的详细概述,并展示如何在不同的应用中应用它们。本章还详细介绍了示例代码的执行过程。

第十三章, 应用 GOF 设计模式 – 第二部分,介绍了 Python 中的结构型和创建型设计模式的概念,旨在为您提供关于结构型和创建型设计模式的详细概述,并展示如何在不同的应用中应用它们。本章还详细介绍了示例代码的执行过程。

第十四章代码生成,介绍了 Python 中的代码生成概念,旨在为您提供代码生成的详细概述,如何开发一个生成可重用代码的代码生成器,以及在哪里使用它。本章还涵盖了示例的详细代码遍历。

第十五章基于案例研究的应用开发,通过开发一个基于案例研究的应用及其测试框架来实施我们迄今为止学到的所有概念。本章涵盖了包含类和方法以及代码解释的详细代码。此外,还涵盖了如何将开发的应用程序打包和部署到 Python 库中的步骤。

第十六章遵循最佳实践,涵盖了在实施元编程概念时可以遵循的最佳实践,并回答了诸如在哪里以及在哪里不使用这些概念的问题在您的 Python 应用程序开发生命周期中。

要充分利用本书

请安装最新版本的 Python,最好是 Python 3.0 或更高版本,并从www.anaconda.com/products/distribution安装最新版本的 Anaconda。安装完成后,打开 Jupyter Notebook 运行本书提供的示例。

如果您使用的是本书的数字版,我们建议您自己输入代码或从本书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将帮助您避免与代码复制粘贴相关的任何潜在错误。

下载示例代码文件

您可以从 GitHub(github.com/PacktPublishing/Metaprogramming-with-Python)下载本书的示例代码文件。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他来自我们丰富的图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们!

下载彩色图像

我们还提供了一份包含本书中使用的截图和图表的彩色 PDF 文件。您可以从这里下载:packt.link/LTQbb

使用的约定

本书使用了多种文本约定。

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“为了进一步解释这一点,让我们看看一个示例,我们将通过使用ast模块解析一系列字符串来生成一个名为VegCounter的类。”

代码块设置如下:

actualclass = compile(class_tree, 'vegctr_tree', 'exec')
actualclass

当我们希望您注意代码块中的特定部分或显示代码的输出时,相关的行或项目将以粗体显示:

<code object <module> at 0x0000028AAB0D2A80, file "vegctr_tree", line 1>

小贴士或重要提示

看起来像这样。

联系我们

我们始终欢迎读者的反馈。

一般反馈: 如果您对本书的任何方面有疑问,请通过 mailto:customercare@packtpub.com 给我们发邮件,并在邮件主题中提及书名。

勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata并填写表格。

盗版: 如果您在互联网上发现我们作品的任何非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 mailto:copyright@packt.com 与我们联系,并提供材料的链接。

如果您有兴趣成为作者: 如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

分享您的想法

一旦您阅读了《Python 元编程》,我们很乐意听到您的想法!请点击此处直接访问此书的亚马逊评论页面并分享您的反馈。

您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。

第一部分:基础 - 面向对象 Python 和元编程的介绍

本节的目标是向您概述元编程的概念、其用法以及在构建基于 Python 的应用程序中的优势。本节还涵盖了 Python 中面向对象编程的基础,例如类、函数和对象的使用,以帮助您熟悉基本概念,在深入探讨元编程的复杂特性之前做好准备。

本部分包含以下章节:

  • 第一章**,元编程的需求及其应用

  • 第二章**,Python 中面向对象概念的复习

第一章:第一章:元编程的需求与应用

使用 Python 进行元编程是学习 Python 元编程的实用指南。

在当今的编程世界中,Python 被认为是学习和使用最简单、开发有用应用程序最方便的语言之一。在 Python 中理解编程概念并将其应用比其他任何编程语言都更容易。一个 Python 程序可以通过添加现有库并利用其内置方法来简单地编写。同时,该语言还具有许多强大的功能,可以帮助开发健壮的库和应用。

本书涵盖了 Python 中最先进的功能之一——元编程的需求,以及对其实际应用的见解。理解元编程的概念有助于挖掘 Python 3 的高级功能,并了解在哪里应用它们以使 Python 代码更具可重用性。

与遵循面向对象编程的常规 Python 应用程序开发不同,元编程涵盖了 Python 的一些高级概念,这些概念涉及操作 Python 的可编程对象,例如其类、方法、函数和变量。在本书中,我们将探讨一些应用和示例,以帮助用户以用户友好的方式理解这些概念。

在本章中,我们将介绍元编程以及使用 Python 3 进行元编程的需求。我们将涵盖以下主题:

  • 元编程概述

  • 理解为什么我们需要元编程

  • 探索元编程的应用

到本章结束时,你将对 Python 3 中的元编程有一个高级理解,了解使用它的需求,并知道一些实际的应用示例。

技术要求

本章中的代码示例可在 GitHub 仓库中找到,该仓库地址为github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter01

元编程概述

元编程是一个在 C++、Java、.NET 和 Ruby 等其他编程语言中广泛听说,但在 Python 中并不那么广泛听说的概念。Python 是一种易于编程初学者学习且对高级程序员高效实现的编程语言。因此,它在将元编程等技术与开发过程相结合时,具有提高效率和优化的额外优势。

在本书中,我们将深入探讨使用 Python 3 的元编程概念。

术语,正如其名,是一个引用自身或其高级信息的进程。在编程的上下文中,元编程也描述了程序引用自身或程序对象引用自身的类似概念。程序引用自身或其实体提供了有关程序或编程实体的数据,这些数据可以在各种级别上用于执行活动,如转换或操作。

为了理解术语,让我们考虑术语元数据。例如,让我们看看一个 Python DataFrame。对于那些不熟悉术语 DataFrame 的人来说,我们可以使用术语。以下截图显示的是称为Employee Data的表:

图 1.1 – 员工数据表

图 1.1 – 员工数据表

此员工数据表包含员工信息,如员工姓名、员工 ID、资格、经验、薪资等。

所有这些信息都是单个或多个员工的属性,它是组织中员工的资料。那么,元数据是什么呢?元元数据是员工数据在员工数据表中存储的数据。

员工数据表的元数据定义了每个列及其值在表中是如何存储的。例如,在以下截图中,我们可以看到Name作为长度为 64 个字符的字符串存储,而Salary作为长度为 12 位的Float存储:

图 1.2 – 员工数据表的元数据表示

图 1.2 – 员工数据表的元数据表示

使用员工姓名或 ID 等信息访问、修改、转换和更新员工数据表是数据操作,而访问、修改、转换和更新列名或员工 ID 或薪资的数据类型或大小是元数据操作。

通过这种理解,让我们看看元编程的一个例子。

元编程 – 实用介绍

任何可以用来编写执行动作的代码的编程语言都包含一个基本单元或代码片段,可以编写以执行动作。这被称为函数。

如果我们在两个变量ab中存储两个数字,要执行add动作,你可以简单地通过编写一个函数来添加这两个数字,如下面的代码块所示:

def add(a,b):    
    c = a + b    
    return c  

现在,如果我们执行此代码,它可以通过add函数提供的输入数据执行不同的场景。让我们逐一仔细看看它们。

场景 1

使用两个整数运行add函数会导致两个数字相加,如下所示:

add(1,3)  
4

场景 2

使用两个字符串运行add函数会导致两个单词的连接,如下所示:

add('meta','program')   
metaprogram

场景 3

让我们看看运行add函数时一个字符串和一个整数的情况:

add('meta',1)  

上述代码将导致以下错误:

图 1.3 – TypeError

图 1.3 – TypeError

让我们详细检查这个错误。

上述代码片段中的错误表示一个TypeError,这是由于尝试将一个meta字符串与一个整数值1相加而引起的。你可能想知道,我们能否使用元编程来解决这个问题

本例中的add函数表示一段代码或程序,类似于图 1.1中的员工数据表表示数据。在同一行中,我们可以识别add函数的元数据,并使用它来解决以下代码返回的TypeError对象:

add('meta',1)  

接下来,我们将查看元编程的实际示例。我们将利用add函数的元数据来理解这个概念。

add 函数的元数据

任何编程语言中的函数都是编写来对输入变量执行一系列操作的;它将根据对这些变量执行的操作返回结果。在本节中,我们将查看一个简单的函数示例,该函数用于添加两个变量。这将帮助我们理解元编程可以应用于函数,并可以操纵函数的行为而不修改函数的算法。我们将通过编写一个add函数来添加这两个变量。为了改变add函数的结果,我们将操作其两个输入变量的元数据,从而在每次提供不同类型的输入变量来执行函数时得到不同的结果。就像我们可以通过编写执行各种操作的代码行来操纵函数应该做什么一样,我们也可以通过编程其元数据和设置限制来操纵函数本身,以确定它应该做什么以及不应该做什么。就像数据集、DataFrame 或表格有数据和元数据一样,Python 3 中的程序或函数也有数据和元数据。在本例中,我们将通过限制其行为来操纵add函数执行的操作,而不是基于提供给函数的输入数据,而是基于提供给add函数的输入数据的类型。请看以下截图:

图 1.4 – 检查 add 函数的数据和元数据

图 1.4 – 检查 add 函数的数据和元数据

以下代码帮助我们识别add函数中每个数据项的元数据:

def add(a,b):    
    c = a +  b    
    print ("Metadata of add", type(add))    
    print ("Metadata of a", type(a))    
    print ("Metadata of b", type(b))    
    print ("Metadata of c", type(c))   

对前面函数的函数调用现在将返回add函数的元数据而不是其结果。现在,让我们用整数作为输入调用add方法:

add(1,3)  

我们将得到以下输出:

Metadata of add <class 'function'>
Metadata of a <class 'int'>
Metadata of b <class 'int'>
Metadata of c <class 'int'>

同样,我们也可以检查字符串的相加,如下所示:

add('test','string')

我们将得到以下输出:

Metadata of add <class 'function'>
Metadata of a <class 'str'>
Metadata of b <class 'str'>
Metadata of c <class 'str'>

Python 3 允许我们使用代码的元数据来操作它,使其偏离其实际行为。这还将为我们试图解决的问题提供定制化的解决方案。

在前面的例子中,我们使用了type函数,这是 Python 中的一个方法,它返回任何对象或变量所属的类或数据类型。

从前面的输出中可以看出,我们传递给add函数的ab变量属于整数数据类型,其结果c也是一个整数。add函数本身是function类/类型。

使用元编程解决类型错误

我们可以使用元编程从上一节中看到的add函数解决类型错误的变体有很多。我们将在本节中探讨这一点。

场景 1

以下元程序处理错误,并允许add函数添加两个字符串或两个整数。它还建议用户以正确的数据类型输入数据:

def add(a,b):
    if (type(a) is str and type(b) is int) or\
        (type(a) is int and type(b) is str):
        return "Please enter both input values as integers or\
          string"
    else:
        c = a + b
        return c  

add函数的定义中,我们添加了两个条件——一个用于检查a的类型是否为字符串且b的类型为 int,或者a的类型是否为 int 且b的类型为字符串。我们正在检查这些输入变量的组合以处理类型不匹配错误,并指导用户为输入变量提供正确的数据类型。

以下表格显示了输入变量数据类型的各种组合以及根据add函数的元数据上设置的条件得出的输出或结果,基于场景 1

图 1.5 – 场景 1 元数据组合

图 1.5 – 场景 1 元数据组合

以下代码执行add函数以强化在图 1.5中解释的输入-输出组合:

add(1,3)  
4
add('meta','program')  
metaprogram
add('meta',1)  
'Please enter both input values as integers or string'
add(1,'meta')  
'Please enter both input values as integers or string'

场景 2

以下元程序通过将不匹配的数据类型转换为字符串变量并执行字符串连接来解决类型不匹配错误。使用+运算符连接字符串和整数是合乎逻辑的,因为我们不能对这两种不同的数据类型执行算术加法。看看以下程序:

def add(a,b):
    if type(a) is int and type(b) is int:
        c = a +  b
        return c
    elif type(a) is str and type(b) is int or\
          type(a) is int and type(b) is str or \
          type(a) is str and type(b) is str:
        c = str(a) + str(b)
        return c
    else:
        print("Please enter string or integer")

在这里,无论我们为ab变量提供什么输入,它们都会被转换为字符串变量,然后使用+进行连接,而如果这两个输入变量都是整数,它们将使用算术加法相加。

以下表格显示了输入变量数据类型的各种组合以及根据add函数的元数据上设置的条件得出的输出或结果,基于场景 2

图 1.6 – 场景 2 元数据组合

图 1.6 – 场景 2 元数据组合

执行以下代码将提供我们在前表中看到的输出值组合:

add(1343,35789)  
37132
add('Meta',' Programming')  
'MetaProgramming'
add('meta',157676)  
'meta157676'
add(65081, 'meta')  
'65081meta'
add(True, 'meta')
Please enter string or integer

场景 3

现在,让我们更进一步,限制add函数本身的性质,确保它只执行算术加法,不接受任何其他数据类型或数据类型的组合。

在下面的代码块中,我们添加了另一个条件来对浮点数值进行数据类型检查,同时还有对字符串和整型输入值的数据类型检查。

此函数仅接受数值作为输入,并将返回一条消息,指导用户输入数字,以便仅执行算术加法。让我们看看代码:

def add(a,b):
    if type(a) is int and type(b) is int or\
       type(a) is float and type(b) is float or\
       type(a) is int and type(b) is float or\
       type(a) is float and type(b) is int:
        c = a +  b
        return c
    else:
        return 'Please input numbers'

下表显示了输入变量数据类型的不同组合以及根据在add函数的元数据上设置的场景 3条件所对应的输出或结果:

图 1.7 – 场景 3 元数据组合

图 1.7 – 场景 3 元数据组合

执行以下代码将提供图 1.7中显示的输出值组合,包括浮点数的加法:

add(15443,675683)  
691126
add(54381,3.7876)  
54384.7876
add(6.7754,543.76)  
550.5354
add(79894,0.6568)  
79894.6568
add('meta',14684)  
'Please input numbers'
add(6576,'meta')  
'Please input numbers'
add('meta','program')  
'Please input numbers'

这些是一些可以应用于在函数上执行简单元编程的方法。然而,这些并不是解决类型错误或操作函数的唯一解决方案。使用元编程实现解决方案的方法或途径不止一种。

理解为什么我们需要元编程

考虑到我们已经学到的关于元编程的知识,我们可能会思考以下问题:

在使用 Python 3 或更高版本开发应用程序时,是否总是必须应用元编程技术或操作代码的元数据?

这是一个常见问题,不仅在开发使用 Python 3 或更高版本的应用程序时会被问到,而且在使用任何支持元编程技术并允许开发者在应用程序开发过程中应用它们的编程语言时也会被问到。

要回答这个问题,了解元编程的灵活性和 Python 支持处理代码操作的技术是非常重要的,这些内容将在本书的后续章节中介绍。

应用元编程的一个原因是为了避免在基于 Python 的应用程序开发过程中的各个方面重复。我们将在不要重复自己部分中查看一个例子。

换句话说,在元级别引入诸如代码生成器等概念可以节省函数级或领域级编程的开发和执行时间。领域级编程对应于为特定领域编写代码,例如金融、网络、社交媒体等。

另一个需求是在程序元数据级别而不是在功能级别提高代码的抽象程度。抽象在字面上或面向对象编程的术语中是指信息隐藏的概念。在元程序级别实现抽象将帮助我们决定向下一级编码提供哪些信息,以及不提供哪些信息。

例如,在元编程级别开发一个函数模板将隐藏在域或功能级别的函数定义,以及限制传递给功能级别代码的信息量。

元编程允许我们使用元数据在元级别上操作程序,这有助于定义你的程序语法和语义应该如何。例如,在使用元编程解决类型错误这一节中,我们探讨了通过操作函数的变量来控制函数的数据类型结果。

不要重复自己

在任何应用程序开发过程中,都会编写数千行代码。不要重复自己(Don’t Repeat Yourself)是安迪·亨特(Andy Hunt)和大卫·托马斯(Dave Thomas)在他们所著的《程序员修炼之道》(The Pragmatic Programmer)一书中提出的原则。该原则指出:“系统中的每一项知识都必须有一个单一、明确、权威的表示。

在编写代码时,有很大可能性会编写多个执行类似重复任务的函数或方法,而这些函数或方法本身也可能是重复的。这导致应用程序开发中的冗余。冗余的最大缺点是,当你对某个位置进行任何修改时,实现、修改或代码修复需要在多个位置重复进行。

库是通过类和方法开发的,包括面向对象编程技术,如抽象、继承、封装等,以尽可能避免冗余并维护编码标准。即便如此,类中仍有可能存在可以简化的重复方法。

元编程可以通过实现动态代码生成、动态函数创建等方法来帮助处理此类情况。在这本书的整个过程中,我们将探讨各种方法,帮助你在开发应用程序时避免重复。

为了了解我们如何动态生成代码并避免重复,让我们看看一个简单的例子,其中算术运算被实现为重复函数。

以下代码由四个基本算术运算组成,这些运算可以在两个数值变量上执行。我们将声明并定义四个函数,这些函数将执行两个变量ab的加、减、乘、除运算,将结果存储在变量c中,并在函数执行时返回它:

def add(a,b):  
    c = a + b  
    return c  
def sub(a,b):  
    c = a - b  
    return c  
def multiply(a,b):  
    c = a * b  
    return c  
def divide(a,b):  
    c = a / b  
    return c  

前面的每个函数都需要单独调用,并且需要提供变量作为输入以单独执行,如下所示:

add(2,5)  
7
sub(2,5)  
-3
multiply(2,5)  
10
divide(2,5)  
0.4

在这个例子中,只有一个区别——函数定义中使用的算术运算符。这个代码可以通过不实现元编程,仅通过声明一个接受额外输入变量运算符的新函数来简化。

让我们学习如何避免这种重复的函数定义并简化逻辑。以下代码块定义了一个通用函数,可以重复使用以执行所有四个算术运算。让我们首先导入 Python 的内置module运算符,它包含支持多个算术运算的方法:

import operator as op
def arithmetic(a, b, operation):
    result = operation(a, b)
    return result

在这个代码片段中,我们声明了三个变量,包括函数算术中的操作。让我们看看它是如何工作的:

arithmetic('2', '5', op.add) '25'

使用输入变量执行此函数将返回一个连接的字符串,25,这将用于创建通用的arithmetic函数以执行多个操作。我们可以查看提供各种操作作为输入,看看这个通用函数如何服务于多个目的。

使用不同的算术运算符调用此函数可以解决重复函数定义的需求:

arithmetic(2, 5, op.add)
7
arithmetic(2 , 5, op.sub)
-3
arithmetic(2, 5, op.mul)
10
arithmetic(2 , 5, op.truediv)
0.4

这是解决代码冗余和避免多次函数定义的一种方法。但如果我们不想在需要之前定义函数本身怎么办?

为了回答这个问题,我们可以通过元编程实现动态函数的创建。动态函数在代码运行时根据需要创建。

尽管我们还在介绍章节中,但我们将讨论动态函数创建的示例,以了解本书将涵盖哪种类型的编程。

创建动态函数

在本节中,我们将查看一个示例,说明如何为之前在本节中讨论的相同算术运算集创建动态函数。

为了动态创建算术函数,我们需要导入库typesFunctionType类型。FunctionType是用户在基于 Python 的应用程序开发过程中创建的所有用户定义函数的类型:

from types import FunctionType  

为了开始这个过程,我们将创建一个字符串变量,它是算术函数的函数定义:

functionstring = '''
def arithmetic(a, b):
    op = __import__('operator')
    result = op.add(a, b)
    return result
    '''  
print(functionstring)

我们将得到以下输出:

 def arithmetic(a, b):
    op = __import__('operator')
    result = op.add(a, b)
return result 

现在,我们将创建另一个变量,functiontemplate,并将'functionstring'编译成一个代码对象。我们还将设置代码对象使用'exec'来执行。compile方法用于将 Python 中的字符串转换为代码对象,该对象可以进一步使用exec方法执行:

functiontemplate = compile(functionstring, 'functionstring', 'exec')  
functiontemplate 
<code object <module> at 0x000001E20D498660, file "functionstring", line 1>

函数定义算术的代码对象将被存储在functiontemplate中的元组中,可以按以下方式访问:

functiontemplate.co_consts[0]  
<code object arithmetic at 0x000001E20D4985B0, file "functionstring", line 1>

下一步涉及使用functiontemplate代码对象创建一个函数对象。这可以通过使用FunctionType方法完成,该方法接受代码对象和全局变量作为输入参数:

dynamicfunction = FunctionType(functiontemplate.co_consts[0], globals(),"add")
dynamicfunction  
<function _main_.arithmetic(a,b)> 

执行dynamicfunction后,它将表现得与算术函数中操作模块的add方法中的add操作相同:

dynamicfunction(2,5)  
7

现在我们知道了如何动态创建函数,我们可以进一步扩展它以创建多个函数,每个函数具有不同的操作和不同的名称,动态地。

要做到这一点,我们必须创建一个操作符列表和一个函数名列表:

operator = ['op.add','op.sub','op.mul','op.truediv','op.pow','op.mod', 'op.gt', 'op.lt'] 
functionname = ['add','sub', 'multiply', 'divide', 'power',\
 'modulus', 'greaterthan', 'lesserthan']  

我们之前列出的四个函数只包含加、减、乘和除操作。

之前functionname列表包含八个函数。这是我们在创建动态函数时获得的灵活性。

为了便于使用,我们还创建了两个输入变量ab,用于执行函数时使用:

a = 2  
b = 5  

在以下代码中,我们将创建一个名为functiongenerator()的函数,该函数实现元编程以动态生成我们想要的任意数量的算术函数。此函数将接受四个输入参数——即列表的functionnameoperatorab

下面是代码:

def functiongenerator(functionname, operator, a,b):    
    from types import FunctionType    
    functionstring = []    
    for i in operator:    
        functionstring.append('''
def arithmetic(a, b):
    op = __import__('operator')
    result = '''+ i + '''(a, b)
    return result
    ''')    
        functiontemplate = []    
    for i in functionstring:    
        functiontemplate.append(compile(i, 'functionstring', 'exec'))    
        dynamicfunction = []    
    for i,j in zip(functiontemplate,functionname):    
        dynamicfunction.append(FunctionType(i.co_consts[0], \
          globals(), j))    
        functiondict = {}    
    for i,j in zip(functionname,dynamicfunction):    
        functiondict[i]=j    
    for i in dynamicfunction:    
        print (i(a,b))    
    return functiondict    

functiongenerator()内部,发生以下情况:

  • 使用操作符列表中提供的每个算术运算符定义,创建一个新的functionstring列表。

  • 为每个函数定义创建一个新的functiontemplate列表,包含代码对象。

  • 为每个代码对象创建一个新的dynamicfunction列表,包含一个函数对象。

  • 创建一个新的functiondict字典,包含函数名-函数对象的键值对。

  • Functiongenerator返回生成的函数作为字典。

  • 此外,functiongenerator执行动态函数并打印结果。

执行此函数将产生以下输出:

funcdict = functiongenerator(functionname, operator, a,b)  
7
-3
10
0.4
32
2
False
True
funcdict  
{'add': <function _main_.arithmetic(a,b)>,
 'sub': <function _main_.arithmetic(a,b)>,
 'multiply': <function _main_.arithmetic(a,b)>,
 'divide': <function _main_.arithmetic(a,b)>,
 'power': <function _main_.arithmetic(a,b)>,
 'modulus': <function _main_.arithmetic(a,b)>,
 'greaterthan': <function _main_.arithmetic(a,b)>,
 'lesserthan': <function _main_.arithmetic(a,b)>,} 

可以单独调用前面生成的任何特定函数,并进一步使用,如下所示:

funcdict'divide'  
0.4

以下图表显示了开发这些动态函数的元编程的完整过程:

图 1.8 – 动态函数生成器

图 1.8 – 动态函数生成器

现在我们了解了动态函数生成器,让我们看看元编程的其他应用。

探索元编程的应用

元编程可以应用于各种基于 Python 的应用程序开发解决方案,例如自动化代码生成器、基于组件或基于流程的应用程序开发、特定领域语言开发等等。

您开发的任何代码,无论是类还是方法,都内部应用了元编程,并且在 Python 应用程序开发过程中其使用是不可避免的。然而,明确应用元编程概念是一个有意识的决策过程,并且它完全取决于您应用程序的预期结果。

在我们的动态函数创建示例中,我们实现了元编程来避免重复,并确保代码在元级别的抽象。

让我们考虑一个场景,其中我们想要开发一个基于功能流程的应用程序,供非程序员使用。例如,该应用程序可以是一个特定领域的数据库转换工具,它以高层次的抽象工作,并且不向最终用户提供太多设计或开发信息。然而,它也帮助最终用户动态创建模块,这些模块可以帮助他们在特定领域的问题解决中,而无需编写任何程序。在这种情况下,元编程在应用程序开发过程中非常有用:

图 1.9 – 编程层次

图 1.9 – 编程层次

在本书的其余部分,我们将更详细地探讨元编程的案例研究和应用。

摘要

在本章中,我们提供了一个关于元编程编程范式的快速概述,并查看了一个使用 Python 3 进行元编程解决类型错误的示例。

我们学习了为什么在 Python 应用程序开发过程中需要应用元编程技术。我们还通过查看一个解释动态函数创建的示例实现,了解了“不要重复自己”的概念,该示例强调了避免重复和在代码的元级别实现抽象的概念。最后,我们提供了对本书中将探讨的元编程应用的高级概述。这些技能将帮助我们理解如何在各种应用中应用元编程。

在下一章中,我们将回顾 Python 的对象导向编程概念。下一章更多地是对对象导向编程概念的复习,如果你已经熟悉这些概念,则是可选的。

第二章:第二章:Python 中 OOP 概念的复习

在上一章中,我们概述了元编程及其实际应用,例如使用添加函数的需求。但在深入探讨元编程的概念之前,了解 Python 中可用的基本面向对象编程(OOP)概念对你来说很重要。本章概述了现有的 OOP 概念,并附有示例。

本章我们将讨论的主要主题如下:

  • 介绍我们的核心示例

  • 创建类

  • 理解对象

  • 应用方法

  • 实现继承

  • 扩展到多重继承

  • 理解多态性

  • 使用抽象隐藏细节

  • 使用封装保护信息

到本章结束时,你将能够理解 Python 中 OOP 的概念,并附带一些实际示例。

注意

本章完全可选,所以如果你已经熟悉 OOP 的概念,你可以直接学习元编程的概念。

技术要求

本章中分享的代码示例可在 GitHub 上找到,地址为:github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter02

介绍我们的核心示例

在本章中,我们将使用一个名为 ABC Megamart 的模拟模式来解释面向对象编程(OOP)的概念。编程语言中面向对象方法的可用性有助于提高语言的有效重用性和抽象性。我们的示例,ABC Megamart,是一个模拟的大型零售店,在不同城市销售多种产品,并包含多个分店。

让我们为这个商店的不同实体提供一个结构,并看看它们如何适应一个有组织的面向对象范式。我们的商店包括以下内容:

  • 产品

  • 分支

  • 发票

  • 假期

  • 货架

  • 库存

  • 促销

  • 优惠/提供

  • 兑换柜台

  • 财务

这些实体中的每一个都可以有多个数据或信息属性,这些属性对于在商店的顺畅和高效管理中执行多个功能是必需的。

让我们探索如何将这些实体及其属性结构化到通过应用 OOP 概念开发的软件模型中:

  • 前面的 10 个实体可以直接或间接地相互连接

  • 每个分支都会有促销活动,每个促销活动都会有发票

  • 每个分支城市都会有假期,促销活动可以在假期季节进行

  • 每个分支(商店)可以有货架,产品将被放置在货架上

  • 每个产品都可以有促销或优惠,促销会影响销售

因此,多个实体可以链接在一起以开发软件、维护数据库模式或两者兼而有之,具体取决于所建模的应用。以下是这些实体如何相互连接的表示:

图 2.1 – 简单链接如何建模以连接各种实体的示例

图 2.1 – 简单链接如何建模以连接各种实体的示例

我们可以以多种方式来构建之前的实体模型,但我们不会涵盖所有这些。这更多的是在更高层次上对实体关系的简单表示。

以这个例子为基础,现在让我们深入探讨在 Python 中创建类的话题。

创建类

是一组可以由创建类的实例重用的公共属性和方法。通过创建一个类,我们只定义一次并多次重用它,从而避免冗余。

让我们看看一个类可以是什么样的。我们可以考虑ABC MegamartBranch实体。一个Branch可以有IDAddressAddress可以进一步细分为StreetCityStateZip code。如果我们把Branch看作一个类,IDStreetCityStateZip code将成为其属性。所有可以由分支机构执行的操作将成为其方法。

分支可以销售产品、维护发票、维护库存等等。类的通用格式如下:

图 2.2 – 类

图 2.2 – 类

类可以这样定义:

class ClassName:  
    '''attributes...'''        
    '''methods...'''  

Branch类的格式如下:

图 2.3 – Branch 类

图 2.3 – Branch 类

类似地,可以定义一个Branch类如下:

class Branch:  
    '''attributes...'''        
    '''methods...'''  

一个Branch类可以有多个属性和方法来执行各种操作。在这个例子中,这些属性和方法将被初始化为 NULL,并添加到类中,如下所示:

class Branch:  
    '''attributes'''  
    branch_id = None  
    branch_street = None  
    branch_city = None  
    branch_state = None  
    branch_zip = None  
    '''methods'''  
    def get_product(self):  
        return 'product'        
    def get_sales(self):  
        return 'sales'            
    def get_invoice(self):  
        return 'invoice'  

类的属性可以用特定的值初始化,也可以初始化为 NULL,然后在定义类对象并调用它执行各种功能时进行修改。

让我们进一步探讨通过创建类对象来利用和修改这些类属性。

理解对象

ClassName

没有对象的类实际上是不可用的。一旦我们创建了一个对象实例,就可以有效地利用为该类创建的所有属性和方法,如下所示:

obj_name = ClassName()  

考虑到之前的Branch类示例,我们可以创建并使用其对象如下:

branch_albany = Branch()  

现在,branch_albanyBranch类的一个实例,并且可以修改这个实例的所有属性,而不会影响Branch类定义中的属性。一个实例更像是一个类的副本,可以在不影响类本身的情况下使用。以下代码作为例子:

branch_albany.branch_id = 123  
branch_albany.branch_street = '123 Main Street'  
branch_albany.branch_city = 'Albany'  
branch_albany.branch_state = 'New York'  
branch_albany.branch_zip = 12084  

调用先前定义的属性会返回为这些属性定义的以下值:

branch_albany.branch_id  
123
branch_albany.branch_street  
'123 Main Street'

我们可以为Branch类创建另一个对象,而类本身不会受到影响。然后我们可以为新创建的branch对象分配一个branch_id的值,如下所示:

branchNevada = Branch()  
branchNevada.branch_id  

现在,branchNevada.branch_idbranchNevada 对象的变量,它不返回任何值,因为它可以为此实例定义:

branchNevada.branch_id = 456  
branchNevada.branch_id  
456

这不是使用对象定义类变量值的唯一方法。作为替代,可以将所有属性作为参数添加到类定义中的 init 方法中,并在创建对象实例时初始化这些属性的值。为了使这生效,我们必须重新定义 Branch 类,如下所示:

class Branch:  
    def __init__(self, branch_id, branch_street,
       branch_city, branch_state, branch_zip):  
        self.branch_id = branch_id  
        self.branch_street = branch_street  
        self.branch_city = branch_city  
        self.branch_state = branch_state  
        self.branch_zip = branch_zip    
    def get_product(self):  
        return 'product'  
    def get_sales(self):  
        return 'sales'            
    def get_invoice(self):  
        return 'invoice'  

在与之前相同的方法中创建先前重新定义的类的对象实例会导致错误:

object_albany = Branch()  

以下是我们收到的错误信息:

图 2.4 – 缺少必需的参数错误

图 2.4 – 缺少必需的参数错误

是的,我们在 init 类中声明的所有参数在早期对象实例化中缺失。这个类的新对象需要创建并带有所有初始化的值,如下所示:

object_albany = Branch(101,'123 Main Street','Albany','New York', 12084)  
print (object_albany.branch_id, 
       object_albany.branch_street,
       object_albany.branch_city,
       object_albany.branch_state,
       object_albany.branch_zip)  
101 123 Main Street Albany New York 12084

通过这种理解,让我们来看看在类内部定义方法并使用对象调用它们的概念。

应用方法

方法类似于我们创建以在程序中执行各种操作的用户定义函数,区别在于方法是在类内部定义的,并受类规则的约束。方法只能通过调用为该类创建的对象实例来使用。另一方面,用户定义函数是全局的,可以在程序中的任何地方自由调用。一个方法可以像打印一个语句那样简单,也可以是一个高度复杂的数学计算,涉及大量参数。

Branch 类内部使用简单的打印语句定义方法如下:

class Branch:  
    def __init__(self, branch_id, branch_street, 
      branch_city, branch_state, branch_zip):           
        self.branch_id = branch_id  
        self.branch_street = branch_street  
        self.branch_city = branch_city  
        self.branch_state = branch_state  
        self.branch_zip = branch_zip    
    def get_product(self):  
        return 'product'        
    def get_sales(self):  
        return 'sales'            
    def get_invoice(self):  
        return 'invoice'  
object_albany = Branch(101,'123 Main Street','Albany','New York', 12084) 

通过从 object_albany 调用上述方法,我们将得到以下输出:

object_albany.get_invoice()  
'invoice'
object_albany.get_sales()  
'sales'
object_albany.get_product()  
'product'

作为一种变体,我们可以看看创建带有参数和计算的方法的例子。对于这个例子,让我们考虑一个场景,其中我们需要根据州的销售税率、产品的购买价格和利润率来计算特定分支的产品销售价格。在计算产品的销售价格后,该方法应返回分支详情、产品详情、销售价格和销售税。

要编写这个方法,我们将使用 Python 关键字参数创建三个字典变量,并分别命名为 **branch**sales**product。我们将创建三个方法来设置分支、销售和产品信息,如下所示:

class Branch:       
    def set_branch(self, **branch):  
        return branch        
    def set_sales(self, **sales):  
        return sales        
    def set_product(self, **product):  
        return product  

上述代码接收了可以为分支、销售和产品包含的所有值。我们将为 Branch 类创建一个对象:

branch_nyc = Branch()  

在下面的代码中,我们将使用 set_branch 方法来存储 Branch 对象内部的 branch 字典变量中的值:

branch_nyc.branch = branch_nyc.set_branch(branch_id = 202,  
branch_street = '234 3rd Main Street',  
branch_city = 'New York City',  
branch_state = 'New York',  
branch_zip = 11005)  

现在,我们将按照以下方式在 branch_nyc 对象上调用 branch 属性:

branch_nyc.branch

执行前面的代码会产生以下输出,这是一个包含branch_id及其地址的字典:

{'branch_id': 202,
 'branch_street': '234 3rd Main Street',
 'branch_city': 'New York City',
 'branch_state': 'New York',
 'branch_zip': 11005}

同样,在下面的代码中,我们将使用set_product方法在Branch对象的product字典变量中存储值:

branch_nyc.product = branch_nyc.set_product(  
    product_id = 100001,  
    product_name = 'Refrigerator',  
    productBrand = 'Whirlpool'  )

现在,我们将按照以下方式在branch_nyc对象上调用product属性:

branch_nyc.product

执行前面的代码会产生以下输出,这是一个包含所有产品 ID 及其详细信息的字典:

{'product_id': 100001,
 'product_name': 'Refrigerator',
 'productBrand': 'Whirlpool'}

同样,在以下代码中,我们将使用set_sales方法在Branch对象的sales字典变量中存储值:

branch_nyc.sales = branch_nyc.set_sales(  
    purchase_price = 300,  
    profit_margin = 0.20,  
    tax_rate = 0.452  
)  

现在,我们将按照以下方式在branch_nyc对象上调用sales属性:

branch_nyc.sales

执行前面的代码会产生以下输出,这是一个包含所有销售信息的字典:

{'purchase_price': 300,
 'profit_margin': 0.2,
 'tax_rate': 0.452,
 'selling_price': 522.72}

计算销售价格将通过以下两个步骤来完成:

  1. 通过将购买价格加上购买价格和利润率百分比之间的产品,来计算税前价格。

  2. 通过将税前价格加上产品之间的价格,来计算销售价格。

在以下代码中,我们将包含calc_tax方法来执行前面的计算步骤,并返回分支详情以及产品信息和销售数据:

class Branch:  
     def set_branch(self, **branch):  
        return branch        
    def set_sales(self, **sales):  
        return sales        
    def set_product(self, **product):  
        return product    
    def calc_tax(self):  
        branch = self.branch  
        product = self.product  
        sales = self.sales  
        pricebeforetax = sales['purchase_price'] + \
        sales['purchase_price'] * sales['profit_margin']  
        finalselling_price = pricebeforetax + \
        (pricebeforetax * sales['tax_rate'])  
        sales['selling_price'] = finalselling_price  
        return branch, product, sales  

调用前面的函数会提供以下结果:

branch_nyc.calc_tax()
({'branch_id': 202,
  'branch_street': '234 3rd Main Street',
  'branch_city': 'New York City',
  'branch_state': 'New York',
  'branch_zip': 11005},
 {'product_id': 100001,
  'product_name': 'Refrigerator',
  'productBrand': 'Whirlpool'},
 {'purchase_price': 300,
  'profit_margin': 0.2,
  'tax_rate': 0.452,
  'selling_price': 522.72})

现在我们知道了如何应用方法,我们可以进一步了解继承的概念。

实现继承

继承在字面上的意思是通过子类获得父类的属性,在面向对象编程中也意味着相同。一个新类可以继承父类的属性和方法,它也可以有自己的属性和方法。继承父类的新类将被称为子类或子类,而父类也可以被称为基类。以下是对其的简单表示:

图 2.5 – 继承

图 2.5 – 继承

将我们最新的Branch类定义扩展为具有单独的NYC类——因为它有多个城市内部分支,并且它还具有除了Branch类之外的其他属性——我们将应用继承来创建一个新的子类或子类,名为NYC。它具有如具有多个管理层级等属性。NYC 有一个区域经理,每个分支都有自己的分支经理。对于 NYC,我们还将向销售价格的计算中添加一个额外的本地税率组件,该税率因分支而异。

图 2.6 – NYC 类继承自 Branch 类

图 2.6 – NYC 类继承自 Branch 类

定义一个从父类继承的子类时的继承一般结构如下所示:

class Parent:  
    '''attributes...'''  
    '''methods...'''   
class Child(Parent):  
    '''attributes...'''  
    '''methods...'''  

Branch父类继承NYC子类可以定义如下:

class NYC(Branch):  
    def set_management(self, **intercitybranch):  
        return intercitybranch  
    def calc_tax_nyc(self):  
        branch = self.branch  
        intercitybranch = self.intercitybranch  
        product = self.product  
        sales = self.sales  
        pricebeforetax = sales['purchase_price'] + \
        sales['purchase_price'] * sales['profit_margin']  
        finalselling_price = pricebeforetax + \
        (pricebeforetax * (sales['tax_rate'] +\
         sales['local_rate']))    
        sales['selling_price'] = finalselling_price  
        return branch,intercitybranch, product, sales    

在进一步创建对象之前,让我们检查前面的代码。NYC子类有自己的附加属性intercitybranch,它作为其自己的方法set_management的参数引入。纽约市还有一个自己的计算税的方法,即calc_tax_nyc。纽约市的calc_tax_nyc方法包括一个额外的组件local_rate来计算售价。

现在,让我们检查纽约市是否可以使用Branch类的这些方法来设置分支、产品和销售的新值:

branch_manhattan = NYC()

通过检查branch_manhattan对象中可用的方法,如下面的截图所示,我们可以看到纽约市可以利用Branch类中定义的集合方法:

图 2.7 – 从 Branch 继承的设置方法

图 2.7 – 从 Branch 继承的设置方法

我们可以通过使用所有这些方法设置属性,并在计算曼哈顿分支的销售税和地方税率后计算售价,如下所示:

branch_manhattan.branch = branch_manhattan.set_branch(branch_id = 2021,  
branch_street = '40097 5th Main Street',  
branch_borough = 'Manhattan',
branch_city = 'New York City',  
branch_state = 'New York',  
branch_zip = 11007)  

我们将调用branch_manhattan对象上的branch属性,如下所示:

branch_manhattan.branch
{'branch_id': 2021,
 'branch_street': '40097 5th Main Street',
 'branch_borough': 'Manhattan',
 'branch_city': 'New York City',
 'branch_state': 'New York',
 'branch_zip': 11007}

在下面的代码中,我们将使用set_management方法将值存储在纽约市对象内的intercitybranch字典变量中:

branch_manhattan.intercitybranch = branch_manhattan.set_management(  
    regional_manager = 'John M',  
    branch_manager = 'Tom H',  
    subBranch_id = '2021-01'      
)  

让我们称branch_manhattan对象上的intercitybranch属性,如下所示:

branch_manhattan.intercitybranch
{'regional_manager': 'John M',
 'branch_manager': 'Tom H',
 'subBranch_id': '2021-01'}

同样,在下面的代码中,我们将使用set_product方法将值存储在纽约市对象内的product字典变量中:

branch_manhattan.product = branch_manhattan.set_product(  
    product_id = 100002,  
    product_name = 'WashingMachine',  
    productBrand = 'Whirlpool'    
)  

现在,我们将调用branch_manhattan对象上的product属性:

branch_manhattan.product
{'product_id': 100002,
 'product_name': 'WashingMachine',
 'productBrand': 'Whirlpool'}

同样,在下面的代码中,我们将使用set_sales方法将值存储在纽约市对象内的sales字典变量中:

branch_manhattan.sales = branch_manhattan.set_sales(  
    purchase_price = 450,  
    profit_margin = 0.19,  
    tax_rate = 0.4,  
    local_rate = 0.055      

我们将进一步调用branch_manhattan对象上的sales属性,如下所示:

branch_manhattan.sales
{'purchase_price': 450,
 'profit_margin': 0.19,
 'tax_rate': 0.4,
 'local_rate': 0.055}

在所有前面的属性及其值分配之后,我们可以使用以下代码计算曼哈顿分支的税:

branch_manhattan.calc_tax_nyc()
({'branch_id': 2021,
  'branch_street': '40097 5th Main Street',
  'branch_borough': 'Manhattan',
  'branch_city': 'New York City',
  'branch_state': 'New York',
  'branch_zip': 11007},
 {'regional_manager': 'John M',
  'branch_manager': 'Tom H',
  'subBranch_id': '2021-01'},
 {'product_id': 100002,
  'product_name': 'WashingMachine',
  'productBrand': 'Whirlpool'},
 {'purchase_price': 450,
  'profit_margin': 0.19,
  'tax_rate': 0.4,
  'local_rate': 0.055,
  'selling_price': 779.1525})

如果我们不想根据地方税率计算售价,我们仍然可以使用Branch类中可用的calc_tax方法:

branch_manhattan.calc_tax()
({'branch_id': 2021,
  'branch_street': '40097 5th Main Street',
  'branch_borough': 'Manhattan',
  'branch_city': 'New York City',
  'branch_state': 'New York',
  'branch_zip': 11007},
 {'product_id': 100002,
  'product_name': 'WashingMachine',
  'productBrand': 'Whirlpool'},
 {'purchase_price': 450,
  'profit_margin': 0.19,
  'tax_rate': 0.4,
  'local_rate': 0.055,
  'selling_price': 749.7})

前面的代码及其输出展示了面向对象编程中继承的可重用性。现在让我们看看一个扩展的概念,即多重继承。

扩展到多重继承

Python 也支持ProductBranch,并让Sales类继承这两个基类。以下是我们将使用的逻辑的快速表示:

图 2.8 – 多重继承示例

图 2.8 – 多重继承示例

在下面的代码中,我们将创建一个Product类,在其中我们将定义产品的属性和一个get_product方法来返回产品详情:

class Product:  
    _product_id = 100902  
    _product_name = 'Iphone X'  
    _product_category = 'Electronics'  
    _unit_price = 700  
    def get_product(self):  
        return self._product_id, self._product_name,\
           self._product_category, self._unit_price  

我们还将创建另一个类Branch,在其中我们将定义分支的属性和一个get_branch方法来返回分支详情:

class Branch:  
    _branch_id = 2021  
    _branch_street = '40097 5th Main Street'  
    _branch_borough = 'Manhattan'  
    _branch_city = 'New York City'  
    _branch_state = 'New York'  
    _branch_zip = 11007  
    def get_branch(self):  
        return self._branch_id, self._branch_street, \
          self._branch_borough, self._branch_city, \
          self._branch_state, self._branch_zip  

我们将通过将两个父类 ProductBranch 继承到子类 Sales 中来实现多重继承的概念:

class Sales(Product, Branch):  
    date = '08/02/2021'  
    def get_sales(self):  
        return self.date, Product.get_product(self), \
          Branch.get_branch(self)  

在前面的代码中,Sales 类分别从 Product 类和 Branch 类继承了两个方法,分别是 get_productget_branch

在以下代码中,我们将为 Sales 类创建一个对象:

sales = Sales()

Sales 类调用 get_sales 方法会返回 Sales 类的 date 属性以及其父类中的 productbranch 属性:

sales.get_sales()
('08/02/2021',
 (100902, 'Iphone X', 'Electronics', 700),
 (2021,
  '40097 5th Main Street',
  'Manhattan',
  'New York City',
  'New York',
  11007))

通过这些示例,我们可以进一步了解多态的概念,它扩展了我们之前关于继承的例子。

理解多态

多态 是面向对象范式中的一个概念,我们可以通过重新定义或覆盖现有函数,或者为两个具有相同名称的不同类创建两个不同的函数来重用父类中函数的名称。在本节中,我们将探讨多态的两种变体示例:

  • 多态在继承中

  • 独立类中的多态

多态在继承中

让我们看看之前提到的子类 NYC 的例子,它从 Branch 类继承。为了计算特定分支的销售价格以及本地税率,我们在 NYC 类中创建了一个名为 calc_tax_nyc 的新方法。我们也可以在子类中通过覆盖父类 calc_tax 方法并使用新的计算来实现这一点。这个概念是继承中的多态。以下是它的表示:

图 2.9 – NYC 子类中覆盖的 calc_tax 方法

图 2.9 – NYC 子类中覆盖的 calc_tax 方法

首先,让我们回顾一下 Branch 类中的 calc_tax 方法,然后我们可以在子类 NYC 中覆盖它:

class Branch:
    def calc_tax(self):
        branch = self.branch
        product = self.product
        sales = self.sales
        pricebeforetax = sales['purchase_price'] + \
        sales['purchase_price'] * sales['profit_margin']
        finalselling_price = pricebeforetax + \
        (pricebeforetax * sales['tax_rate'])
        sales['selling_price'] = finalselling_price
        return branch, product, sales

现在,我们将通过继承 Branch 类来定义 NYC 类。这个类有两个方法,set_managementcalc_taxset_management 方法返回 intercitybranch 作为字典属性。calc_tax 方法现在在子类 NYC 中被覆盖,并返回分支详情、城际分支详情、产品详情和销售详情:

class NYC(Branch):  
    def set_management(self, **intercitybranch):  
        return intercitybranch  
    def calc_tax(self):  
        branch = self.branch  
        intercitybranch = self.intercitybranch  
        product = self.product  
        sales = self.sales  
        pricebeforetax = sales['purchase_price'] + \
        sales['purchase_price'] * sales['profit_margin']  
        finalselling_price = pricebeforetax + \
        (pricebeforetax * (sales['tax_rate'] + \
        sales['local_rate']))    
        sales['selling_price'] = finalselling_price  
        return branch,intercitybranch, product, sales     
branch_manhattan = NYC()

以下是对子类 NYCbranch_manhattan 对象支持的所有方法的表示:

图 2.10 – 多态后的 calc_tax

图 2.10 – 多态后的 calc_tax

以下代码显示了调用 branch_manhattan 中的 calc_tax 方法的输出,这是从其父类中覆盖的方法,用于在应用本地税率后计算销售价格:

branch_manhattan.calc_tax()
({'branch_id': 2021,
  'branch_street': '40097 5th Main Street',
  'branch_borough': 'Manhattan',
  'branch_city': 'New York City',
  'branch_state': 'New York',
  'branch_zip': 11007},
 {'regional_manager': 'John M',
  'branch_manager': 'Tom H',
  'subBranch_id': '2021-01'},
 {'product_id': 100002,
  'product_name': 'WashingMachine',
  'productBrand': 'Whirlpool'},
 {'purchase_price': 450,
  'profit_margin': 0.19,
  'tax_rate': 0.4,
  'local_rate': 0.055,
  'selling_price': 779.1525})

如我们所见,calc_tax 方法返回了在 NYC 中定义的输出。

独立类中的多态

多态不一定要发生在父-子类关系中。我们总是可以有两个完全不同的类,它们可以有两个具有相同名称的不同函数定义,并且可以通过调用它们的类对象实例来利用这两个函数。

对于这个例子,我们将创建两个独立的类,QueensBrooklyn,它们是ABC Megamart的两个不同分支。我们将不会将这些分支与Branch父类关联,以解释独立类中多态的概念。Brooklyn 分支只存储maintenance_cost,并按照每个分支的存储要求来定义它们。

图 2.11 – 独立类中一个方法的多态

图 2.11 – 独立类中一个方法的多态

在下面的代码中,对于Brooklyn类,我们只有在产品类型是FMCG时才计算维护成本。我们将计算数量成本为 0.25 的乘积,并额外加 100 美元用于冷藏。如果产品类型不是 FMCG,我们将通知您该产品将不会入库。让我们看看代码:

class Brooklyn:  
    def maintenance_cost(self, product_type, quantity):  
        self.product_type = product_type  
        self.quantity = quantity  
        coldstorage_cost = 100  
        if (product_type == 'FMCG'):  
            maintenance_cost = self.quantity * 0.25 + \
              coldstorage_cost      
            return maintenance_cost  
        else:  
            return "We don't stock this product"  

在下面的代码中,对于Queens类,我们只有在产品类型是Electronics时才计算维护成本。由于电子产品的维护成本较低,并且这里也不需要冷藏成本,我们将计算数量成本为 0.05。如果产品类型不是Electronics,我们将通知您该产品将不会入库:

class Queens:  
    def maintenance_cost(self, product_type, quantity):  
        self.product_type = product_type  
        self.quantity = quantity  
        if (product_type == 'Electronics'):  
            maintenance_cost = self.quantity * 0.05  
            return maintenance_cost  
        else:  
            return "We don't stock this product"  

请注意,我们在前几个例子中都使用了相同的函数名。下一步是调用这些函数。每个函数都可以通过为每个类创建一个对象来调用,即使它们在同一个程序中使用,这些函数也可以分别访问以执行不同的计算:

object_brooklyn = Brooklyn()  
object_queens = Queens()  
object_brooklyn.maintenance_cost('FMCG', 2000)  
600.0
object_queens.maintenance_cost('Electronics', 2000)  
100.0

我们现在已经理解了类中多态的概念。接下来,我们将探讨抽象,它的工作方式与多态类似,但有一个将在下一节中进一步解释的差异。

通过抽象隐藏细节

可以导入ABC来定义抽象基类。抽象更像是向外部用户提供一个黑盒,不透露类内部定义的各种方法的全部细节,而是提供一个参考类,帮助外部用户根据他们的需求实现方法。

例如,布鲁克林分部的用户不需要知道由皇后分部处理的计算来计算他们的维护成本。布鲁克林分部用户需要知道的信息是他们可以继承Branch类,并根据他们自己的账簿实现维护成本的计算,他们无需担心皇后分部是如何计算他们的维护成本的。同时,作为他们父类的Branch类,将无法提供一个通用的计算维护成本的实施方案,因为计算将根据分部而有所不同。在这种场景下,Branch类可以创建一个抽象方法maintenance_cost,并让它的子类或子类根据它们的要求实现它。布鲁克林对maintenance_cost方法的实现不会影响皇后对同一方法的实现;实现的目的是在子类内部结束,父抽象类始终可供其他子类定义它们自己的实现。

如果这种实现可以通过简单地应用多态到父类方法来完成,那么我们为什么还需要一个抽象类来做同样的事情?让我们首先通过实现一个父类及其子类,而不实际将其实现为抽象类来看一下:

class Branch():     
    def maintenance_cost(self):     
        pass    
class Brooklyn(Branch):     
    def maintenance_cost(self, product_type, quantity):    
        self.product_type = product_type    
        self.quantity = quantity    
        coldstorage_cost = 100    
        if (product_type == 'FMCG'):    
            maintenance_cost = self.quantity * 0.25 + \
              coldstorage_cost        
            return maintenance_cost    
        else:    
            return "We don't stock this product"    
class Queens(Branch):    
    def maintenance_cost(self, product_type, quantity):    
        self.product_type = product_type    
        self.quantity = quantity    
        if (product_type == 'Electronics'):    
            maintenance_cost = self.quantity * 0.05    
            return maintenance_cost    
        else:    
            return "We don't stock this product"    

在前面的实现中,我们为Branch类创建了两个子类,并且已经应用了多态来重写父类方法,但这仍然不是一个抽象,因为我们仍然能够为父类创建一个对象实例,并且当创建对象时,父类的方法可以被暴露出来。

与前面的实现不同,如果我们稍作修改,将Branch创建为一个抽象基类,让我们看看会发生什么。以下是我们要达到的表示:

图 2.12 -– 抽象类被两个类继承并实现了方法

图 2.12 -– 抽象类被两个类继承并实现了方法

这里,我们将从abc库中导入ABCabstractmethod,并创建一个名为Branch的抽象类,后面跟着两个子类BrooklynQueens,它们继承自父类Branch

from abc import ABC,abstractmethod   
class Branch(ABC):   
    @abstractmethod  
    def maintenance_cost(self):   
        pass  
class Brooklyn(Branch):   
    def maintenance_cost(self, product_type, quantity):  
        self.product_type = product_type  
        self.quantity = quantity  
        coldstorage_cost = 100  
        if (product_type == 'FMCG'):  
            maintenance_cost = self.quantity * 0.25 + \
              coldstorage_cost      
            return maintenance_cost  
        else:  
            return "We don't stock this product"  
class Queens(Branch):  
    def maintenance_cost(self, product_type, quantity):  
        self.product_type = product_type  
        self.quantity = quantity  
        if (product_type == 'Electronics'):  
            maintenance_cost = self.quantity * 0.05  
            return maintenance_cost  
        else:  
            return "We don't stock this product"  

我们导入了ABC库,将Branch创建为一个抽象类,并使用@abstractmethod关键字定义了maintenance_cost为抽象方法。

现在我们尝试创建一个Branch类的对象:

branch = Branch()

它抛出了以下错误:

图 2.13 – 抽象方法实例化错误

图 2.13 – 抽象方法实例化错误

如果为类实例化一个对象,则可以通过该对象访问类的所有属性和方法。这在常规类中是可能的;而在抽象类的情况下,不能实例化对象。这就是为什么隐藏不需要与外部用户共享的信息是有帮助的。

抽象是 Python 或其他任何面向对象语言中信息保护的一种方法。现在我们将看看封装以及如何在类中保护信息的更多细节。

使用封装保护信息

__(双下划线)和受保护的成员或变量以前缀_(单下划线)开头。我们将查看一些私有和受保护的类成员的示例。

私有成员

在 Python 中,不存在像其他面向对象语言那样的私有变量概念。然而,我们可以在变量或方法名前添加两个下划线符号,以表示特定的变量将在类内部用作私有成员。这样做是为了让开发者理解程序如何将变量视为私有。在变量或方法名前添加两个下划线可以防止 Python 解释器在继承期间进行名称混淆,以避免与变量发生冲突,并且它并不是像其他语言那样的实际私有成员。

在这个例子中,我们将定义我们熟悉的Branch类,其中包含产品 ID、产品名称、品牌、购买价格和利润率的私有变量,并创建一个用于显示产品详情的私有方法。我们还将创建分支 ID、区域经理和分支经理作为非私有类变量,并查看使用对象从类外部访问这些变量的区别。

图 2.14 – 类的私有成员及其通过对象的可访问性

图 2.14 – Branch类的私有成员及其通过Branch对象的可访问性

让我们看一下以下代码来实现这个示例:

class Branch():  
    branch_id = 2021  
    regional_manager = 'John M'  
    branch_manager = 'Tom H'  
    __product_id = None  
    __product_name = None  
    __productBrand = None  
    __purchase_price = None  
    __profit_margin = None  
    def __display_product_details(self):  
        self.__product_id = 100002  
        self.__product_name = 'Washing Machine'  
        self.__productBrand = 'Whirlpool'  
        self.__purchase_price = 450  
        self.__profit_margin = 0.19  
        print('Product ID: ' + str(self.__product_id) + ',\
          Product Name: ' + self.__product_name +  
          ', Product Brand: ' + self.__productBrand + ',\
          Purchase Price: ' + str(self.__purchase_price)
          + ', Profit Margin: ' +  str(self.__profit_margin))  
    def __init__(self):  
        self.__display_product_details()  

在为Branch类创建对象实例时,我们将能够查看__display_product_details方法的输出结果,因为它是在类内部使用默认的__init__方法调用的:

branch = Branch()

输出如下:

Product ID: 100002, Product Name: Washing Machine, Product Brand: Whirlpool, Purchase Price: 450, Profit Margin: 0.19

让我们尝试访问branch_id变量,它没有被声明为私有:

branch.branch_id

输出如下:

2021

我们能够访问这个变量。现在让我们尝试访问profit_margin,它使用双下划线前缀声明:

branch.__profit_margin

它给出了以下错误:

图 2.15 – 访问类私有变量的错误

图 2.15 – 访问类私有变量的错误

我们得到一个错误,因为这个变量只能在类内部访问,不能通过类的对象访问,这是由于名称混淆造成的。同样的情况也适用于创建用于显示产品详情的私有方法:

branch.__display_product_details()

我们看到以下内容:

图 2.16 – 访问类私有方法的错误

以下截图显示了 Branch 类可以由其对象访问的类成员列表:

图 2.17 – 包含私有成员后分支对象可访问的成员

图 2.17 – 包含私有成员后分支对象可访问的成员

然而,这些私有成员可以通过创建一个 API 来在类外访问。

受保护成员

在本例中,我们将重新创建一个 Branch 类,其中包含用于产品 ID、产品名称、品牌、购买价格和利润率的受保护变量,并创建一个用于显示产品详情的受保护方法。我们将创建一个分支经理作为私有变量。我们还将创建分支 ID 和区域经理作为非受保护或私有的类变量,并查看使用类外对象访问这些变量的差异。我们还将进一步继承 Branch 类以检查哪些成员是可访问的。

图 2.18 – 分支类的受保护成员及其被继承子类访问的权限

图 2.18 – 分支类的受保护成员及其被继承子类访问的权限

让我们查看以下代码以实现此示例:

class Branch():  
    branch_id = 2022  
    regional_manager = 'Ron D'  
    __branch_manager = 'Sam J'  
    _product_id = None  
    _product_name = None  
    _productBrand = None  
    _purchase_price = None  
    _profit_margin = None  
    def _display_product_details(self):  
        self._product_id = 100003  
        self._product_name = 'Washing Machine'  
        self._productBrand = 'Samsung'  
        self._purchase_price = 430  
        self._profit_margin = 0.18  
        print('Product ID: ' + str(self._product_id) + \
          ', Product Name: ' + self._product_name +  
          ', Product Brand: ' + self._productBrand +
          ', Purchase Price: ' + str(self._purchase_price) 
         + ', Profit Margin: ' +  str(self._profit_margin))
    def __init__(self):  
        self._display_product_details()  
branch = Branch()

输出如下:

Product ID: 100003, Product Name: Washing Machine, Product Brand: Samsung, Purchase Price: 430, Profit Margin: 0.18

Branch 创建的对象也无法访问其受保护的成员,这与私有成员类似,如下所示:

图 2.19 – 包含受保护成员后分支对象可访问的成员

图 2.19 – 包含受保护成员后分支对象可访问的成员

让我们创建一个名为 Brooklyn 的子类,它继承自父类 Branch。子类将继承父类的所有受保护变量和方法,而它仍然不会继承私有成员:

class Brooklyn(Branch):  
    def __init__(self):  
        print(self._product_id)  
        self._display_product_details()  
branch_brooklyn = Brooklyn()

输出如下:

None
Product ID: 100003, Product Name: Washing Machine, Product Brand: Samsung, Purchase Price: 430, Profit Margin: 0.18

product_id 变量是父类的受保护成员,display_product_details 也是父类的受保护成员,它可以通过子类 Brooklyninit 方法访问。

现在,让我们包括父类的一个私有成员并检查它是否可以从子类访问:

class Brooklyn(Branch):  
    def __init__(self):  
        print(self._product_id)  
        self._display_product_details()  
        print(self.__branch_manager)  
branch_brooklyn = Brooklyn()

输出如下:

None
Product ID: 100003, Product Name: Washing Machine, Product Brand: Samsung, Purchase Price: 430, Profit Margin: 0.18

以下错误说明私有成员仍然不会被子类访问:

图 2.20 – 从子类访问父类私有属性的错误

图 2.20 – 从子类访问父类私有属性的错误

这些示例让我们了解了如何在 Python 中实现封装。

摘要

本章我们回顾了类和对象的概念,并探讨了如何创建类和对象实例的示例。我们还学习了方法的概念以及如何在类内部创建方法。此外,我们还看到了如何将继承和多继承应用于类,以及如何将多态应用于方法。然后我们学习了如何创建抽象类和方法。最后,我们学习了封装的概念以及如何限制对类的方法和变量的访问。

本章回顾了 Python 中所有面向对象编程(OOP)的概念,这些概念将作为本书主要主题——元编程的基础。

在下一章中,我们将详细探讨装饰器的概念及其通过示例的实现。

第二部分:深入探讨 - 元编程的构建块 I

本节的目标是通过详细查看每个构建块以及它们在实际场景中的应用示例,来加深你对元编程概念的理解。本节将包含章节,这些章节将概念解释与基于实现的途径相结合,以在阅读本书时为用户提供动手经验和指导性编码知识。本节中的章节可以按顺序或独立阅读。

本部分包含以下章节:

  • 第三章, 理解装饰器及其应用

  • 第四章, 与元类一起工作

  • 第五章, 理解内省

  • 第六章, 在 Python 对象上实现反射

  • 第七章, 理解泛型和类型

  • 第八章, 定义算法模板

第三章:第三章:理解装饰器和它们的用途

从本章开始,我们将开始查看元编程的各种概念,以及如何应用它们的示例。我们首先将查看装饰器以及如何在 Python 3 中实现装饰器。

装饰器是元编程概念之一,它处理在不修改实际函数主体的同时装饰函数。正如其名所示,装饰器通过允许函数成为另一个函数的参数来为函数、方法或类添加额外的价值,该函数“装饰”或提供有关被装饰的函数、方法或类的更多信息。装饰器可以在单个用户定义的函数或定义在类内部的函数上开发,或者也可以在类本身上定义。理解装饰器将帮助我们通过外部操作来增强函数、方法和类的可重用性,而不会影响实际的实现。

在上一章中,我们回顾了面向对象编程的概念,它是本章以及本书未来章节的基础。

本章,我们将探讨以下主要主题:

  • 查看简单的函数装饰器

  • 在一个函数和另一个函数之间交换装饰器

  • 将多个装饰器应用于一个函数

  • 探索类装饰器

  • 了解内置装饰器

到本章结束时,你应该能够创建自己的装饰器,在函数/方法或类上实现用户定义的装饰器,并重用内置装饰器。

技术要求

本章中共享的代码示例可以在 GitHub 上找到,这里是本章代码的链接:github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter03

查看简单的函数装饰器

现在,我们将通过一个示例来查看不同类型的函数装饰器。我们将继续使用我们在上一章中查看的ABC Megamart示例。Python 中的每个用户定义函数都可以执行不同的操作。但如果我们想让不同的函数显示特定的附加信息,无论这些函数执行什么操作呢?我们可以通过定义另一个函数来实现,该函数装饰任何作为输入提供的函数。

让我们看看以下步骤来更好地理解:

  1. 函数装饰器可以这样定义:

    def functiondecorator(inputfunction):  
        def decorator():  
            print("---Decorate function with this line---
              ")  
            return inputfunction()  
        return decorator  
    

这段代码定义了一个简单的函数装饰器,它接受任何输入函数作为参数,并在函数结果上方添加一行,打印“---使用此行装饰函数---”作为任何输入函数的第一个输出行。

  1. 这个函数装饰器可以通过一个新定义的用户函数以两种不同的语法来调用。让我们定义两个简单的函数:

    def userfunction1():  
        return "A picture is worth a thousand words "  
    

这个函数返回短语“一张图片胜过千言万语”。

  1. 我们将添加一个返回不同短语的功能:“行动胜于言语”:

    def userfunction2():  
        return "Actions speak louder than words"  
    
  2. 在以下步骤中,让我们将函数装饰器添加到前面定义的两个用户定义函数中,并查看结果:

    decoratedfunction1 = functiondecorator(userfunction1)
    decoratedfunction2 = functiondecorator(userfunction2)
    
  3. 在前面的代码中,我们通过向它们添加装饰器函数来重新分配了函数。执行装饰函数 1 的结果如下:

    decoratedfunction1()  
    ---Decorate function with this line---
    'A picture is worth a thousand words'
    
  4. 同样,我们也可以执行装饰函数 2:

    decoratedfunction2()
    ---Decorate function with this line---
    'Actions speak louder than words'
    

这两个函数结果都添加了一条额外的行,---使用此行装饰函数---,这不是它们函数定义的一部分,而是装饰器函数的一部分。这些示例展示了函数装饰器的可重用性。

  1. 让我们进一步探讨语法 2,这是添加装饰器到其他函数、方法或类中最广泛使用的方法:

    @functiondecorator  
    def userfunction1():  
        return "A picture is worth a thousand words"  
    @functiondecorator  
    def userfunction2():  
        return "Actions speak louder than words"  
    

在前面的代码中,在定义用户定义函数时,我们在@functiondecorator定义上方添加了额外的行。这一行表示我们在定义阶段本身添加了装饰器。这个装饰器可以声明一次,并用于任何新定义的相关函数。

  1. 执行前面的代码提供了与使用语法 1 的示例代码执行相同的输出:

    userfunction1()
    ---Decorate function with this line---
    'A picture is worth a thousand words'
    userfunction2()
    ---Decorate function with this line---
    'A picture is worth a thousand words'
    

现在你已经了解了简单的函数装饰器,我们可以看看一个演示其应用的示例。

通过应用理解函数装饰器

我们可以进一步探讨一个使用ABC Megamart场景的函数装饰器示例。在这个例子中,我们将创建一个函数,为不同分支的分支经理添加不同格式的电子邮件签名。我们将定义两个函数,manager_albanymanager_manhattan,它们具有不同的字体颜色和高亮显示。

让我们看看这段代码的第一部分:

def manager_albany(*args):  
    BLUE = '\03394m'  
    BOLD = '\33[5m'  
    SELECT = '\33[7m'
    for arg in args:
        print(BLUE + BOLD + SELECT + str(arg))
manager_albany('Ron D','ron.d@abcmegamart.com','123 Main Street','Albany','New York', 12084)  

前面的代码打印了分支经理的电子邮件签名,带有白色、粗体和蓝色高亮文本:

Ron D
ron.d@abcmegamart.com
123 Main Street
Albany
New York
12084

现在,让我们快速看一下这段代码:

def manager_manhattan(*args):
    GREEN = '\033[92m'
    SELECT = '\33[7m'
    for arg in args:
        print(SELECT + GREEN + str(arg))
manager_manhattan('John M',  'john.m@abcmegamart.com', '40097 5th Main Street',   'Manhattan', 'New York City',  'New York',  11007)

这个示例将打印带有高亮文本的分支经理的电子邮件签名:

John M
john.m@abcmegamart.com
40097 5th Main Street
Manhattan
New York City
New York
11007

现在,让我们在两个签名中添加ABC Megamart的名称,并使用黄色高亮显示,同时保持签名高亮颜色不变。为此,我们将创建一个函数装饰器,该装饰器接受前面函数的参数,并将ABC Megamart添加为黑色字体和黄色高亮:

def signature(branch):  
    def footnote(*args):  
        LOGO = '\33[43m'  
        print(LOGO + 'ABC Mega Mart')  
        return branch(*args)  
    return footnote  

下面的图表示了如何在两个不同的签名上实现电子邮件签名装饰器。

![图 3.1 – 电子邮件签名装饰器

图 3.1 – 电子邮件签名装饰器

前面的签名装饰器在两个签名中添加了ABC Megamart的名称,并使用黄色高亮显示,同时保持签名高亮颜色不变。

首先,让我们将@signature添加到manager_manhattan

@signature
def manager_manhattan(*args):
    GREEN = '\03392m'
    SELECT = '\33[7m'
    for arg in args:
        print(SELECT + GREEN + str(arg))
manager_manhattan('John M',  'john.m@abcmegamart.com', '40097 5th Main Street',   'Manhattan', 'New York City',  'New York',  11007)

此代码返回以下电子邮件签名:

ABC Mega Mart
John M
john.m@abcmegamart.com
40097 5th Main Street
Manhattan
New York City
New York
11007

现在让我们将 @signature 添加到 manager_albany

@signature
def manager_albany(*args):  
    BLUE = '\033[94m'  
    BOLD = '\33[5m'  
    SELECT = '\33[7m'
    for arg in args:
        print(BLUE + BOLD + SELECT + str(arg))
manager_albany('Ron D','ron.d@abcmegamart.com','123 Main Street','Albany','New York', 12084)  

这样做会返回以下电子邮件签名:

ABC Mega Mart
Ron D
ron.d@abcmegamart.com
123 Main Street
Albany
New York
12084

在前面的代码片段中添加一个函数装饰器到不同的函数中,使它们具有共同的功能——在这种情况下,ABC Megamart 的标题具有黄色高亮作为共同功能,同时保留各个分支经理的签名。这是一个简单示例,说明了可重用装饰器可以是什么,以及如何在保持函数实际功能完整的同时,向函数添加元数据或附加信息。

现在我们已经了解了函数装饰器是什么以及我们如何使用它们,让我们看看通过交换它们来利用装饰器为不同的函数,使它们更具可重用性。

在函数之间交换装饰器

我们现在已经了解了什么是函数装饰器以及函数装饰器可以用于多个函数。我们将通过创建两个不同的装饰器来进一步探索装饰器的可重用性概念,这两个装饰器将服务于不同的目的,并在不同函数之间交换装饰器。

为了演示这个概念,我们将为函数 1 创建装饰器 1,为函数 2 创建装饰器 2,然后我们将它们从一个函数交换到另一个函数。让我们创建两个装饰器来装饰两个不同的函数。

装饰器 1 将被创建来转换提供的作为假日日期的日期参数,用于设置 ABC Megamart 阿拉巴马分支的假日。

下面的图示是 装饰器 1 和其 函数 1 的表示。

![图 3.2 – 作为装饰器的日期转换器

图 3.2 – 作为装饰器的日期转换器

让我们看看我们将使用以下代码来实现我们的示例:

def dateconverter(function):  
    import datetime  
    def decoratedate(*args):     
        newargs = []  
        for arg in args:  
            if(isinstance(arg,datetime.date)):  
                arg = arg.weekday(),arg.day,arg.month,
                  arg.year  
            newargs.append(arg)  
        return function(*newargs)  
    return decoratedate    

前面的 dateconverter 是一个装饰器函数,它接受另一个函数作为参数。为了执行这个函数,我们导入了 datetime 库,这个库帮助我们把输入的日期参数转换成星期、月份中的天数、年份以及月份的格式。这个装饰器函数内部接受所有传递给内部函数的参数,并检查是否有任何函数参数是 datetime 数据类型,如果找到 datetime 对象,它将被转换以显示星期、月份中的天数、年份和月份。

此装饰器还将转换后的 datetime 对象格式与函数的其他参数一起存储在一个列表中,并将该列表作为参数传递给作为此装饰器输入的函数。现在让我们创建一个函数来设置阿拉巴马分支的假日日历,并使用此装饰器函数对其进行装饰。

*args参数。此函数的第一个参数将被设置为branch_id,第二个参数为holiday_type,第三个参数为holiday_name,第四个参数为holiday_date。所有这些输入参数也被函数转换为字典变量,并返回带有其键值对的字典,表示每个值。

下面是使用我们刚才讨论的细节的代码看起来像什么:

@dateconverter  
def set_holidays_alabama(*args):  
    holidaydetails = {}  
    holidaydetails['branch_id'] = args[0]  
    holidaydetails['holiday_type'] = args[1]  
    holidaydetails['holiday_name'] = args[2]  
    holidaydetails['holiday_date'] = args[3]  
    return holidaydetails  

在前面的代码中,我们通过添加装饰器@dateconverter开始函数定义,该装饰器负责将假日日期转换为上述格式。现在让我们通过提供创建假日详情字典所需的参数来调用此函数:

from datetime import datetime  
holiday =datetime.strptime('2021-01-18', '%Y-%m-%d')  

在前面的代码中,我们创建了一个datatime对象,并将其存储在名为 holiday 的变量中,该变量将被作为set_holidays_alabama函数的输入之一:

set_holidays_alabama('id1000',  
                   'local',  
                   'Robert E. Lee's Birthday',  
                   holiday)  

前面的代码给出了以下装饰后的输出:

{'branch_id': 'id1000',  
 'holiday_type': 'local',  
 'holiday_name': 'Robert E. Lee's Birthday',  
 'holiday_date': (0, 18, 1, 2021)}  

我们现在可以继续创建另一个装饰器,它对提供的另一个函数执行不同的操作。

让我们现在看看id是否存在于输入中,这表示输入值是任何类型的标识符,并通过移除其前缀来返回标识符的数值。这个装饰器将被添加到一个函数中,用于设置任何输入产品为 Malibu 分公司的促销详情。

下面的图表示了装饰器 2函数 2

图 3.3 – ID 标识符作为装饰器

图 3.3 – ID 标识符作为装饰器

下面是我们将用于装饰器的代码:

def identifier(function):  
    def decorateid(*args):     
        newargs = []  
        for arg in args:  
            if(isinstance(arg,str)):  
                arg = arg.lower()  
                if 'id' in arg:  
                    arg = int(''.join(filter(str.isdigit,
                      arg)))  
            newargs.append(arg)  
        return function(*newargs)  
    return decorateid   

前一个标识符是一个装饰器函数,它接受另一个函数作为参数。这个装饰器函数还内部接受传递给其内部函数的所有参数,并遍历每个单独的参数以检查它是否是一个字符串。如果参数是一个字符串,装饰器将字符串转换为小写并检查它是否包含子串 ID。如果子串 ID 存在于变量中,那么变量中的所有字符串都将被移除,并且只存储其中的数字,其余的函数参数以列表的形式传递给作为输入提供给此装饰器的函数。现在让我们创建一个函数来设置 Malibu 分公司的促销详情,并使用这个装饰器函数来装饰其 ID。

*args类似于set_holidays_alabama函数。此函数的第一个参数将被设置为branch_id,第二个参数为product_id,第三个参数为promotion_date,第四个参数为promotion_type,第五个参数为promotion_reason。所有这些输入参数也被函数转换为字典变量,并返回带有其键值对的字典,表示每个值。此函数中有两个id参数,它们被标识符装饰。

下面是使用我们刚才讨论的细节所看到的代码样子:

@identifier
def set_promotion_malibu(*args):  
    promotiondetails = {}  
    promotiondetails['branch_id'] = args[0]  
    promotiondetails['product_id'] = args[1]  
    promotiondetails['product_name'] = args[2]  
    promotiondetails['promotion_date'] = args[3]  
    promotiondetails['promotion_type'] = args[4]  
    promotiondetails['promotion_reason'] = args[5]  
    return promotiondetails  

在前面的代码中,我们通过添加装饰器 @identifier 开始了函数定义,该装饰器负责从 id 变量中移除前缀。现在让我们通过提供创建产品促销详细信息字典所需的参数来调用这个函数:

from datetime import datetime  
promotion_date = datetime.strptime('2020-12-23', '%Y-%m-%d')  

这里,我们创建了一个 datetime 对象并将其存储在促销日期中,这个日期将作为 set_promotion_malibu 函数的一个输入参数传递,但这个日期变量将保持与定义相同的格式:

set_promotion_malibu('Id23400','ProdID201','PlumCake',promotion_date,'Buy1Get1','Christmas')  

前面的代码给出了以下装饰后的输出:

{'branch_id': 23400,  
 'product_id': 201,  
 'product_name': 'plumcake',  
 'promotion_date': datetime.datetime(2020, 12, 23, 0, 0),  
 'promotion_type': 'buy1get1',
 'promotion_reason': 'christmas'}  

现在我们有两个装饰器和两个由它们装饰的不同函数。为了检查这些装饰器是否可以交换,我们现在通过以下代码重新定义这些函数,使用交换装饰器:

@identifier  
def set_holidays_alabama(*args):  
    holidaydetails = {}  
    holidaydetails['branch_id'] = args[0]  
    holidaydetails['holiday_type'] = args[1]  
    holidaydetails['holiday_name'] = args[2]  
    holidaydetails['holiday_date'] = args[3]  
    return holidaydetails  
@dateconverter  
def set_promotion_malibu(*args):  
    promotiondetails = {}  
    promotiondetails['branch_id'] = args[0]  
    promotiondetails['product_id'] = args[1]  
    promotiondetails['product_name'] = args[2]  
    promotiondetails['promotion_date'] = args[3]  
    promotiondetails['promotion_type'] = args[4]  
    promotiondetails['promotion_reason'] = args[5]  
    return promotiondetails  

让我们输入所需的参数并执行前面的函数 set_holidays_alabama

from datetime import datetime  
holiday =datetime.strptime('2021-01-18', '%Y-%m-%d')  
set_holidays_alabama('id1000',  
                   'local',  
                   'Robert E. Lee's Birthday',  
                   holiday)  

这段代码给出了以下装饰后的输出:

{'branch_id': 1000,  
 'holiday_type': 'local',  
 'holiday_name': 'robert e. lee's birthday',  
 'holiday_date': datetime.datetime(2021, 1, 18, 0, 0)}  

在前面的输出中,标识符应用于分支 ID,而假日日期没有变化。同样,让我们执行以下代码:

promotion_date = datetime.strptime('2020-12-23', '%Y-%m-%d')  
set_promotion_malibu('Id23400','ProdID201','PlumCake',promotion_date,'Buy1Get1','Christmas')  

这段代码给出了以下装饰后的输出:

{'branch_id': 'Id23400',  
 'product_id': 'ProdID201',  
 'product_name': 'PlumCake',  
 'promotion_date': (2, 23, 12, 2020),  
 'promotion_type': 'Buy1Get1',  
 'promotion_reason': 'Christmas'}  

以下图表示了两个装饰器如何在它们的功能之间交换或替换:

图 3.4 – 交换装饰器

图 3.4 – 交换装饰器

让我们重用之前的示例来进一步探讨将多个装饰器应用到单个函数的概念。

将多个装饰器应用到单个函数

到目前为止,我们已经了解到装饰器可以被创建并添加到函数中,以在函数上执行元编程。我们也了解到装饰器可以被重用并交换到不同的函数中。我们还了解到装饰器可以从函数外部添加装饰或值到函数中,并帮助通过附加信息来改变函数。如果我们想通过装饰器让函数执行两种不同的操作,同时又不想让装饰器变得更加具体,我们可以创建两个或更多不同的装饰器并将它们应用到单个函数上吗?是的,我们可以。现在我们将查看如何使用多个装饰器装饰一个函数以及它是如何工作的。

对于这个示例,让我们重用装饰器 dateconverteridentifier。为了理解这个概念,我们可以重用之前声明的函数之一,set_promotion_malibu,它既有 datetime 对象作为输入参数(促销日期)也有两个 ID 值作为输入参数(branch_idproduct_id)。

以下图表示了将两个装饰器添加到函数中:

图 3.5 – 一个函数的多个装饰器

图 3.5 – 一个函数的多个装饰器

以下代码将我们的示例付诸实践:

@identifier  
@dateconverter  
def set_promotion_malibu(*args):  
    promotiondetails = {}  
    promotiondetails['branch_id'] = args[0]  
    promotiondetails['product_id'] = args[1]  
    promotiondetails['product_name'] = args[2]  
    promotiondetails['promotion_date'] = args[3]  
    promotiondetails['promotion_type'] = args[4]  
    promotiondetails['promotion_reason'] = args[5]  
    return promotiondetails  

在这段代码中,我们将两个装饰器都添加到了set_promotion_malibu函数中:

promotion_date = datetime.strptime('2021-01-01', '%Y-%m-%d')  
set_promotion_malibu('Id23400','ProdID203','Walnut Cake',promotion_date,'Buy3Get1','New Year')  

执行前面的代码会导致对输入值应用两个装饰器:

{'branch_id': 23400,  
 'product_id': 203,  
 'product_name': 'walnut cake',  
 'promotion_date': (4, 1, 1, 2021),  
 'promotion_type': 'buy3get1',  
 'promotion_reason': 'new year'}  

从前面的输出中,我们可以看到@identifier应用于branch_idproduct_id。同时,@dateconverter应用于promotion_date。现在让我们探索装饰器的其他变体。

探索类装饰器

__init____call__。在创建类的对象实例时,作为__init__函数一部分初始化的任何变量都成为类的变量本身。同样,类的__call__函数返回一个函数对象。如果我们想将类用作装饰器,我们需要利用这两个内置方法的组合。

让我们看看如果我们不使用call方法会发生什么。看看以下代码片段:

class classdecorator:  
    def __init__(self,inputfunction):  
        self.inputfunction = inputfunction  
    def decorator(self):  
        result = self.inputfunction()  
        resultdecorator = ' decorated by a class decorator'  
        return result + resultdecorator  

在这里,我们创建了一个名为classdecorator的类,并添加了init方法来接收一个函数作为输入。我们还创建了一个decorator方法,它存储初始化的函数变量结果,并将由类装饰器装饰的装饰器字符串添加到输入函数结果中。

现在让我们创建一个输入函数来测试前面的classdecorator

@classdecorator  
def inputfunction():  
    return 'This is input function'  

添加这个类装饰器应该会装饰输入函数。让我们检查当我们调用这个输入函数时会发生什么:

inputfunction()

我们得到了以下类型错误,指出classdecorator不可调用:

图 3.6 – 由于类装饰器定义不正确导致的错误

图 3.6 – 由于类装饰器定义不正确导致的错误

我们收到这个错误是因为我们没有使用正确的方法使类表现得像一个装饰器。前面代码中的decorator方法返回一个变量而不是一个函数。为了使这个类作为一个装饰器工作,我们需要按照以下方式重新定义类:

class classdecorator:  
    def __init__(self,inputfunction):  
        self.inputfunction = inputfunction  
    def __call__(self):  
        result = self.inputfunction()  
        resultdecorator = ' decorated by a class decorator'  
        return result + resultdecorator  

在这里,我们将decorator方法替换为内置方法__call__。现在让我们重新定义输入函数并看看会发生什么:

@classdecorator  
def inputfunction():  
    return 'This is input function'  

我们可以调用前面的函数来检查这个类装饰器的行为:

inputfunction()
'This is input function decorated by a class decorator'

下面的图是一个简单的表示,展示了创建类装饰器的不正确方法:

图 3.7 – 创建类装饰器的错误方法

图 3.7 – 创建类装饰器的错误方法

这里是创建它的正确方式:

图 3.8 – 创建类装饰器的正确方法

图 3.8 – 创建类装饰器的正确方法

现在你对类装饰器有了更好的理解,我们可以继续分析类装饰器在ABC Megamart上的应用。

通过应用来理解类装饰器

我们将通过将类装饰器应用于 ABC Megamart 的一个场景来详细了解类装饰器的应用。让我们考虑一个场景,其中 ABC Megamart 为每个分店创建了一个单独的类。让我们还假设每个类都有自己的方法 buy_product,该方法通过具体应用购买的分店和产品的销售税率来计算产品的销售价格。当商场想要应用涉及八种通用促销类型的季节性促销时,每个分支类不需要有应用于其计算出的销售价格的促销计算方法。相反,我们可以创建一个类装饰器,可以应用于每个分支的 buy_product 方法,并且类装饰器将反过来通过在分支计算的实际销售价格上应用促销折扣来计算最终销售价格。

我们将创建两个类,并将 buy_product 方法添加到每个类中,以计算销售价格而不添加类装饰器。这是为了理解实际方法的返回值:

class Alabama():  
    def buy_product(self,product,unitprice,quantity,
      promotion_type):  
        alabamataxrate = 0.0522  
        initialprice = unitprice*quantity   
        salesprice = initialprice + 
          initialprice*alabamataxrate  
        return salesprice, product,promotion_type  

为前面的类创建一个对象实例,并使用其参数调用该方法,返回以下结果:

alb1 = Alabama()    
alb1.buy_product('Samsung-Refrigerator',200,1,'20%Off')   
(210.44, 'Samsung-Refrigerator', '20%Off')

同样,我们可以定义一个名为 Arizona 的类,并添加 buy_product 方法,然后执行以下代码来验证其返回值,而不使用装饰器:

class Arizona():  
    def buy_product(self,product,unitprice,quantity,
      promotion_type):  
        arizonataxrate = 0.028  
        initialprice = unitprice*quantity   
        salesprice = initialprice + 
          initialprice*arizonataxrate  
        return salesprice, product,promotion_type  
arz1 = Arizona()  
arz1.buy_product('Oreo-Cookies',0.5,250,'Buy2Get1')  
(128.5, 'Oreo-Cookies', 'Buy2Get1')

前面的 buy_product 方法接受产品名称、单价、数量和促销类型作为输入,并通过将单价乘以产品的数量来计算初始价格。然后,它进一步通过将初始价格与上一步骤中计算的初始价格以及州税率相加来计算销售价格。最后,该方法返回销售价格、产品名称和促销类型。销售税率因州而异,销售价格的计算也根据销售税率的不同而不同。

我们现在可以创建一个类装饰器来应用促销折扣到销售价格,并通过包括优惠率或折扣率来计算产品的最终销售价格。

在下面的代码中,让我们定义一个名为 applypromotion 的类,并添加两个内置方法,使该类表现得像一个装饰器:

  • __init__ 方法:在这个场景中,这是一个作为输入变量的函数或方法

  • __call__ 方法:此方法接受多个输入参数,这些参数也是被装饰的函数或方法的参数

输入参数应用于被装饰的函数或方法,并且它进一步通过检查八种不同的促销类型,重新计算销售价格,并将其存储为最终销售价格,如下所示:

class applypromotion:  
    def __init__(self, inputfunction):  
        self.inputfunction = inputfunction  
    def __call__(self,*arg):  
        salesprice, product,promotion_type = 
          self.inputfunction(arg[0],arg[1],arg[2],arg[3])  
        if (promotion_type == 'Buy1Get1'):  
            finalsalesprice = salesprice * 1/2  
        elif (promotion_type == 'Buy2Get1'):  
            finalsalesprice = salesprice * 2/3  
        elif (promotion_type == 'Buy3Get1'):  
            finalsalesprice = salesprice * 3/4  
        elif (promotion_type == '20%Off'):  
            finalsalesprice = salesprice - salesprice * 0.2  
        elif (promotion_type == '30%Off'):  
            finalsalesprice = salesprice - salesprice * 0.3  
        elif (promotion_type == '40%Off'):  
            finalsalesprice = salesprice - salesprice * 0.4  
        elif (promotion_type == '50%Off'):  
            finalsalesprice = salesprice - salesprice * 0.5  
        else:  
            finalsalesprice = salesprice   
        return "Price of - " + product + ": " + '$' + str(finalsalesprice)  

类装饰器 @applypromotion 现在可以进一步由其他函数或方法使用。我们现在可以将这个装饰器应用到 Alabama 类的 buy_product 方法上:

class Alabama():  
    @applypromotion  
    def buy_product(product,unitprice,quantity,promotion_type):  
        alabamataxrate = 0.0522  
        initialprice = unitprice*quantity   
        salesprice = initialprice + initialprice*alabamataxrate  
        return salesprice, product,promotion_type  

为前面的代码创建一个对象实例并调用其方法的工作方式如下:

alb = Alabama()  
alb.buy_product('Samsung-Refrigerator',200,1,'20%Off')  
'Price of - Samsung-Refrigerator: $168.352'

同样,我们也可以通过添加类装饰器来重新定义名为 Arizona 的类及其 buy_product 方法,如下所示:

class Arizona():  
    @applypromotion  
    def buy_product(product,unitprice,quantity,
      promotion_type):  
        arizonataxrate = 0.028  
        initialprice = unitprice*quantity   
        salesprice = initialprice + 
          initialprice*arizonataxrate  
        return salesprice, product,promotion_type  

为前面的代码创建一个对象实例并调用其方法的工作方式如下:

arz = Arizona()  
arz.buy_product('Oreo-Cookies',0.5,250,'Buy2Get1')  
'Price of - Oreo-Cookies: $85.66666666666667'

让我们回顾在添加装饰器之前和之后 buy_product 方法的 Arizona 的结果。前面的代码是在添加装饰器后的输出,以下代码是在添加装饰器之前的输出:

arz1.buy_product('Oreo-Cookies',0.5,250,'Buy2Get1')  
(128.5, 'Oreo-Cookies', 'Buy2Get1')

在添加 applypromotion 装饰器后,250 包巧克力的销售价格在促销前后的价格相差$128.50,折扣后的价格为$85.66。商店不必总是对产品进行促销,而 buy_product 方法只有在需要促销销售产品时才能重用 applypromotion 装饰器,从而在保持 buy_product 方法实际功能完整的同时,使装饰器能够外部改变类的行为。

本例的简单表示如下:

图 3.9 – 应用促销折扣的产品类装饰器

图 3.9 – 应用促销折扣的产品类装饰器

在学习了如何将类装饰器应用于来自其他类的函数或方法之后,我们将进一步探讨 Python 中可用的内置装饰器。

了解内置装饰器

现在,问题是,我们是否总是需要创建用户定义或自定义装饰器来应用于类和方法,或者我们是否有一些预定义的装饰器可以用于特定目的。

除了本章中我们查看的用户定义装饰器之外,Python 还有一些自己的内置装饰器,如 @staticmethod@classmethod,可以直接应用于方法。这些装饰器在类定义的过程中为方法和类添加了某些重要的功能。我们将详细探讨这两个装饰器,如下所示。

静态方法

@staticmethod – 是一个装饰器,它接受一个常规的 Python 函数作为输入参数,并将其转换为静态方法。静态方法可以在类内部创建,但不会使用类对象实例的隐式第一个参数,通常表示为名为 self 的参数,就像其他基于实例的方法一样。

为了理解这个概念,让我们首先创建一个名为 Alabama 的类,并向该类添加一个名为 buy_product 的函数,该函数不带 self 参数,也不带静态方法装饰器,并检查其行为:

class Alabama:  
    def buy_product(product,unitprice,quantity,promotion_type):  
        alabamataxrate = 0.0522  
        initialprice = unitprice*quantity   
        salesprice = initialprice + 
          initialprice*alabamataxrate  
        return salesprice, product,promotion_type  

在这里,我们定义了一个名为 Alabama 的类,其中包含一个名为 buy_product 的函数。现在,让我们创建一个对象实例,并在类内部调用该函数以检查其行为:

alb = Alabama()  
alb.buy_product('Samsung-Refrigerator',200,1,'20%Off')  

执行此代码会导致以下错误:

图 3.10 – 调用不带静态方法和 self 的函数时的错误

图 3.10 – 在调用没有静态方法和 self 的函数时出错

重新运行前面的函数而不创建对象的工作方式如下:

Alabama.buy_product('Samsung-Refrigerator',200,1,'20%Off')  
(210.44, 'Samsung-Refrigerator', '20%Off')

为了避免前面的错误,并在创建或不创建对象的情况下调用类内的函数,我们可以通过向函数中添加 @staticmethod 装饰器将其转换为静态方法。现在我们可以看看它是如何工作的:

class Alabama:  
    @staticmethod  
    def buy_product(product,unitprice,quantity,
      promotion_type):  
        alabamataxrate = 0.0522  
        initialprice = unitprice*quantity   
        salesprice = initialprice + 
          initialprice*alabamataxrate  
        return salesprice, product,promotion_type  
    def another_method(self):  
        return "This method needs an object"  

我们添加了一个名为 another_method 的额外方法,它只能通过对象实例来调用。现在让我们为该类创建一个对象并调用前面的两个方法:

albstatic = Alabama()  
albstatic.buy_product('Samsung-Refrigerator',200,1,'20%Off')  
(210.44, 'Samsung-Refrigerator', '20%Off')  
albstatic.another_method()  
'This method needs an object'  

这两种方法,staticinstance,都可以使用类的对象来调用。同时,静态方法也可以使用类本身来调用,而无需创建对象:

Alabama.buy_product('Samsung-Refrigerator',200,1,'20%Off')  
(210.44, 'Samsung-Refrigerator', '20%Off')  
Alabama.another_method()  

执行此代码会导致以下错误:

图 3.11 – 使用其类调用实例方法时出错

图 3.11 – 使用其类调用实例方法时出错

当使用其类调用时,静态方法生成了预期的输出,而实例方法没有运行。这是使用静态方法将函数转换为类内部方法的优点。

类方法

@classmethod – 也是一个类似于 @staticmethod 的内置装饰器,这个装饰器也将一个函数转换为类内部的静态方法。@staticmethod 没有对类的对象的隐含参数,而 @classmethod 有一个隐含参数 cls,它被添加到函数中,而 @classmethod 装饰器则添加到它上面,如下面的代码块所示:

class Alabama:  
    @classmethod  
    def buy_product(cls,product,unitprice,quantity,
      promotion_type):  
        alabamataxrate = 0.0522  
        initialprice = unitprice*quantity   
        salesprice = initialprice + 
          initialprice*alabamataxrate  
        return cls,salesprice, product,promotion_type  

这个函数可以带或不带创建类实例来调用。我们可以在下面的代码中查看这两种情况:

Alabama.buy_product('Samsung-Refrigerator',200,1,'20%Off')  
(__main__.Alabama, 210.44, 'Samsung-Refrigerator', '20%Off')  
alb = Alabama()  
alb.buy_product('Samsung-Refrigerator',200,1,'20%Off')  
(__main__.Alabama, 210.44, 'Samsung-Refrigerator', '20%Off')  

在前面的代码中,我们可以看到通过 @classmethod 转换为类方法的函数可以直接使用类或通过创建类的对象来调用。

这些是一些内置的装饰器,Python 3 中还有更多这样的装饰器可供探索和重用。

摘要

在本章中,我们学习了如何创建简单的装饰器,以及如何通过示例应用装饰器。我们看到了如何将装饰器从一个函数交换到另一个函数,以及如何将多个装饰器添加到同一个函数中。

我们现在理解了类装饰器的概念,并查看了一个如何应用它们的示例。最后,我们学习了如何使用一些内置装饰器,如 @staticmethod@classmethod

所有这些概念都是 Python 元编程的一部分,并且它们被用来在外部更改函数或方法的行为,而不影响函数或方法的内部功能。

在下一章中,我们将通过不同的示例来探讨元类概念。

第四章:第四章:与元类一起工作

元类,本章的重点,可以通过装饰参数的方式来操纵新类的创建方式,而不会影响实际的类定义本身。除非需要更高级的实现框架或 API,例如需要操纵类或动态生成类等功能,否则元类在 Python 应用开发中并不常用。

在上一章中,我们通过一些示例了解了装饰器的概念。理解装饰器有助于更轻松地理解元类,因为装饰器和元类都通过外部操作处理 Python 3 程序对象的元编程。

在本章中,我们将涵盖以下主要主题:

  • 元类的概述

  • 元类的结构

  • 元类的应用

  • 切换元类

  • 元类中的继承

  • 操纵类变量

到本章结束时,你应该能够创建自己的元类,实现元类上的继承,并重用已经创建的元类。

技术要求

本章中分享的代码示例可在 GitHub 上找到,地址为:github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter04

元类的概述

元类是可以单独创建的类,具有可以改变其他类行为或帮助动态制造新类的某些特性。所有元类的基类是type类,元类的对象或实例将是一个类。我们创建的任何自定义元类都将继承自type类。type也是 Python 中所有数据类型的类,Python 3 中的其他一切都是type类的对象。我们可以通过检查 Python 中不同程序对象的类型来测试这个陈述,如下所示:

class TestForType:  
    pass  
type(TestForType)
type
type(int)
type
type(str)
type
type(object)
type
type(float)
type
type(list)
type

在本章中,我们将探讨一些如何使用这些元类、如何实现它们以及如何重用它们的示例。我们将继续使用我们的ABC Megamart示例,以进一步理解元类。

元类的结构

元类就像任何其他类一样,但它有能力改变将其作为元类的其他类的行为。理解元类的结构有助于我们创建自己的定制元类,这些元类可以进一步用于操纵新类。元类的超类是type本身。当我们使用type作为超类创建一个类,并重写__new__方法来操纵返回的类的元数据时,我们就创建了一个元类。让我们通过一些简单的示例来更深入地了解。

__new__方法接受cls作为其第一个参数,即类本身。具有cls作为第一个参数的类的成员可以通过类名和其余参数作为类的其他元数据来访问,如下所示:

class ExampleMetaClass1(type):  
    def __new__(classitself, *args):  
        print('class itself: ', classitself)  
        print('Others: ', args)  
        return type.__new__(classitself, *args)  

在前面的代码中,我们创建了名为ExampleMetaClass1的类,它继承自类type,并重写了__new__方法以打印类实例及其其他参数。

现在让我们创建名为ExampleClass1的类,并将前面的元类添加到其中:

class ExampleClass1(metaclass = ExampleMetaClass1):      
    int1 = 123  
    str1 = 'test'  
    def test():  
        print('test')  

运行前面的代码显示以下结果:

class itself:  <class '__main__.ExampleMetaClass1'>
Others:  ('ExampleClass1', (), {'__module__': '__main__', '__qualname__': 'ExampleClass1', 'int1': 123, 'str1': 'test', 'test': <function ExampleClass1.test at 0x00000194A377E1F0>})

此输出的第一部分是类实例<class '__main__.ExampleMetaClass1'>,其余参数是类名和类的参数。元类定义的简单表示如下:

图 4.1 – 示例元类定义

图 4.1 – 示例元类定义

让我们在下一个小节中通过另一个示例来深入了解。

参数分析

我们现在将深入探讨元类中__new__方法的参数。分析元类的参数将有助于了解可以使用元类自定义的类的哪些信息。在定义类时添加元类可以操纵的数据在以下图中表示:

图 4.2 – 带更多参数的示例元类

图 4.2 – 带更多参数的示例元类

现在让我们遵循以下步骤,看看参数的行为如何影响类:

  1. 首先,看看以下代码,其中我们将元类的所有参数分离——类实例;类名;类的所有父类、超类或基类;以及类内创建的所有变量和方法:

    class ExampleMetaClass2(type):  
        def __new__(classitself, classname, baseclasses, 
                    attributes):  
            print('class itself: ', classitself)  
            print('class name: ', classname)  
            print('parent class list: ', baseclasses)  
            print('attribute list: ', attributes)  
            return type.__new__(classitself, classname, 
                baseclasses, attributes)  
    
  2. 接下来,我们将创建两个父类——ExampleParentClass1ExampleParentClass2

    class ExampleParentClass1():      
           def test1():  
                print('parent1 - test1')  
    class ExampleParentClass2():      
           def test2():  
                print('parent2 - test2')  
    
  3. 现在,我们将创建名为ExampleClass2的类,我们将继承前两个父类,并将元类作为ExampleMetaClass2添加:

    class ExampleClass2(ExampleParentClass1,ExampleParentClass2, metaclass = ExampleMetaClass2):      
        int1 = 123  
        str1 = 'test'  
    
        def test3():  
            print('child1 - test3')  
    
  4. 执行前面的代码会产生以下输出:

    class itself:  <class '__main__.ExampleMetaClass2'>
    class name:  ExampleClass2
    parent class:  (<class '__main__.ExampleParentClass1'>, <class '__main__.ExampleParentClass2'>)
    attributes:  {'__module__': '__main__', '__qualname__': 'ExampleClass2', 'int1': 123, 'str1': 'test', 'test3': <function ExampleClass2.test3 at 0x00000194A3994E50>}
    

此示例展示了元类返回的突出显示的参数,并概述了使用元编程从类中可能操纵哪些值。

  1. 让我们看看在这个例子中创建的每个类的类型:

    type(ExampleParentClass1)
    type
    type(ExampleParentClass2)
    type
    type(ExampleMetaClass2)
    type
    type(ExampleClass2)
    __main__.ExampleMetaClass2
    

如我们所见,所有其他类的类型都是其自身类型,而ExampleClass2的类型是ExampleMetaClass2

现在你已经了解了元类的结构,我们可以进一步探讨在“ABC Megamart”示例中元类的应用。

元类应用

在本节中,我们将查看一个示例,我们将创建一个元类,它可以自动修改新创建的任何分支类的用户定义方法属性。为了测试这一点,让我们遵循以下步骤:

  1. 创建一个名为BranchMetaclass的元类:

    class BranchMetaclass(type):  
    
  2. 创建一个 __new__ 方法,其参数为类实例、类名、基类和属性。在 __new__ 方法中,导入 inspect 库,该库可以帮助检查输入属性:

        def __new__(classitself, classname, baseclasses, 
             attributes):  
            import inspect  
    
  3. 创建一个新的字典 newattributes

         newattributes = {}  
    

遍历类属性,检查属性是否以 __ 开头,并且不要改变其值。

  1. 继续遍历其他属性,并检查属性是否为函数。如果是函数,则在类方法前添加 branch 前缀,并将方法名改为标题格式:

    for attribute, value in attributes.items():  
                if attribute.startswith("__"):  
                    newattributes[attribute] = value  
                elif inspect.isfunction(value):  
                    newattributes['branch' +
                        attribute.title()] = value for a
                        attribute, value in 
                        attributes.items():
                if attribute.startswith("__"):  
                    newattributes[attribute] = value  
                elif inspect.isfunction(value):  
                    newattributes['branch' + 
                        attribute.title()] = value  
    
  2. 如果不满足前面的条件,则按原样保存属性的值:

    else:  
                    newattributes[attribute] = value  
    
  3. 返回带有新属性的 new 方法:

                         return type.__new__(classitself, 
                             classname, baseclasses, 
                             newattributes) 
    
  4. 在元类内部,还创建一个常规用户定义方法 buy_product 来计算产品的销售价格:

    def buy_product(product,unit_price,quantity,statetax_rate,promotiontype):  
            statetax_rate = statetax_rate          
            initialprice = unit_price*quantity   
            sales_price = initialprice + 
                initialprice*statetax_rate  
            return sales_price, product,promotiontype  
    
  5. 接下来,我们将创建另一个新的类,名为 Brooklyn,并将这个元类添加到该类中。通过添加元类,我们希望 Brooklyn 类中的方法具有前缀分支,并在创建 Brooklyn 类的方法时将方法名改为标题格式。

Brooklyn 类有四个变量,分别是 product_idproduct_nameproduct_categoryunit_price。我们还将创建一个计算维护成本的方法,由于元类会改变新创建类的行为,因此这个方法应从 maintenance_cost 改为 branchMaintenance_cost。以下是新类:

class Brooklyn(metaclass = BranchMetaclass):  
    product_id = 100902  
    product_name = 'Iphone X'  
    product_category = 'Electronics'  
    unit_price = 700  

    def maintenance_cost(self,product_type, quantity):
        self.product_type = product_type  
        self.quantity = quantity  
        cold_storage_cost = 100  
        if (product_type == 'Electronics'):  
            maintenance_cost = self.quantity * 0.25 + 
                cold_storage_cost      
            return maintenance_cost  
        else:  
            return "We don't stock this product"  
  1. 我们可以列出 Brooklyn 类的所有参数,并检查元类是否改变了其行为:

    dir(Brooklyn)
    ['__class__',
     '__delattr__',
     '__dict__',
     ‚__dir__',
     ‚__doc__',
     ‚__eq__',
     ‚__format__',
     ‚__ge__',
     ‚__getattribute__',
     ‚__gt__',
     ‚__hash__',
     ‚__init__',
     ‚__init_subclass__',
     ‚__le__',
     ‚__lt__',
     ‚__module__',
     ‚__ne__',
     ‚__new__',
     ‚__reduce__',
     ‚__reduce_ex__',
     ‚__repr__',
     ‚__setattr__',
     ‚__sizeof__',
     ‚__str__',
     ‚__subclasshook__',
     ‚__weakref__',
     'branchMaintenance_cost',
     'product_category',
     'product_id',
     'product_name',
     'unit_price']
    
  2. 现在让我们创建一个对象,并查看其方法和变量,如下所示:

    brooklyn = Brooklyn()
    brooklyn.branchMaintenance_Cost('Electronics',10)
    102.5
    brooklyn.product_id
    100902
    brooklyn.product_name
    'Iphone X'
    brooklyn.product_type
    'Electronics'
    

这个示例的简单表示如下:

图 4.3 – 在 ABC Megamart 上应用元类 – 分支示例

图 4.3 – 在 ABC Megamart 上应用元类 – 分支示例

到目前为止,我们已经概述了元类,了解了其结构,对其参数进行了分析,并通过在核心示例上创建自定义元类来应用我们的理解。在下一节中,我们将探讨更多应用。

继承元类

在本节中,我们将通过一个示例来演示如何继承元类,以检查它是否可以作为常规父类继承,而不会改变正在创建的新类的行为。请看以下代码:

class Queens(BranchMetaclass):  
    def maintenance_cost(product_type, quantity):  
        product_type = product_type  
        quantity = quantity  
        if (product_type == ‹FMCG›):  
            maintenance_cost = quantity * 0.05  
            return maintenance_cost  
        else:  
            return "We don't stock this product"  

现在让我们为前面的类创建一个对象来检查是否可以创建对象:

queens = Queens()

我们得到以下 TypeError

图 4.4 – 为继承元类的类创建对象时出错

图 4.4 – 为继承元类的类创建对象时出错

这个错误发生是因为__new__是一个静态方法,它被调用以创建类的新的实例,并且它期望类有三个参数,但在创建类对象时没有提供这些参数。然而,还有一种方法可以调用新创建的类Queens。可以直接调用这个类,并使用其方法,而无需创建对象:

Queens.maintenance_cost('FMCG',120)
6.0

由于元类没有被用作元类,而是作为父类使用,maintenance_cost方法没有被修改成branchMaintenance_cost。由于元类被继承,Queens也继承了BranchMetaclass的用户定义方法,如下所示:

Queens.buy_product('Iphone',1000,1,0.04,None)
(1040.0, 'Iphone', None)

作为父类和元类的继承

现在我们来看看当我们继承一个类作为父类,并在创建新类时将其添加为元类时会发生什么:

class Queens(BranchMetaclass, metaclass = BranchMetaclass):  
    def maintenance_cost(product_type, quantity):  
        product_type = product_type  
        quantity = quantity  
        if (product_type == ‹FMCG›):  
            maintenance_cost = quantity * 0.05  
            return maintenance_cost  
        else:  
            return "We don't stock this product"  

在前面的代码中,我们将BranchMetaclass添加为Queens类的父类,并且我们还将其添加为元类。这个定义应该使Queens类继承BranchMetaclass的自定义方法,并将maintenance_cost方法改为branchMaintenance_cost。让我们看看它是否真的做到了:

Queens.branchMaintenance_Cost('FMCG',2340)
117.0

在前面的代码执行和输出中,maintenance_cost方法被转换成了预期的branchMaintenance_cost方法。现在运行以下命令:

Queens.buy_product('Iphone',1500,1,0.043,None)
(1564.5, 'Iphone', None)

来自BranchMetaclass的自定义方法buy_product也被继承,因为它是一个父类。

下面是这个示例的一个简单表示:

图 4.5 – 在 ABC Megamart 分支示例中应用元类及其继承

图 4.5 – 在 ABC Megamart 分支示例中应用元类及其继承

让我们进一步探讨从一类切换到另一类元类的示例。

切换元类

我们现在可以探讨一个类中切换元类的概念。你可能想知道,为什么我们需要切换元类呢? 切换元类强化了元编程的可重用性概念,在这种情况下,它有助于理解一个为使用一个类而创建的元类也可以用于不同的类,而不会影响类的定义。

在本节的示例中,我们将创建两个元类 – IncomeStatementMetaClassBalanceSheetMetaClass。对于ABC Megamart的 Malibu 分支,我们将创建一个类来捕获其财务报表所需的信息。与这个示例相关的两个财务报表是 Malibu 分支的损益表属性和资产负债表属性。为了区分一个类的特定属性或方法应该放在哪里,我们将创建两个元类,它们会查看属性的名称,并根据损益表或资产负债表相应地标记它们。

以下是对上述元类将要操作的属性的一个简单表示:

图 4.6 – 在这个元类示例中使用的财务属性

图 4.6 – 在此元类示例中使用的财务属性

看一下下面的代码片段:

class IncomeStatementMetaClass(type):  
    def __new__(classitself, classname, baseclasses, 
                attributes):  
        newattributes = {}  
        for attribute, value in attributes.items():  
            if attribute.startswith("__"):  
                newattributes[attribute] = value  
            elif («revenue» in attribute) or \  
            ("expense" in attribute) or \  
            ("profit" in attribute) or \  
            ("loss" in attribute):  
                newattributes['IncomeStatement_' + 
                    attribute.title()] = value  
            else:  
                newattributes[attribute] = value  
        return type.__new__(classitself, classname, 
            baseclasses, newattributes)  

在这里,new 方法被修改为检查具有作为收入报表参数之一的键的属性,例如 revenueexpenseprofitloss。如果这些术语中的任何一个出现在方法名或变量名中,我们将添加 IncomeStatement 前缀以隔离这些方法和变量。

为了测试这个元类,我们将创建一个新的类 Malibu,它有四个变量和四个方法,如下所示:

class Malibu(metaclass = IncomeStatementMetaClass):  
    profit = 4354365  
    loss = 43000  
    assets = 15000  
    liabilities = 4000  
    def calc_revenue(quantity,unitsales_price):  
        totalrevenue = quantity * unitsales_price   
        return totalrevenue  
    def calc_expense(totalrevenue,netincome, netloss):  
        totalexpense = totalrevenue - (netincome + netloss)  
        return totalexpense    
    def calc_totalassets(cash,inventory,accountsreceivable):
        totalassets = cash + inventory + accountsreceivable  
        return totalassets  
    def calc_totalliabilities(debt,accruedexpense,
         accountspayable):  
        totalliabilities = debt + accruedexpense + 
            accountspayable  
        return totalliabilities  

在前面的代码中,我们添加了元类 IncomeStatementMetaClass,我们看到 Malibu 类的属性按以下方式修改了变量和方法的行为:

图 4.7 – 没有元类的 Malibu(左)和有元类的 Malibu(右)

图 4.7 – 没有元类的 Malibu(左)和有元类的 Malibu(右)

我们将进一步添加另一个元类,BalanceSheetMetaClass,以处理类 Malibu 中的资产负债表相关属性。在以下元类中,新方法被修改为检查具有作为资产负债表参数之一的键的属性,例如 assetsliabilitiesgoodwill 和现金。如果这些术语中的任何一个出现在方法名或变量名中,我们将添加 BalanceSheet 前缀以隔离这些方法和变量:

class BalanceSheetMetaClass(type):  
    def __new__(classitself, classname, baseclasses, 
                attributes):  
        newattributes = {}  
        for attribute, value in attributes.items():  
            if attribute.startswith("__"):  
                newattributes[attribute] = value  
            elif («assets» in attribute) or \  
            ("liabilities" in attribute) or \  
            ("goodwill" in attribute) or \  
            ("cash" in attribute):  
                newattributes['BalanceSheet_' + 
                    attribute.title()] = value  
            else:  
                newattributes[attribute] = value  
        return type.__new__(classitself, classname, 
            baseclasses, newattributes)  

在前面的代码中,我们添加了元类 BalanceSheetMetaClass,我们看到 Malibu 类的属性按以下方式修改了变量和方法的行为:

图 4.8 – 使用 IncomeStatementMetaClass 的 Malibu(左)和使用 BalanceSheetMetaClass 的 Malibu(右)

图 4.8 – 使用 IncomeStatementMetaClass 的 Malibu(左)和使用 BalanceSheetMetaClass 的 Malibu(右)

既然你知道为什么我们需要切换元类,让我们看看元类在继承中的应用。

元类中的继承

继承,在字面上,意味着子类获得父类的属性,在面向对象编程的情况下也是同样的意思。一个新的类可以继承父类的属性和方法,它也可以有自己的属性和方法。

在这个例子中,我们将通过创建两个类 CaliforniaPasadena 来查看元类上的继承是如何工作的 – California 是父类,而 Pasadena 是子类。

让我们检查这些步骤以更好地理解继承:

  1. 在上一节中,我们已经创建了两个元类,它们以类型作为其父类 – IncomeStatementMetaClassBalanceSheetMetaClass。我们将首先使用 IncomeStatement 元类创建类 California

    class California(metaclass = IncomeStatementMetaClass):  
        profit = 4354365  
        loss = 43000  
        def calc_revenue(quantity,unitsales_price):  
            totalrevenue = quantity * unitsaleprice   
            return totalrevenue  
    
        def calc_expense(totalrevenue,netincome, netloss):  
            totalexpense = totalrevenue - (netincome + netloss)  
            return totalexpense   
    

在这里,我们只定义了可以被 IncomeStatement 元类修改的属性。

  1. 接下来,我们将使用 BalanceSheet 元类创建另一个类 Pasadena

    class Pasadena(California,metaclass = BalanceSheetMetaClass):  
        assets = 18000  
        liabilities = 5000  
        def calc_totalassets(cash,inventory,
            accountsreceivable):  
            totalassets = cash + inventory + 
                accountsreceivable  
            return totalassets  
    
        def calc_totalliabilities(debt,accruedexpense,
            accountspayable):  
            totalliabilities = debt + accruedexpense + 
                accountspayable  
            return totalliabilities  
    

我们在这里定义了那些可以被 BalanceSheet 元类修改的属性。

  1. 执行 Pasadena 类的代码会导致以下错误:

图 4.9 – 执行具有不同元类的子类时出错

图 4.9 – 执行具有不同元类的子类时出错

抛出这个错误是因为 Pasadena 继承了父类 California,它有一个不同的元类 IncomeStatementMetaClass,它是从类型继承的,而 Pasadena 的元类 BalanceSheetMetaClass 也是从类型继承的。

  1. 要解决这个错误,我们可以将 BalanceSheetMetaClass 重新定义为以父类 IncomeStatementMetaClass 代替类型类,如下所示:

    class BalanceSheetMetaClass(IncomeStatementMetaClass):  
        def __new__(classitself, classname, baseclasses, 
                    attributes):  
            newattributes = {}  
            for attribute, value in attributes.items():  
                if attribute.startswith("__"):  
                    newattributes[attribute] = value  
                elif («assets» in attribute) or \  
                ("liabilities" in attribute) or \  
                ("goodwill" in attribute) or \  
                ("cash" in attribute):  
                    newattributes['BalanceSheet_' + 
                        attribute.title()] = value  
                else:  
                    newattributes[attribute] = value  
            return type.__new__(classitself, classname, 
                baseclasses, newattributes)  
    
  2. 现在让我们重新运行 California 父类和 Pasadena 子类,以检查两个元类的行为修改是否在 Pasadena 类中实现:

    class California(metaclass = IncomeStatementMetaClass):  
        profit = 4354365  
        loss = 43000  
        def calc_revenue(quantity,unitsales_price):  
            totalrevenue = quantity * unitsaleprice   
            return totalrevenue  
        def calc_expense(totalrevenue,netincome, netloss):  
            totalexpense = totalrevenue - (netincome + 
                netloss)  
            return totalexpense    
    class Pasadena(California,metaclass = BalanceSheetMetaClass):  
        assets = 18000  
        liabilities = 5000  
        def calc_totalassets(cash,inventory,
            accountsreceivable):  
            totalassets = cash + inventory + 
                accountsreceivable  
            return totalassets  
        def calc_totalliabilities(debt,accruedexpense,
            accountspayable):  
            totalliabilities = debt + accruedexpense + 
                accountspayable  
            return totalliabilities  
    
  3. 这是 Pasadena 类的输出,正如我们所见,BalanceSheetIncomeStatement 属性都按照它们的元类进行了修改:

图 4.10 – 具有继承的 Pasadena 类

图 4.10 – 具有继承的 Pasadena 类

该应用的简单表示如下:

图 4.11 – 元类中的继承

图 4.11 – 元类中的继承

在这种情况下,我们将 BalanceSheetMetaClass 的父类重新定义为 IncomeStatementMetaClass,因为 Python 在它们都从类型继承时不会自动解析它们的父类,而是抛出元类冲突。重新定义 BalanceSheetMetaClass 的父类不仅解决了错误,而且不会影响类的整体功能,因为 IncomeStatementMetaClass 最终也是从类型继承的。

让我们看看另一个例子,我们将向类属性添加额外的信息。

操作类变量

在本节中,我们将通过一个例子进一步探讨使用元类操作类变量。我们将创建一个名为 SchemaMetaClass 的元类,并将定义 __new__ 方法以操作属于 integerfloatstringboolean 数据类型的类属性。让我们快速浏览一下步骤:

  1. 我们现在将创建 SchemaMetaClass,以类型作为父类,并修改了 new 方法以检查以下条件:

    class SchemaMetaClass(type):  
    
  2. 创建字典对象 newattributes。如果 class 属性是一个以 __ 开头的内置 class 方法,则将属性的值存储为 newattributes 中的此类:

        def __new__(classitself, classname, baseclasses, 
                    attributes):  
    
            newattributes = {}  
            for attribute, value in attributes.items():  
                if attribute.startswith("__"):  
                    newattributes[attribute] = value 
    
  3. 如果 class 属性是整数或浮点变量,则类返回一个字典项,其中属性名为 ColumnName,值为 ValueTypeNUMERICLength 为值的长度:

                elif type(value)==int or type(value)==float:  
                    newattributes[attribute] = {}  
                    newattributes[attribute]['ColumnName']
                         = attribute.title()  
                    newattributes[attribute]['Value'] 
                         = value  
                    newattributes[attribute]['Type'] 
                         = 'NUMERIC'  
                    newattributes[attribute]['Length'] = len(str(value))  
    
  4. 如果class属性是一个字符串变量,那么类返回一个类似的字典项,其中TypeVARCHAR

                elif type(value)==str:  
                    newattributes[attribute] = {}  
                    newattributes[attribute]['ColumnName']
                         = attribute.title()  
                    newattributes[attribute]['Value']
                         = value  
                    newattributes[attribute]['Type']
                         = 'VARCHAR'  
                    newattributes[attribute]['Length']
                         = len(value)  
    
  5. 类似地,如果class属性是一个布尔对象,则返回一个类似类型的字典项,其中TypeBOOLEAN

                elif type(value)==bool:  
                    newattributes[attribute] = {}  
                    newattributes[attribute]['ColumnName']
                         = attribute.title()  
                    newattributes[attribute]['Value']
                         = value  
                    newattributes[attribute]['Type']
                         = 'BOOLEAN'  
                    newattributes[attribute]['Length']
                         = None  
    
  6. 任何其他变量或方法都像这样存储在newattributes中:

                else:  
                    newattributes[attribute] = value                  
            return type.__new__(classitself, classname,
                 baseclasses, newattributes)  
    
  7. 现在,我们将创建一个具有元类SchemaMetaClassArizona类,定义产品的所有变量,并定义一个从元编程类属性创建模式的方法:

    class Arizona(metaclass = SchemaMetaClass):  
        product_id = 200443  
        product_name = 'Iphone'  
        product_category = 'Electronics'  
        sales_quantity = 2  
        tax_rate = 0.05  
        sales_price = 1200  
        profit = 70  
        loss = 0  
        sales_margin = 0.1  
        promotion = '20%Off'  
        promotion_reason = 'New Year'    
        in_stock = True  
    
        def create_schema(self):  
            import pandas as pd  
            tableschema = pd.DataFrame([self.product_id,  
                                      self.product_name,  
                                  self.product_category,  
                                    self.sales_quantity,  
                                          self.tax_rate,  
                                       self.sales_price,  
                                            self.profit,  
                                              self.loss,  
                                      self.sales_margin,  
                                         self.promotion,  
                                  self.promotion_reason,  
                                         self.in_stock])  
            tableschema.drop(labels = ['Value'], axis = 1,
                             inplace = True)  
            return tableschema   
    

我们添加了一个示例产品的产品详情(在这种情况下,是一个 iPhone)和变量是不同数据类型的组合 – stringintegerfloatbool。我们将定义create_schema方法,该方法导入 pandas 库以创建一个 DataFrame,该 DataFrame 为变量提供类似表格的结构,并将数据帧作为表模式返回。

  1. 现在,考虑一个场景,其中元类没有被添加到前面的代码中。调用product_name变量将导致以下结果:

    objarizona = Arizona()  
    objarizona.product_name  
    'Iphone'
    
  2. 由于我们在前面的Arizona类定义中添加了元类,调用product_name结果如下:

    objarizona = Arizona()
    objarizona.product_name
    {'ColumnName': 'Product_name',
     'Value': 'Iphone',
     'Type': 'VARCHAR',
     'Length': 6}
    
  3. 类似地,我们可以查看其他几个变量的结果如下:

    objarizona.product_category  
    
    {'ColumnName': 'Product_category',  
     'Value': 'Electronics',  
     'Type': 'VARCHAR',  
     'Length': 11}  
    
    objarizona.sales_quantity  
    {'ColumnName': 'Sales_quantity', 'Value': 2, 'Type': 'NUMERIC', 'Length': 1}  
    
    objarizona.tax_rate  
    {'ColumnName': 'Tax_rate', 'Value': 0.05, 'Type': 'NUMERIC', 'Length': 4}  
    
  4. 进一步使用元编程类变量,我们定义了create_schema方法来返回一个表模式:

    objarizona.create_schema()
    

我们得到以下表,其中包含类中定义的所有变量:

图 4.12 –  方法的输出

图 4.12 – create_schema 方法的输出

这些是元类在开发应用程序中可以使用的几个示例。元类还可以在更复杂的场景中使用,例如自动代码生成和框架开发。

摘要

在本章中,我们学习了如何创建元类以及元类的一些应用。

然后,我们看到了如何切换元类,重用功能,以及如何在使用元类的类上实现继承。最后,我们还看到了如何进一步操作元类的变量。

所有这些概念都是 Python 元编程的一部分,并且它们被用来在类外部更改类的行为,而不影响类本身的内部功能。

在下一章中,我们将通过不同的示例来探讨反射的概念。

第五章:第五章:理解内省

在本章中,我们将探讨 Python 3 中的内省,并了解它在元编程中的有用性。内省是一个概念,我们可以在 Python 运行时使用一系列 Python 内置方法来了解对象的属性或属性。

为什么需要内省?内省是针对 Python 对象的信息收集过程,收集到的信息可以帮助我们通过外部操作来利用对象执行通用操作,从而有助于我们编写元程序。

在我们理解如何实现内省之前,我们将查看 Python 中帮助执行内省的内置函数。在本章中,我们将查看每个帮助我们内省并理解我们在程序中使用对象的函数。

在本章中,我们将探讨以下主要主题:

  • 介绍内置函数

  • 使用内置的id函数

  • 使用id调试意外的赋值

  • 检查对象是否可调用

  • 检查对象是否有属性

  • 检查对象是否为实例

  • 检查对象是否为子类

  • 理解属性的使用

  • 将属性用作装饰器

到本章结束时,你应该能够应用内置函数来内省 Python 对象,将它们应用于示例,并使用它们来调试代码。

技术要求

本章中分享的代码示例可在 GitHub 上找到,具体位置为github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter05

介绍内置函数

为了理解内省以及如何使用 Python 的内置函数进行内省,我们将继续在本章中使用我们的核心示例ABC Megamart

我们将介绍以下内置函数的用法,以用于内省 Python 对象:

  • id()

  • eval()

  • callable()

  • hastattr()

  • getattr()

  • isinstance()

  • issubclass()

  • property()

内省 Python 对象有助于理解对象的属性,这反过来又有助于对这些对象进行元编程,并使用它们来调试对象,我们将在后续章节中进一步探讨。

通过这种理解,让我们进一步探讨如何使用这些内置函数以及如何内省对象。

使用内置的id函数

理解 Python 对象的特征有助于编写针对该对象的元程序。对象的内存地址是其特征或属性之一,可以使用元编程进行操作。Python 3 中的 id 函数可以调用以使用对象的内存地址来识别对象。通过对象的内存地址来识别对象有助于分析对象,以找出在代码开发过程中是否无意中创建了多个对象的赋值或副本。

为了进一步理解,以下是我们的工作方式:

  1. 我们将创建一个名为 Billing 的类,该类计算并打印任何作为输入提供的产品的简单账单。请参阅以下代码:

    class Billing:
        def __init__(self,product_name,unit_price,quantity,tax):
            self.product_name = product_name
            self.unit_price = unit_price
            self.quantity = quantity
            self.tax = tax
    
        def generate_bill(self):
            total = self.unit_price * self.quantity 
            final_total = total + total*self.tax
            print('***********------------------
                   **************')
            print('Product:', self.product_name)
            print('Total:',final_total)
            print('***********------------------
                   **************')
    
  2. 现在,让我们为 Billing 类创建一个对象:

    billing = Billing('Delmonte Cheese',6,4,0.054)
    
  3. 现在,让我们调用 generate_bill 方法来打印账单:

    billing.generate_bill()
    

此代码将产生以下输出:

***********------------------**************
Product: Delmonte Cheese
Total: 25.296
***********------------------**************
  1. 在下一步中,让我们创建一个单独的 generate_bill 函数,该函数执行与 Billing 类内部创建的 generate_bill 方法相同的操作集。该函数将接受四个参数(product_nameunit_pricequantitytax):

    def generate_bill(product_name,unit_price,quantity,tax):
    total = unit_price * quantity 
        final_total = total + total*tax
        print('***********------------------
               **************')
        print('Product:', product_name)
        print('Total:',final_total)
        print('***********------------------
               **************')
    
  2. 在下一步中,我们将把 Billing 类复制到另一个名为 Invoicing 的变量中:

    Invoicing = Billing
    

到目前为止,我们有三个对象:

  • 一个名为 Billing 的类

  • 一个名为 generate_bill 的函数

  • Billing 类赋值给名为 Invoicing 的变量的变量

  1. 现在,让我们使用 Python 的内置 id 函数来获取这些对象的内存地址:

    id(Billing)
    2015859835472
    id(Invoicing)
    2015859835472
    id(generate_bill)
    2015871203792
    

在前面的输出中,我们可以注意到 BillingInvoicing 具有相同的内存地址,因为 InvoicingBilling 类的副本。以下图是此例的简单表示:

图 5.1 – 将 Billing 类复制到 Invoicing

图 5.1 – 将 Billing 类复制到 Invoicing

通过这种理解,我们可以进一步探讨如何在实现元编程时使用 id 函数。

使用 id 调试无意中的赋值

在本节中,我们将讨论当我们定义属性、方法或函数时对对象进行无意引用或赋值会发生什么,以及如何使用内置的 id 函数解决此类错误赋值。当无意中创建引用时,实际对象和引用对象的内存地址是共享的。在本例中,我们将使用 id 来调试前面章节中创建的 Python 对象,并识别在开发应用程序时可能无意中创建的对象的重复赋值或引用。以下是它是如何工作的:

  1. 首先,让我们创建一个字典项,class_id_count,以捕获每个类内存地址出现的次数:

    class_id_count = {}
    
  2. 在下一步中,我们将创建以下四个列表:

    duplicates = []
    ids = []
    classes = []
    classnames = []
    

在这里,我们使用 duplicates 捕获重复的内存地址,使用 ids 捕获 id 函数的结果,使用 classes 捕获类详细信息,使用 classnames 捕获类的名称。

  1. 在此步骤中,我们将遍历 Python 对象的目录,并检查对象的类型是否为 type,因为在 Python 中类的类型是 type。这一步骤有助于识别所有类对象,然后使用 idsclassesclassnames 创建的列表进行更新。请参考以下代码块:

    for obj in dir():
        if type(eval(obj)) == type:
            ids.append(id(eval(obj)))
            classes.append(eval(obj))
            classnames.append(obj)
    
  2. 现在,我们将遍历 ids 列表,检查 id 是否不在 class_id_count 中,然后添加它;如果它已经在 class_id_count 中,我们将将其添加到 duplicates 列表中:

    for i in ids:
        if i not in class_id_count:
            class_id_count[i] = 1
        elif (class_id_count[i] == 1):
            duplicates.append(i)
            class_id_count[i] += 1
    
  3. 我们将进一步遍历 classesclassnames 列表,检查是否存在重复项。然后,我们将打印出具有重复项的类:

    for cls,clsname in zip(classes,classnames):
        for clsid in duplicates:
            if (id(cls)==clsid):
                print(clsname,cls)
    

前面代码的输出如下:

Billing <class '__main__.Billing'>
Invoicing <class '__main__.Billing'>
  1. 执行前面的代码会产生以下输出:

    class_id_count
    {2196689735984: 2}
    duplicates
    [2196689735984]
    ids
    [2196689735984, 2196689735984]
    classes
    [__main__.Billing, __main__.Billing]
    classnames
    ['Billing', 'Invoicing']
    

在前面的输出中,我们可以看到 BillingInvoicing 两个类具有相同的内存地址,它们是重复的。可能存在我们有意引用一个类的情况,也可能存在由于错误地将多个变量分配到同一内存地址而导致的场景。在这种情况下,可以使用 id 来检查对内存地址的重复分配。

下图是本例的简单表示:

图 5.2 – 具有单个内存地址的两个类

图 5.2 – 具有单个内存地址的两个类

通过这种理解,我们将进一步探讨另一个内置函数,callable

查找对象是否可调用

在本节中,我们将探讨另一个内置函数,名为 callable。正如其名所示,此函数有助于识别 Python 对象是否可被调用。函数和方法可以被调用以执行对输入参数的各种操作。并非所有 Python 对象都是可调用的。例如,字符串变量或数值变量存储信息,但在执行时不会执行任何操作。callable 函数有助于验证哪些对象可以被调用,哪些不能在函数中被调用。

为什么我们需要检查一个对象是否可调用?Python 是一种面向对象的编程语言,在其中我们可以在库中编写类,这些类被封装起来。类的最终用户或库的用户不一定总是需要访问类定义或方法定义。在导入 Python 库时,我们有时可能想知道导入的对象只是一个存储值的变量,还是一个可以被重用的函数。检查这一点最简单的方法是看对象是否可调用,因为函数或方法通常是可调用的。这在某些情况下非常有用,尤其是当库的开发者没有为其方法和属性提供任何文档时。

让我们以下面的例子来使用callable

  1. 让我们创建一个新的 Python 文件,并将其保存为product.py。转到github.com/PacktPublishing/Metaprogramming-with-Python/blob/main/Chapter05/product.py并添加以下代码,该代码创建一个名为Product的类。向其中添加以下四个属性:Product ID(产品 ID)、Product Name(产品名称)、Product Category(产品类别)和Unit Price(单价)。我们现在将为这四个属性分配值,如下所示:

    class Product:
        _product_id = 100902
        _product_name = 'Iphone X'
        _product_category = 'Electronics'
        _unit_price = 700
    
  2. 现在,让我们在Product类中添加一个名为get_product的方法。这个方法将简单地返回前面步骤中创建的四个属性:

        def get_product(self):
            return self._product_id, self._product_name, 
                 self._product_category, self._unit_price
    
  3. 在这个步骤中,我们将从product.py导入Product类并为其创建一个对象:

    import product
    prodobj = product.Product()
    
  4. 让我们现在使用内置的callable函数来检查类是否可调用。该类是可调用的,因此函数返回True

    callable(product.Product)
    True
    
  5. 在这个步骤中,我们还可以检查类对象是否可调用。该对象不可调用,因为我们没有重写类的__call__方法使其可调用,因此函数返回False

    callable(prodobj)
    False
    
  6. 我们现在可以检查一个 Python 对象是否可调用,然后获取其属性:

    if callable(prodobj.get_product):
        print(prodobj.get_product())
    else:
        print("This object is not callable")
    
  7. 同样,我们也可以检查一个 Python 对象是否可调用,如果返回True,则打印该对象详情:

    if callable(prodobj):
        print(prodobj)
    else:
        print('This is not a method')
    

通过这个例子,我们可以进一步了解下一个函数,hasattr

检查对象是否有属性

当通过将库导入另一个程序中来使用框架或库中定义的方法或函数对象时,我们可能并不总是知道对象的所有属性。在这种情况下,我们有一个内置的hasattr函数,可以用来检查 Python 对象是否有特定的属性。

这个函数检查给定对象是否有属性。为了测试这个函数,我们将为ABC Megamart的库存创建一个类,为库存中存储的产品添加所需的属性,包括产品的价格和税费成分。库存中的产品的价格将在税前和税后计算。以下是步骤:

  1. 我们将创建一个名为Inventory的类,并用库存所需的变量来初始化它,例如product_id(产品 ID)、product_name(产品名称)、date(购买日期)、unit_price(单价)、quantity(数量)、unit_discount(折扣)和tax(税),如下面的代码所示:

    class Inventory:
        def __init__(self,product_id,product_name,date,unit_price,quantity,unit_discount,tax):  
            self.product_id = product_id
            self.product_name = product_name
            self.date = date
            self.unit_price = unit_price
            self.quantity = quantity
            self.unit_discount = unit_discount
            self.tax = tax
    
  2. 在这一步中,我们将向Inventory类添加一个方法来计算不含税金额,在这个方法中,我们将有三个输入参数:quantity(数量)、unit_price(单价)和unit_discount(折扣)。如果这三个变量都是None,则此方法将使用在Inventory类实例化期间初始化的相同变量来计算不含税金额:

    def calc_amount_before_tax(self,quantity=None,unit_price=None, unit_discount=None):
            if quantity is None:
                self.quantity = self.quantity
            else:
                self.quantity = quantity
    
            if unit_price is None:
                self.unit_price = self.unit_price
            else:
                self.unit_price = unit_price
    
            if unit_discount is None:
                self.unit_discount = self.unit_discount
            else:
                self.unit_discount = unit_discount
            amount_before_tax = self.quantity * 
               (self.unit_price - self.unit_discount)
            return amount_before_tax
    
  3. 我们还将为Inventory类添加另一个方法来计算含税金额。此方法与calc_amount_before_tax定义的格式类似:

    def calc_amount_after_tax(self, quantity=None,unit_price=None,unit_discount=None,tax=None):
            if quantity is None:
                self.quantity = self.quantity
            else:
                self.quantity = quantity
    
            if unit_price is None:
                self.unit_price = self.unit_price
            else:
                self.unit_price = unit_price
    
            if unit_discount is None:
                self.unit_discount = self.unit_discount
            else:
                self.unit_discount = unit_discount
    
            if tax is None:
                self.tax = self.tax
            else:
                self.tax = tax
            amount_after_tax = 
                self.calc_amount_before_tax(
                self.quantity,self.unit_price,
                self.unit_discount) + self.tax
            return amount_after_tax
    
  4. 我们现在将为这个类创建最后一个方法,该方法返回合并的库存详情,创建一个 DataFrame,并返回该 DataFrame:

        def return_inventory(self):
            import pandas as pd
            inventory_schema = pd.DataFrame([
                               self.product_id,
                               self.product_name,
                               self.date,
                               self.unit_price,
                               self.quantity,
                               self.unit_discount,
                               self.tax,
                               self.calc_unt_before_tax(),
                self.calc_amount_after_tax()]).transpose()
            inventory_schema.columns = ["Product_id",
                "Product_name","Date","Unit_price",
                "Quantity","Unit_discount","Tax",
                "Amount Before Tax", "Amount After Tax"]
            return inventory_schema    
    
  5. 然后,为Inventory类创建一个对象并初始化其属性:

    inventory = Inventory(300021,
                    'Samsung-Refrigerator',
                    '08/04/2021',
                    200,
                    25,
                    10,
                    0.0522)
    
  6. 检查对象是否返回属性:

    inventory.product_id
    300021
    inventory.product_name
    'Samsung-Refrigerator'
    inventory.date
    '08/04/2021'
    inventory.unit_price
    200
    inventory.quantity
    25
    inventory.unit_discount
    10
    inventory.tax
    0.0522
    inventory.calc_amount_before_tax()
    4750
    inventory.calc_amount_after_tax()
    4750.0522
    inventory.return_inventory()
    

前面代码的输出如下:

图 5.3 – 输出 – 库存详情

图 5.3 – 输出 – 库存详情

  1. 接下来,让我们使用dir列出Inventory类中所有参数的名称:

    dir(Inventory)
    ['__class__',
     '__delattr__',
     '__dict__',
     ‚__dir__',
     ‚__doc__',
     ‚__eq__',
     ‚__format__',
     ‚__ge__',
     ‚__getattribute__',
     ‚__gt__',
     ‚__hash__',
     ‚__init__',
     ‚__init_subclass__',
     ‚__le__',
     ‚__lt__',
     ‚__module__',
     ‚__ne__',
     ‚__new__',
     ‚__reduce__',
     ‚__reduce_ex__',
     ‚__repr__',
     ‚__setattr__',
     ‚__sizeof__',
     ‚__str__',
     ‚__subclasshook__',
     ‚__weakref__',
     ‚calc_amount_after_tax',
     ‚calc_amount_before_tax',
     ‚return_inventory']
    
  2. 现在,让我们使用hasattr来检查类是否有属性。如果属性的类型是方法,则使用getattr来获取属性。执行以下循环将得到Inventory的所有属性列表:

    for i in dir(Inventory):
         if (hasattr(Inventory,i)):
                if type(getattr(inventory, i)) is type(getattr(inventory,  '__init__')):
                    print(getattr(Inventory,i))<class 'type'>
    <function Inventory.__init__ at 0x000001C9BBB46CA0>
    <function Inventory.calc_amount_after_tax at 0x000001C9BBB46DC0>
    <function Inventory.calc_amount_before_tax at 0x000001C9BBB46D30>
    <function Inventory.return_inventory at 0x000001C9BBB46E50>
    

通过这种理解,我们可以进一步了解另一个内置函数,isinstance

检查对象是否是实例

在本节中,我们将探讨另一个名为isinstance的函数,它可以用来检查一个对象是否是特定类的实例。由于我们在本章中讨论的是内省,我们更关注可用于内省对象的函数,而不是如何进一步使用这些函数来操作或调试代码。第六章将涵盖这些函数在元编程中的使用,并附带示例。

在前面的章节中,我们创建了一个名为Inventory的类。在本节中,我们可以继续使用相同的类并为该类创建另一个对象。如下所示:

inventory_fmcg = Inventory(100011,
                'Delmonte Ketchup',
                '09/04/2021',
                5,
                0.25,
                0.10,
                0.0522)
inventory_fmcg.product_id
100011
inventory_fmcg.calc_amount_before_tax()
1.225
inventory_fmcg.calc_amount_after_tax()
1.2772000000000001
inventory_fmcg.return_inventory()

前面代码的输出如下:

图 5.4 – 输出 – 的库存详情

图 5.4 – 输出 – inventory_fmcg的库存详情

现在,让我们使用isinstance检查inventory_fmcg是否是Inventory类的对象:

isinstance(inventory_fmcg,Inventory)
True

类似地,我们也可以检查之前创建的inventory对象是否仍然是Inventory类的实例:

isinstance(inventory,Inventory)
True

让我们考虑一个场景,在编写代码时错误地将对象库存重新分配给另一个值,我们可能仍然需要使用该对象并调用其方法来返回库存详情。为了使用isinstance测试这个场景,我们可以查看以下步骤:

  1. 检查一个对象是否是Inventory类的实例,并调用该函数的方法。如果对象不是类的实例,检查它被重新分配到的变量类型:

    if isinstance(inventory,Inventory):
        display(inventory.return_inventory())
    else:
        print("Object reallocated to",  type(inventory), 
              ", please correct it")
    
  2. 由于inventory仍然是Inventory类的对象,前面的代码会产生以下输出:

图 5.5 – 输出 – 库存详情

图 5.5 – 输出 – 库存详情

  1. 现在,让我们将inventory变量重新分配给某个其他的字符串值,并在其上调用return_inventory方法:

    inventory = "test"
    
  2. 调用inventory对象的return_inventory方法将产生以下错误:

图 5.6 – 在重新分配的对象上调用 return_inventory 方法时的错误

图 5.6 – 在重新分配的对象上调用 return_inventory 方法时的错误

  1. 为了避免前面的错误,并让代码优雅地处理这个错误,同时向开发者提供更多信息,我们可以使用isinstance方法修改代码如下:

    if isinstance(inventory,Inventory):
        print(inventory.return_inventory())
    else:
        print("Object reallocated to",  type(inventory), 
              ", please correct it")
    

上述代码的输出如下:

Object reallocated to <class 'str'> , please correct it

通过这种理解,我们可以进一步了解另一个内置函数issubclass

检查一个对象是否是子类

在本节中,我们将查看issubclass函数。此函数用于检查给定的输入类是否实际上是特定父类的子类或子类。要使用此函数进行类内省,让我们查看以下步骤:

  1. 通过初始化供应商信息变量(如supplier_namesupplier_codesupplier_addresssupplier_contract_start_datesupplier_contract_end_datesupplier_quality_code)创建一个FMCG类,如下所示:

    class FMCG:
        def __init__(self,supplier_name,supplier_code,
           supplier_address,supplier_contract_start_date,\
        supplier_contract_end_date,supplier_quality_code):
            self.supplier_name = supplier_name
            self.supplier_code = supplier_code
            self.supplier_address = supplier_address
            self.supplier_contract_start_date = 
                 supplier_contract_start_date
            self.supplier_contract_end_date = 
                 supplier_contract_end_date
            self.supplier_quality_code = 
                 supplier_quality_code
    
  2. 在类中添加一个方法,简单地获取类中初始化的供应商详情,并将其作为包含键和值的字典对象返回:

        def get_supplier_details(self):
            supplier_details = {
               'Supplier_name': self.supplier_name, 
                'Supplier_code': self.supplier_code,
                'Supplier_address': self.supplier_address,
                'ContractStartDate': 
                        self.supplier_contract_start_date,
                'ContractEndDate': 
                          self.supplier_contract_end_date, 
                'QualityCode': self.supplier_quality_code
            }
            return supplier_details
    
  3. 创建一个FMCG类的对象,用供应商数据初始化变量,然后通过调用前面的方法显示供应商详情:

    fmcg = FMCG('Test Supplier','a0015','5093 9th Main Street, Pasadena,California, 91001', '05/04/2020', '05/04/2025',1)
    fmcg.get_supplier_details()
    {'Supplier_name': 'Test Supplier',
     'Supplier_code': 'a0015',
     'Supplier_address': '5093 9th Main Street, 
        Pasadena,California, 91001',
     'ContractStartDate': '05/04/2020',
     'ContractEndDate': '05/04/2025',
     'QualityCode': 1}
    
  4. 在这里,我们可以创建另一个用于香料的类,该类通过从FMCG类和Inventory类继承来覆盖库存详情和 FMCG 供应商详情。这个类将初始化所有产品级别的库存变量和供应商级别的变量:

    class Condiments(FMCG,Inventory):
        def __init__(self,*inventory):
            self.product_id = inventory[0]
            self.product_name = inventory[1]
            self.date = inventory[2]
            self.unit_price = inventory[3]
            self.quantity = inventory[4]
            self.unit_discount = inventory[5]
            self.tax = inventory[6]
            self.supplier_name = inventory[7]
            self.supplier_code = inventory[8]
            self.supplier_address = inventory[9]
            self.supplier_contract_start_date = 
                                    inventory[10]
            self.supplier_contract_end_date = 
                                    inventory[11]
            self.supplier_quality_code = inventory[12]
    
  5. 然后,让我们添加一个方法,简单地返回在Condiments类中初始化的所有变量,通过将它们存储为 DataFrame 或表格:

        def return_condiment_inventory(self):
            import pandas as pd
            inventory_schema = pd.DataFrame([
                            self.product_id,
                            self.date,
                            self.unit_price,
                            self.quantity,
                            self.unit_discount,
                            self.tax,
                            self.calc_amount_before_tax(),
                            self.calc_amount_after_tax(),
                            self.get_supplier_details()
                                           ]).transpose()
            inventory_schema.columns = ["Product_id",
                "Date","Unit_price","Quantity",
                "Unit_discount","Tax","Amount Before Tax", 
                "Amount After Tax",'Supplier_details']
            return inventory_schema          
    
  6. 我们现在可以创建这个类的对象并调用其方法:

    ketchup = Condiments(100011,'Delmonte Ketchup','09/04/2021',5,0.25,0.10,0.0522,'Test Supplier','a0015','5093 9th Main Street, Pasadena,California, 91001', '05/04/2020', '05/04/2025',1)
    ketchup.return_condiment_inventory()
    
  7. 执行前面的代码会产生以下输出:

图 5.7 – 输出 – 调味品库存详情

图 5.7 – 输出 – 调味品库存详情

  1. 现在我们检查 FMCG 类是否是 Inventory 类的子类。它将返回 False,因为 FMCG 不是 Inventory 的子类:

    issubclass(FMCG,Inventory)
    False
    
  2. 在这一步中,我们将检查 Condiments 是否是 FMCG 的子类,以及它是否是 Inventory 的子类。两者都应该返回 True,因为 Condiments 从这两个类中继承而来:

    issubclass(Condiments,FMCG)
    True
    issubclass(Condiments,Inventory)
    True
    
  3. 接下来,我们将通过首先检查一个类是否是特定父类的子类,然后相应地创建一个对象,最后在新建的对象上调用一个方法来创建一个类的对象:

    if issubclass(Condiments,FMCG):
        fmcg = Condiments(100011,'Delmonte 
          Ketchup','09/04/2021',5,0.25,0.10,0.0522,
          'Test Supplier','a0015','5093 9th Main Street, 
          Pasadena,California, 91001', '05/04/2020', 
          '05/04/2025',1)
    else:
        fmcg = FMCG('Test Supplier','a0015','5093 9th Main
          Street, Pasadena,California, 91001', 
          '05/04/2020', '05/04/2025',1)
    display(fmcg.get_supplier_details())
    
  4. 执行前面的代码会产生以下输出:

    {'Supplier_name': 'Test Supplier',
     'Supplier_code': 'a0015',
     'Supplier_address': '5093 9th Main Street, 
        Pasadena,California, 91001',
     'ContractStartDate': '05/04/2020',
     'ContractEndDate': '05/04/2025',
     'QualityCode': 1}
    

通过这个理解,我们可以进一步探讨本章的最后一个主题。

理解 property 的用法

在本节中,我们将查看本章最后介绍的最后一个内置函数,即 property。这个函数用于在 Python 中初始化、设置、获取或删除属性的属性。这些值被称为对象的属性。让我们首先通过创建一个示例来理解 property 在 Python 对象上的工作方式。

我们可以通过简单地调用 property 函数并将其存储为变量来创建一个属性。参考以下代码:

test_property = property()
test_property
<property at 0x1c9c9335950>

我们仍未回答这个函数是如何创建属性的问题。property 函数接收四个变量以获取、设置、删除和记录属性的属性。为了进一步检查它,让我们更详细地看看它。步骤如下:

  1. 创建一个名为 TestPropertyClass 的类。

  2. 使用 test 属性初始化它,并将其设置为 None

  3. 我们将添加三个方法来执行初始化 test 属性的获取、设置和删除功能。

  4. 然后,我们将在类内部创建另一个名为 test_attr 的变量,并将 property 函数分配给它,使用在这个类中创建的 getsetdelete 方法。

这个示例的代码如下:

class TestPropertyClass:
    def __init__(self):
        self._test_attr = None
    def get_test_attr(self):
        print("get test_attr")
        return self._test_attr
    def set_test_attr(self, value):
        print("set test_attr")
        self._test_attr = value
    def del_test_attr(self):
        print("del test_attr")
        del self._test_attr
    test_attr = property(get_test_attr, set_test_attr, 
        del_test_attr, "test_attr is a property")

在前面的代码中,get_test_attr 简单地返回 test 属性,set_test_attr 将值设置到 test 属性,而 del_test_attr 删除 test 属性。

现在我们创建这个类的对象,并检查 property 在其上的工作方式:

test_property_object = TestPropertyClass()
test_property_object.test_attr
get test_attr

在前面的代码中,调用 test 属性,反过来,调用了 get_test_attr 方法,因为它被作为 get 方法提供给 property 函数。让我们进一步确认这一理解,通过设置 test_attr 的值:

test_property_object.test_attr = 1980
set test_attr

将值赋给 test_attr 变量现在调用了 set_test_attr 方法,因为它被作为 set 方法提供给 property 函数。再次调用 test_attr 属性将返回前面步骤中设置的值:

test_property_object.test_attr
get test_attr
1980

同样,删除属性,反过来,会调用 del_test_attr 方法,因为它被作为 delete 方法提供给 property 函数:

del test_property_object.test_attr
del test_attr

一旦属性被删除,在调用属性时get方法仍然会被调用,但由于它已被删除,因此不会返回之前分配的值:

test_property_object.test_attr

前面代码的输出现在将如下所示:

图 5.8 – 在已删除属性上调用 get 方法

图 5.8 – 在已删除属性上调用 get 方法

通过修改gettersetterdeleter方法的行为,我们可以修改属性本身的属性。我们将在第六章中详细探讨这个说法。

通过理解将property函数分配给变量然后调用其gettersetterdeleter方法,我们将进一步探讨实现property的另一种变体。

使用 property 作为装饰器

在前面的部分,我们探讨了如何使用property函数来修改类中属性的属性。在本节中,我们将探讨如何使用property作为装饰器。让我们考虑与前面示例相同的TestPropertyClass,并将类定义修改为使用@property装饰器语句而不是property()函数语句。参考以下代码:

class TestPropertyClass:
    def __init__(self):
        self._test_attr = None
    @property
    def test_attr(self):
        return self.test_attr
    @test_attr.getter
    def test_attr(self):
        print("get test_attr")
        return self._test_attr
    @test_attr.setter
    def test_attr(self, value):
        print("set test_attr")
        self._test_attr = value
    @test_attr.deleter
    def test_attr(self):
        print("del test_attr")
        del self._test_attr

在前面的代码中,我们为test_attr添加了@property作为装饰器,并且我们也为set方法添加了@test_attr.setter,为get方法添加了@test_attr.getter,为delete方法添加了@test_attr.deleter

让我们继续执行代码以检查gettersetterdeleter是否按预期工作:

test_property_object = TestPropertyClass()
test_property_object.test_attr
get test_attr

在前面的代码中,调用属性调用了getter方法。同样,setterdeleter也分别调用了setdelete方法:

test_property_object.test_attr = 1982
set test_attr
test_property_object.test_attr
get test_attr
1982
del test_property_object.test_attr
del test_attr

这些是一些如何使用 Python 内置函数将内省应用于 Python 对象的例子。

摘要

在本章中,我们学习了如何使用内置函数内省 Python 对象。

我们然后看到了如何使用id函数,以及如何使用id进行代码调试。我们还探讨了如何检查一个对象是否可调用,如何检查一个对象是否有属性,如何检查一个对象是否是实例,如何检查一个对象是否是子类,最后,我们探讨了如何在属性上获取、设置和删除属性。从所有这些概念中,我们学习了如何检查 Python 对象,如类、方法和函数。从每个主题下的示例中,我们也学习了如何在实际用例中应用内省。

在下一章中,我们将扩展从内省中学到的知识,并将其进一步应用于理解 Python 对象的反射。

第六章:第六章:在 Python 对象上实现反射

在本章中,我们将探讨 Python 3 中的反射,并了解它在元编程中的有用性。反射是内省的延续,或者可以将其视为一个概念,我们可以利用从 Python 对象的属性或属性的内省中学习到的信息来操纵对象,从而进行元编程。

为什么需要反射?正如我们从上一章所知,内省是 Python 对象的收集信息过程。反射是通过内省从对象中获取信息的过程,然后通过外部操作它们来执行通用操作,从而进行元编程。

在本章中,我们将查看使用每个在上一章帮助我们内省对象的函数来实现反射,并在我们的程序中使用的对象上进行元编程。

在本章中,我们将探讨以下主要主题:

  • 介绍在反射中使用的内置函数

  • 使用 id 删除重复项

  • 使用 callable 动态检查和生成方法

  • 使用 hasattr 设置值

  • 使用 isinstance 修改一个对象

  • 使用 issubclass 修改一个类

  • 在优惠券上应用 property

到本章结束时,你应该能够应用内置函数来反射 Python 对象,将它们应用于示例,并使用它们来生成或修改代码。

技术要求

本章中分享的代码示例可在 GitHub 上找到:github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter06.

介绍在反射中使用的内置函数

为了理解反射以及使用 Python 的内置函数进行反射的使用,我们将继续在本章中使用我们的核心示例 ABC Megamart。我们将具体探讨本章中基于零售店优惠券的概念和示例。优惠券是零售店或制造商用来在消费者中推广其产品的技术。优惠券通过各种广告方式生成和发布,并用于吸引顾客到特定的商店或产品。

我们将使用与内省相同的内置函数集,以对 Python 对象进行反射:

  • id()

  • eval()

  • callable()

  • hastattr()

  • getattr()

  • isinstance()

  • issubclass()

  • property()

对 Python 对象的反射有助于使用内置函数进行元编程,这些函数可以内省 Python 对象,我们将在本章中查看一些示例。

使用 id 删除重复项

我们在上一章中回顾了 id 函数,它涵盖了内省。在 Python 3 中,id 用于通过对象的内存地址来识别对象。识别对象的 id 可以用来反思对象,并避免在使用对象时可能发生的冗余或错误。

为了进一步理解这一点,我们将创建一个名为 Coupon 的类,该类生成唯一的随机优惠券 ID,并打印出任何作为输入提供的产品优惠券。在下面的代码中,我们将从创建一个名为 Coupon 的类开始,并将添加优惠券详情作为属性。我们还将创建一个名为 generate_coupon 的方法,用于打印产品的五个优惠券及其唯一的随机优惠券 ID:

class Coupon:
    def __init__(self, product_name, product_category, \
      brand,source, expiry_date, quantity):
        self.product_name = product_name
        self.product_category = product_category
        self.brand = brand
        self.source = source
        self.expiry_date = expiry_date
        self.quantity = quantity
   def generate_coupon(self):
        import random
        couponId =  random.sample(range(1,9),5)
        for i in couponId:
            print('***********------------------**************')
            print('Product:', self.product_name)
            print('Product Category:', \
              self.product_category)
            print('Coupon ID:', i)
            print('Brand:', self.brand)
            print('Source:', self.source)
            print('Expiry Date:', self.expiry_date)
            print('Quantity:', self.quantity)
            print('***********------------------**************')

现在让我们创建一个 Coupon1 变量并将 Coupon 类分配给它:

Coupon1 = Coupon

在这种情况下,我们有意将 Coupon 类分配给一个变量以演示 id 函数的使用。理想情况下,这个函数将非常有用,用于调试并确保类实际上被无意中分配,从而在代码的后续部分导致问题。到目前为止,让我们假设将 Coupon 类分配给变量的操作是无意的。

让我们看看如何识别和解决这种无意中的类分配,并回顾如果 Coupon 是唯一应该可用于生成优惠券的类时,当调用 Coupon1 时优惠券生成结果。

在前面的 Coupon 类中,预期只为具有唯一随机优惠券标识符的产品生成五个优惠券。由于我们已经将类分配给了一个变量,类标识符也被分配给了该变量:

id(Coupon)
2175775727280
id(Coupon1)
2175775727280

现在让我们调用 Coupon 类的 generate_coupon 方法及其属性,并查看结果:

Coupon("Potato Chips","Snacks","ABCBrand1","Manhattan Store","10/1/2021",2).generateCoupon()

优惠券 1 的输出如下:

***********------------------**************
Product: Potato Chips
Product Category: Snacks
Coupon ID: 5
Brand: ABCBrand1
Source: Manhattan Store
Expiry Date: 10/1/2021
Quantity: 2
***********------------------**************

优惠券 2 的输出如下:

***********------------------**************
Product: Potato Chips
Product Category: Snacks
Coupon ID: 1
Brand: ABCBrand1
Source: Manhattan Store
Expiry Date: 10/1/2021
Quantity: 2
***********------------------**************

优惠券 3 的输出如下:

***********------------------**************
Product: Potato Chips
Product Category: Snacks
Coupon ID: 4
Brand: ABCBrand1
Source: Manhattan Store
Expiry Date: 10/1/2021
Quantity: 2
***********------------------**************

优惠券 4 的输出如下:

***********------------------**************
Product: Potato Chips
Product Category: Snacks
Coupon ID: 8
Brand: ABCBrand1
Source: Manhattan Store
Expiry Date: 10/1/2021
Quantity: 2
***********------------------**************

优惠券 5 的输出如下:

***********------------------**************
Product: Potato Chips
Product Category: Snacks
Coupon ID: 3
Brand: ABCBrand1
Source: Manhattan Store
Expiry Date: 10/1/2021
Quantity: 2
***********------------------**************

调用前面的方法生成了薯片产品的五个独特优惠券。优惠券标识符是唯一的,未来代码的任何其他部分都不应重新生成;因此,前面的方法在代码中只调用一次。由于我们已经将 Coupon 类分配给另一个名为 Coupon1 的变量,让我们看看如果无意中在代码的其他部分调用 Coupon1 会发生什么:

Coupon1("Potato Chips","Snacks","ABCBrand1","Manhattan Store","10/1/2021",2).generate_coupon()

优惠券 1 的输出如下:

***********------------------**************
Product: Potato Chips
Product Category: Snacks
Coupon ID: 7
Brand: ABCBrand1
Source: Manhattan Store
Expiry Date: 10/1/2021
Quantity: 2
***********------------------**************

优惠券 2 的输出如下:

***********------------------**************
Product: Potato Chips
Product Category: Snacks
Coupon ID: 8
Brand: ABCBrand1
Source: Manhattan Store
Expiry Date: 10/1/2021
Quantity: 2
***********------------------**************

优惠券 3 的输出如下:

***********------------------**************
Product: Potato Chips
Product Category: Snacks
Coupon ID: 1
Brand: ABCBrand1
Source: Manhattan Store
Expiry Date: 10/1/2021
Quantity: 2
***********------------------**************

优惠券 4 的输出如下:

***********------------------**************
Product: Potato Chips
Product Category: Snacks
Coupon ID: 6
Brand: ABCBrand1
Source: Manhattan Store
Expiry Date: 10/1/2021
Quantity: 2
***********------------------**************

优惠券 5 的输出如下:

***********------------------**************
Product: Potato Chips
Product Category: Snacks
Coupon ID: 2
Brand: ABCBrand1
Source: Manhattan Store
Expiry Date: 10/1/2021
Quantity: 2
***********------------------**************

在本例中,代码中不应调用Coupon1,因为调用它将生成重复的优惠券,可能具有相同的 ID。这可能导致为同一产品创建五个额外的优惠券,这是不必要的;在这些优惠券中,有两个将是重复的,具有优惠券标识符18。这导致在分发给消费者时使优惠券18失效,因为每个优惠券都应有一个唯一的标识符以供兑换。

现在我们来看看如何通过开发一个名为delete_duplicates的函数来解决这个问题,该函数检查并删除此类重复分配。该函数查看目录中具有重复项的 Python 对象列表,并删除类的重复项。请参考以下代码:

def delete_duplicates(directory = dir()):
    class_id_count = {}
    duplicates = []
    ids = []
    classes = []
    classnames = []
    for obj in directory:
        if type(eval(obj)) == type:
            ids.append(id(eval(obj)))
            classes.append(eval(obj))
            classnames.append(obj)
    for i in ids:
        if i not in class_id_count:
            class_id_count[i] = 1
        elif (class_id_count[i] == 1):
            duplicates.append(i)
            class_id_count[i] += 1
    dupe_set = {}
    for cls,clsname in zip(classes,classnames):
        for clsid in duplicates:
            if (id(cls)==clsid):
                print(clsname,cls)
                dupe_set[clsname] = \
                  str(cls).split('.')[1].rstrip("'>'")
    for key,value in dupe_set.items():
        if (key!=value):
            del globals()[key]

上述代码中的前三个for循环在上一章的使用 id 调试意外分配部分已有讨论,该部分涵盖了内省。最后一个for循环检查名为dupe_set的字典中是否存在重复项,并且只删除重复变量,而不是实际的类。

调用前面的函数会导致删除重复的Coupon1变量:

delete_duplicates(directory = dir())
Coupon <class '__main__.Coupon'>
Coupon1 <class '__main__.Coupon'>
Coupon
__main__.Coupon
Coupon1

检查Coupon1是否仍然存在会导致以下错误:

图 6.1 – 删除后调用错误

图 6.1 – 删除Coupon1后调用错误

前面的错误确认了重复变量已被delete_duplicates函数删除。在下一节中,我们将探讨使用名为callable的函数应用反射。

使用callable动态检查和生成方法

我们现在将探讨另一个熟悉的功能callable,看看它如何被用来对一个对象进行反射。

注意

在 Python 中,类、方法或函数是可调用的,而类对象或变量不是可调用的。

在本例中,我们将检查一个类是否可调用,如果返回true,我们将动态地向该类添加一个方法。为了测试callable函数的用法,我们将首先创建一个优惠券类,并将其命名为SimpleCoupon

class SimpleCoupon:
    pass

让我们检查前面的类是否可调用:

callable(SimpleCoupon)
True

在以下代码中,我们将创建一个函数,如果被调用,将生成另一个函数。我们将创建一个create_coupon函数,当被调用时,它将创建一个generate_coupon函数或方法:

def create_coupon( product, product_category, brand, source, expiry_date, quantity:
    def generate_coupon(product, product_category, brand, \
      source, expiry_date, quantity):
        import random
        couponId =  random.sample(range(100000000000,900000000000),3)
        for i in couponId:
            print(\
             '***********------------------**************')
            print('Product:',product)
            print('Product Category:', product_category)
            print('Coupon ID:', i)
            print('Brand:', brand)
            print('Source:',  source)
            print('Expiry Date:',  expiry_date)
            print('Quantity:',  quantity)
            print(\
             '***********------------------**************')
    return generate_coupon

让我们现在检查SimpleCoupon类是否可调用,如果它是可调用的,我们将添加一个coupon_details变量,以提供所有必要的输入参数来初始化调用:

if callable(SimpleCoupon):
    SimpleCoupon.coupon_details = {
                          "product": "Honey Mustard Sauce",
                          "product_category": "Condiments",
                          "brand": "ABCBrand3",
                          "source": "Pasadena Store",
                          "expiry_date": "10/1/2021",
                          "quantity": 2}

在下一步中,让我们检查如何动态创建一个方法并将其添加到类中,如果该类是可调用的。为了将generate_coupon方法添加到SimpleCoupon类中,让我们调用create_coupon函数:

if callable(SimpleCoupon):
    SimpleCoupon.generate_coupon = create_coupon(SimpleCoupon.coupon_details['product'], SimpleCoupon.coupon_details['product_category'],                                                SimpleCoupon.coupon_details['brand'], SimpleCoupon.coupon_details['source'],                                                SimpleCoupon.coupon_details['expiry_date'],SimpleCoupon.coupon_details['quantity'])

在添加了generate_coupon方法之后,我们可以按照以下方式运行该方法并检查结果:

SimpleCoupon.generate_coupon(SimpleCoupon.coupon_details['product'], SimpleCoupon.coupon_details['product_category'],                              SimpleCoupon.coupon_details['brand'], SimpleCoupon.coupon_details['source'],                             SimpleCoupon.coupon_details['expiry_date'], SimpleCoupon.coupon_details['quantity'])

优惠券 1 的输出如下:

***********------------------**************
Product: Honey Mustard Sauce
Product Category: Condiments
Coupon ID: 579494488135
Brand: ABCBrand3
Source: Pasadena Store
Expiry Date: 10/1/2021
Quantity: 2
***********------------------**************

优惠券 2 的输出如下:

***********------------------**************
Product: Honey Mustard Sauce
Product Category: Condiments
Coupon ID: 657317674875
Brand: ABCBrand3
Source: Pasadena Store
Expiry Date: 10/1/2021
Quantity: 2
***********------------------**************

优惠券 3 的输出如下:

***********------------------**************
Product: Honey Mustard Sauce
Product Category: Condiments
Coupon ID: 689256610872
Brand: ABCBrand3
Source: Pasadena Store
Expiry Date: 10/1/2021
Quantity: 2
***********------------------**************

在本节中,我们探讨了如何使用callable来修改一个类,并通过外部验证类是否可调用来向类添加属性和方法。我们已经成功验证了SimpleCoupon类是否可调用的,并且我们还向该类添加了一个coupon_details列表和一个generate_coupon方法。这解释了callable作为处理 Python 对象反射的内置函数的使用。

通过这种理解,我们将探讨hasattr函数如何帮助在 Python 对象上应用反射。

使用hasattr设置值

现在,我们将深入研究hasattr函数,该函数可以用来检查 Python 对象是否具有属性。使用这个函数作为条件来测试对象,我们可以对外部应用反射。

在这个例子中,我们将通过使用hasattr函数更改其中一个变量来创建自定义优惠券。本章中的类和方法用于通过相关示例来理解反射。现在我们将创建另一个名为CustomCoupon的类。我们将在类内部添加并定义这个类的属性,并且我们将添加一个生成优惠券的方法:

class CustomCoupon:
    product_name = "Honey Mustard Sauce"
    product_category = "Condiments"
    brand = "ABCBrand3"
    source = "Store"
    expiry_date = "10/1/2021"
    quantity = 10
    manufacturer = None
    store = None
    def generate_coupon(self):
        import random
        couponId =  random.sample(
          range(100000000000,900000000000),1)
        for i in couponId:
            print('***********------------------**************')
            print('Product:', self.product_name)
            print('Product Category:', 
              self.product_category)
            print('Coupon ID:', i)
            print('Brand:', self.brand)
            print('Source:', self.source)
            print('Expiry Date:', self.expiry_date)
            print('Quantity:', self.quantity)
            if(self.manufacturer is not None):
                print('Manufacturer:', self.manufacturer)
            elif(self.store is not None):
                print('Store:', self.store)
            print('***********------------------**************')

看一下前面类中的三个属性——sourcemanufacturerstore。如果我们想从类外部改变这些属性的行为,我们可以通过首先检查类是否具有这些属性来实现,当属性存在时,我们可以修改这些属性的行为。让我们看看如何使用hasattr函数来完成这个操作。我们首先为这个类创建一个对象:

coupon = CustomCoupon()

让我们检查对象优惠券是否有一个名为source的属性,如果存在,我们将获取该属性的值:

if hasattr(coupon, 'source'):
    print(getattr(coupon, 'source'))
Store

现在我们继续调用生成优惠券的方法:

coupon.generate_coupon()
***********------------------**************
Product: Honey Mustard Sauce
Product Category: Condiments
Coupon ID: 728417424745
Brand: ABCBrand3
Source: Store
Expiry Date: 10/1/2021
Quantity: 10
***********------------------**************

对象优惠券的反射实现将从以下代码开始。我们将创建一个名为check_attribute的函数,它接受三个参数。第一个参数是类对象的名称,然后是store属性和manufacturer属性。这个函数检查给定的输入对象是否具有名为source的属性,并且当它返回true时,为属性store设置一个值,当它返回false时,为属性store设置None作为值。同样,当source属性具有Manufacturer值时,则将值设置为另一个属性manufacturer,如下所示:

def check_attribute(couponobj, store, manufacturer):
    if hasattr(couponobj, 'source'):
        if(str(getattr(couponobj, 'source')) == 'Store'):
            setattr(couponobj, 'store', store)
        else:
            setattr(couponobj, 'store', None)
        if(str(getattr(couponobj,'source')) == 
          'Manufacturer'):
            setattr(couponobj, 'manufacturer', 
              manufacturer)
        else:
            setattr(couponobj, 'manufacturer', None)    

现在我们检查source属性的值:

coupon.source
'Store'

我们现在可以调用check_attribute来添加一个商店,并也添加一个制造商。由于source已被设置为Store,函数应该设置store变量的值,而不是manufacturer变量的值:

check_attribute(coupon,"Brooklyn Store", "XYZ Manufacturer")
coupon.generate_coupon()
***********------------------**************
Product: Honey Mustard Sauce
Product Category: Condiments
Coupon ID: 220498341601
Brand: ABCBrand3
Source: Store
Expiry Date: 10/1/2021
Quantity: 10
Store: Brooklyn Store
***********------------------**************

现在我们将source值重置为Manufacturer并再次运行check_attribute

coupon.source = 'Manufacturer'
check_attribute(coupon,"Brooklyn Store", "XYZ Manufacturer")
coupon.manufacturer
'XYZ Manufacturer'

现在我们检查store变量发生了什么:

coupon.store

它不返回任何值。再次将源重置为Store将设置store值并将manufacturer重置如下:

coupon.source = 'Store'
check_attribute(coupon,"Malibu Store", "XYZ Manufacturer")
coupon.generate_coupon()
***********------------------**************
Product: Honey Mustard Sauce
Product Category: Condiments
Coupon ID: 498746188585
Brand: ABCBrand3
Source: Store
Expiry Date: 10/1/2021
Quantity: 10
Store: Malibu Store
***********------------------**************

在这个例子中,我们查看在ABC Megamart优惠券信息上实现hasattr函数。这个例子通过使用hasattr函数解释了反射。有了这个理解,让我们进一步看看isinstance

使用isinstance修改对象

我们现在将查看另一个名为isinstance的内置函数。这个函数用于确定一个对象是否是类的实例。我们将通过检查它们是否是特定类的实例来实现对类对象的反射,并相应地定制类的对象。这个例子使用了与前面hasattr函数示例中相同的属性(sourcestoremanufacturer)。

首先,让我们为两个不同的类创建两个对象,并将isinstance函数应用于这些类的对象,以了解这个函数如何帮助改变 Python 对象的行为。我们将重用上一节中的CustomCoupon类,并创建另一个SimpleCoupon类。然后,我们将添加两个对象,coupon1coupon2,如下所示:

coupon1 = CustomCoupon()
class SimpleCoupon:
    product_name = "Strawberry Ice Cream"
    product_category = "Desserts"
    brand = "ABCBrand3"
    store = "Los Angeles Store"
    expiry_date = "10/1/2021"
    quantity = 10
coupon2 = SimpleCoupon()

在下面的图中,让我们看看每个对象的属性:

图 6.2 – coupon 1 和 coupon 2 对象的属性

图 6.2 – coupon 1 和 coupon 2 对象的属性

现在我们使用isinstance函数检查对象是否是特定类的实例:

isinstance(coupon1,CustomCoupon)
True
isinstance(coupon2,SimpleCoupon)
True

我们现在定义一个名为check_instance的函数,该函数使用isinstance来实现外部定制的反射。这个函数接受一个对象、一个类名、一个store值和一个manufacturer值作为输入参数,并检查对象是否是特定优惠券类的实例,并检查它是否有名为source的属性,并相应地更新storemanufacturer值。如果没有满足这些条件之一,它将返回一个消息,表明对象无法被定制:

def check_instance(couponobject, couponclass, store, manufacturer):
    if isinstance(couponobject, couponclass):
        if hasattr(couponobject, 'source'):
            if(str(getattr(couponobject, 'source')) == 
              'Store'):
                setattr(couponobject, 'store', store)
            else:
                setattr(couponobject, 'store', None)
            if(str(getattr(couponobject,'source')) == 
              'Manufacturer'):
                setattr(couponobject,'manufacturer', 
                  manufacturer)
            else:
                setattr(couponobject,'manufacturer', None)
    else:
        print(couponobject,'cannot be customized')

现在我们调用coupon1对象上的check_instance并查看对象的store值是否已更新:

check_instance(coupon1, CustomCoupon, 'Malibu Beach Store', 'XYZ Manufacturer')
coupon1.store
'Malibu Beach Store'
coupon1.generate_coupon()
***********------------------**************
Product: Honey Mustard Sauce
Product Category: Condiments
Coupon ID: 535933905876
Brand: ABCBrand3
Source: Store
Expiry Date: 10/1/2021
Quantity: 10
Store: Malibu Beach Store
***********------------------**************

现在我们进一步在coupon2对象上调用check_instance并检查该对象是否被定制:

check_instance(coupon2, CustomCoupon, 'Malibu Beach Store', 'XYZ Manufacturer')
<__main__.SimpleCoupon object at 0x0000023B51AD2B88> cannot be customized

在前面的对象中,check_instance中指定的条件没有满足,因此对象无法被定制。

这个例子通过使用isinstance函数解释了反射。有了这个理解,让我们进一步看看issubclass

使用issubclass修改类

在本节中,我们将探讨issubclass内置函数。此函数可用于对继承自一个或多个父类或超类的类进行反射。此函数用于验证一个类是否是特定父类的子类,然后相应地修改该类。

让我们从创建两个具有简单变量集的类开始。这些类将被命名为StoreCouponManufacturerCoupon

class StoreCoupon:
    product_name = "Strawberry Ice Cream"
    product_category = "Desserts"
    brand = "ABCBrand3"
    store = "Los Angeles Store"
    expiry_date = "10/1/2021"
    quantity = 10
class ManufacturerCoupon:
    product_name = "Strawberry Ice Cream"
    product_category = "Desserts"
    brand = "ABCBrand3"
    manufacturer = "ABC Manufacturer"
    expiry_date = "10/1/2021"
    quantity = 10

我们还将创建两个函数,这两个函数依次创建新的函数来生成store couponmanufacturer coupon

def create_store_coupon(product_name, product_category, brand, store, expiry_date, quantity):
    def generate_store_coupon(product_name, 
      product_category, brand, store, expiry_date, 
      quantity):
        import random
        couponId =  random.sample(
          range(100000000000,900000000000),1)
        for i in couponId:
            print('***********------------------**************')
            print('Product:', product_name)
            print('Product Category:', product_category)
            print('Coupon ID:', i)
            print('Brand:', brand)
            print('Store:', store)
            print('Expiry Date:', expiry_date)
            print('Quantity:', quantity)
            print('***********------------------**************')
    return generate_store_coupon
def create_manufacturer_coupon(product_name, product_category, brand, manufacturer, expiry_date, quantity):
    def generate_manufacturer_coupon(product_name, product_category, brand, manufacturer, expiry_date, quantity):
        import random
        couponId =  random.sample(
          range(100000000000,900000000000),1)
        for i in couponId:
            print('***********------------------**************')
            print('Product:', product_name)
            print('Product Category:', product_category)
            print('Coupon ID:', i)
            print('Brand:', brand)
            print('Manufacturer:', manufacturer)
            print('Expiry Date:', expiry_date)
            print('Quantity:', quantity)
            print('***********------------------**************')
    return generate_manufacturer_coupon

我们将进一步创建一个名为IceCreamCoupon的新类,将其作为StoreCoupon的父类:

class IceCreamCoupon(StoreCoupon):
    pass

现在让我们定义一个函数来检查一个特定类是否是IceCreamCoupon的父类。如果子类以StoreCoupon作为父类,则应创建一个生成StoreCoupon的函数;如果它以ManufacturerCoupon作为父类,则应创建一个生成ManufacturerCoupon的函数:

 def check_parent():
    if issubclass(IceCreamCoupon, StoreCoupon):
        IceCreamCoupon.generate_store_coupon = create_store_coupon(IceCreamCoupon.product_name,                                                                    IceCreamCoupon.product_category,                                                                   IceCreamCoupon.brand, IceCreamCoupon.store,                                                                   IceCreamCoupon.expiry_date, IceCreamCoupon.quantity)
    elif issubclass(IceCreamCoupon, ManufacturerCoupon):
        IceCreamCoupon.generate_manufacturer_coupon = create_manufacturer_coupon(IceCreamCoupon.product_name,                                                                                  IceCreamCoupon.product_category,                                                                                 IceCreamCoupon.brand,                                                                                  IceCreamCoupon.manufacturer,                                                                                 IceCreamCoupon.expiry_date,                                                                                  IceCreamCoupon.quantity)

运行check_parent现在将generate_store_coupon添加到IceCreamCoupon类中,如下所示:

check_parent()
IceCreamCoupon.generate_store_coupon(IceCreamCoupon.product_name, IceCreamCoupon.product_category,                                     IceCreamCoupon.brand,IceCreamCoupon.store,                                     IceCreamCoupon.expiry_date,IceCreamCoupon.quantity)
***********------------------**************
Product: Strawberry Ice Cream
Product Category: Desserts
Coupon ID: 548296039957
Brand: ABCBrand3
Store: Los Angeles Store
Expiry Date: 10/1/2021
Quantity: 10
***********------------------**************
class IceCreamCoupon(ManufacturerCoupon):
    pass
check_parent()
IceCreamCoupon.generate_manufacturer_coupon(IceCreamCoupon.product_name,IceCreamCoupon.product_category,                                            IceCreamCoupon.brand,IceCreamCoupon.manufacturer,                                            IceCreamCoupon.expiry_date,IceCreamCoupon.quantity)
***********------------------**************
Product: Strawberry Ice Cream
Product Category: Desserts
Coupon ID: 193600674937
Brand: ABCBrand3
Manufacturer: ABC Manufacturer
Expiry Date: 10/1/2021
Quantity: 10
***********------------------**************

在本例中,我们探讨了如何利用issubclass函数来实现对 Python 类的反射,并通过元编程而不是直接更改函数定义来修改类。有了这个理解,我们将查看本章的最后一节,关于在类上实现属性的实现。

在类上应用属性

在本节中,我们将探讨property的用法,这是另一个可以作为类装饰器添加的内置函数,可以通过在类方法上实现gettersetterdelete方法来更新类的属性。在第五章中,我们探讨了property作为函数的用法。在本节中,我们将通过一个示例来实现property,以检查它在反射中的工作方式。我们将查看相同的优惠券示例来理解这一点。

现在让我们创建一个新的类,并将其命名为CouponwithProperty,用_coupon_details变量初始化该类,并将其设置为none。然后我们将添加property作为装饰器,并定义一个coupon_details方法,并添加gettersetterdelete来获取、设置和删除优惠券详情的值。在这个例子中,我们将定义getter来获取优惠券详情,setter来设置优惠券详情,但我们将deleter定义为coupon_details永远不会被删除。这是通过反射实现的:

class CouponwithProperty:
    def __init__(self):
        self._coupon_details = None
    @property
    def coupon_details(self):
        return self.coupon_details    
    @coupon_details.getter
    def coupon_details(self):
        print("get coupon_details")
        return self._coupon_details
    @coupon_details.setter
    def coupon_details(self, coupon):
        print("set coupon_details")
        self._coupon_details = coupon
    @coupon_details.deleter
    def coupon_details(self):
        print("Sorry this attribute cannot be 
          deleted")      

现在让我们为前面的类创建一个对象:

fmcgCoupon = CouponwithProperty()

我们可以通过调用coupon_details属性来测试getter是否工作:

fmcgCoupon.coupon_details
get coupon_details

同样,我们可以通过为coupon_details属性设置一个值来测试setter是否工作:

fmcgCoupon.coupon_details = {
        'Product': 'Strawberry Ice Cream',
        'Product Category': 'Desserts',
        'Coupon ID': 190537749828,
        'Brand': 'ABCBrand3',
        'Manufacturer': 'ABCBrand3',
        'Expiry Date': 'ABC Manufacturer',
        'Quantity': '10/1/2021'
        }
set coupon_details

在设置值之后再次调用getter将导致以下结果:

fmcgCoupon.coupon_details
get coupon_details
{'Product': 'Strawberry Ice Cream',
 'Product Category': 'Desserts',
 'Coupon ID': 190537749828,
 'Brand': 'ABCBrand3',
 'Manufacturer': 'ABCBrand3',
 'Expiry Date': 'ABC Manufacturer',
 'Quantity': '10/1/2021'}

我们在属性上所做的最重要的更改是,通过设置deleter来禁用delete操作。让我们检查它是否按预期工作:

del fmcgCoupon.coupon_details
Sorry this attribute cannot be deleted
fmcgCoupon.coupon_details
get coupon_details
{'Product': 'Strawberry Ice Cream',
 'Product Category': 'Desserts',
 'Coupon ID': 190537749828,
 'Brand': 'ABCBrand3',
 'Manufacturer': 'ABCBrand3',
 'Expiry Date': 'ABC Manufacturer',
 'Quantity': '10/1/2021'}

当我们在属性上调用del时,它会删除该属性,但在这个情况下,del无法删除该属性,因为我们已经编程deleter来禁用删除。

这些是一些如何使用 Python 的内置函数将反射应用于 Python 对象的例子。

摘要

在本章中,我们学习了如何使用反射的概念及其相应应用来检查 Python 中的函数对象,其中我们看到了如何使用内置函数如idcallablehasattrisinstanceissubclassproperty在各种 Python 对象上实现反射,我们还学习了如何将它们应用于我们的核心示例。从所有这些概念中,我们学习了如何检查 Python 对象,如类、方法和函数。从每个主题下的例子中,我们还学习了如何在实际用例中应用反射。

与本书中涵盖的其他章节类似,本章涵盖了反射的概念,也涵盖了使用元编程在外部更改 Python 对象的行为。

在下一章中,我们将通过一些有趣的例子来探讨泛型的概念。

第七章:第七章:理解泛型和类型化

在本章中,我们将探讨泛型是什么,如何在 Python 3 中执行类型检查,以及它在元编程中的有用性。

Python 是一种编程语言,变量被声明为泛型,它们在声明时不会分配数据类型。Python 在运行时根据分配给变量的值动态地解决数据类型。在其他编程语言,如 C++中,泛型需要通过编程设计来使变量泛型,而在 Python 中,泛型是变量的定义方式。在这种情况下,我们将详细关注如何使用类型声明变量并限制变量的行为。

在本章中,我们将了解泛型在 Python 中的工作方式以及如何定义类型检查,以便我们可以对变量应用元编程,以静态类型化它们,这样我们就不必等待整个程序运行来确定我们在代码中无意中使用了不正确的类型。

在本章中,我们将涵盖以下主要主题:

  • 泛型是什么?

  • 当指定数据类型时会发生什么?

  • 使用显式类型检查进行类型化 – 方法 1

  • 使用显式类型检查进行类型化 – 方法 2

  • 添加具有约束的数据类型

  • 创建一个简单的自定义数据类型

  • 创建一个领域特定数据类型

到本章结束时,你应该能够将泛型和类型检查应用于 Python 变量。你还应该能够创建自己的领域特定数据类型。

技术要求

本章中分享的代码示例可在 GitHub 上找到,地址为:github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter07

泛型是什么?

泛型是一种编程范式,在这种范式中,任何属性或变量都是一种在语言中未分配给任何特定类型的函数。当我们谈论类型时,它要么是变量数据类型,要么是函数返回类型。

泛型如何与元编程相关联?

元编程处理 Python 3 及以上版本的概念,其中我们可以开发脚本或程序,在外部操作 Python 对象,而不会实际影响程序中类、方法或函数的定义。泛型是 Python 构建其对象数据类型处理的方式。如果我们需要将 Python 中的数据类型处理从泛型更改为特定类型,我们可以通过元编程来实现。为了理解如何使具体实现工作,我们需要通过示例理解泛型。让我们在下一节中看看泛型。

Python 中如何处理泛型?

在这里,我们可以通过一个例子来研究泛型。在本章中,我们将探讨核心示例的另一个有趣部分,ABC Megamart。在本章中,我们将使用ABC Megamart的服装和时尚部门来介绍我们的示例。

让我们以ABC Megamart的时尚部门为例。这个部门涵盖了各种服装产品。为了检验泛型,我们首先定义一个名为Fashion的类,具有clothing_categorygendermodeldesigndress_typesizecolor等属性。我们还将添加一个名为get_item的方法来返回前面的属性。代码定义如下:

class Fashion:
    def __init__(self,clothing_category,gender,model,design,dress_type,size, color):
        self.clothing_category = clothing_category
        self.gender = gender
        self.model = model
        self.design = design
        self.dress_type = dress_type
        self.size = size
        self.color = color
     def get_item(self):
        return self.clothing_category,self.gender,self.model,self.design,self.dress_type, self.size,self.color        

此代码处理泛型。让我们通过将任何数据类型的值分配给Fashion类的属性来解释这个声明:

fashion = Fashion("Clothing","Women","Western","Dotted","Jumpsuits",38,"blue")

我们已经为clothing_categorygendermodeldesigndress_typecolor添加了字符串值,而将整数值添加到size属性中。由于语言默认处理泛型,我们不必声明数据类型,值被接受而没有抛出任何错误。我们可以调用get_item方法来显示这些泛型值:

fashion.get_item()
('Clothing', 'Women', 'Western', 'Dotted', 'Jumpsuits', 38, 'blue')

检查clothing_categorysize的数据类型结果如下:

type(fashion.clothing_category)
str
type(fashion.size)
int

现在我们来双重检查关于泛型的声明。当我们改变输入变量的数据类型时会发生什么?Python 会接受它们吗?为了测试这一点,让我们改变clothing_categorysize的数据类型:

fashion = Fashion(102,"Women","Western","Floral","T-Shirt","XS","green")
fashion.get_item()
(102, 'Women', 'Western', 'Floral', 'T-Shirt', 'XS', 'green')

数据类型的改变被 Python 接受并处理,可以如下查看:

type(fashion.clothing_category)
int
type(fashion.size)
str

在前面的例子中,无论输入值属于哪种数据类型,它们都能被成功处理。在下一节中,我们将显式地分配数据类型并进一步检查。

指定数据类型会发生什么?

Python 中的注解被添加到代码中以提供额外的信息或帮助最终用户在创建库时理解一段代码。注解可以用来向特定代码添加数据类型,以便开发人员可以通过注解检索数据类型的信息。

类型注解

在本章的主题——类型注解的上下文中,让我们在本节中看看类型注解。在 Python 中,可以使用注解的功能来定义函数或方法的数据类型,通过在类的属性上声明类型注解来实现。为了实现这一点,我们可以在声明变量时显式地分配一个数据类型及其返回类型,并将其添加到 Python 中的方法中。我们还将为方法的返回类型添加类型注解。

让我们声明一个Fashion类,它初始化了其属性或变量以及我们期望变量具有的数据类型:

class Fashion:
    def __init__(self,clothing_category: str,gender:str,model:str,design:str,dress_type:str,size:int, color:str):
        self.clothing_category = clothing_category
        self.gender = gender
        self.model = model
        self.design = design
        self.dress_type = dress_type
        self.size = size
        self.color = color
    def get_item(self) -> list:
        return self.clothing_category,self.gender,self.model,self.design,self.dress_type, self.size,self.color

在前面的代码中,我们已为每个变量特别标记了一个数据类型。在这个类中,我们也将添加一个get_item方法,并添加带有类型提示的注释,指定该方法返回一个list项。

现在检查在创建对象时未遵循这些数据类型,并将值分配给这些变量时会发生什么:

fashion = Fashion(104,"Women","Western","Cotton","Shirt","S","white")
fashion.get_item()
[104, 'Women', 'Western', 'Cotton', 'Shirt', 'S', 'white']

在前面的类定义中,我们将clothingCategory_c声明为字符串,将size声明为整数,但我们为clothing_category变量分配了整数,为size变量分配了字符串。程序仍然成功运行,没有抛出任何类型错误,而理想情况下应该出现类型错误。这个例子再次证明,当我们变量声明时分配数据类型时,Python 将类型处理为泛型。

让我们看看以下代码中get_item方法的注释:

print(Fashion.get_item.__annotations__)

在方法上调用__annotations__提供了作为方法返回类型注解的列表数据类型:

{'return': <class 'list'>}

让我们进一步探讨类型的概念,我们可以看看如何处理特定类型而不是泛型。

使用显式类型检查进行类型检查 – 方法 1

在前面的部分中,我们探讨了 Python 处理数据类型作为泛型的能力。在构建应用程序时,可能会有需要特定数据类型的场景,我们可能期望元编程具有处理此类特定数据类型的能力。在本节中,让我们看看创建一个执行类型检查的类。

创建一个类以实现类型检查

在这个例子中,我们将创建一个名为typecheck的类,并添加方法以特定地检查每种数据类型。例如,如果将整数类型作为输入提供给方法,它将返回输入值,如果条件失败,它将返回一条消息,提供输入值作为整数。类似地,我们将添加各种方法来检查字符串、浮点数、列表、元组和字典对象:

class typecheck:

现在定义一个名为intcheck的方法。这个方法的目的是对任何输入进行显式的整数类型检查。在这个方法中,将提供一个值作为输入,并且该方法将验证输入值是否为整数。如果输入值是整数,我们将返回输入值。如果值不是整数,我们将返回一条消息,表示"value should be an integer"

    def intcheck(self,inputvalue):
        if type(inputvalue) != int:
            print("value should be an integer")
        else:
            return inputvalue

在以下方法中,让我们检查输入变量是否不是字符串(例如,Orangesexample),当条件为true时返回错误消息,当条件为false时返回输入值:

    def stringcheck(self,inputvalue):
        if type(inputvalue) != str:
            print("value should be a string")
        else:
            return inputvalue

在以下方法中,让我们检查输入变量是否不是浮点值(例如,example, 2335.2434),当条件为true时返回错误消息,当条件为false时返回输入值:

    def floatcheck(self,inputvalue):
        if type(inputvalue) != float:
            print("value should be a float")
        else:
            return inputvalue

在以下方法中,让我们检查输入变量不是一个包含变量的列表(例如,['fruits','flowers',1990]),当条件为true时返回错误信息,当条件为false时返回输入值:

    def listcheck(self,inputvalue):
        if type(inputvalue) != list:
            print("value should be a list")
        else:
            return inputvalue

在以下方法中,让我们检查输入变量不是一个包含变量的元组(例如,example, ('fruits','flowers',1990)),当条件为true时返回错误信息,当条件为false时返回输入值:

    def tuplecheck(self,inputvalue):
        if type(inputvalue) != tuple:
            print("value should be a tuple")
        else:
            return inputvalue

在以下方法中,让我们检查输入变量不是一个包含键/值对的字典(例如,example: {'one': 1, 'two': 2}),当条件为true时返回错误信息,当条件为false时返回输入值:

    def dictcheck(self,inputvalue):
        if type(inputvalue) != dict:
            print("value should be a dict")
        else:
            return inputvalue

现在,我们将进一步创建Fashion类,使用typecheck类执行类型检查。

创建一个类来测试类型检查

现在让我们创建一个具有相同变量集的Fashion类,即clothing_categorygendermodeldesigndress_typesizecolor。在这个例子中,我们也将为每个变量分配一个特定的数据类型。在以下类定义中,让我们创建一个typecheck类的对象,并调用特定类型的方法来存储每种类型的变量。例如,price变量将被声明为float,我们将使用typecheck中的floatcheck方法来存储变量,而不是使用泛型:

class Fashion:

在以下方法中,让我们使用typecheck类的类型检查方法初始化Fashion类的变量及其特定的数据类型:

    def __init__(self,clothing_category: str,gender:str,price:float,design:str,dress_type:str,size:int, color:list):
        tc = typecheck()
        self.clothing_category = tc.stringcheck(clothing_category)
        self.gender = tc.stringcheck(gender)
        self.price = tc.floatcheck(price)
        self.design = tc.stringcheck(design)
        self.dress_type = tc.stringcheck(dress_type)
        self.size = tc.intcheck(size)
        self.color = tc.listcheck(color)

在以下方法中,让我们返回在Fashion类中初始化的所有变量:

   def get_item(self):
        return self.clothing_category,self.gender,self.price,self.design,self.dress_type, self.size,self.color

price变量上调用floatcheck方法作为变量声明的类型机制,如果提供的输入不是浮点数,那么在变量声明阶段本身就会显示错误:

fashion = Fashion(112,"Men","Western","Designer","Shirt",38.4,"black")
value should be a string
value should be a float
value should be an integer
value should be a list

在前面的例子中,我们声明了四个具有错误数据类型的变量;clothing_category应该是字符串,price应该是浮点数,size应该是整数,color应该是列表。所有这些错误的变量都没有被代码接受,因此我们收到了相应的变量类型错误:

fashion.get_item()
(None, 'Men', None, 'Designer', 'Shirt', None, None)

当我们从时尚对象中获取项目时,所有错误类型的变量都没有分配值。现在让我们看看正确的值以及它们是如何被fashion对象接受的:

:fashion = Fashion("112","Men",20.0,"Designer","Shirt",38,["blue","white"])
fashion.get_item()
('112', 'Men', 20.0, 'Designer', 'Shirt', 38, ['blue', 'white'])

在前面的代码中,我们通过分配特定数据类型的值来纠正输入值,错误现在已解决。通过开发这样的显式类型库,我们可以将 Python 的泛型转换为具体类型。

使用显式类型检查进行类型化 – 方法 2

在本节中,我们将探讨另一种将特定数据类型应用于变量的方法。在第一种方法中,我们开发了一个 typecheck 类,并使用类型检查方法本身来创建新的数据类型。在本例中,我们将为每个类型检查方法创建 typecheck 类,以检查输入值是否属于预期的类型,并根据条件的结果返回一个布尔值。这种类型检查方法使我们能够修改 Fashion 类,以便在条件不满足时提供特定变量的错误消息。

创建一个类以实现类型检查

在本例中,让我们首先创建 typecheck 类。

这里创建 typecheck 类是为了使本类中的所有方法可重用,以防类型检查代码中的所有方法需要导出到不同的文件以供以后使用。

本例中的所有方法都可以有或没有类创建,并在本章中使用:

class typecheck
  • 在下面的方法中,让我们检查输入变量不是一个整数(例如,23348),当条件为真时返回 False,当条件为假时返回 True

        def intcheck(self,inputvalue):
            if type(inputvalue) != int:
                return False
            else:
                return True
    
  • 在下面的方法中,让我们检查输入变量不是一个字符串(例如,Orangesexample),当条件为真时返回 False,当条件为假时返回 True

        def stringcheck(self,inputvalue):
            if type(inputvalue) != str:
                return False
            else:
                return True
    
  • 在以下方法中,让我们检查输入变量不是一个浮点数值(例如,2335.2434),当条件为真时返回 False,当条件为假时返回 True

       def floatcheck(self,inputvalue):
            if type(inputvalue) != float:
                return False
            else:
                return True
    
  • 在下面的方法中,让我们检查输入变量不是一个变量列表(例如,['fruits','flowers',1990]),当条件为真时返回 False,当条件为假时返回 True

       def listcheck(self,inputvalue):
            if type(inputvalue) != list:
                return False
            else:
                return True
    
  • 在以下方法中,让我们检查输入变量不是一个变量元组(例如,('fruits','flowers',1990)),当条件为真时返回 False,当条件为假时返回 True

       def tuplecheck(self,inputvalue):
            if type(inputvalue) != tuple:
                return False
            else:
                return True
    
  • 在以下方法中,让我们检查输入变量不是一个包含键/值对的字典(例如,{'one': 1, 'two': 2}),当条件为真时返回 False,当条件为假时返回 True

       def dictcheck(self,inputvalue):
            if type(inputvalue) != dict:
                return False
            else:
                return True
    

现在,我们可以进一步创建一个名为 Fashion 的类,使用 typecheck 类来进行类型检查。

创建一个类以测试类型检查

在本节中,让我们看看如何创建一个具有不同变量类型定义的 Fashion 类,如下所示:

class Fashion:
  • 让我们初始化变量以及每个变量的具体数据类型:

        def __init__(self,clothing_category: str,gender:str,model:tuple,design:int,price:float,size:dict, color:list):
            tc = typecheck()
    
  • 在以下代码中,让我们检查 clothing_category 输入是否为字符串,如果是,则返回值;如果不是,则返回针对 clothing_category 的特定错误:

            if tc.stringcheck(clothing_category):
                self.clothing_category = clothing_category
            else:
                print("clothing category should be a string")
    
  • 在以下代码中,让我们检查 gender 输入是否为字符串,如果是,则返回值;如果不是,则返回针对 gender 变量的特定错误:

            if tc.stringcheck(gender):
                self.gender = gender
            else: 
                print("gender should be a string")
    
  • 在以下代码中,让我们检查model输入是否为元组,如果是,则返回值;如果不是,则返回针对model变量的特定错误:

            if tc.tuplecheck(model):
                self.model = model
            else:
                print("model should be a tuple")
    
  • 在以下代码中,让我们检查design输入是否为整数,如果是,则返回值;如果不是,则返回针对design变量的特定错误:

    if tc.intcheck(design):
                self.design = design
            else:
                print("design should be an integer")
    
  • 在以下代码中,让我们检查price输入是否为浮点值,如果是,则返回值;如果不是,则返回针对price变量的特定错误:

    if tc.floatcheck(price):
                self.price = price
            else:
                print("price should be a floating point value")
    
  • 在以下代码中,让我们检查size输入是否为字典对象,如果是,则返回值;如果不是,则返回针对size变量的特定错误:

    if tc.dictcheck(size):
                self.size = size
            else:
                print("size should be a dictionary object")
    
  • 在以下代码中,让我们检查color输入是否为列表对象,如果是,则返回值;如果不是,则返回针对color变量的特定错误:

    if tc.listcheck(color):       
                self.color = color
            else:
                print("color should be a list of values")
    
  • 在以下代码中,让我们创建一个方法来返回先前代码中列出的所有变量:

        def get_item(self):
            return self.clothing_category,self.gender,self.model,self.design,self.price, self.size,self.color
    

为了测试这种类型检查方法,让我们将这些变量的一些不正确值作为输入传递并检查:

fashion = Fashion(12,"Women","Western","Floral","Maxi Dress",34,"yellow")

执行前面的代码会产生以下错误列表:

clothing category should be a string
model should be a tuple
price should be a floating point value
size should be a dictionary object
color should be a list of values

此外,在先前的fashion对象上调用get_item方法会导致以下错误:

fashion.get_item()

错误信息的图形表示如下:

图 7.1 – 调用 get_item 方法时的错误

图 7.1 – 调用 get_item 方法时的错误

在先前的错误中,第一个变量clothing-category没有被方法接受,因为该变量的类型期望没有得到满足。

我们可以通过提供正确的输入类型来进一步检查,如下所示:

fashion = Fashion("Rayon","Women",("Western","Floral"),12012,100.50,{'XS': 36, 'S': 38, 'M': 40},["yellow","red"])

在先前的值赋值中没有错误。现在在fashion对象上调用get_item方法会产生以下输出:

fashion.get_item()
('Rayon',
 'Women',
 ('Western', 'Floral'),
 12012,
 100.5,
 {'XS': 36, 'S': 38, 'M': 40},
 ['yellow', 'red'])

之前的输出满足所有类型要求,并且通过这种方法成功实现了类型检查的最终目标。现在你理解了这个,让我们进一步探讨具有约束的数据类型的概念。

添加具有约束的数据类型

在本节中,我们将查看一个向数据类型添加约束并在类型检查的同时检查约束的示例。可能会有这样的场景,我们想要创建一个整数变量并限制其长度为两位数,或者创建一个字符串并限制其长度为 10 个字符以上。通过这个例子,让我们探索如何在静态类型检查期间添加这样的约束或限制。

在这个例子中,让我们创建一个只包含两个方法的typecheck类来检查整数和字符串。在检查这些数据类型的同时,我们也在方法定义中添加了一些额外的约束:

class typecheck:
  • 在以下方法中,让我们检查输入变量不是整数或其长度大于两个,当条件为真时返回False,当条件为假时返回True

        def intcheck(self,inputvalue):
            if (type(inputvalue) != int) and (len(str(inputvalue))>2):
                return False
            else:
                return True
    
  • 在下面的方法中,让我们检查输入变量不是字符串或其长度大于 10,当条件为真时返回False,当条件为假时返回True

        def stringcheck(self,inputvalue):
            if (type(inputvalue) != str) and (len(str(inputvalue))>10):
                return False
            else:
                return True
    

只需两个带有类型检查和约束的方法,我们就可以创建一个具有两个变量和一个方法的Fashion类:

class Fashion:
  • 让我们用字符串clothing_category和整型size初始化类:

        def __init__(self,clothing_category: str,size:int):
            tc = typecheck()
    
  • 在下面的代码中,让我们使用stringcheck方法声明clothing_category

            if tc.stringcheck(clothing_category):
                self.clothing_category = clothing_category
            else:
                print("value should be a string of length less than or equal to 10")
    
  • 在下面的代码中,让我们使用intcheck方法声明size

            if tc.intcheck(size):
                self.size = size
            else:
                print("value should be an integer of 2 digits or less")
    
  • 在下面的代码中,让我们添加一个方法来获取项目并返回它们:

        def get_item(self):
            return self.clothing_category,self.size
    

让我们进一步创建一个fashion类的对象,并分配两个不满足类型检查条件的变量:

fashion = Fashion("Clothing & Accessories",384)
value should be a string of length less than or equal to 10
value should be an integer of 2 digits or less

之前的错误信息表明,字符串类型以及整型数据类型都没有满足类型检查和约束条件。现在,让我们提供正确的输入值并执行静态类型检查:

fashion = Fashion("Cotton",34)
fashion.get_item()
('Cotton', 34)

在前面的代码中,值赋值现在按预期工作。有了这个理解,让我们进一步创建简单的自定义数据类型。

创建一个简单的自定义数据类型

在上一节之前,我们探讨了添加显式类型检查以及将泛型类型变量转换为特定类型来处理在编写应用程序时可能遇到的具体数据需求,我们还添加了错误信息来帮助调试分配给变量的不正确数据类型。

在本节中,我们将探讨创建我们自己的简单数据类型以及这样做需要满足什么条件。首先,让我们回答为什么我们需要自己的数据类型。任何自定义数据类型都是 Python 基本数据类型的一个派生,并伴随一些变化以满足我们在应用程序中的数据需求。任何数据类型都将有一组可以在该特定类型数据上执行的操作。例如,整型数据类型将支持加法、减法、乘法和除法等算术操作。同样,字符串支持使用连接代替加法,等等。因此,当我们创建自己的数据类型时,我们可以覆盖这些基本操作以满足我们自定义数据类型的需求。

为了演示这一点,让我们首先创建我们自己的数据类型并覆盖基本运算符以执行我们期望的操作。请注意,自定义数据类型可能只在以下情况下需要:我们希望使其具有领域特定性或应用特定性。我们始终可以使用默认数据类型,并在没有要求的情况下避免创建自定义数据类型:

  1. 我们将创建一个名为DressSize的类,并用整型变量size初始化它。如果size的输入值不是整数,或者输入值不遵循特定的服装尺寸列表,类型检查将返回一个红色的错误信息(如图7.2所示):

    class DressSize:
        def __init__(self,size:int):
            self.limit = [28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48]
            if type(size)==int and size in self.limit:
                self.size = size
            else:
                print("\x1B31mSize should be a valid dress size")  
    
  2. 接下来,让我们重写一个类的默认 str 方法,使其返回 size 变量的字符串版本:

        def __str__(self):
            return str(self.size)
    
  3. 然后,让我们添加一个名为 value 的新方法,用于返回 size 属性的值:

        def value(self):
            return self.size
    
  4. 现在,让我们重写整数方法的加法(+)运算符,以增加为 DressSize 类创建的一个服装尺寸对象中的 size 值:

        def __add__(self, up):
            result = self.size + up
            if result in self.limit:
                return result
            else:
                return "Input valid size increments"
    
  5. 然后,让我们重写整数方法的减法(-)运算符,以减少为 DressSize 类创建的一个尺寸对象中的 size 值:

        def __sub__(self, down):
            result = self.size - down
            if result in self.limit:
                return result
            else:
                return "Input valid size decrements"
    
  6. 然后,我们将为该类创建一个对象,在这种情况下,我们的新自定义数据类型 DressSize,并用字符串而不是整数来初始化它,如下所示:

    s = DressSize("30")
    

不正确的输入类型会导致出现类似调试时通常显示的错误信息的红色字体错误:

![图 7.2 – DressSize 的错误信息图 7.2 – DressSize 的错误信息 1. 调用 value 方法也会导致错误,因为 DressSize 数据类型的类型检查失败了: py s.value() 值错误显示如下:图 7.3 – 由于输入类型不正确导致的值错误

图 7.3 – 由于输入类型不正确导致的值错误

  1. 让我们通过在创建 DressSize 对象时提供正确的输入类型来纠正这个错误:

    s = DressSize(30)
    s
    <__main__.DressSize at 0x22c4bfc4a60>
    
  2. 在下面的代码中,我们可以看看加法操作(+)是如何在 DressSize 对象上工作的:

    DressSize(30) + 6
    36
    DressSize(30) + 3
    'Input valid size increments'
    
  3. 两个对象的加法操作与常规加法类似,因为我们已经重载了加法运算符(+)来添加两个对象的初始化变量。同样,我们可以检查减法的结果,如下所示:

    DressSize(32) - 4
    26
    DressSize(30) – 3
    'Input valid size decrements'
    
  4. 两个对象的减法操作与常规减法类似,因为我们已经重载了减法运算符(-)来从两个对象的初始化变量中减去。同样,打印对象会打印 size 变量的字符串格式,因为我们已经重载了 str 方法来完成这项工作:

    print(s)
    30
    
  5. 我们还添加了一个 value 方法来显示 size 变量的值,它的工作方式如下:

    s.value()
    30
    
  6. 在变量或 s 对象上调用 type 方法会显示类名 DressSize,这是在这种情况下 s 的数据类型:

    type(s)
    __main__.DressSize
    

现在,我们可以考虑在下一节创建一个更详细的自定义数据类型。

创建一个特定领域的数据类型

在本节中,让我们创建一个更定制的数据类型来处理 ABC Megamart 时尚部门的服装尺寸。我们在前一节中定义的 DressSize 数据类型可以处理任何整数作为输入并执行我们重载的操作。当我们查看时尚行业的领域并考虑服装尺寸作为特定领域的变量时,DressSize 数据类型理想情况下应该只考虑 size 的特定值,而不是接受所有整数。服装尺寸将基于 ABC Megamart 库存中持有的服装尺寸:

在这个例子中,接受服装尺寸的输入应该是整数列表[36,38,40,42,44,46,48],或者表示服装尺寸等效文本值的字符串列表,例如[XS,S,M,L,XL,XXL,XXXL]

  1. 让我们从创建DressSize类及其方法开始,使其作为特定领域的数据类型工作,并将size初始化为其唯一的输入值:

    class DressSize:
        def __init__(self, size):
    
  2. 让我们进一步定义两个特定领域的列表,分别用于存储服装尺寸的有效值集合,一个是文本格式,另一个是整数格式:

    self.romanchart = ['XS','S','M','L','XL','XXL','XXXL']
    self.sizenum = [36,38,40,42,44,46,48]
    
  3. 在下面的代码中,我们将创建一个字典对象,它包含size的整数和文本格式的键/值对。添加这个字典对象的原因是将其用于为该数据类型创建的特定数据类型方法中:

    self.chart = {}dict(zip(self.romanchart,self.sizenum))
    
  4. 现在让我们添加一个条件,如果输入值符合数据类型标准,则接受该输入值作为size,如果不满足标准,则使用错误信息拒绝输入值:

            if (size in self.romanchart) or (size in self.sizenum ):
                self.size = size
            else:
                print("\x1B[31mEnter valid size")
    

在前面的代码中,如果输入值存在于romanchart列表变量中,或者存在于sizenum列表变量中,则将接受该输入值。如果这两个条件都不满足,DressSize数据类型将拒绝该值,并在红色字体中显示错误信息。为什么我们需要在这个特定领域的特定数据类型中设置这些严格的约束?如果我们看看服装的size值,尺寸通常是一个偶数,购物车或服装店中没有奇数尺寸的服装。此外,大多数通用服装店的服装尺寸通常在 36 到 48 之间。如果商店持有较小或较大的尺寸的服装,我们可以相应地调整列表并重新定义数据类型。在这个特定场景中,让我们考虑 36 到 48 之间的服装尺寸及其对应的文本代码 XS 到 XXXL 作为可接受值。现在,我们已经添加了数据类型的接受标准:

  1. 让我们添加可以在此数据类型上处理的具体方法。在以下方法中,让我们重写类的默认str方法,以返回size变量的字符串版本:

        def __str__(self):
            return str(self.size)
    
  2. 在下面的代码中,让我们添加一个名为value的新方法来返回size属性的值:

        def value(self):
            return self.size
    
  3. 在下面的代码中,让我们添加一个方法来增加size值。由于服装尺寸总是以偶数测量,size值应该增加2

        def increase(self):
            if (self.size in self.romanchart) :
                result = self.chart[self.size] + 2
                for key, value in self.chart.items():
                    if value == result:
                        return resultkey
            elif (self.size in self.sizenum ):
                return self.size + 2
    

在前面的代码中,我们添加了一个查找服装尺寸值(如XL)的逻辑,如果DressSize是数据类型的文本输入,然后增加该值2。我们还添加了一个检查服装尺寸整数值的逻辑,如果服装尺寸输入是整数,则增加2

  1. 让我们再添加一个方法来减少DressSize属性:

        def decrease(self):
            if self.size in self.romanchart :
                result = self.chart[self.size] - 2
                for key, value in self.chart.items():
                    if value == result:
                        return key
            elif (self.size in self.sizenum ):
                return self.size – 2
    

在上述代码中,我们添加了一个查找连衣裙尺寸值(如XL)的逻辑,如果DressSize是数据类型的文本输入,然后减去2。我们还添加了一个检查DressSize的整数值的逻辑,如果连衣裙尺寸输入是整数,则减去2。这定义了名为DressSize的特定领域数据类型的整体创建。

  1. 下一步是通过创建一个对象来测试这种数据类型:

    s = DressSize("XXL")
    

在上述代码中,我们创建了一个名为s的对象,因此让我们看看各种方法和属性在这个对象上的工作方式:

图 7.4 – DressSize 的属性

图 7.4 – DressSize 的属性

  1. 在以下代码中,让我们从s对象中调用chart

    s.chart
    {'XS': 36, 'S': 38, 'M': 40, 'L': 42, 'XL': 44, 'XXL': 46, 'XXXL': 48}
    
  2. 打印对象会产生s对象值的字符串格式表示:

    print(s)
    XS
    XL
    
  3. 调用值方法的结果如下:

    s.value()
    'XXL'
    
  4. 调用增量方法的结果如下:

    s.increase()
    XXXL
    
  5. 调用减量方法的结果如下:

    s.decrease()
    XL
    
  6. 让我们现在创建Fashion类并初始化变量,其中size变量将被初始化为DressSize类型:

    class Fashion:
        def __init__(self,clothing_category: str,gender:str,model:str,design:str,dress_type:str,color:str,size:DressSize):
            self.clothing_category = clothing_category
            self.gender = gender
            self.model = model
            self.design = design
            self.dress_type = dress_type
            self.color = color
    
  7. 在以下代码中,让我们定义DressSize的类型检查条件。如果sizeDressSize的实例,则返回该实例;如果不是实例,将显示适当的错误消息:

    if isinstance(size,DressSize):
                self.size = size
            else:
                print("value should be of type DressSize")   
    
  8. 让我们进一步添加get_item方法来返回Fashion类的属性:

        def get_item(self):
            return self.clothing_category,self.gender,self.model,self.design,self.dress_type,self.color,self.size
    
  9. 创建对象进一步的结果如下:

    fashion = Fashion("Clothing","Women","Western","Dotted","Jumpsuits",'blue',"XL")
    value should be of type DressSize
    

在上述代码中,我们没有为size变量分配正确的数据类型。

  1. 为了纠正它,让我们创建一个DressSize的实例并将其作为输入提供给Fashion类:

    M = DressSize("M")
    fashion = Fashion("Clothing","Women","Western","Dotted","Jumpsuits",'blue',M)
    

上述代码没有产生任何错误,并且被Fashion类接受为输入。调用get_item方法会产生以下输出:

fashion.get_item()
('Clothing',
 'Women',
 'Western',
 'Dotted',
 'Jumpsuits',
 'blue',
 <__main__.DressSize at 0x22c4cf4ba60>)

如果我们想查看M对象的特定值,可以按照以下方式调用value方法:

fashion.size.value()
'M'

在本节中,我们探讨了如何创建特定领域的自定义数据类型,以及如何将其用作另一个类的类型变量。

这些是一些 Python 中泛型工作方式的示例,以及如何使用用户定义的函数将具体内容应用于 Python 对象。

概述

在本章中,我们学习了泛型和类型检查的概念。我们还探讨了创建具有特定约束的用户定义数据类型,并看到了如何将它们应用于我们的核心示例。我们创建了自己的特定领域数据类型,并重载了运算符和方法以根据数据类型工作。类似于本书中涵盖的其他章节,本章也用于使用元编程的概念在外部更改 Python 对象的行为。

在下一章中,我们将通过一些有趣的示例来探讨模板的概念。

第八章:第八章:定义算法模板

在本章中,我们将探讨模板是什么以及如何在 Python 中实现模板编程。

模板是什么?它们在哪里有用?在开发应用程序的过程中应用元编程概念的主要用途是设计一个可重用的框架,可以通过编程 Python 对象的元数据来操作,而不是修改对象本身。正如其名所示,模板可以作为模板、格式或模型,说明如何在 Python 对象上执行一系列操作。这些模板可以用来定义类内方法的共同功能,并通过应用面向对象编程的继承概念来重用它们。

在本章中,我们将探讨如何在 Python 中定义和使用模板,以及如何将一系列常见操作设计成适合框架的模板。说到设计,模板编程是 Python 设计模式中的主要概念之一。设计模式将在第十二章中详细讨论。

在本章中,我们将探讨以下主要主题:

  • 解释操作序列

  • 定义方法序列

  • 识别共同功能

  • 设计模板

到本章结束时,你应该能够对 Python 变量应用泛型和类型检查。你还应该能够创建自己的特定领域数据类型。

技术要求

本章中分享的代码示例可在 GitHub 上找到,地址为:github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter08

解释操作序列

开发算法总是很有趣,尤其是在像 Python 这样的语言中,与任何其他编程语言相比,完成一个动作所需的代码更少。算法是一系列简单的步骤,需要执行以完成任务。在开发任何算法时,最重要的方面是确保我们正在按照正确的顺序执行动作的步骤。本节涵盖了操作序列的示例以及如何在 Python 程序中定义它们。

回到我们的核心示例

在本章中,我们将继续使用我们的核心示例ABC Megamart,并特别关注计费柜台,在那里我们可以执行一系列操作。我们之所以关注操作序列,是为了特别了解模板如何被用来执行一系列任务,以及它们如何被重用来执行类似的其他任务。因此,让我们开始吧。

ABC Megamart,我们有四个不同的结账柜台来检查购物车中的商品。各柜台详情如下:

  • 第一个是检查包含蔬菜和乳制品的物品。

  • 第二个是检查包含少于 10 种不同物品的物品,不包括电子产品、蔬菜和乳制品。

  • 第三个是检查包含超过 10 种不同物品的物品,不包括电子产品、蔬菜和乳制品。

  • 第四个是检查电子产品。

这些柜台中的每一个都在执行一系列操作,在这一点上,它们可能看起来像是一组独立的操作。本章的目标是创建模板并查看连接这些独立操作的一种通用方式。为了连接它们并创建模板,我们需要了解每个柜台中的操作顺序。

现在我们来看看每个柜台将处理什么。

蔬菜和乳制品柜台

顾客前往结账柜台的路程始于蔬菜区,在那里蔬菜被添加到购物车中,顾客然后站在相应的结账柜台队列中,蔬菜和水果被称重并包装,包装上添加了一个带有条形码的价格标签,条形码被扫描,并为每件物品添加账单,为每件物品添加一个税费组成部分,账单总计,打印并交给顾客,然后顾客支付账单。

步骤的图形表示如下:

图 8.1 – 蔬菜柜台

图 8.1 – 蔬菜柜台

将定义以下功能来执行这些操作:

return_cart()
goto_vege_counter()
weigh_items()
add_price_tag()
scan_bar_code()
add_billing()
add_tax()
calc_bill()
print_invoice()
receive_payment()

让我们进一步看看下一个计数器,它处理少于 10 件物品。

少于 10 件物品柜台

当顾客将少于 10 件物品添加到购物车,并且这些物品不包含蔬菜、水果、乳制品或电子产品时,顾客将前往少于 10 件物品的柜台,在那里每件物品的条形码被扫描,并为每件物品添加账单,为每件物品添加一个税费组成部分,账单总计,打印并交给顾客,然后顾客支付账单。

步骤的图形表示如下:

图 8.2 – 少于 10 件物品柜台

图 8.2 – 少于 10 件物品柜台

将定义以下功能来执行这些操作:

return_cart()
goto_less_t10_counter()
review_items()
count_items()
scan_bar_code()
add_billing()
add_tax()
calc_bill()
print_invoice()
receive_payment()

让我们进一步看看下一个计数器,它处理超过 10 件物品。

超过 10 件物品的计数器

当顾客将超过 10 个物品添加到购物车,并且这些物品不包含蔬菜、水果、乳制品或电子产品时,顾客将前往超过 10 个物品的计数器,在那里扫描每个物品的条形码,并为每个物品添加账单,应用优惠券,为每个物品添加一个税费组成部分,然后计算总账单,打印并交给顾客,顾客随后支付账单。

步骤的图形表示如下:

图 8.3 – 超过 10 个物品计数器

图 8.3 – 超过 10 个物品计数器

以下函数将被定义以执行这些操作中的每一个:

return_cart()
gotoGreatT10Counter()
review_items()
count_items()
scan_bar_code()
add_billing()
apply_coupon()
add_tax()
calc_bill()
print_invoice()
receive_payment()

让我们进一步查看下一个计数器,该计数器处理电子产品。

电子产品计数器

最后一个计数器是电子产品计数器,顾客前往该计数器,对电子产品进行测试,扫描物品,并为每个物品添加账单。为每个物品添加一个税费组成部分,然后计算总账单,打印并交给顾客,顾客随后支付账单。

步骤的图形表示如下:

图 8.4 – 电子产品计数器

图 8.4 – 电子产品计数器

以下函数将被定义以执行这些操作中的每一个:

return_cart()
goto_electronics_counter()
review_items()
test_electronics()
scan_bar_code()
add_billing()
apply_coupon()
add_tax()
calc_bill()
print_invoice()
receive_payment()

在前面的每个计费计数器中,我们查看了一个销售完成时发生的操作序列。

基于这种理解,让我们在下一节中定义每个操作为方法。

定义方法的序列

定义方法有助于我们详细了解每个计数器上执行的每个操作。让我们定义执行每个操作所需的类和方法。在本节中,我们将涵盖以下计数器:

  • 蔬菜计数器

  • 少于 10 个物品计数器

  • 超过 10 个物品计数器

  • 电子产品计数器

让我们从蔬菜计数器开始。

蔬菜计数器

以下是这个计数器操作的步骤:

  1. 我们首先创建以下VegCounter类:

    class VegCounter():
    
  2. 在以下代码中,我们将定义return_cart方法,该方法返回添加到购物车中的物品列表:

        def return_cart(self,*items):
            cart_items = list(items)
            return cart_items
    
  3. 现在让我们返回要包含在账单中的计数器名称。在这个例子中,计数器名称是Vegetables & Dairy

        def goto_vege_counter(self):
            return 'Vegetables & Dairy'
    
  4. 在以下代码中,让我们定义一个方法来称量购物车中的物品,并返回一个包含物品及其对应重量的字典:

        def weigh_items(self,*weights,cart_items = None):
            weight = list(weights)
            item_weight = dict(zip(cart_items, weight))
            return item_weight
    
  5. 接下来,让我们定义一个方法,以单价和重量作为输入,通过乘以重量和单价来计算每个物品的价格:

        def add_price_tag(self,*units,weights = None):
            pricetag = []
            for item,price in zip(weights.items(),list(units)):
                pricetag.append(item[1]*price)
            return pricetag        
    
  6. 在以下方法中,让我们输入购物车中每个物品的条形码,并返回一个条形码列表:

        def scan_bar_code(self,*scan):
            codes = list(scan)
            return codes
    
  7. 接下来,让我们添加一个方法,通过创建一个字典对象并添加代码及其对应的价格标签作为键值对,为条形码添加价格标签:

        def add_billing(self,codes=None,pricetag=None):
            self.codes = codes
            self.pricetag = pricetag
            bill = dict(zip(self.codes, self.pricetag))
            return bill
    
  8. 然后,让我们为每个物品添加税率并返回税率列表:

        def add_tax(self,*tax):
            taxed = list(tax)
            return taxed
    
  9. 让我们进一步使用价格标签和税率,计算购物车中每个物品的账单,并创建一个字典来添加物品及其相应的账单金额:

        def calc_bill(self,bill,taxes,cart_items):
            items = []
            calc_bill = []
            for item,tax in zip(bill.items(),taxes):
                items.append(item[1])
                calc_bill.append(item[1] + item[1]*tax)
            finalbill = dict(zip(cart_items, calc_bill))
            return finalbill
    
  10. 在以下方法中,让我们打印带有计数器名称、购物车中的物品、价格和总账单金额的发票:

        def print_invoice(self,finalbill):
            final_total = sum(finalbill.values())
            print('**************ABC Megamart*****************')
            print('***********------------------**************')
            print('Counter Name: ', self.goto_vege_counter())
            for item,price in finalbill.items():
                print(item,": ", price)
            print('Total:',final_total)
            print('***********------------------**************')
    
  11. 然后,让我们打印带有声明发票已付款的发票:

        def receive_payment(self,finalbill):
            final_total = sum(finalbill.values())
            print('**************ABC Megamart*****************')
            print('***********------------------**************')
            print('Counter Name: ', self.goto_vege_counter())
            for item,price in finalbill.items():
                print(item,": ", price)
            print('Total:',final_total)
            print('***********------------------**************')
            print('***************PAID************************')
    
  12. 执行前面的代码会产生以下结果。方法按顺序调用,以便一个方法的结果作为下一个步骤的输入:

    veg = VegCounter()
    cart = veg.return_cart('onions','tomatoes','carrots','lettuce')
    item_weight = veg.weigh_items(1,2,1.5,2.5,cart_items = cart)
    pricetag = veg.add_price_tag(7,2,3,5,weights = item_weight)
    codes = veg.scan_bar_code(113323,3434332,2131243,2332783)
    bill = veg.add_billing(codes,pricetag)
    taxes = veg.add_tax(0.04,0.03,0.035,0.025)
    finalbill = veg.calc_bill(bill,taxes,cart)
    veg.print_invoice(finalbill)
    

打印的发票输出如下:

**************ABC Megamart*****************
***********------------------**************
Counter Name:  Vegetables & Dairy
onions :  7.28
tomatoes :  4.12
carrots :  4.6575
lettuce :  12.8125
Total: 28.87
***********------------------**************
  1. 接下来,让我们打印顾客已支付的发票,veg.receive_payment(finalbill)

已付款发票的输出如下:

**************ABC Megamart*****************
***********------------------**************
Counter Name:  Vegetables & Dairy
onions :  7.28
tomatoes :  4.12
carrots :  4.6575
lettuce :  12.8125
Total: 28.87
***********------------------**************
***************PAID************************

10 件以下物品计数器

与为蔬菜计数器定义的类类似,我们也可以为剩余的三个计数器定义方法。剩余计数器的详细代码可在github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter08找到。

对于这个计数器的代码,让我们创建LessThan10Counter类并添加所有方法,包括return_cartgoto_less_t10_counterreview_itemscount_itemsscan_bar_codeadd_billingadd_taxcalc_billprint_invoicereceive_payment。为了简单起见,让我们看看每个计数器中我们拥有的额外方法,而不是重复所有方法:

  1. 让我们先创建LessThan10Counter类:

    class LessThan10Counter():
    …    
    
  2. 在这个类中,我们有一个goto_less_t10_counter方法,它返回计数器的名称:

        def goto_less_t10_counter(self):
              return 'Less than 10 counter'
    
  3. 我们还有一个以下方法来检查购物车中的物品,确保它们不是电子产品、蔬菜、水果或乳制品:

         def review_items(self,item_type = None):
            veg_cart = ['Vegetables', 'Dairy', 'Fruits']
            if (item_type == 'Electronics'):
                print("Move to Electronics Counter")
            elif (item_type in veg_cart):        
                print("Move to Vege Counter")
    
  4. 在以下方法中,让我们计数以确保购物车中的物品总数少于10

        def count_items(self,cart_items = None):
            if len(cart_items)<=10:
                print("Move to Less than 10 items counter")
            else:
                print("Move to Greater than 10 items counter")
        …
    
  5. 按顺序执行本类的所有方法,结果如下:

    less10 = LessThan10Counter()
    cart = less10.return_cart('paperclips','blue pens','stapler','pencils')
    less10.review_items(item_type = ['stationary'])
    less10.count_items(cart)
    codes = less10.scan_bar_code(113323,3434332,2131243,2332783)
    bill = less10.add_billing(10,15,12,14,codes = codes)
    taxes = less10.add_tax(0.04,0.03,0.035,0.025)
    finalbill = less10.calc_bill(bill,taxes,cart)
    less10.print_invoice(finalbill)
    less10.receive_payment(finalbill)
    

已付款发票的输出如下:

**************ABC Megamart*****************
***********------------------**************
Counter Name:  Less than 10 counter
paperclips :  10.4
blue pens :  15.45
stapler :  12.42
pencils :  14.35
Total: 52.620000000000005
***********------------------**************
***************PAID************************

大于 10 件物品计数器

在本节中,让我们定义大于 10 件物品的计数器类和方法。

对于这里的代码,让我们创建GreaterThan10Counter类并添加所有方法,包括return_cartgoto_greater_t10_counterreview_itemscount_itemsscan_bar_codeadd_billingadd_taxapply_couponcalc_billprint_invoicereceive_payment。为了简单起见,让我们看看每个计数器中我们拥有的额外方法,而不是重复所有方法:

  1. 我们将首先创建GreaterThan10Counter类:

    class GreaterThan10Counter():
    …
    
  2. 在这个类中,我们有一个goto_greater_t10_counter方法计数器,它返回计数器的名称:

        def goto_greater_t10_counter(self):
            return 'Greater than 10 counter'
     …   
    
  3. 接下来,让我们添加一个方法来应用折扣券到所购买的物品上:

        def apply_coupon(self):
            coupon_discount = 0.1
            return coupon_discount        
       …     
    
  4. 按顺序执行这个类的所有方法会产生以下结果:

    greater = GreaterThan10Counter()
    cart = greater.return_cart('paper clips','blue pens','stapler','pencils','a4paper','a3paper','chart',
                              'sketch pens','canvas','water color','acrylic colors')
    greater.review_items(item_type = ['stationary'])
    greater.count_items(cart)
    codes = greater.scan_bar_code(113323,3434332,2131243,2332783)
    bill = greater.add_billing(10,15,12,14,codes = codes)
    taxes = greater.add_tax(0.04,0.03,0.035,0.025)
    greater.apply_coupon()
    finalbill = greater.calc_bill(bill,taxes,cart)
    greater.print_invoice(finalbill)
    greater.receive_payment(finalbill)
    

已支付发票的输出如下:

**************ABC Megamart*****************
***********------------------**************
Counter Name:  Greater than 10 counter
paper clips :  10.4
blue pens :  15.45
stapler :  12.42
pencils :  14.35
Total: 47.358000000000004
***********------------------**************
***************PAID************************

在这个类中,我们为goto_greater_t10_counter定义了不同的方法,并添加了新的apply_coupon方法。

电子计数器

在本节中,让我们定义电子物品计数器的类和方法。在下面的代码中,让我们创建ElectronicsCounter类并添加其所有方法,包括return_cartgoto_electronics_counterreview_itemstest_electronicsscan_bar_codeadd_billingadd_taxapply_couponcalc_billprint_invoicereceive_payment。为了简单起见,让我们看看每个计数器中都有哪些额外的方法,而不是重复所有的方法:

  1. 我们将首先为电子计数器创建一个类:

    class ElectronicsCounter():
    …
    
  2. 在这个类中,我们有一个方法可以转到电子计数器,并返回计数器的名称:

        def goto_electronics_counter(self):
            return 'Electronics counter'
    
  3. 接下来,让我们定义一个方法,它提供电子商品的状态并检查它们是否正常工作:

          def test_electronics(self,*status):
            teststatus = list(status)
            return teststatus            
    
  4. 按顺序执行这个类的所有方法会产生以下结果:

    electronics = ElectronicsCounter()
    cart = electronics.return_cart('television','keyboard','mouse')
    electronics.review_items(item_type = ['Electronics'])
    electronics.test_electronics('pass','pass','pass')
    codes = electronics.scan_bar_code(113323,3434332,2131243)
    bill = electronics.add_billing(100,16,14,codes = codes)
    taxes = electronics.add_tax(0.04,0.03,0.035)
    electronics.apply_coupon()
    finalbill = electronics.calc_bill(bill,taxes,cart)
    electronics.print_invoice(finalbill)
    electronics.receive_payment(finalbill)
    

已支付发票的输出如下:

**************ABC Megamart*****************
***********------------------**************
Counter Name:  Greater than 10 counter
television :  104.0
keyboard :  16.48
mouse :  14.49
Total: 134.97
***********------------------**************
***************PAID************************

在这个课程中,我们为goto_electronics_counter和新的test_electronics方法定义了不同的方法。

定义了序列后,让我们进一步看看这些计数器的共同功能。

识别共同功能

在本节中,让我们看看一个图形表示,它显示了在每个计数器上要执行的功能列表以及所有四个计数器之间的共同功能,如下所示。以下图中的共同功能以粗体字突出显示:

图 8.5 – 在每个计数器上执行的操作

图 8.5 – 在每个计数器上执行的操作

图 8.5中,所有以粗体字突出显示的功能在所有四个计数器中都是共同的。review_items函数在少于 10 个物品的计数器、多于 10 个物品的计数器和电子计数器中是共同的。count_items函数在少于 10 个物品的计数器和多于 10 个物品的计数器中是共同的。apply_coupon函数在多于 10 个物品的计数器和电子计数器中是共同的。由于所有计数器都执行了共同的功能或操作,我们可以考虑创建一个共同的方式来设计它们。这就是我们可以引入模板概念的地方。

设计模板

正如其名所示,模板定义了一个通用的模板或格式,我们可以在这个格式中设计算法流程,并在执行类似活动时重用它们。模板是 Python 中设计模式的方法之一,在开发框架或库时可以有效地使用。模板强调了编程中的可重用性概念。

在本节中,我们将查看创建一个处理本章中讨论的所有四个计数器共同功能的类,并创建一个处理所有计数器中要执行的步骤序列或管道的模板的方法:

  1. 首先,让我们创建一个名为CommonCounter的抽象类,并用所有四个计数器将使用的所有变量初始化类。参考以下代码:

    from abc import ABC, abstractmethod 
    class CommonCounter(ABC):
        def __init__(self,items,name,scan,units,tax,item_type = None, weights = None, status = None):
            self.items = items
            self.name = name
            self.scan = scan
            self.units = units
            self.tax = tax
            self.item_type = item_type
            self.weights = weights
            self.status = status
    
  2. 接下来,我们将定义return_cartgoto_counterscan_bar_code方法,以获取在类中初始化的输入变量:

        def return_cart(self):
            cart_items = []
            for i in self.items:
                cart_items.append(i)
            return cart_items
        def goto_counter(self):
            countername = self.name
            return countername
        def scan_bar_code(self):
            codes = []
            for i in self.scan:
                codes.append(i)
            return codes
    
  3. 然后,我们将定义add_billingadd_taxcalc_bill方法,以获取在类中初始化的输入变量:

    def add_billing(self):
            self.codes = self.scan_bar_code()
            pricetag = []
            for i in self.units:
                pricetag.append(i)
            bill = dict(zip(self.codes, pricetag))
            return bill
         def add_tax(self):
            taxed = []
            for i in self.tax:
                taxed.append(i)
            return taxed
         def calc_bill(self):
            bill = self.add_billing()
            items = []
            cart_items = self.return_cart()
            calc_bill = []
            taxes = self.add_tax()
            for item,tax in zip(bill.items(),taxes):
                items.append(item[1])
                calc_bill.append(item[1] + item[1]*tax)
            finalbill = dict(zip(cart_items, calc_bill))
            return finalbill
    
  4. 为了简单起见,我们不会定义打印发票方法,而是定义包含打印发票方法定义的receive_payment方法,以下代码中包含:

    def receive_payment(self):
            finalbill = self.calc_bill()
            final_total = sum(finalbill.values())
            print('**************ABC Megamart*****************')
            print('***********------------------**************')
            print('Counter Name: ', self.goto_counter())
            for item,price in finalbill.items():
                print(item,": ", price)
            print('Total:',final_total)
            print('***********------------------**************')
            print('***************PAID************************')
    
  5. 接下来,我们将定义apply_coupon方法,它返回0值。如果需要,此方法可以在子类中重新定义:

    def apply_coupon(self):
            return 0
    
  6. 在前面的代码片段中,我们定义了所有四个计数器中通用的方法,而在以下代码中,我们将定义不带语句的方法,以便可以在子类中按需重新定义:

    def weigh_items(self):
            pass
    def add_price_tag(self):
            pass
    def count_items(self):
            pass
    def test_electronics(self):
            pass
    
  7. 然后,让我们创建一个作为需要子类中定义的抽象方法的review_items

    @abstractmethod
        def review_items(self):
            pass
    

现在,模板最重要的概念定义在下一行代码中。

  1. 让我们定义一个处理计费计数器操作序列的方法,并使用此方法作为为每个计费计数器创建的子类模板:

    def pipeline_template(self):
            self.return_cart()
            self.goto_counter()
            self.review_items()
            self.count_items()
            self.test_electronics()
            self.weigh_items()
            self.add_price_tag()
            self.scan_bar_code()
            self.add_billing()
            self.add_tax()
            self.apply_coupon()
            self.calc_bill()
            self.receive_payment()
    
  2. 我们已定义了所有计数器的通用类及其模板方法,这些方法可以用于每个单独的计费计数器。

  3. 在以下代码中,我们将为VegeCounter创建一个子类,以CommonCounter为父类:

    class VegeCounter(CommonCounter):
        def review_items(self):
            if ('Vegetables' in self.item_type):
                print("Move to Vege Counter")
            if ('Dairy' in self.item_type):
                print("Move to Vege Counter")
            if ('Fruits' in self.item_type):
                print("Move to Vege Counter")
        def weigh_items(self):
            item_weight = dict(zip(self.items, self.weights))
            return item_weight
        def add_price_tag(self):
            pricetag = []
            item_weight = self.weigh_items()
            for item,price in zip(item_weight.items(),self.units):
                pricetag.append(item[1]*price)
            return pricetag        
    
  4. 在前面的代码中,我们已定义了review_items抽象方法,并在weight_itemsadd_price_tag方法的定义中添加了语句。

  5. 类似地,在以下代码中,让我们为ElectronicsCounter创建一个子类,并定义review_items(这是一个抽象方法),然后重新定义test_electronics(在CommonCounter基类中没有定义):

    class ElectronicsCounter(CommonCounter):
        def review_items(self):
            if ('Electronics' in self.item_type):
                print("Move to Electronics Counter")
          def test_electronics(self):
            teststatus = []
            for i in self.status:
                teststatus.append(i)
            return teststatus
    
  6. 让我们现在创建一个函数来为每个子类运行pipeline_template方法:

    def run_pipeline(counter = CommonCounter):
        counter.pipeline_template()
    
  7. 对每个子类执行run_pipeline方法会导致根据每个计费计数器执行一系列步骤。让我们为蔬菜计数器执行pipeline方法:

    run_pipeline(VegeCounter(items = ['onions', 'lettuce', 'apples', 'oranges'],
                             name = ['Vegetable Counter'],
                             scan = [113323,3434332,2131243,2332783],
                             units = [10,15,12,14],
                             tax = [0.04,0.03,0.035,0.025],
                             item_type = ['Vegetables'],
                             weights = [1,2,1.5,2.5]))
    

执行VegeCounterpipeline方法后的输出如下:

Move to Vege Counter
**************ABC Megamart*****************
***********------------------**************
Counter Name:  ['Vegetable Counter']
paperclips :  10.4
blue pens :  15.45
stapler :  12.42
pencils :  14.35
Total: 52.620000000000005
***********------------------**************
***************PAID************************
  1. 让我们现在为ElectronicsCounter执行pipeline方法:

    run_pipeline(ElectronicsCounter(items = ['television','keyboard','mouse'],
                                    name = ['Electronics Counter'],
                                    scan = [113323,3434332,2131243],
                                    units = [100,16,14],
                                    tax = [0.04,0.03,0.035],
                                    item_type = ['Electronics'],
                                    status = ['pass','pass','pass']))
    

执行ElectronicsCounterpipeline方法后的输出如下:

Move to Electronics Counter
**************ABC Megamart*****************
***********------------------**************
Counter Name:  ['Electronics Counter']
television :  104.0
keyboard :  16.48
mouse :  14.49
Total: 134.97
***********------------------**************
***************PAID************************

在本节中,我们创建了一个模板,但我们没有在多个类定义中重复相同的方法。相同的CommonCounter抽象类也可以用于小于 10 个项目的计数器和大于 10 个项目的计数器的定义。我们学习了如何创建模板并实现模板编程,这强调了在 Python 应用程序开发中的可重用性。我们创建了一个覆盖多组操作的所有常见功能的模板,并且多次重用了该模板。

摘要

在本章中,我们学习了为遵循算法的一系列操作定义方法的概念。我们还定义了遵循从我们的核心示例中一系列操作的类。我们创建了一个抽象类,它定义了我们的核心示例的所有常见功能,并且我们通过使用我们的核心示例中的序列来应用模板设计模式,以理解模板的概念。

与本书中其他章节类似,本章也涵盖了模板,这是一种在元编程中应用的设计模式,用于外部改变 Python 对象的行为。

在下一章中,我们将通过一些有趣的例子来探讨抽象语法树的概念。

第三部分:深入探讨 – 元编程的构建块 II

本节是第二部分的延续。本节的目标是通过详细探讨更多高级构建块,如抽象语法树和 MRO 等,来加深你对元编程概念的理解,同时提供如何在实际场景中应用它们的示例。本节将包含通过基于实现的解释方法来解释概念的章节,以在阅读本书时为用户提供动手经验和指导性编码知识。本节中的章节可以按顺序或独立阅读。

本部分包含以下章节:

  • 第九章, 通过抽象语法树理解代码

  • 第十章, 理解继承的方法解析顺序

  • 第十一章, 创建动态对象

  • 第十二章, 应用 GOF 设计模式第一部分

  • 第十三章, 应用 GOF 设计模式第二部分

  • 第十四章, 代码生成

  • 第十五章, 基于端到端案例研究的应用开发

  • 第十六章, 遵循最佳实践

第九章:第九章:通过抽象语法树理解代码

在本章中,我们将探讨抽象语法树是什么,以及如何理解我们编写的 Python 代码中每个单元的语法树。

任何编程语言都是设计有其自己的语法,开发者在使用该语言编码时遵循特定的语法。编程语言的解释器或编译器解释语言的语法,编译或解释代码并执行它以实现预期的结果。

在 Python 中,ast 可以用来理解我们开发的代码的抽象语法。

在本章中,我们将探讨理解我们在前几章中开发的一些重要代码片段的语法树,同时我们还将通过几个示例来查看修改或添加更多信息的代码。我们将在本章中使用抽象语法树来对代码进行分析。

在本章中,我们将探讨以下主要主题:

  • 探索 ast

  • 使用抽象语法树检查 Python 代码

  • 通过应用理解抽象语法树

到本章结束时,你应该能够理解 Python 代码的抽象语法树。你还应该能够通过元编程检查、解析和修改源代码的抽象语法树。

技术要求

本章中分享的代码示例可在 GitHub 上找到,地址为:github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter9

探索 ast

在本节中,我们将探索 ast Python 库,该库可以从 Python 3 中导入以分析开发者编写的 Python 代码。我们还可以通过其抽象语法树在元编程级别上修改代码,而不是修改代码本身的语法。这有助于理解代码是如何在语法上表示的,以及代码的语法树如何被用来修改其行为而不修改原始源代码。我们将查看 ast 库的一些重要功能,因为这些功能将在本章中用于理解我们的核心示例。

让我们从导入 ast 库开始:

import ast

一旦我们导入库,我们就可以使用这个库来分析一段代码。现在我们将创建一个名为 assignment 的变量,并将代码的字符串格式赋值给它:

assignment = "product_name = 'Iphone X'"

assignment 变量的输出如下所示:

assignment
"product_name = 'Iphone X'"

前面的代码可以使用 ast 库的 parse 方法解析为其对应的节点。现在我们将创建一个名为 assign_tree 的变量,并将存储在 assignment 下的代码行的解析节点存储到它中:

assign_tree = ast.parse(assignment)
assign_tree

解析节点的输出如下所示:

<ast.Module at 0x1b92b3f6520>

现在,我们可以使用另一个名为dump的方法来打印节点的树结构,包括其每个值和字段。这有助于调试代码:

print(ast.dump(assign_tree,indent = 4))

代码的输出如下:

图 9.1 – 抽象语法树示例

图 9.1 – 抽象语法树示例

"product_name = 'Iphone X'"代码被分解成多个部分。任何 Python 代码的语法都是语法嵌入到Module后面跟着body。我们将Iphone X的值赋给product_name变量,因此执行值赋值的代码被识别为Assign分支,该分支具有与相应 ID、上下文和值映射的属性。这是一个简单节点表示的例子。对于多行代码和多种其他操作,节点将在树中有多个其他分支。

让我们从以下部分开始检查使用抽象语法树的 Python 代码的几个示例。

使用抽象语法树检查 Python 代码

在本节中,我们将回顾和理解简单算术加法示例的代码,并且将进一步探讨使用抽象语法树解析和修改代码。

使用 ast 回顾简单代码

在本节中,让我们回顾一下简单的加法代码,并查看节点中的所有元素,以及这些元素在树中的组织方式。让我们先编写代码来赋值两个变量ab的数值,以及c作为ab的和。最后,让我们打印c的值。这在上面的代码中显示如下:

addfunc = """
a = 1098
b = 2032
c = a + b
print(c)
"""

我们现在将解析前面的addfunc并将节点存储在另一个名为add_tree的变量中:

add_tree = ast.parse(addfunc)
add_tree

解析后的节点输出如下:

<ast.Module at 0x19c9b2bf2e0>

节点的基元素是Module,所有其他代码行都被分割成存储在节点模块中的语义。

让我们通过在树上调用dump方法来查看以下代码中的详细树表示:

print(ast.dump(add_tree, indent=4))

树以Module作为其基本元素,或者说是树干,随后是多个分支。Module后面跟着一个body作为列表项,列出了代码的所有其他元素。

body中,将有四个列表项描述addfunc的操作。第一个,也是addfunc的第一行,是将Constant1098赋给一个名为 id 的变量,其值为a,上下文为Store,因为我们正在将值存储在变量中。它看起来是这样的:

图 9.2 – 代码片段输出

图 9.2 – 代码片段输出

同样,addfunc的第二行是将2032值存储在b变量中,这在以下列表项中以语法形式表示:

图 9.3 – 代码片段输出

图 9.3 – 代码片段输出

addfunc中的第三行代码执行了将存储在ab中的两个值相加的算术操作:

图 9.4 – 代码片段输出

图 9.4 – 代码片段输出

前面的代码有一个额外的元素BinOp,后面跟着leftopright变量,分别表示左数值、加法操作和右数值。

addfunc中的最后一行代码是Expr表达式元素,它表示以Load上下文值打印c变量:

图 9.5 – 代码片段输出

图 9.5 – 代码片段输出

要执行addfunc,我们需要首先按照以下方式编译解析树:

add_code = compile(add_tree, 'add_tree', 'exec')

编译后,我们应该能够执行编译后的树,这将导致ab的相加:

exec(add_code)

以下为代码的输出:

3130

在本节中,我们回顾了简单算术add函数的抽象语法树。在下一节中,让我们看看如何使用元编程修改add函数的代码。

使用 ast 修改简单代码

在本节中,让我们考虑上一节中的addfunc示例,并看看如何通过元编程修改示例中的代码,而不修改实际代码。addfunc中代码执行的操作是算术加法。如果我们想执行算术乘法而不是加法,并且不想修改实际代码,会怎样?如果我们想在多个位置将算术加法替换为算术乘法,而浏览数千行代码并修改它们不是一个可行的选项,因为它可能会影响或破坏代码中的其他部分,会怎样?在这种情况下,我们可以通过修改代码的节点而不是修改实际代码本身来修改代码的节点。为了实现这一点,让我们利用代码的抽象语法树。

让我们重用前面代码中的add_tree解析树变量:

add_tree
<ast.Module at 0x19c9b2bf2e0>

要了解哪些字段需要修改,让我们看看以下节点的表示,并查看节点中由标识符标记的每个部分。本例中感兴趣的元素在以下图中用方框表示:

图 9.6 – 的解析节点

图 9.6 – addfunc的解析节点

要将加法操作修改为乘法操作,此节点的树遍历body,然后是其列表项2,然后是项的value字段,然后是op字段。op字段的Add()操作必须修改为乘法操作,以实现本节的目标。以下是方法:

add_tree.body[2].value.op=ast.Mult()

执行前面的代码会导致树的变化:

print(ast.dump(add_tree, indent=4))

更新后的树结构图如下所示,其中Add()操作被Mult()操作所替代:

图 9.7 – 修改后的树以执行乘法

图 9.7 – 修改后的树以执行乘法

为了验证前面在树节点上的修改是否有效,让我们编译树并执行它以检查结果:

add_code = compile(add_tree, 'add_tree', 'exec')
exec(add_code)

执行前面的代码理想情况下应该提供输出 3130,这是两个数字 10982032 的和。但我们已经修改了 ast 以执行乘法,因此它将得到的结果是两个数字的乘积:

2231136

因此,树现在已经被修改,可以编译以实现所需的结果,而无需修改实际代码。

通过这种理解,让我们进一步探讨如何解析和理解 Python 中的类。

理解抽象语法树及其应用

在本节中,我们将探讨将抽象语法树的概念应用于我们的核心示例 ABC Megamart,并探索 ast 在类中的定义,例如 ABC MegamartBranch 类和 VegCounter 类。我们还将探讨如何使用 ast 在元编程级别上修改这些类的行为,而不是修改类的实际源代码。

理解类的 ast

在本节中,我们将探讨理解类的抽象语法树,这将帮助我们探索如何通过元编程修改类的元素。我们可以尝试如下:

  1. 让我们从创建一个具有空定义的类开始,并查看其抽象语法树:

    branch_code = """
    class Branch:
        '''attributes...'''
        '''methods...'''
    """ 
    
  2. 接下来,让我们解析代码:

    branch_tree = ast.parse(branch_code)
    branch_tree
    <ast.Module at 0x216ed8b5850>
    
  3. 让我们进一步查看节点元素,并了解类是如何在语法上定义的:

    print(ast.dump(branch_tree, indent=4))
    

节点的结构如下:

图 9.8 – 代码片段输出

图 9.8 – 代码片段输出

在前面的输出中,我们有 Module 后跟 body,在 body 元素内部有 ClassDef。这个 ClassDef 有一个 name 元素,后面跟着两个表达式。

  1. 让我们重新定义这个空类定义,并添加一个属性和一个方法,以及一个装饰器,然后重新检查节点的结构:

    branch_code = """class Branch:
        branch_id = 1001
        @staticmethod
        def get_product(self):
            return 'product'
            """
    
  2. 我们将在以下步骤中解析 branch_code

    branch_tree = ast.parse(branch_code)
    print(ast.dump(branch_tree, indent=4))
    

Branch 类的抽象语法树结构如下。我们可以看到节点从 Module 元素开始,后面跟着 body

body 中,我们有一个包含类名及其属性的 ClassDef 元素,这些属性包括存储为常量的 branch_id,后面跟着带有其参数的 get_product 方法。请参阅以下输出:

图 9.9 – 代码片段输出

图 9.9 – 代码片段输出

  1. 我们还在 decorator_list 下加载了一个 decorator 方法,如下所示:

图 9.10 – 代码片段输出

图 9.10 – 代码片段输出

  1. 如果我们为该类创建一个对象,该对象的代码也可以像前面的类示例一样进行解析:

    branch_code = """
    branch_albany = Branch()
    """
    branch_tree = ast.parse(branch_code)
    print(ast.dump(branch_tree, indent=4))
    
  2. 对象的节点将具有以下结构:

图 9.11 代码片段输出

图 9.11 代码片段输出

在本节中,我们回顾了类的抽象语法树,以了解其语法的各种元素。有了这种理解,让我们进一步探讨从我们的核心示例 ABC Megamart 中修改类的抽象语法树。

通过解析修改代码块的 ast

在本节中,让我们看看如何通过使用类的抽象语法树来修改代码中的属性,而不是直接修改类本身。

让我们考虑已经开发了一个具有多个类和方法的健壮库。健壮的库定义可能太大,无法被打扰或修改。而不是修改源代码,我们可以通过元编程在库中的一些特定属性上进行更改,而不影响实际的代码。在这种情况下,修改库的 ast 将是比影响库的源代码更好的更改方式。

在本例中,我们将遵循以下步骤:

  1. 我们将创建一个 vegCounter 类,并添加一个 return_cart 方法来返回购物车中的项目。我们还将创建类的对象,并在对象上调用 return_cart 方法。请参考以下代码:

    vegctr = """
    class VegCounter():
        def return_cart(self,*items):
            cart_items = list(items)
            return cart_items
    veg = VegCounter()
    print(veg.return_cart('onions','tomatoes','carrots','lettuce'))
    """
    
  2. 接下来,让我们解析 vegCounter 的代码,并查看节点的结构:

    vegctr_tree = ast.parse(vegctr)
    print(ast.dump(vegctr_tree, indent=4))
    

节点的输出如下。在 ast 中,有一个类定义后跟一个函数定义:

图 9.12 – 代码片段输出

图 9.12 – 代码片段输出

  1. 以下输出具有列表项和读取项目到列表的逻辑元素:

图 9.13 – 代码片段输出

图 9.13 – 代码片段输出

  1. 以下输出显示了创建 VegCounter 类对象的语法:

图 9.14 – 代码片段输出

图 9.14 – 代码片段输出

  1. 以下输出显示了通过在购物项列表上调用 return_cart 方法来打印购物项的元素:

图 9.15 – 代码片段输出

图 9.15 – 代码片段输出

  1. 现在让我们编译抽象语法树并执行它以显示添加到购物车中的项目列表:

    vegctr_code = compile(vegctr_tree, 'vegctr_tree', 'exec')
    exec(vegctr_code)
    ['onions', 'tomatoes', 'carrots', 'lettuce']
    
  2. 接下来,让我们在购物项的值中导航,并查看 return_cart 方法输出中第二个值的路径:

    vegctr_tree.body[2].value.args[0].args[1].n
    'tomatoes'
    
  3. 现在让我们通过逐级解析节点元素,将购物项的第二个值从 tomatoes 更改为 potatoes

    vegctr_tree.body[2].value.args[0].args[1].n = 'potatoes'
    print(ast.dump(vegctr_tree, indent=4))
    
  4. 在以下输出中,让我们看看购物车中第二个项目的更新值,该值在未更改源代码的情况下进行了修改:

图 9.16 – 在 ast 中修改值

图 9.16 – 在 ast 中修改值

  1. 我们现在可以使用 ast 库中的 unparse 方法来解析节点,如下所示:

    print(ast.unparse(vegctr_tree))
    
  2. 修改后的源代码现在看起来如下所示:

    class VegCounter:
        def return_cart(self, *items):
            cart_items = list(items)
            return cart_items
    veg = VegCounter()
    print(veg.return_cart('onions', 'potatoes', 'carrots', 'lettuce'))
    

这是一种使用抽象语法树修改 Python 源代码的方法。

基于这种理解,让我们继续探讨下一个方法,即我们将转换抽象语法树的节点。

通过转换节点修改代码块的 ast

在本节中,我们将探讨另一种通过修改抽象语法树而不是实际代码来修改类源代码的方法:

  1. 让我们现在创建一个名为 VegCounter 的类,如下所示:

    class VegCounter():
        def return_cart(self,*items):
            cart_items = []
            for i in items:
                cart_items.append(i)
            return cart_items
    veg = VegCounter()
    
  2. 接下来,让我们创建一个名为 cart 的变量,并将对象上的函数调用作为一个字符串添加:

    cart = """veg.return_cart('onions','tomatoes','carrots','lettuce')"""
    cart_tree = ast.parse(cart)
    print(ast.dump(cart_tree, indent = 4))
    
  3. 解析前面的代码提供了以下输出:

图 9.17 – 对象变量的 AST

图 9.17 – 对象变量的 AST

在本节中,我们不会遍历节点的结构,而是将使用 ast 库的 NodeTransformer 来执行代码转换:

from ast import NodeTransformer
  1. NodeTransformer 的属性如下:

图 9.18 – NodeTransformer 的属性

图 9.18 – NodeTransformer 的属性

  1. 接下来,让我们创建一个名为 ModifyVegCounter 的类,它继承自 NodeTransfomer 类。我们将重新定义 visit_Constant 方法,以便在代码中常数值出现时添加一个字符串前缀来修改购物项的常数值:

    class ModifyVegCounter(NodeTransformer):
        def visit_Constant(self, node):
            modifiedValue = ast.Constant('item:' + str(node.value))
            return modifiedValue
    
  2. 我们可以利用 visit 方法来访问节点,并使用 dump 方法来打印树:

    ModifyVegCounter().visit(cart_tree)
    print(ast.dump(cart_tree, indent = 4))
    

转换后的节点如下所示:

图 9.19 – 使用 NodeTransformer 转换的源代码

图 9.19 – 使用 NodeTransformer 转换的源代码

  1. 我们可以使用 ast 库的 unparse 方法进一步将代码反解析:

    print(ast.unparse(cart_tree))
    

代码的输出如下所示:

veg.return_cart('item:onions', 'item:tomatoes', 'item:carrots', 'item:lettuce')

这又是抽象语法树在元编程中应用的另一个例子。

在本节中,我们介绍了使用 ast 库的 NodeTransformer 方法来转换抽象语法树节点的技术。

摘要

在本章中,我们通过探索 Python 3 中的 ast 库来了解抽象语法树的概念。我们还使用抽象语法树检查了 Python 代码。通过使用我们的核心示例中的源代码在节点级别修改代码,我们理解了抽象语法树的应用。

与本书中的其他章节类似,本章介绍了元编程中的抽象语法树的概念。这也有助于理解如何在不修改源代码的情况下,从外部修改 Python 对象的行为。通过修改代码中的实际方法和属性,而不是抽象语法树,可以方便地将源代码从不同的 Python 版本或应用程序开发平台迁移,而不会影响代码的实际逻辑。

在下一章中,我们将探讨方法解析顺序的概念,并伴随一些有趣的示例。

第十章:第十章:理解继承的方法解析顺序

在本章中,我们将探讨 Python 3 中方法解析顺序MRO)的概念以及它在继承中的工作方式。

如其名所示,MRO 是类在程序中调用方法时解析方法的顺序。

在本章中,我们将通过几个示例来了解 MRO,了解方法解析可能出错的情况,以及当前 Python 3 实现如何处理在类中定义的方法。我们将在本章中使用 MRO 来理解在 Python 3 中实现继承时的代码行为。

为什么我们应该理解 MRO?在 Python 代码中使用多个类的情况下,我们需要从多个父类或超类继承方法。了解方法从现有类到其父类解析的顺序有助于避免错误的调用方法。这反过来又有助于避免 Python 代码算法中的错误结果。

本章将探讨以下主要主题:

  • 理解类的 MRO

  • 理解修改继承顺序的影响

  • 不当改变继承顺序的影响

到本章结束时,你应该能够理解 Python 类层次结构中方法的解析方式,了解在多继承中方法是如何处理的,并根据自己的知识编写方法。

技术要求

本章中分享的代码示例可在 GitHub 上找到,位于本章代码的以下位置:github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter10.

理解类的 MRO

在本节中,让我们探索在代码中没有指定继承的类中方法是如何解析的。在 Python 3 中,类默认继承自object。为了理解 MRO 在没有父类的类上的工作方式,以最简单的方式查看它是最容易的方法。然后我们将看到 MRO 在单继承、多继承和多级继承中的工作方式。

在这个例子中,让我们创建一个代表ABC Megamart分支的类,如下所示:

  1. Branch类中,让我们为分支 ID、街道、城市、州和邮政编码、产品、销售额和发票创建属性。让我们还创建一些方法,如get_product(返回产品)、get_sales(返回销售额)和get_invoice(返回发票)。以下代码表示Branch类:

    class Branch:
        def __init__(self, branch_id, branch_street, 
                     branch_city, branch_state, 
                     branch_zip, product, sales, invoice):
            self.branch_id = branch_id
            self.branch_street = branch_street
            self.branch_city = branch_city
            self.branch_state = branch_state
            self.branch_zip = branch_zip
            self.product = product
            self.sales = sales
            self.invoice = invoice        
        def get_product(self):
            return self.product
        def get_sales(self):
            return self.sales
        def get_invoice(self):
            return self.invoice
    

在前面的类中,有五个属性和三个方法。可以通过在类上调用一个内置方法来查看前面类的 MRO,该方法称为mro

  1. 接下来,让我们调用Branch类的mro方法:

    Branch.mro()
    

Branch类的mro表示如下:

[__main__.Branch, object]

在前面的输出中,我们可以看到Branch类没有对超类或父类进行任何显式定义,因此它默认继承自object

在本节中,我们了解了 MRO 的概念,并通过一个例子展示了如何查看类的 MRO。现在,让我们进一步看看 MRO 在只有一个父类或超类的类上是如何工作的。

理解单一继承中的 MRO

当一个类继承一个父类或超类时,这是单一继承。让我们看看在Branch类成为父类时,方法是如何解决的。

  1. 在创建子类之前,让我们重新定义Branch类,并添加适合测试这个概念的方法:

    class Branch:
        def __init__(self, branch, sales, product):
            self.branch = branch
            self.sales = sales
            self.product = product
        def set_branch(self, value):
            self.branch = value          
    
        def set_sales(self, value):
            self.sales = value            
        def set_product(self, value):
            self.product = value        
        def calc_tax(self):
            branch = self.branch
            product = self.product
            sales = self.sales
            pricebeforetax = sales['purchase_price'] + 
                             sales['purchase_price'] * 
                             sales['profit_margin']
            finalselling_price = pricebeforetax + 
                (pricebeforetax * sales['tax_rate'])
            sales['selling_price'] = finalselling_price
            return branch, product, sales
    
  2. 对于这个例子,让我们创建另一个类,命名为NYC,它继承自Branch类:

    class NYC(Branch):
        def __init__(self, intercitybranch):
            self.intercitybranch = intercitybranch
    
        def set_management(self, value):
            self.intercitybranch = value
    
        def calc_tax_nyc(self):
            branch = self.branch
            intercitybranch = self.intercitybranch
            product = self.product
            sales = self.sales
            pricebeforetax = sales['purchase_price'] + 
                             sales['purchase_price'] * 
                             sales['profit_margin']
            finalselling_price = pricebeforetax + 
                (pricebeforetax * (sales['tax_rate'] + 
                 sales['local_rate']))  
            sales['selling_price'] = finalselling_price
            return branch, intercitybranch, product, 
                   sales    
    NYC.mro()
    

在前面的代码中,我们有从Branch类继承的NYC类,NYC类定义了两个方法。set_management方法返回存储在intercitybranch中的值,而calc_tax_nyc方法计算NYC的税费。

NYC类的 MRO 在以下输出中表示:

[__main__.NYC, __main__.Branch, object]

NYC类中存在的方法将首先被解决,然后是Branch类的方法,最后是object类的方法。

  1. 让我们看看当NYC需要的方法不在NYC中定义,而是在其父类中定义时会发生什么。在NYC类中,calc_tax_nyc是计算NYC分支税费的函数,这个函数需要branchintercitybranchproductsales等属性的值。intercitybranch属性的值可以在NYC类中使用set_management方法单独设置,而其他属性,如branchproductsales,在NYC中没有设置方法。

  2. 让我们从创建一个名为intercitybranch的变量并定义NYC的实例开始:

    intercitybranch = {
        }
    branch_manhattan = NYC(intercitybranch)
    
  3. 让我们先设置intercitybranch的值,然后看看如何处理剩余属性的设置方法:

    branch_manhattan.set_management({'regionalManager' : 'John M',
        'branchManager' : 'Tom H',
        'subbranch_id' : '2021-01' })
    
  4. 设置branchproductsales所需的设置方法在Branch类的父类中可用。由于NYC类的 MRO 是先从NYC开始,然后是Branch,最后是object,因此NYC现在可以调用Branch的设置方法来设置branchproductsales的值,如下所示:

    branch = {'branch_id' : 2021,
    'branch_street' : '40097 5th Main Street',
    'branchBorough' : 'Manhattan',
    'branch_city' : 'New York City',
    'branch_state' : 'New York',
    'branch_zip' : 11007}
    product = {'productId' : 100002,
        'productName' : 'WashingMachine',
        'productBrand' : 'Whirlpool'  
    }
    sales = {
        'purchase_price' : 450,
        'profit_margin' : 0.19,
        'tax_rate' : 0.4,
        'local_rate' : 0.055      
    }
    branch_manhattan.set_branch(branch)
    branch_manhattan.set_product(product)
    branch_manhattan.set_sales(sales)
    
  5. 现在所需的值都已设置,我们可以从继承自Branch类的NYC类中调用calc_tax_nyc方法:

    branch_manhattan.calc_tax_nyc()
    
  6. 使用税率和其他支持值(branchproductsales)计算出的销售价格,这些值是通过父类设置的,在以下输出中表示:

    ({'branch_id': 2021,
      'branch_street': '40097 5th Main Street',
      'branchBorough': 'Manhattan',
      'branch_city': 'New York City',
      'branch_state': 'New York',
      'branch_zip': 11007},
     {'regionalManager': 'John M',
      'branchManager': 'Tom H',
      'subbranch_id': '2021-01'},
     {'productId': 100002,
      'productName': 'WashingMachine',
      'productBrand': 'Whirlpool'},
     {'purchase_price': 450,
      'profit_margin': 0.19,
      'tax_rate': 0.4,
      'local_rate': 0.055,
      'selling_price': 779.1525})
    

在本节中,我们探讨了具有单一继承的类中 MRO 的工作方式。现在,让我们看看当一个类从两个类继承时会发生什么。

理解多重继承中的 MRO

在本节中,我们将探讨从多个超类或父类继承及其相应的 MRO。

对于这个例子,让我们创建两个父类,ProductBranch,如下所示:

  1. Product类将有一组属性,后面跟着一个名为get_product的方法:

    class Product:
        _product_id = 100902
        _product_name = 'Iphone X'
        _product_category = 'Electronics'
        _unit_price = 700
    
        def get_product(self):
            return self._product_id, self._productName, self._product_category, self._unit_price
    
  2. Branch类将有一组属性,后面跟着一个名为get_branch的方法:

    class Branch:
        _branch_id = 2021
        _branch_street = '40097 5th Main Street'
        _branch_borough = 'Manhattan'
        _branch_city = 'New York City'
        _branch_state = 'New York'
        _branch_zip = 11007
    
        def get_branch(self):
            return self._branch_id, self._branch_street, 
                self._branch_borough, self._branch_city, 
                self._branch_state, self._branch_zip
    
  3. 接下来,让我们创建一个名为Sales的子类或子类,并从ProductBranch类继承。Sales将有一个属性date和一个get_sales方法:

    class Sales(Product, Branch):
        date = '08/02/2021'
        def get_sales(self):
            return self.date, Product.get_product(self), 
                   Branch.get_branch(self)
    
  4. Sales类继承自Product,然后是Branch

    Sales.mro()
    
  5. 让我们看看其方法解析的顺序:

    [__main__.Sales, __main__.Product, __main__.Branch, object]
    

在前面的输出中,方法的解析顺序是按照SalesProductBranchobject的顺序进行的。如果一个由Sales类的对象调用的方法不在Sales中,MRO 算法将在Product类中搜索它,然后是Branch类。

  1. 让我们创建另一个类(命名为Invoice),并以与Sales类继承不同的顺序继承BranchProduct

    class Invoice(Branch, Product):
        date = '08/02/2021'
        def get_invoice(self):
            return self.date, Branch.get_branch(self), 
                   Product.get_product(self)
    
  2. 让我们检查Invoice类的mro

    Invoice.mro()
    
  3. Invoice类的mro(方法解析顺序)在以下输出中表示:

    [__main__.Invoice, __main__.Branch, __main__.Product, object]
    

在前面的输出中,方法的解析顺序是按照InvoiceBranchProductobject的顺序进行的。如果一个由Invoice类的对象调用的方法不在Invoice中,MRO 算法将在Branch类中搜索它,然后是Product类。

在多重继承的情况下,我们回顾了在 Python 3 中,当继承超类或父类的顺序改变时,方法解析顺序如何变化。

现在,让我们看看在多层继承的情况下 MRO(Method Resolution Order,方法解析顺序)会发生什么。

复习多层继承中的 MRO

Python 中的类也可以在多个级别上从超类继承,随着超类或父类数量的增加,MRO 变得更加复杂。在本节中,我们将通过一些额外的示例来查看这种多重继承的方法解析顺序。

在这个例子中,我们将执行以下步骤:

  1. 让我们首先创建一个名为StoreCoupon的类,我们将在这个类中定义商店的属性,例如产品名称、产品类别、产品的品牌、销售产品的商店名称、产品的过期日期以及购买以获得优惠券的数量:

  2. 然后,我们将定义一个名为generate_coupon的方法,我们将在这个方法中为产品生成两个具有随机优惠券 ID 值和产品及其商店的所有详细信息的优惠券:

    class StoreCoupon:
        productName = "Strawberry Ice Cream"
        product_category = "Desserts"
        brand = "ABCBrand3"
        store = "Los Angeles Store"
        expiry_date = "10/1/2021"
        quantity = 10
    
        def generate_coupon(self):
            import random
            coupon_id =  random.sample(range(
                         100000000000,900000000000),2)
            for i in coupon_id:
                print('***********------------------**************')
                print('Product:', self.productName)
                print('Product Category:', 
                       self.product_category)
                print('Coupon ID:', i)
                print('Brand:', self.brand)
                print('Store:', self.store)
                print('Expiry Date:', self.expiry_date)
                print('Quantity:', self.quantity)
                print('***********------------------
                       **************')
    
  3. 现在,让我们定义一个类SendStoreCoupon,它继承自StoreCoupon,并且不对它添加任何方法或属性:

    class SendStoreCoupon(StoreCoupon):
        pass
    SendStoreCoupon.mro()
    
  4. 这个类的 MRO 在以下输出中表示:

    [__main__.SendStoreCoupon, __main__.StoreCoupon, object]
    
  5. SendStoreCoupon中的方法首先解析,然后是StoreCoupon类中的方法,最后是object

  6. 让我们通过定义另一个名为SendCoupon的类并从SendStoreCoupon类继承它来添加一个继承级别:

    class SendCoupon(SendStoreCoupon):
        pass
    SendCoupon.mro()
    
  7. 该类的 MRO(Method Resolution Order,方法解析顺序)在以下输出中展示:

    [__main__.SendCoupon,
      __main__.SendStoreCoupon,
     __main__.StoreCoupon,
     object]
    
  8. 在前面的输出中,方法是从SendCoupon解析到SendStoreCoupon,然后是StoreCoupon,最后是object

  9. 让我们为SendCoupon类创建一个对象并调用generate_coupon方法:

    coupon = SendCoupon()
    coupon.generate_coupon()
    
  10. SendCoupon类没有为generate_coupon方法定义,因此,根据 MRO,将调用父类或超类的SendStoreCoupon方法,如下所示输出:

    ***********------------------**************
    Product: Strawberry Ice Cream
    Product Category: Desserts
    Coupon ID: 532129664296
    Brand: ABCBrand3
    Store: Los Angeles Store
    Expiry Date: 10/1/2021
    Quantity: 10
    ***********------------------**************
    ***********------------------**************
    Product: Strawberry Ice Cream
    Product Category: Desserts
    Coupon ID: 183336814176
    Brand: ABCBrand3
    Store: Los Angeles Store
    Expiry Date: 10/1/2021
    Quantity: 10
    ***********------------------**************
    

在这个例子中,我们探讨了方法是如何从继承的一个级别解析到另一个级别的。

现在,让我们进一步探讨修改继承顺序的影响。

理解修改继承顺序的重要性

在本节中,我们将探讨从多个父类继承的情况。我们将看到当父类顺序改变时,除了上一节中创建的SendStoreCoupon类之外,方法解析会发生什么变化:

  1. 首先,我们将创建另一个名为ManufacturerCoupon的类,在该类中我们将定义制造商的属性,例如产品名称、产品类别、产品品牌、产品销售制造商名称、产品有效期以及购买以获得优惠券的数量。

  2. 我们将定义一个名为generate_coupon的方法,在该方法中我们将为产品生成两个优惠券,具有随机的优惠券 ID 值以及产品及其制造商的所有详细信息:

    class ManufacturerCoupon:
        productName = "Strawberry Ice Cream"
        product_category = "Desserts"
        brand = "ABCBrand3"
        manufacturer = "ABC Manufacturer"
        expiry_date = "10/1/2021"
        quantity = 10
    
        def generate_coupon(self):
            import random
            coupon_id =  random.sample(range(
                         100000000000,900000000000),2)
            for i in coupon_id:
                print('***********------------------**************')
                print('Product:', self.productName)
                print('Product Category:', 
                       self.product_category)
                print('Coupon ID:', i)
                print('Brand:', self.brand)
                print('Manufacturer:', self.manufacturer)
                print('Expiry Date:', self.expiry_date)
                print('Quantity:', self.quantity)
                print('***********------------------
                       **************')
    
  3. 让我们也定义具有两个父类——ManufacturerCouponSendStoreCouponSendCoupon类:

    class SendCoupon(ManufacturerCoupon,SendStoreCoupon):
        pass
    SendCoupon.mro()
    
  4. 该类的 MRO 在以下输出中展示:

    [__main__.SendCoupon,
     __main__.ManufacturerCoupon,
     __main__.SendStoreCoupon,
     __main__.StoreCoupon,
     object]
    
  5. 让我们进一步为该类创建一个对象并调用generate_coupon方法:

    coupon = SendCoupon()
    coupon.generate_coupon()
    
  6. 在这个例子中,generate_coupon方法为制造商生成了优惠券,因为第一个具有generate_coupon方法定义的父类是ManufacturerCoupon。以下是从generate_coupon方法生成的优惠券:

    ***********------------------**************
    Product: Strawberry Ice Cream
    Product Category: Desserts
    Coupon ID: 262335232934
    Brand: ABCBrand3
    Manufacturer: ABC Manufacturer
    Expiry Date: 10/1/2021
    Quantity: 10
    ***********------------------**************
    ***********------------------**************
    Product: Strawberry Ice Cream
    Product Category: Desserts
    Coupon ID: 752333180295
    Brand: ABCBrand3
    Manufacturer: ABC Manufacturer
    Expiry Date: 10/1/2021
    Quantity: 10
    ***********------------------**************
    
  7. 让我们在SendCoupon类中进一步改变继承顺序,并查看方法是如何解析的:

    class SendCoupon(SendStoreCoupon,ManufacturerCoupon):
        pass
    SendCoupon.mro()
    
  8. 该类的 MRO 在以下输出中展示:

    [__main__.SendCoupon,
     __main__.SendStoreCoupon,
     __main__.StoreCoupon,
     __main__.ManufacturerCoupon,
     object]
    
  9. 让我们进一步为该类创建一个对象并调用generate_coupon方法:

    coupon = SendCoupon()
    coupon.generate_coupon()
    
  10. 在这个例子中,generate_coupon方法为商店生成了优惠券,因为第一个具有generate_coupon方法定义的父类是SendStoreCoupon,它反过来又从其StoreCoupon父类继承了这个方法,如下所示输出:

    ***********------------------**************
    Product: Strawberry Ice Cream
    Product Category: Desserts
    Coupon ID: 167466225705
    Brand: ABCBrand3
    Store: Los Angeles Store
    Expiry Date: 10/1/2021
    Quantity: 10
    ***********------------------**************
    ***********------------------**************
    Product: Strawberry Ice Cream
    Product Category: Desserts
    Coupon ID: 450583881080
    Brand: ABCBrand3
    Store: Los Angeles Store
    Expiry Date: 10/1/2021
    Quantity: 10
    ***********------------------**************
    

在本节中,我们了解了子类解决父类或超类顺序的影响。

通过这种理解,让我们看看当继承变得更加复杂时会发生什么,以及它可能导致错误的地方。

继承顺序无意改变的 影响

在本节中,我们将通过示例来展示在多层继承的情况下,方法的解析顺序是多么重要,以及当父类或超类中顺序无意中改变时会发生什么。

这就是它的工作原理:

  1. 首先,我们创建一个名为 CommonCounter 的类,该类初始化时包含两个属性,itemsname。同时,我们还要为这个类添加两个方法,return_cart(返回购物车中的商品)和 goto_counter(返回计数器的名称)。代码如下所示:

    class CommonCounter():
        def __init__(self,items,name):
            self.items = items
            self.name = name
        def return_cart(self):
            cartItems = []
            for i in self.items:
                cartItems.append(i)
            return cartItems
        def goto_counter(self):
            countername = self.name
            return countername
    CommonCounter.mro()
    
  2. 类的 MRO 如下所示:

    [__main__.CommonCounter, object]
    
  3. 现在,让我们创建另一个类,命名为 CheckItems,它也将成为本节多层继承中的一个父类。这个类将有一个名为 item_type 的属性和一个名为 review_items 的方法,该方法根据购物车中物品的类型返回计数器的名称:

    class CheckItems():
        def __init__(self, item_type = None):
            self.item_type = item_type
    
        def review_items(self, item_type = None):
            veg_cart = ['Vegetables', 'Dairy', 'Fruits']
            if (item_type == 'Electronics'):
                print("Move to Electronics Counter")
            elif (item_type in veg_cart):        
                print("Move to Vege Counter") 
    CheckItems.mro()
    
  4. 类的 MRO 如下所示:

    [__main__.CheckItems, object]
    
  5. 在继承的第二层,我们创建一个名为 ElectronicsCounter 的类,它按顺序继承自 CommonCounterCheckItems 类:

    class ElectronicsCounter(CommonCounter,CheckItems):
        def __init__(status = None):
            self.status = status
        def test_electronics(self):
            teststatus = []
            for i in self.status:
                teststatus.append(i)
            return teststatus
    ElectronicsCounter.mro()
    
  6. 类的 MRO 如下所示:

    [__main__.ElectronicsCounter,
     __main__.CommonCounter,
     __main__.CheckItems,
     object]
    
  7. 在继承的第二层,我们还要创建一个名为 VegeCounter 的类,它按顺序继承自 CheckItemsCommonCounter 类:

    class VegeCounter(CheckItems,CommonCounter):
        def __init__(weights = None):
            self.weights = weights
        def weigh_items(self):
            item_weight = dict(zip(self.items, 
                                   self.weights))
            return item_weight
    VegeCounter.mro()
    
  8. 类的 MRO 如下所示:

    [__main__.VegeCounter, 
    __main__.CheckItems, 
    __main__.CommonCounter, 
    object]
    
  9. 现在,让我们创建另一个类,命名为 ScanCode,它继承自 ElectronicsCounterVegCounter 类:

    class ScanCode(ElectronicsCounter,VegeCounter):
        pass
    

上述代码导致以下错误信息:

图 10.1 – MRO 错误

图 10.1 – MRO 错误

  1. 尽管类的 MRO 是 ScanCode 后跟 ElectronicsCounter,然后是 VegeCounter,接着是 CommonCounter,然后是 CheckItems,最后是 object,但 CommonCounterCheckItems 基类的 MRO 是相反的。因此,在这种情况下,整体类定义会抛出错误。

此示例演示了继承顺序无意中改变的影响。在 Python 中定义具有多层继承的类时,确保类顺序正确非常重要,以确保基类的 MRO 一致。

摘要

在本章中,我们通过探索 Python 3 中的 MRO 方法来了解方法解析的概念。我们还通过实现不同类型的继承来检查 Python 代码的 MRO。我们通过修改多个类在各个继承层次上的顺序来理解 MRO 的影响。

与本书中涵盖的其他章节类似,本章解释了 MRO 也关注元编程及其对 Python 代码的影响。

在下一章中,我们将探讨动态对象的概念,并给出一些有趣的示例。

第十一章:第十一章:创建动态对象

在本章中,我们将探讨 Python 3 中的动态对象概念以及创建任何动态 Python 对象(包括类、类的实例、方法和属性)的过程。

正如其名所示,动态对象是在满足一定条件时在运行时或执行时创建的对象,而不是在编码时创建。

在本章中,我们将通过我们的核心示例ABC Megamart来探讨如何动态创建类、类实例、函数、方法和属性。

为什么我们应该理解动态对象的创建?在需要构建能够在运行时生成代码的应用程序的场景中,Python 代码的基本构建块是在运行时创建的对象。对象的动态创建提供了灵活性,可以选择仅在需要时创建对象。任何定义的对象都将占用一定量的内存。当在编码时创建的对象不再被其他代码或应用程序需要时,它将占用本可以更有效地使用的内存。

在本章中,我们将探讨以下主要主题:

  • 探索用于动态对象的 type

  • 动态创建类的多个实例

  • 创建动态类

  • 创建动态属性和方法

到本章结束时,你应该了解 Python 对象如何在运行时创建,以及它们如何在各种应用中实现。

技术要求

本章中分享的代码示例可在 GitHub 上找到,具体位置为github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter11

探索用于动态对象的 type

在本节中,让我们从动态对象创建的角度来探讨名为type的函数。为什么我们需要动态创建对象呢?让我们考虑以下场景:我们只想为特定实例/对象更改类的属性,而不是整个原始类。在这种情况下,我们可以为该类创建动态对象,并在特定的动态对象内动态定义类的属性,而不是整个类本身。

在本书的多个章节中,我们探讨了type函数的各种用法。在本章中,我们将探讨如何使用type动态创建 Python 对象。

让我们看一下以下屏幕截图中的 Python 中type函数签名的图形表示:

图 11.1 – type 函数的签名

图 11.1 – type 函数的签名

类型函数接受一个 self 对象,后跟一个元组和参数字典作为输入。当我们向 type 函数提供对象作为输入时,它返回对象的类型,如下例所示:

type(object)

对象类型的输出是 type 本身:

type

图 11.1 我们还可以看到,type 的另一种变体接受一个对象后跟 basesdictbases 的参数值表示基类,而 dict 的参数值表示类的各种属性。

为了检查用于创建动态对象的 type 函数,让我们定义一个名为 Branch 的类:

class Branch:
    '''attributes...'''
    '''methods...'''
    pass

让我们进一步使用以下代码中的 type 函数动态创建一个对象:

branchAlbany = type('Branch', (object,), {'branchID' : 123, 
                        'branchStreet' : '123 Main Street',
                        'branchCity' : 'Albany',
                        'branchState' : 'New York',
                        '  'branch'ip' : 12084})

在前面的代码中,branchAlbany 变量是要定义的对象,第一个参数是需要创建对象的类名,第二个参数是类参数的基类元组的集合,第三个参数是要添加到对象中的属性或方法列表。

我们可以在以下代码和输出中查看 branchAlbany 对象的定义:

branchAlbany
__main__.Branch

以下截图是在执行前面的代码后添加到 branchAlbany 的属性表示:

图 11.2 – branchAlbany 的属性

图 11.2 – branchAlbany 的属性

动态类实例的方法解析顺序与 Branch 类相同:

branchAlbany.mro
<function Branch.mro()>

现在添加到类实例的所有动态属性现在都是 branchAlbany 类实例的一部分:

branchAlbany.branchID
123
branchAlbany.branchStreet
'123 Main Street'
branchAlbany.branchCity
'Albany'
branchAlbany.branchState
'New York'
branchAlbany.branchZip
12084

为了更深入地理解这一点,让我们看看 branchAlbany 的属性,并将其与创建 branchAlbany 实例的 Branch 类的属性进行比较。比较的图形表示如图 11.3 所示:

图 11.3 – 分支与 branchAlbany 的属性

图 11.3 – Branch 与 branchAlbany 的属性比较

前面的图明确指出,作为 Branch 类动态对象创建的一部分定义的属性没有包含在 Branch 类本身中。在这种情况下,Branch 类的定义保持不变,只有动态对象的定义发生了变化。

为了进一步探索,我们可以使用不同的属性集创建 Branch 类的另一个动态实例:

branchNYC = type('Branch', (object,), {'branchID' : 202, 
                             'productId': 100001,
                           'productName': 'Refrigerator',
                             'productBrand': 'Whirlpool'})

branchNYC 实例现在有自己的动态属性集,这些属性既不是 Branch 类的一部分,也不是 branchAlbany 实例的一部分。三个实例的比较如图 11.4 所示:

图 11.4 – Branch、branchAlbany 和 branchNYC 的属性

图 11.4 – Branch、branchAlbany 和 branchNYC 的属性

通过这种理解,让我们进一步探讨动态创建一个类的多个实例或对象。

动态创建类的多个实例

在本节中,让我们看看如何动态地创建一个类的多个实例。为此,我们将使用内置的 Python 函数 globals 来创建动态对象名称,以及我们用来创建动态对象的 type 函数。参考以下步骤:

  1. 让我们创建一个名为 Product 的新类,该类没有任何属性或方法。而不是在类中定义属性并创建类的实例,让我们创建多个具有自己属性的实例:

    class Product():
        '''attributes...'''
        '''methods...'''
        pass 
    
  2. 接下来,我们将在名为 details 的列表中创建三个字典项:

    details = [{'branchID' : 202, 
                'ProductID' : 100002, 
                'ProductName' : 'Washing Machine',
                'ProductBrand' : 'Whirlpool', 
                'PurchasePrice' : 450, 
                'ProfitMargin' : 0.19},
               {
                'productID' : 100902,
                'productName' : 'Iphone X',
                'productCategory' : 'Electronics',
                'unitPrice' : 700
               },
               {
                'branchID' : 2021,
                'branchStreet' : '40097 5th Main Street',
                'branchBorough' : 'Manhattan',
                'branchCity' : 'New York City',
                'Product ID': 100003, 
                'Product Name': 'Washing Machine', 
                'Product Brand': 'Samsung', 
                'Purchase Price': 430, 
                'Profit Margin': 0.18
               },
              ]
    
  3. 这些字典项将被提供给我们将使用 globalstype 创建的多个对象实例的属性:

    for obj,var in zip(['product1','product2','product3'],details):
        globals()[obj] = type('Product', (object,), var)
    
  4. 在前面的代码中,我们创建了三个对象 product1product2product3,这些对象使用 details 列表中的变量定义。每个对象都是动态创建的,并且将拥有自己的属性集。

由于我们没有在类中定义任何自定义属性,Product 类有其默认的属性集。这些属性在 图 11.5 中展示:

图 11.5 – Product 的属性

图 11.5 – Product 的属性

  1. 我们在这个例子中创建的三个对象的属性都有自己定义的动态属性集。动态对象的动态属性如下图所示:

图 11.6 – 、 和  的属性

图 11.6 – product1product2product3 的属性

在本节中,我们学习了如何动态地创建一个类的多个实例,每个实例都有其自己的动态属性集。有了这个理解,让我们进一步探讨如何动态地创建多个类。

创建动态类

在本节中,我们将探讨如何利用 typeglobals 的内置函数动态地创建具有不同名称和不同属性的类。为了进一步探索这个概念,我们将首先使用 type 函数创建一个动态类:

Product = type('Product', (object,), {'branchID' : 202, 
                             'productId': 100001,
                             'productName': 'Refrigerator',
                             'productBrand': 'Whirlpool'})

在前面的代码中,我们创建了一个名为 Product 的类,并提供了类名,后面跟着基类及其相应的属性。

让我们用以下代码测试创建的类:

Product
__main__.Product
type(Product)
type
Product.branchID
202

有了这个理解,现在让我们进一步创建多个动态类。

创建多个动态类

在本节中,我们将使用 typeglobals 创建多个动态类:

  1. 让我们定义三个函数,在创建多个动态类时将其作为动态方法添加,如下所示:

    def setBranch(branch):
            return branch
    def setSales(sales):
            return sales
    
    def setProduct(product):
            return product 
    
  2. 接下来,让我们创建一个属性字典:

    details = [{'branch': 202,
                'setBranch' : setBranch
      },
     {'purchasePrice': 430,
      'setSales' : setSales
      },
     {'product': 100902,
      'setProduct' : setProduct
      }]
    
  3. 在下一步中,我们将使用 typeglobals 在循环中动态地创建多个类:

    for cls,var in zip(['productcls1','productcls2','productcls3'],details):
        globals()[cls] = type(cls, (object,), var)
    
  4. 前面的代码创建了三个名为 productcls1productcls2productcls3 的类,并创建了可以进一步审查其使用在以下代码中及其对应输出的动态变量和方法:

    productcls1.setBranch(productcls1.branch)
    202
    productcls2.setSales(productcls2.purchasePrice)
    430
    productcls3.setProduct(productcls3.product)
    100902
    

在前面的代码中,我们已经成功执行了在动态类中创建的方法。

在本节中,我们学习了如何动态创建多个类。有了这个理解,让我们进一步通过在类中创建动态方法来继续前进。

创建动态属性和方法

在本节中,让我们探讨如何在类中创建动态方法。动态方法是在运行时为类创建的方法,与我们在类定义本身内编码时创建的常规类方法不同。

动态方法被创建出来,以避免在定义之后修改结构或原始类定义。而不是修改类定义,我们可以定义并调用一个运行时模板方法,该方法反过来将为该类创建一个动态方法。

让我们从为管理 ABC Megamart 的优惠券并命名为 SimpleCoupon 的简单类定义开始:

class SimpleCoupon:
    '''attributes''''''
 ''''''methods''''''
    pass

我们没有为这个类定义任何属性或方法,但在接下来的章节中我们将更清晰地定义它们。

动态定义属性

现在,让我们使用 Python 内置的 setattr 函数在运行时为 SimpleCoupon 类定义一组优惠券属性。此函数接受一个 Python 对象、属性的名称及其对应值:

setattr(SimpleCoupon,'couponDetails',
[["Honey Mustard Sauce","Condiments","ABCBrand3","Pasadena Store","10/1/2021",2],
["Potato Chips","Snacks","ABCBrand1","Manhattan Store","10/1/2021",2],
["Strawberry Ice Cream","Desserts","ABCBrand3","ABC Manufacturer","10/1/2021",2]])

在前面的代码中,我们提供了类名 SimpleCoupon 作为输入对象,随后是属性名 couponDetails,以及其对应值,即三个产品详情列表,每个列表对应一种优惠券类型:CondimentsSnacksDesserts

现在我们已经动态创建了属性,让我们检查它是否已添加到 SimpleCoupon 类中,并且可以通过查看类中可用的属性和方法列表来使用它,如 图 11.7 所示:

图 11.7 – couponDetails 添加到 SimpleCoupon

图 11.7 – 将 couponDetails 添加到 SimpleCoupon

基于这个理解,让我们进一步在 SimpleCoupon 类中动态创建方法。

动态定义方法

在本节中,让我们创建一个新的函数,它作为一个模板函数,用于在 SimpleCoupon 类中动态生成方法。我们现在创建一个名为 createCoupon 的函数,它接受一个类对象、方法名和优惠券详情作为输入。

在函数定义中,我们还要定义一个 generateCoupon 函数,它将被生成为一个动态方法:

def createCoupon(classname,methodname,couponDetails):
    def generateCoupon(couponDetails):
        import random
        couponId =  random.sample(range(
      100000000000,900000000000),1)
        for i in couponId:
            print('***********------------------
           **************')
            print('Product:', couponDetails[0])
            print('Product Category:', couponDetails[1])
            print('Coupon ID:', i)
            print('Brand:', couponDetails[2])
            print('Source:', couponDetails[3])
            print('Expiry Date:', couponDetails[4])
            print('Quantity:', couponDetails[5])
            print('***********------------------
           **************')
    setattr(classname,methodname,generateCoupon)

在前面的代码中,我们调用 setattr 函数来在作为 setattr 输入的类对象中动态定义方法。

在下一步中,让我们使用相同的方法定义动态地生成三个generateCoupon方法,分别命名为三个不同的名称,并使用三组不同的属性进行测试。

for method,var in zip(['generateCondimentsCoupon','generateSnacksCoupon','generateDessertsCoupon'],SimpleCoupon.couponDetails):
    createCoupon(SimpleCoupon, method,var) 

现在,SimpleCoupon类已添加了三个不同的方法,分别命名为generateCondimentsCoupongenerateSnacksCoupongenerateDessertsCoupon。添加到SimpleCoupon类的动态方法如下所示:

图 11.8 – 添加到 SimpleCoupon 的动态方法

图 11.8 – 添加到 SimpleCoupon 的动态方法

让我们从SimpleCoupon类中调用它们来运行每个方法。以下代码中调用了generateCondimentsCoupon方法:

SimpleCoupon.generateCondimentsCoupon(SimpleCoupon.couponDetails[0])

输出生成如下:

***********------------------**************
Product: Honey Mustard Sauce
Product Category: Condiments
Coupon ID: 666849488635
Brand: ABCBrand3
Source: Pasadena Store
Expiry Date: 10/1/2021
Quantity: 2
***********------------------**************
generateSnacksCoupon is called in the following code:
SimpleCoupon.generateSnacksCoupon(SimpleCoupon.couponDetails[1])

此输出的结果如下:

***********------------------**************
Product: Potato Chips
Product Category: Snacks
Coupon ID: 394693383743
Brand: ABCBrand1
Source: Manhattan Store
Expiry Date: 10/1/2021
Quantity: 2
***********------------------**************

以下代码中调用了generateDessertsCoupon方法:

SimpleCoupon.generateDessertsCoupon(SimpleCoupon.couponDetails[2])

输出生成如下:

***********------------------**************
Product: Strawberry Ice Cream
Product Category: Desserts
Coupon ID: 813638596228
Brand: ABCBrand3
Source: ABC Manufacturer
Expiry Date: 10/1/2021
Quantity: 2
***********------------------**************

在本节中,我们已经理解了在 Python 类中动态生成方法的概念,并附带了示例。这个概念在设计和具有自动化代码生成功能的应用程序时将有所帮助。

摘要

在本章中,我们通过探索 Python 3 中创建各种动态对象的方法,学习了动态对象的概念。我们还涵盖了动态创建类多个实例的概念。我们探讨了创建动态类的概念。此外,我们还研究了在类中动态创建属性和方法的概念。

与本书中其他章节类似,虽然本章解释了动态对象的概念,但也提供了一些关于元编程及其对 Python 代码影响的重点。

在下一章中,我们将通过一些有趣的示例来探讨设计模式的概念。

第十二章:第十二章:应用 GOF 设计模式——第一部分

在本章中,我们将探讨 Python 3 中设计模式的概念及其各种类别,以及它们在用 Python 开发软件时如何应用的示例。

设计模式的概念起源于 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 所著的《设计模式:可复用面向对象软件元素》一书,该书由 Addison-Wesley 出版,用 C++编写。这个概念后来扩展到了其他面向对象编程(OOP)语言。

在本章中,我们将探讨如何使用我们的核心示例ABC Megamart将这些设计模式应用于 Python。

我们将涵盖以下主要主题:

  • 设计模式概述

  • 探索行为设计模式

到本章结束时,你应该理解一些重要的行为设计模式以及它们如何在各种应用程序中实现。

技术要求

本章中的代码示例可在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter12

设计模式概述

每种编程语言都有其独特设计和传达给他人的元素。设计模式为在 Python 中开发软件或应用程序提供了一种结构化和精心设计的方法。在 Python 中,每个元素都是一个对象。设计模式表达了我们将如何对这些对象进行排序或结构化以执行各种操作。这使得它们变得可重用。

设计模式分为三个类别——行为、结构和创建。在本章中,我们将介绍行为设计模式,并特别关注以下三个:

  • 责任链

  • 命令

  • 策略

Python 中有超过 20 种不同的设计模式,涵盖所有这些需要一本自己的书。因此,我们将只关注本章和下一章中一些最有趣的设计模式。有了这个,让我们来探索一些行为设计模式。

探索行为设计模式

正如其名所示,行为设计模式处理对象的行为以及它们如何相互交流。在本节中,我们将学习责任链、命令和策略设计模式的元素,这些模式属于行为设计模式类别,并通过将它们应用于ABC Megamart来理解它们。

理解责任链

责任链是一种设计模式,其中可以由对象执行的动作的责任从一个对象传递到另一个对象,类似于一系列事件或动作。为了进一步解释这一点并实现这种设计模式,我们需要在我们的代码中开发以下元素:

  • 父处理器:一个基类,它定义了一个基础函数,指定如何处理一系列动作。

  • 子处理器:一个或多个子类,它们覆盖基类中的基础函数以执行相应的动作。

  • 异常处理器:一个默认处理器,在发生异常时执行特定操作。它还会覆盖基类中的基础函数。

  • 请求者:一个函数或方法,它调用子处理器来启动责任链。

让我们通过一个例子来看看责任链。

在这个例子中,我们将根据州计算税费并为ABC Megamart的纽约和加利福尼亚分支机构生成发票。请按照以下步骤操作:

  1. 为了进一步说明设计模式,让我们创建一个名为InvoiceHandler的父处理器类。在这个类中,我们将初始化一个next_action变量来处理链中的下一个动作,并定义一个handle方法来处理请求的动作:

    class InvoiceHandler(object):
        def __init__(self):
            self.next_action = None
        def handle(self,calctax):
            self.next_action.handle(calctax)
    
  2. 接下来,我们将创建一个支持类来支持我们在本例中将要执行的操作。在这里,我们希望根据请求计算州的税费并生成发票:

    class InputState(object):
        state_ny = ['NYC','NY','New York','new york']
        state_ca = ['CA', 'California', 'california']
    

InputState类有两个属性,用于列出纽约州和加利福尼亚州的接受值。

  1. 现在,让我们创建另一个类,为发票添加一个标题,如下所示:

    class Print_invoice(object):
        def __init__(self,state):
            self.state = state
            self.header = 'State specific Sales tax is applicable 
                           for the state of ' + self.state
    
  2. 接下来,我们将创建一个子处理器类,该类具有生成发票、计算产品在纽约州的具体税费以及覆盖来自InvoiceHandler类的handle方法的方法:

    class NYCHandler(InvoiceHandler):
        def generate_invoice(self, header, state):
            product = 'WashingMachine'
            pricebeforetax = 450 + (450 * 0.19)
            tax_rate = 0.4
            local_rate = 0.055
            tax = pricebeforetax * (tax_rate + local_rate)
            finalsellingprice = pricebeforetax + tax
            print('**************ABC Megamart*****************')
            print('***********------------------**************')
            print(header)
            print('Product: ', product)
            print('Tax: ', tax)
            print('Total Price: ', finalsellingprice)
            print('***********------------------**************') 
    
        def handle(self,print_invoice):
            if print_invoice.state in InputState.state_ny:
                self.generate_invoice(print_invoice.header, 
                                      print_invoice.state)
            else:
                super(NYCHandler, self).handle(print_invoice)
    
  3. 然后,我们将创建一个子处理器类,该类具有生成发票、计算产品在加利福尼亚州的具体税费以及覆盖来自InvoiceHandler类的handle方法的方法:

    class CAHandler(InvoiceHandler):
        def generate_invoice(self, header, state):
            product = 'WashingMachine'
            pricebeforetax = 480 + (480 * 0.14)
            tax_rate = 0.35
            local_rate = 0.077
            tax = pricebeforetax * (tax_rate + local_rate)
            finalsellingprice = pricebeforetax + tax
            print('**************ABC Megamart*****************')
            print('***********------------------**************')
            print(header)
            print('Product: ', product)
            print('Tax: ', tax)
            print('Total Price: ', finalsellingprice)
            print('***********------------------**************') 
    
        def handle(self,print_invoice):
            if print_invoice.state in InputState.state_ca:
                self.generate_invoice(print_invoice.header, 
                                      print_invoice.state)
            else:
                super(CAHandler, self).handle(print_invoice)
    
  4. 现在,让我们定义一个类来处理异常,例如请求未调用子处理器方法之一的场景:

    class ExceptionHandler(InvoiceHandler):
        def handle(self,print_invoice):
            print("No branches in the state")  
    
  5. 现在,让我们创建一个请求函数,该函数实例化一个子处理器子类,并启动一个责任链,将一个动作传递到另一个动作:

    def invoice_requestor(state):
        invoice = Print_invoice(state)
        nychandler = NYCHandler()
        cahandler = CAHandler()
        nychandler.next_action = cahandler
        cahandler.next_action = ExceptionHandler()
        nychandler.handle(invoice)
    

在前面的代码中,我们定义了请求者来设置NYCHandler的下一个动作是CAHandler,以及CAHandler的下一个动作是异常处理器。让我们通过调用invoice_requestor函数并传入输入州名来测试这个设计模式;即CA

invoice_requestor('CA')

前面的代码返回了加利福尼亚州的发票,因为我们提供的输入是CC而不是NY。如果提供的是NY作为输入,设计模式将调用NYHandler。然而,由于提供了 CA,因此调用链中的下一个相关CAHandler如下:

**************ABC Megamart*****************
***********------------------**************
State specific Sales tax is applicable for the state of CA
Product:  WashingMachine
Tax:  233.6544
Total Price:  780.8544
***********------------------**************

如果invoice_requestor提供的输入州名是NY,它应该调用NYHandler,而不是CAHandler

invoice_requestor('NYC')

前面的代码返回了NYHandler类的发票,而不是预期的CAHandler类的发票:

**************ABC Megamart*****************
***********------------------**************
State specific Sales tax is applicable for the state of NYC
Product:  WashingMachine
Tax:  243.6525
Total Price:  779.1525
***********------------------**************

作为请求的最后部分,让我们通过提供一个既不是NY也不是CA的输入状态来调用ExceptionHandler

invoice_requestor('TEXAS')

以下代码通过从ExceptionHandler调用动作返回以下输出:

No branches in the state

让我们将这个设计模式的元素与其对应的对象连接起来:

图 12.1 – 责任链类

图 12.1 – 责任链类

在本节中,我们探讨了责任链设计模式。现在,让我们看看命令设计模式。

了解命令设计模式

在本节中,我们将探讨下一个感兴趣的设计模式:命令设计模式。命令设计模式可以用来创建执行命令的序列,并在执行命令出错时回滚到之前的状态。类似于责任链模式,命令设计模式也是通过定义多个可以执行动作并撤销对象执行的动作的元素来创建的。

为了进一步解释并实现这个设计模式,我们需要在我们的代码中开发以下元素:

  • 父命令:这是一个基类,定义了需要执行的一个或多个命令的基本功能。

  • 子命令:子命令指定从父命令类继承的一个或多个动作,并在单个子命令级别上覆盖。

  • 执行者:这是一个执行子命令的基类。它提供了一个执行动作的方法和一个撤销动作的方法。

  • 子执行者:这些继承自执行者并覆盖了执行方法,同时撤销子命令执行的动作。

  • 请求者:请求者是请求执行者执行命令并回滚到之前状态的类。

  • 测试者:这个类测试设计模式是否按预期工作。

现在,让我们看看这个设计模式在实际中的应用。为了理解这个设计模式,我们将回到ABC Megamart并计算产品的销售价格,以及应用折扣。命令模式可以帮助我们设计账单,以便我们可以以实际销售价格出售或应用折扣。每当错误地应用折扣时,我们可以撤销它。同样,每当没有应用折扣时,我们可以重新应用它。按照以下步骤进行:

  1. 让我们先创建Billing类。这将是一个父命令,它将有一个名为sales的属性。这是一个字典对象。将有两个抽象方法——一个用于应用折扣,另一个用于移除折扣:

    from abc import ABC, abstractmethod
    class Billing:
        sales = {'purchase_price': 450,
                  'profit_margin': 0.19,
                  'tax_rate': 0.4,
                  'discount_rate': 0.10
                  }        
        @abstractmethod
        def apply_discount(self):
            pass
        @abstractmethod
        def remove_discount(self):
            pass
    
  2. 现在,让我们创建第一个子命令类DiscountedBilling,它将覆盖其父类Billing中的apply_discount方法。应用Discount方法将接受来自Billing类的销售字典对象并计算折扣后的价格,如下所示:

    class DiscountedBilling(Billing):
        def apply_discount(self):
            sales = self.sales
            pricebeforetax = sales['purchase_price'] + 
                 sales['purchase_price'] * sales['profit_margin']
            finalsellingprice = pricebeforetax + (pricebeforetax * 
            sales['tax_rate'])
            sales['sellingPrice'] = finalsellingprice
            discountedPrice = sales['sellingPrice'] * (1 – 
                              sales['discount_rate'])
            return discountedPrice
    
  3. 接下来,我们将创建下一个子命令类,ActualBilling,它将移除折扣。也就是说,它将计算不带折扣的销售价格:

    class ActualBilling(Billing):
        def remove_discount(self):
            sales = self.sales
            pricebeforetax = sales['purchase_price'] + 
                 sales['purchase_price'] * sales['profit_margin']
            actualprice = pricebeforetax + (pricebeforetax * 
                          sales['tax_rate'])
            return actualprice
    
  4. 现在,让我们创建执行器的基类。这将有两个方法:exec_discountrevoke_discount。第一个是一个抽象方法,用于执行应用折扣的命令。第二个是一个抽象方法,用于执行撤销折扣的命令:

    class ExecuteBilling:
        @abstractmethod
        def exec_discount(self):
            pass
        @abstractmethod
        def revoke_discount(self):
            pass
    
  5. 现在,让我们定义一个名为 ExecuteDiscountedBilling 的子类,它继承自 ExecuteBilling 类。这将覆盖其超类中的 exec_discountrevoke_discount 方法。我们将在子类的 exec_discount 方法中调用 DiscountedBilling 类的 apply_discount 方法。我们还将设置 ActualBilling 命令类,从 ExecuteActualBilling 类中在 revoke_discount 方法内:

    class ExecuteDiscountedBilling(ExecuteBilling):
        def __init__(self, instance):
            self.instance = instance        
        def exec_discount(self):
            print('Discount applied...')
            return self.instance.apply_discount()        
        def revoke_discount(self, revokeInstance):
            revokeInstance.reset(ExecuteActualBilling(
                                 ActualBilling()))
            return revokeInstance.runcalc()
    
  6. 现在,让我们定义一个名为 ExecuteActualBilling 的子类,它继承自 ExecuteBilling 类。这将覆盖其超类中的 exec_discountrevoke_discount 方法。我们将在子类的 exec_discount 方法中调用 ActualBilling 类的 remove_discount 方法。我们还将设置 DiscountedBilling 命令类,从 ExecuteDiscountedBilling 类中在 revoke_discount 方法内:

    class ExecuteActualBilling(ExecuteBilling):
        def __init__(self, instance):
            self.instance = instance
    
        def exec_discount(self):
            print('Discount removed...')
            return self.instance.remove_discount()
    
        def revoke_discount(self, revokeInstance):
            revokeInstance.reset(ExecuteDiscountedBilling(
                                 DiscountedBilling()))
            return revokeInstance.runcalc()
    
  7. 接下来,我们将定义请求者类,RequestAction,它将请求执行和撤销所需的命令。我们还将定义三个方法:

    • reset 方法,它将设置或重置命令

    • runcalc 方法,它将执行折扣计算

    • revert 方法,它将通过撤销折扣计算来恢复到之前的操作:

在代码块中:

class RequestAction:
    def __init__(self, action):
        self.action = action
    def reset(self, action):
        print("Resetting command...")
        self.action = action
    def runcalc(self):
        return self.action.exec_discount()
    def revert(self):
        print("Reverting the previous action...")
        return self.action.revoke_discount(self)
  1. 最后,我们必须创建这个设计模式中的最后一个类来测试命令设计模式是否按预期工作:

    class Tester:
        def __init__(self):
            billing = Billing()
            discount = 
                     ExecuteDiscountedBilling (DiscountedBilling())
            actual = ExecuteActualBilling(ActualBilling())
            requestor = RequestAction(discount)  
            print(requestor.runcalc())
            requestor.reset(actual)
            print(requestor.runcalc())
            print(requestor.revert())
            print(requestor.revert())
    

在前面的代码中,我们定义了 Billing 类的对象实例,然后是可折扣的实例和实际的 ExecuteBilling 子类。我们还创建了一个 RequestAction 请求者类的实例。之后,我们按顺序执行了一系列操作以运行折扣计算,然后是 reset 命令,接着重新运行计算以移除折扣。这将撤销之前的命令,从而在撤销之前的命令之前重新应用折扣,这将反过来移除折扣。

让我们称 Tester 类如下:

Tester()

上述代码的输出如下:

Discount applied...
674.73
Resetting command...
Discount removed...
749.7
Reverting the previous action...
Resetting command...
Discount applied...
674.73
Reverting the previous action...
Resetting command...
Discount removed...
749.7
<__main__.Tester at 0x261f09e3b20>

现在,让我们将这个设计模式的元素与其对应对象连接起来:

图 12.2 – 命令设计模式类

图 12.2 – 命令设计模式类

在本节中,我们探讨了命令设计模式的概念。现在,让我们看看策略设计模式。

策略设计模式

在本节中,我们将查看本章将要介绍的行为设计模式类别下的最后一个设计模式。让我们查看策略模式的元素,如下所示:

  • 领域:领域或基类定义了 Python 对象执行一系列操作所需的所有基方法和属性。此类还根据策略类中定义的策略方法在类内做出操作决策。

  • 策略:这些是一个或多个独立的类,它们在其策略方法中定义了一个特定的策略。每个策略类都将使用相同的策略方法名称。

  • 测试者:测试函数调用领域类并执行策略。

为了理解策略设计模式的实现,我们将查看我们在第八章中提到的各种计费计数器。在ABC Megamart中,有各种计费计数器,包括蔬菜计数器、少于 10 件物品的计数器、电子计数器等。

在这个例子中,我们将定义一个蔬菜计数器和电子计数器作为策略类。按照以下步骤进行:

  1. 首先,我们将定义一个名为SuperMarket的领域类,其中包含以下方法:

    1. 初始化属性

    2. 显示购物车中物品的详细信息

    3. 前往特定的计数器

下面是这个代码的样子:

class SuperMarket():

    def __init__(self,STRATEGY, items, name, scan, units, tax, 
                 itemtype = None):
        self.STRATEGY = STRATEGY
        self.items = items
        self.name = name
        self.scan = scan
        self.units = units
        self.tax = tax
        self.itemtype = itemtype

    def return_cart(self):
        cartItems = []
        for i in self.items:
            cartItems.append(i)
        return cartItems

    def goto_counter(self):
        countername = self.name
        return countername
  1. 接下来,我们将定义以下方法:

    • 扫描条形码

    • 添加账单详情

    • 添加税费详情

下面是这个代码:

    def scan_bar_code(self):
        codes = []
        for i in self.scan:
            codes.append(i)
        return codes

    def add_billing(self):
        self.codes = self.scan_bar_code()
        pricetag = []
        for i in self.units:
            pricetag.append(i)
        bill = dict(zip(self.codes, pricetag))
        return bill

    def add_tax(self):
        taxed = []
        for i in self.tax:
            taxed.append(i)
        return taxed
  1. 计算账单和打印发票的操作也定义在SuperMarket类中。请参考以下代码:

        def calc_bill(self):
            bill = self.add_billing()
            items = []
            cartItems = self.return_cart()
            calc_bill = []
            taxes = self.add_tax()
            for item,tax in zip(bill.items(),taxes):
                items.append(item[1])
                calc_bill.append(item[1] + item[1]*tax)
            finalbill = dict(zip(cartItems, calc_bill))
            return finalbill
    
        def print_invoice(self):
            finalbill = self.calc_bill()
            final_total = sum(finalbill.values())
            print('**************ABC Megamart*****************')
            print('***********------------------**************')
            print('Counter Name: ', self.goto_counter())
            for item,price in finalbill.items():
                print(item,": ", price)
            print('Total:',final_total)
            print('***********------------------**************')
            print('***************PAID************************')
    
  2. SuperMarket类中的最后一个方法是pipeline_template方法,它创建一个用于运行方法序列的管道:

        def pipeline_template(self):
            self.return_cart()
            self.goto_counter()
            self.STRATEGY.redirect_counter()
            self.scan_bar_code()
            self.add_billing()
            self.add_tax()
            self.calc_bill()
            self.print_invoice()
    

在这个方法中,我们将改变SuperMarket类执行的策略称为策略方法。

  1. 现在,让我们定义一个简单的蔬菜计数器策略类,如下所示:

    class VegeCounter():
        def redirect_counter():
            print("**************Move to Vege Counter**************")
    
  2. 让我们也创建一个简单的电子计数器策略类,如下所示:

    class ElectronicsCounter():
        def redirect_counter():
            print("**************Move to Electronics 
                  Counter**************")
    
  3. 现在,我们必须定义一个测试函数来测试策略:

    def run_pipeline(domain = SuperMarket):
        domain.pipeline_template()
    
  4. 让我们通过运行管道并提供VegeCounter作为策略值来测试蔬菜计数器的策略:

    run_pipeline(SuperMarket(STRATEGY = VegeCounter,
               items = ['Onions','Tomatoes','Cabbage','Beetroot'],
               name = ['Vegetable Counter'],
               scan = [113323,3434332,2131243,2332783],
               units = [10,15,12,14],
               tax = [0.04,0.03,0.035,0.025],
               itemtype = ['Vegetables'],
               ))
    
  5. VegeCounter策略的输出如下:

    **************Move to Vege Counter**************
    **************ABC Megamart*****************
    ***********------------------**************
    Counter Name:['Vegetable Counter']
    Onions :10.4
    Tomatoes :15.45
    Cabbage :12.42
    Beetroot :14.35
    Total: 52.620000000000005
    ***********------------------**************
    ***************PAID************************
    
  6. 现在,让我们通过运行管道并提供ElectronicsCounter作为策略值来测试电子计数器的策略:

    run_pipeline(SuperMarket(STRATEGY = ElectronicsCounter,
                        items = ['television','keyboard','mouse'],
                        name = ['Electronics Counter'],
                        scan = [113323,3434332,2131243],
                        units = [100,16,14],
                        tax = [0.04,0.03,0.035],
                        itemtype = ['Electronics'],
                        ))
    

ElectronicsCounter策略的输出如下:

**************Move to Electronics Counter**************
**************ABC Megamart*****************
***********------------------**************
Counter Name:  ['Electronics Counter']
television :  104.0
keyboard :  16.48
mouse :  14.49
Total: 134.97
***********------------------**************
***************PAID************************

现在,让我们将这个设计模式的元素与其相应的对象连接起来:

图 12.3 – 使用类的策略模式

图 12.3 – 使用类的策略模式

通过这样,我们已经了解了策略设计模式。现在,让我们总结本章内容。

概述

在本章中,我们通过在 Python 3 中应用其中一些模式,学习了行为设计模式。特别是,我们实现了责任链、命令和策略模式,并理解了它们各自的元素。

与本书中的其他章节类似,本章也被分成了两部分——这一部分解释了设计模式,并专注于元编程及其对 Python 代码的影响。

在下一章中,我们将继续探讨设计模式的概念,通过涵盖结构性和创建性设计模式的例子来展开。

第十三章:第十三章:应用 GOF 设计模式——第二部分

本章,我们将继续探讨 Python 3 中的设计模式概念及其各种类别及其在软件开发中的实现。

在上一章中,我们学习了如何通过示例应用行为型设计模式。在本章中,我们将继续探讨剩余的两个类别——结构型和创建型设计模式。我们将看到它们如何通过我们的核心示例 ABC Megamart 在 Python 中应用。

本章,我们将探讨以下主要主题:

  • 理解结构型设计模式

  • 理解创建型设计模式

到本章结束时,你应该能够理解一些重要的结构和创建型设计模式的例子,并学习它们如何在各种应用中实现。

技术要求

本章中分享的代码示例可在 GitHub 上找到,地址为github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter13

探索结构型设计模式

如其名所示,结构型设计模式用于设计类及其实现的结构,以便类和对象可以有效地扩展或重用。在本节中,我们将介绍三种这样的结构型设计模式——桥接模式、外观模式和代理模式。我们考虑这三种设计模式是因为它们是独特的,并且代表了结构型设计模式可以使用的三个不同方面。

理解桥接模式

桥接设计模式通过抽象或抽象方法的概念应用于桥接多个实现元素或操作。为了进一步解释这一点并实现这个设计模式,我们的代码应该包含以下元素:

  • 抽象超类:具有执行特定操作的抽象方法的基础类,以及桥接任何额外实现的方法

  • 抽象子类:一个或多个子类,它们从抽象超类实现抽象方法以执行它们各自的操作

  • 实现超类:一个基础类,它添加了额外的实现或设计在抽象之上

  • 实现子类:继承实现超类的子类

让我们通过一个例子来了解桥接模式。在这个例子中,我们将查看打印属于两个不同超市——ABC MegamartXYZ Megamart 的分支经理的商务名片。让我们看看如何:

  1. 为了进一步说明设计模式,让我们创建一个名为 PrintCard 的抽象超类,并添加三个方法。add_name 方法添加超市的名称,add_manager 方法添加特定于管理员的格式。add_manager 方法从实现子类获取格式输入,我们将在本节后面讨论。第三个方法是 printcard 方法,它是一个抽象方法,将在子类中定义:

    from abc import abstractmethod, ABC
    class PrintCard(ABC):    
        def add_name(self, name):
            self.name = name        
        def add_manager(self, branch):
            self.branch = branch.FORMATTING        
        @abstractmethod
        def printcard(self):
            pass
    
  2. 让我们进一步创建一个名为 CardABC 的抽象子类。这个类将从超类初始化标志、名称和管理员。printcard 方法将打印标志、超市的名称和分店的地址:

    class CardABC(PrintCard):
        def __init__(self, logo, name, branch):
            self.logo = logo
            super().add_name(name)
            super().add_manager(branch)
    
        def printcard(self, *args):
            print(self.logo + self.name)
            for arg in args:
                print(self.branch + str(arg))
    
  3. 接下来,创建一个名为 CardXYZ 的抽象子类。它将初始化以下变量 – 风格、标志、从超类继承的名称和管理员。printcard 方法将打印标志、卡片的风格、超市的名称和分店的地址:

    class CardXYZ(PrintCard):
        def __init__(self, style, logo, name, branch):
            self.style = style
            self.logo = logo
            super().add_name(name)
            super().add_manager(branch)
    
        def printcard(self, *args):
            print(self.logo + self.style + self.name)
            for arg in args:
                print(self.branch + str(arg))
    
  4. 现在,让我们创建一个名为 Manager 的实现超类,其中包含一个名为 formatting 的方法:

    class Manager:
        def formatting(self):
            pass
    
  5. 接下来,创建一个名为 Manager_manhattan 的实现子类,以添加针对曼哈顿分店经理的商务卡的格式:

    class Manager_manhattan(Manager):
        def __init__(self):
            self.formatting()
    
        def formatting(self):
            self.FORMATTING = '\337m'
    
  6. 现在,让我们创建一个名为 Manager_albany 的实现子类,以添加针对阿尔巴尼分店经理的商务卡的特定格式:

    class Manager_albany(Manager):
        def __init__(self):
            self.formatting()
    
        def formatting(self):
            self.FORMATTING = '\033[94m'
    
  7. 接下来,实例化 CardABC,这是一个抽象子类。此类的三个输入参数是标志的格式、超市的名称以及添加格式的分店:

    manager_manhattan = CardABC(logo = '\33[43m', name = 'ABC Megamart', branch = Manager_manhattan())
    
  8. 现在我们将打印这张卡片:

    manager_manhattan.printcard('John M',
                  'john.m@abcmegamart.com',
      '40097 5th Main Street',
      'Manhattan',
      'New York City',
      'New York',
      11007)
    

输出如下所示,格式与类实例化时提供的一致:

ABC Megamart
John M
john.m@abcmegamart.com
40097 5th Main Street
Manhattan
New York City
New York
11007
  1. 现在,让我们实例化 CardXYZ,这是一个抽象子类。此类的四个输入参数是风格、标志的格式、超市的名称以及添加格式的分店:

    manager_albany = CardXYZ(style = '\33[43m',logo = '\33[5m', name = 'XYZ Megamart', branch = Manager_albany())
    
  2. 现在,让我们打印这张卡片。

    manager_albany.printcard('Ron D','ron.d@abcmegamart.com','123 Main Street','Albany','New York', 12084)
    

输出如下所示,风格和格式与类实例化时提供的一致:

XYZ Megamart
Ron D
ron.d@abcmegamart.com
123 Main Street
Albany
New York
12084

让我们用以下图形表示法将此设计模式的元素与示例中的相应对象联系起来:

![图 13.1 – 桥接模式类

图 13.1 – 桥接模式类

因此,通过在抽象类和实现类之间创建桥梁,已经实现了桥接模式。有了这个理解,让我们来看看外观模式。

理解外观模式

在本节中,我们将探讨外观模式,我们将设计一种类似黑盒的实现,以隐藏处理多个子系统的系统的复杂性,从而保护最终用户或客户端。为了进一步解释并实现这个设计/核心模式,我们的代码需要以下元素:

  • 功能:需要为系统实现的核心功能定义在这些功能类中。

  • 外观:这是一个封装核心功能和其实现的类,供最终用户使用。

  • 最终用户:使用外观类访问系统核心功能的功能、方法或类。

为了进一步理解外观模式,让我们创建一系列功能,从添加购物车中的商品开始,到结账,扫描条形码,开账单,最后打印发票:

  1. 这个系列中的第一个功能类是 Cart,其中将在 return_cart 方法中将商品添加到购物车中:

    class Cart:
        def __init__(self, items):
            self.items = items
        def return_cart(self):
            cart_items = []
            for i in self.items:
                cart_items.append(i)
            print("Running return_cart...")
            return cart_items
    
  2. 第二个功能类是 Counter 类,其中 goto_counter 方法返回收银台的名字:

    class Counter:
        def __init__(self, name):
            self.name = name
        def goto_counter(self):
            countername = self.name
            print("Running goto_counter...")
            return countername
    
  3. 第三个功能类是 BarCode 类,其中 scan_bar_code 方法返回扫描的条形码:

    class BarCode:
        def __init__(self, scan):
            self.scan = scan
        def scan_bar_code(self):
            codes = []
            for i in self.scan:
                codes.append(i)
            print("Running scan_bar_code...")
            return codes
    
  4. 第四个功能是 Billing 类,其中在 add_billing 方法中将价格标记到条形码上,并作为字典对象返回:

    class Billing:
        def __init__(self, codes, units ):
            self.codes = codes
            self.units = units
        def add_billing(self):
            codes = self.codes.scan_bar_code()
            pricetag = []
            for i in self.units:
                pricetag.append(i)
            bill = dict(zip(codes, pricetag))
            print("Running add_billing...")
            return bill
    
  5. 下一个功能类是 Tax 类,其中使用类中的 add_tax 方法返回税率:

    class Tax:
        def __init__(self, tax):
            self.tax = tax
        def add_tax(self):
            taxed = []
            for i in self.tax:
                taxed.append(i)
            print("Running add_tax...")
            return taxed
    
  6. 此后的功能是 FinalBill 类,我们将使用 calc_bill 方法计算最终账单:

    class FinalBill:
        def __init__(self, billing, cart, tax):
            self.billing = billing
            self.cart = cart
            self.tax = tax    
        def calc_bill(self):
            bill = self.billing.add_billing()
            items = []
            cart_items = self.cart.return_cart()
            calc_bill = []
            taxes = self.tax.add_tax()
            for item,tax in zip(bill.items(),taxes):
                items.append(item[1])
                calc_bill.append(item[1] + item[1]*tax)
            finalbill = dict(zip(cart_items, calc_bill))
            print("Running calc_bill...")
            return finalbill
    
  7. 外观模式中的最后一个功能类是 Invoice 类,我们将创建一个 print_invoice 方法来打印最终的发票:

    class Invoice:
        def __init__(self, finalbill, counter):
            self.finalbill = finalbill
            self.counter = counter
        def print_invoice(self):
            finalbill = self.finalbill.calc_bill()
            final_total = sum(finalbill.values())
            print("Running print_invoice...")
            print('**************ABC 
                   Megamart*****************')
            print('***********------------------
                   **************')
            print('Counter Name: ', 
                   self.counter.goto_counter())
            for item,price in finalbill.items():
                print(item,": ", price)
            print('Total:',final_total)
            print('***********------------------
                   **************')
            print('***************PAID********************
                   ****')
    
  8. 现在,让我们创建名为 QueueFacade 类。它有两个功能 – pipeline 方法用于显式运行功能类中的某些方法,以及 pipeline_implicit 方法用于从 Invoice 类运行 print_invoice 方法,这将反过来调用其他功能类中的所有其他方法:

    class Queue:
        def __init__(self, items, name, scan, units, tax):
            self.cart = Cart(items)
            self.counter = Counter(name)
            self.barcode = BarCode(scan)
            self.billing = Billing(self.barcode, units)
            self.tax = Tax(tax)
            self.finalbill = FinalBill(self.billing, 
                             self.cart, self.tax)
            self.invoice = Invoice(self.finalbill, 
                                   self.counter)
        def pipeline(self):
            self.cart.return_cart()
            self.counter.goto_counter()
            self.barcode.scan_bar_code()
            self.tax.add_tax()
        def pipeline_implicit(self):
            self.invoice.print_invoice()
    
  9. 让我们创建一个最终用户功能,通过为 Queue 创建一个实例并调用 pipeline 方法,使用 Facade 类来运行功能类中的方法:

    def run_facade():
        queue = Queue(items = ['paperclips','blue 
                        pens','stapler','pencils'],
                 name = ['Regular Counter'],
                 scan = [113323,3434332,2131243,2332783],
                 units = [10,15,12,14],
                 tax = [0.04,0.03,0.035,0.025],
                 )
        queue.pipeline()
    
  10. 现在,让我们调用 run_facade 方法来测试设计模式:

    run_facade()
    

前面测试的输出如下:

Running return_cart...
Running goto_counter...
Running scan_bar_code...
Running add_tax...
  1. 最后,让我们创建另一个最终用户功能,通过为 Queue 创建一个实例并调用 pipeline_implicit 方法,使用 Facade 类来运行功能类中的方法:

    def run_facade_implicit():
        queue = Queue(items = ['paperclips','blue 
                         pens','stapler','pencils'],
                 name = ['Regular Counter'],
                 scan = [113323,3434332,2131243,2332783],
                 units = [10,15,12,14],
                 tax = [0.04,0.03,0.035,0.025],
                 )
        queue.pipeline_implicit()
    
  2. 然后,让我们调用 run_facade_implicit 方法来测试设计模式:

    run_facade_implicit()
    

前面测试的输出如下:

Running scan_bar_code...
Running add_billing...
Running return_cart...
Running add_tax...
Running calc_bill...
Running print_invoice...
**************ABC Megamart*****************
***********------------------**************
Running goto_counter...
Counter Name:  ['Regular Counter']
paperclips :  10.4
blue pens :  15.45
stapler :  12.42
pencils :  14.35
Total: 52.620000000000005
***********------------------**************
***************PAID************************

让我们在以下图形表示中连接这个设计模式的元素及其对应的对象:

图 13.2 – 外观模式类

图 13.2 – 外观模式类

因此,外观模式是通过创建一个黑盒来实现的,它为最终用户提供了一个接口,以便在不担心实现细节的情况下访问复杂系统的功能。现在,让我们看看代理模式。

理解代理模式

在本节中,我们将探讨代理设计模式。正如其名所示,代理模式用于在真实功能周围创建一个代理,以便只有在代理根据某些先决条件允许时,才会执行实际功能。为了进一步解释这一点并实现此设计模式,我们的代码需要以下元素:

  • 功能类:系统的基本功能设计在这个类中,作为方法。

  • functionality 类,并提供在何时从 functionality 类执行基本功能的限制。

在这个例子中,让我们考虑 ABC Megamart 的纽约分公司,并创建一个名为 NYC 的类:

  1. NYC 类使用四个名为 managerbranchproductsales 的空字典参数进行初始化。让我们也添加三个方法,分别命名为 set_parameters(用于设置四个字典参数)、get_parameters(用于返回参数)和 calc_tax_nyc(用于计算税费并返回参数以及销售价格数据):

    class NYC:
        def __init__(self):
            self.manager = {}
            self.branch = {}
            self.product = {}
            self.sales = {}        
        def set_parameters(self, manager, branch, product,
                           sales):
            self.manager = manager
            self.branch = branch
            self.product = product
            self.sales = sales        
        def get_parameters(self):
            return self.manager, self.branch, 
                   self.product, self.sales    
        def calc_tax_nyc(self):
            branch = self.branch
            manager = self.manager
            product = self.product
            sales = self.sales
            pricebeforetax = sales['purchase_price'] + 
                             sales['purchase_price'] * 
                             sales['profit_margin']
            finalselling_price = pricebeforetax + 
                (pricebeforetax * (sales['tax_rate'] + 
                 sales['local_rate']))  
            sales['selling_price'] = finalselling_price
            return branch, manager, product, sales   
    
  2. 实现的下一步是创建一个代理 ReturnBook 类,以调用 NYC 类的方法来设置参数、获取参数和计算税费:

    class ReturnBook(NYC):
        def __init__(self, nyc):
            self.nyc = nyc
        def add_book_details(self, state, manager, branch, 
                             product, sales):
            if state in ['NY', 'NYC', 'New York']:
                self.nyc.set_parameters(manager, branch, 
                                        product, sales)
            else:
                print("There is no branch in the state:", 
                      state)
        def show_book_details(self, state):
            if state in ['NY', 'NYC', 'New York']:
                return self.nyc.get_parameters()
            else:
                print(state, "has no data")
        def calc_tax(self, state):
            if state in ['NY', 'NYC', 'New York']:
                return self.nyc.calc_tax_nyc()
            else:
                print("The state", state, "is not 
                       supported") 
    
  3. 现在,让我们实例化代理 ReturnBook 类,并将 NYC 功能类作为输入参数提供:

    branch_manhattan = ReturnBook(NYC())
    
  4. 要从 NYC 类设置参数,我们将从代理类调用 add_book_details 方法。只有当输入状态参数成功满足 add_book_details 中提供的条件时,参数才会设置在 NYC 类中:

    branch_manhattan.add_book_details(state = 'NY', manager = {'regional_manager': 'John M',
      'branch_manager': 'Tom H',
      'sub_branch_id': '2021-01'},
       branch = {'branchID': 2021,
      'branch_street': '40097 5th Main Street',
      'branch_borough': 'Manhattan',
      'branch_city': 'New York City',
      'branch_state': 'New York',
      'branch_zip': 11007},
       product = {'productId': 100002,
      'product_name': 'WashingMachine',
      'product_brand': 'Whirlpool'},
       sales = {'purchase_price': 450,
      'profit_margin': 0.19,
      'tax_rate': 0.4,
      'local_rate': 0.055})
    
  5. 让我们进一步调用 show_book_details 方法来获取 NYC 类的参数,前提是输入的状态参数为 NYNYCNew York

    branch_manhattan.show_book_details('NY')
    

上述代码的输出如下:

({'regional_manager': 'John M',
  'branch_manager': 'Tom H',
  'sub_branch_id': '2021-01'},
 {'branchID': 2021,
  'branch_street': '40097 5th Main Street',
  'branch_borough': 'Manhattan',
  'branch_city': 'New York City',
  'branch_state': 'New York',
  'branch_zip': 11007},
 {'productId': 100002,
  'product_name': 'WashingMachine',
  'product_brand': 'Whirlpool'},
 {'purchase_price': 450,
  'profit_margin': 0.19,
  'tax_rate': 0.4,
  'local_rate': 0.055})
  1. 让我们进一步从代理类调用 calc_tax 方法来计算销售价格,前提是状态参数成功:

    branch_manhattan.calc_tax('NY')
    
  2. 让我们通过向状态参数提供错误输入来测试代理方法中的限制:

    branch_manhattan.add_book_details(state = 'LA', manager = {'regional_manager': 'John M',
      'branch_manager': 'Tom H',
      'sub_branch_id': '2021-01'},
       branch = {'branchID': 2021,
      'branch_street': '40097 5th Main Street',
      'branch_borough': 'Manhattan',
      'branch_city': 'New York City',
      'branch_state': 'New York',
      'branch_zip': 11007},
       product = {'productId': 100002,
      'product_name': 'WashingMachine',
      'product_brand': 'Whirlpool'},
       sales = {'purchase_price': 450,
      'profit_margin': 0.19,
      'tax_rate': 0.4,
      'local_rate': 0.055})
    

上述代码的输出如下:

There is no branch in the state: LA
  1. 同样,让我们也测试 show_book_details 方法:

    branch_manhattan.show_book_details('LA')
    

上述代码的输出如下:

LA has no data
  1. 最后,让我们测试代理中的 calc_tax 方法:

    branch_manhattan.calc_tax('LA')
    

输出如下:

The state LA is not supported

让我们将此设计模式的元素与以下图形表示中的相应对象相连接:

图 13.3 – 代理设计模式类

图 13.3 – 代理设计模式类

因此,代理模式是通过创建一个代理类来实现的,该代理类添加了执行实际功能所需的条件。接下来,我们将继续探索创建型设计模式。

探索创建型设计模式

创建型设计模式是添加对象创建过程中的抽象的各种方法。在本节中,我们将探讨三种这样的设计模式,即工厂方法、原型模式和单例模式。

理解工厂方法

工厂设计模式是一种抽象方法,其中创建一个工厂类来创建工厂类中的对象,而不是直接实例化对象。为了进一步解释这一点并实现此设计模式,我们的代码需要以下元素:

  • 抽象类:具有在子类中定义的功能的抽象方法的抽象类。

  • 抽象子类:子类继承自抽象类并覆盖了抽象方法。

  • 工厂类:用于为抽象子类创建对象的类。

  • 终端用户方法:用于测试或调用工厂方法的类或方法。

对于这个例子,让我们使用来自 ABC Megamart 的另一个场景来实现:

  1. 让我们创建一个具有两个方法 buy_productmaintenance_cost 的抽象类:

    from abc import abstractmethod
    class Branch:
        @abstractmethod
        def buy_product(self):
            pass
        @abstractmethod
        def maintenance_cost(self):
            pass
    
  2. 现在,让我们为 Branch 类创建一个名为 Brooklyn 的子类,并实现 buy_productmaintenance_cost 方法:

    class Brooklyn(Branch):
        def __init__(self,product,unit_price,quantity,
                     product_type):
            self.product = product
            self.unit_price = unit_price
            self.quantity = quantity
            self.product_type = product_type        
        def buy_product(self):
            if (self.product_type == 'FMCG'):
                self.statetax_rate = 0.035
                self.promotiontype = 'Discount'
                self.discount = 0.10
                self.initialprice = 
                    self.unit_price*self.quantity 
                self.salesprice = self.initialprice + 
                    self.initialprice*self.statetax_rate
                self.finalprice = self.salesprice * 
                    (1-self.discount)
                return self.salesprice, 
                    self.product,self.promotiontype
            else:
                return "We don't stock this product"
         def maintenance_cost(self):
            self.coldstorageCost = 100
            if (self.product_type == 'FMCG'):
                self.maintenance_cost = self.quantity * 
                    0.25 + self.coldstorageCost    
                return self.maintenance_cost
            else:
                return "We don't stock this product"
    
  3. 类似地,让我们创建另一个名为 Manhattan 的子类,它继承自 Branch 类,如下所示:

    class Manhattan(Branch):
        def __init__(self,product,unit_price,quantity,
                     product_type):
            self.product = product
            self.unit_price = unit_price
            self.quantity = quantity
            self.product_type = product_type
    
  4. 让我们进一步定义一个名为 buy_product 的方法,在产品是电子产品的场合返回产品价格、产品名称和促销信息:

        def buy_product(self):
            if (self.product_type == 'Electronics'):
                self.statetax_rate = 0.05        
                self.promotiontype = 'Buy 1 Get 1'
                self.discount = 0.50
                self.initialprice = 
                    self.unit_price*self.quantity 
                self.salesprice = self.initialprice + 
                    self.initialprice*self.statetax_rate
                self.finalprice = self.salesprice * 
                    (1-self.discount)
                return self.finalprice, 
                    self.product,self.promotiontype
            else:
                return "We don't stock this product"
    
  5. 现在,让我们定义另一个方法来计算维护成本:

        def maintenance_cost(self):
            if (self.product_type == 'Electronics'):
              self.maintenance_cost = self.quantity * 0.05
                return self.maintenance_cost
            else:
                return "We don't stock this product"
    
  6. 在下一步中,让我们创建一个名为 BranchFactory 的工厂类,它为分支子类 BrooklynManhattan 创建实例:

    Class BranchFactory:
        def create_branch(self,branch,product,unit_price,
                          quantity,product_type):
            if str.upper(branch) == 'BROOKLYN':
                return Brooklyn(product,unit_price,
                                quantity,product_type)
    
            elif str.upper(branch) == 'MANHATTAN':
                return Manhattan(product,unit_price,
                                 quantity,product_type)
    
  7. 现在,让我们通过创建一个名为 test_factory 的函数来测试工厂方法:

    def test_factory(branch,product,unit_price,quantity,product_type):
        branchfactory = BranchFactory()
        branchobject = branchfactory.create_branch(branch,
                product,unit_price,quantity,product_type) 
        print(branchobject)
        print(branchobject.buy_product())
        print(branchobject.maintenance_cost())
    
  8. 现在,使用以下输入调用 test_factory 函数:BrooklynMilk10.5FMCG

    test_factory('Brooklyn','Milk', 10,5,'FMCG')
    

上述代码的输出如下:

<__main__.Brooklyn object at 0x000002101D4569A0>
(51.75, 'Milk', 'Discount')
101.25
  1. 现在,使用以下输入调用 test_factory 函数:manhattaniPhone10001Electronics

    test_factory('manhattan','iPhone', 1000,1,'Electronics')
    

上述代码的输出如下:

<__main__.Manhattan object at 0x000002101D456310>
(525.0, 'iPhone', 'Buy 1 Get 1')
0.05

让我们用以下图形表示法将此设计模式的元素与示例中的相应对象联系起来:

图 13.4 – 工厂模式类

图 13.4 – 工厂模式类

因此,通过创建一个工厂类来实例化 Abstraction 子类,我们已经通过一个示例学习了创建型设计模式。

理解原型方法

原型设计模式也用于在创建 Python 对象时实现抽象。终端用户可以使用原型来创建类的对象的副本,而不需要理解其背后的详细实现。为了进一步解释这一点并实现此设计模式,我们的代码需要以下元素:

  • 原型类:这个类有一个方法可以克隆或复制具有实现的另一个 Python 对象。

  • 实现类:这个类具有作为属性和方法的实际功能实现。

对于这个例子,让我们使用来自 ABC Megamart 的另一个场景来实现:

  1. 让我们创建一个名为 Prototype 的类,并定义一个名为 clone 的方法来复制作为方法输入提供的 Python 对象:

    class Prototype:
        def __init__(self):
            self.cp = __import__('copy')
    
        def clone(self, objname):
            return self.cp.deepcopy(objname)
    
  2. 现在让我们创建一个名为 FMCG 的实现类,并初始化一组与供应商详情相关的变量,并添加一个获取供应商详情的方法:

    class FMCG:
        def __init__(self,supplier_name,supplier_code,
        supplier_address,supplier_contract_start_date,\
        supplier_contract_end_date,supplier_quality_code):
            self.supplier_name = supplier_name
            self.supplier_code = supplier_code
            self.supplier_address = supplier_address
            self.supplier_contract_start_date = 
                 supplier_contract_start_date
            self.supplier_contract_end_date = 
                 supplier_contract_end_date
            self.supplier_quality_code = 
                 supplier_quality_code
    
        def get_supplier_details(self):
            supplierDetails = {
               'Supplier_name': self.supplier_name, 
                'Supplier_code': self.supplier_code,
                'Supplier_address': self.supplier_address,
                'ContractStartDate': 
                     self.supplier_contract_start_date,
                'ContractEndDate': 
                     self.supplier_contract_end_date, 
                'QualityCode': self.supplier_quality_code
            }
            return supplierDetails
    
  3. 在下一步中,让我们为 FMCG 类创建一个名为 fmcg_supplier 的对象:

    fmcg_supplier = FMCG('Test Supplier','a0015','5093 9th Main Street, Pasadena,California, 91001', '05/04/2020', '05/04/2025',1)
    
  4. 让我们也为 Prototype 类创建一个名为 proto 的对象:

    proto = Prototype()
    
  5. 现在,我们可以直接克隆 fmcg_supplier 对象,而无需传递 FMCG 类的所有属性作为输入。为此,我们将使用 Prototype 类中的 clone 方法:

    fmcg_supplier_reuse = proto.clone(fmcg_supplier)
    
  6. fmcg_supplier_reuse 对象是 fmcg_supplier 对象的克隆,并且它本身不是同一个对象。这可以通过查看这两个对象的 ID 来验证:

    id(fmcg_supplier)
    

输出如下:

2268233820528
  1. 同样,我们也可以查看克隆对象的 ID:

    id(fmcg_supplier_reuse)
    

输出如下:

2268233819616
  1. 让我们也验证一下克隆的对象可以被修改而不会影响实际对象:

    fmcg_supplier_reuse.supplier_name = 'ABC Supplier'
    fmcg_supplier_reuse.get_supplier_details()
    

输出如下:

{'Supplier_name': 'ABC Supplier',
 'Supplier_code': 'a0015',
 'Supplier_address': '5093 9th Main Street, Pasadena,California, 91001',
 'ContractStartDate': '05/04/2020',
 'ContractEndDate': '05/04/2025',
 'QualityCode': 1}
  1. 在前面的输出中,我们已经修改了克隆的对象,这不应该影响原始对象。让我们验证原始对象:

    fmcg_supplier.get_supplier_details()
    

输出如下:

{'Supplier_name': 'Test Supplier',
 'Supplier_code': 'a0015',
 'Supplier_address': '5093 9th Main Street, Pasadena,California, 91001',
 'ContractStartDate': '05/04/2020',
 'ContractEndDate': '05/04/2025',
 'QualityCode': 1}

因此,通过创建一个 Prototype 类来复制实现类的对象,我们已经实现了原型模式。现在你已经理解了这个,让我们看看单例设计模式。

理解单例模式

如其名所示,单例模式是一种设计模式,其中我们可以在初始化类本身的同时限制为该类创建的实例数量。为了进一步解释并实现这个设计模式,我们需要在我们的代码中开发 单例类 的元素。

与其他模式不同,这个模式只有一个元素——单例类。单例类将在其 init 方法中设置一个约束,以限制实例数量为一个是。

对于这个例子,让我们使用来自 ABC Megamart 的另一个场景来实现:

  1. 让我们定义一个名为 SingletonBilling 的类。这个类将包含生成产品账单所需的属性:

    class SingletonBilling:
        billing_instance = None
        product_name = 'Dark Chocolate'
        unit_price = 6
        quantity = 4
        tax = 0.054
    
  2. 让我们在该类的 init 方法中添加一个约束来限制类实例的数量为一个是:

        def __init__(self):
            if SingletonBilling.billing_instance == None:
                SingletonBilling.billing_instance = self
            else:
                print("Billing can have only one 
                       instance")
    
  3. 在下一步中,让我们也添加一个 generate_bill 方法来根据类的属性生成产品的账单:

        def generate_bill(self):
            total = self.unit_price * self.quantity 
            final_total = total + total*self.tax
            print('***********------------------
                   **************')
            print('Product:', self.product_name)
            print('Total:',final_total)
            print('***********------------------
                   **************')
    
  4. 在下一步中,我们可以首次实例化类对象并调用其 generate_bill 方法:

    invoice1 = SingletonBilling()
    invoice1.generate_bill()
    

输出如下:

***********------------------**************
Product: Dark Chocolate
Total: 25.296
***********------------------**************
  1. 现在让我们通过为类实例化另一个实例来测试单例模式:

    invoice2 = SingletonBilling()
    

由于其单例属性,无法为该类创建第二个实例。输出符合预期:

Billing can have only one instance

因此,单例模式是通过限制单例类不能创建超过一个实例来实现的。通过这个例子,我们已经涵盖了三种类型的创建型设计模式及其实现。

摘要

在本章中,我们通过在 Python 3 中应用一些这些设计模式,学习了结构型和创建型设计模式的概念。我们实现了桥接设计模式并理解了其各个元素。我们还理解了外观设计模式及其各种元素。此外,我们还通过一个示例实现了代理设计模式。我们还涵盖了创建型设计模式,如工厂方法、原型和单例模式及其相应的示例。

与本书中涵盖的其他章节类似,本章解释设计模式的第二部分,也专注于元编程及其对 Python 代码的影响。

在下一章中,我们将通过一些示例继续进行代码生成。

第十四章:第十四章:从抽象语法树生成代码

在本章中,我们将学习如何使用 Python 中的 AST 为各种应用程序生成代码。我们将应用这些抽象语法树到元编程中,以实现本章中自动生成的代码。

自动代码生成是使程序员生活更轻松的一种方式。抽象语法树是一个出色的功能,可以帮助我们以更简单的方式生成代码。

本书第九章第九章中讨论了 AST 的概念,并附有示例。在本章中,我们将利用 AST 的优势来自动生成代码。代码生成可以实现,以便在开发应用程序时无需代码或仅限少量编码。在本章中,我们将继续使用ABC Megamart的例子来从 AST 生成代码。

在本章中,我们将探讨以下主要主题:

  • 使用模板生成简单类

  • 从列表生成多个类

  • 生成具有属性的类

  • 生成具有方法的类

  • 定义自定义类工厂

  • 开发代码生成器以生成简单库

到本章结束时,你应该能够理解如何使用 Python 中ast库的现有方法来使你的应用程序生成自己的代码,如何避免重复,以及如何动态生成代码。

技术要求

本章中分享的代码示例可在 GitHub 上找到,地址为github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter14

使用模板生成简单类

在本节中,我们将探讨如何在不实际定义类本身的情况下生成类的代码。我们将创建一个基于字符串的模板,具有我们想要开发的类的结构,但不包含可执行的代码。为了进一步解释这一点,让我们看一个例子,我们将通过使用ast模块解析一系列字符串来生成名为VegCounter的类。

生成类代码的步骤顺序表示在以下流程图中:

图 14.1 – 简单类的代码生成序列

图 14.1 – 简单类的代码生成序列

让我们看看这个例子的实现:

  1. 我们将首先导入ast库:

    import ast
    
  2. 现在让我们创建一个变量,用于传递需要生成代码的类名:

    classname = "VegCounter"
    
  3. 我们接下来将定义一个变量,它将成为本例中生成的类的模板:

    classtemplate = """class """ +classname+ """():pass"""
    
  4. 在下一步中,我们将使用ast模块中的parse方法解析类模板:

    print(ast.dump(class_tree, indent = 4))
    
  5. 上述代码的输出显示了类模板的抽象语法树:

    Module(
        body=[
            ClassDef(
                name='VegCounter',
                bases=[],
                keywords=[],
                body=[
                    Pass()],
                decorator_list=[])],
        type_ignores=[])
    
  6. 前面的树可以按以下方式编译和执行:

    actualclass = compile(class_tree, 'vegctr_tree', 'exec')
    actualclass
    

因此,这导致了以下输出:

<code object <module> at 0x0000028AAB0D2A80, file "vegctr_tree", line 1>
  1. 在下一步中,我们将反解析树以生成类的实际代码:

    VegCounter.
    print(ast.unparse(class_tree))
    

执行前面的代码会导致以下输出:

class VegCounter:
    pass
  1. 在下一步中,让我们将前面的类代码写入名为 classtemplate.py 的文件:

    code = open("classtemplate.py", "w")
    script = code.write(ast.unparse(class_tree))
    code.close()
    
  2. classtemplate 文件看起来如下所示:

图 14.2 –  文件

图 14.2 – classtemplate.py 文件

  1. 现在我们导入 classtemplate 并创建一个对象:

    import classtemplate as c
    vegc = c.VegCounter()
    vegc
    

输出如下所示:

<classtemplate.VegCounter at 0x28aab1d6a30>

在本节中,我们使用 ast 模块生成了一个简单的类代码。这个例子帮助我们理解生成自定义类代码的步骤,因为从简单开始理解代码生成更容易。有了这个理解,让我们为多个类生成代码。

从列表生成多个类

在本节中,我们将探讨如何使用 ast 模块及其 unparse 方法动态地为多个类生成代码。

动态地为多个类生成代码,为我们实现应用程序多个功能的代码生成提供了方向。这些类不需要具有相同的功能,因此生成的类代码可以稍后修改,以包括应用程序所需的额外方法或属性。示例中将生成骨架类代码。

为了进一步理解,我们将遵循以下流程图描述的顺序。

图 14.3 – 多个类的代码生成序列

图 14.3 – 多个类的代码生成序列

现在我们来看看如何实现这个场景:

  1. 我们首先定义一个变量,该变量可以分配一个包含类名的列表作为值:

    classnames = ["VegCounter", "ElectronicsCounter", "PasadenaBranch", "VegasBranch"]
    
  2. 在下一步中,让我们看看如何从前面的列表中为每个类名生成类模板:

    classgenerator = []
    for classname in classnames:
        classcode = """class """ +classname+ """():pass"""
        classgenerator.append(classcode)
    classgenerator
    

类模板被添加到另一个名为 classgenerator 的列表中,该列表如下所示:

['class VegCounter():pass',
 'class ElectronicsCounter():pass',
 'class PasadenaBranch():pass',
 'class VegasBranch():pass']
  1. 为了解析前面的输出中的字符串模板并生成它们的抽象语法树,让我们创建另一个名为 classtrees 的列表并存储这些树:

    classtrees = []
    for i in classgenerator:
        classtree = ast.parse(i)
        classtrees.append(classtree)
    classtrees
    

分解的类树,分配给 classtrees 列表变量,显示如下:

[<ast.Module at 0x1efa91fde20>,
 <ast.Module at 0x1efa91e6d30>,
 <ast.Module at 0x1efa91e6220>,
 <ast.Module at 0x1efa91e6370>]
  1. 在本步骤中,我们将审查其中一个树以确保抽象语法树按预期为类生成:

    print(ast.dump(classtrees[0], indent = 4))
    

输出如下所示:

Module(
    body=[
        ClassDef(
            name='VegCounter',
            bases=[],
            keywords=[],
            body=[
                Pass()],
            decorator_list=[])],
    type_ignores=[])
  1. 我们可以进一步将 classtrees 变量反解析,为每个类生成代码:

    print(ast.unparse(classtrees[1]))
    

一个示例输出如下所示:

class ElectronicsCounter:
    pass
  1. 让我们将所有生成的类写入一个文件:

    code = open("classtemplates.py", "w")
    for i in classtrees:
        code.write(ast.unparse(i))
        code.write("\n")
        code.write("\n")
    code.close()
    

生成的 classtemplates.py 文件看起来如下所示:

图 14.4 –  文件

图 14.4 – classtemplates.py 文件

  1. 让我们导入文件并调用每个类的实例以检查其是否工作:

    import classtemplates as ct
    print(ct.ElectronicsCounter())
    print(ct.PasadenaBranch())
    print(ct.VegasBranch())
    print(ct.VegCounter())
    

前面代码的输出如下所示:

<classtemplates.ElectronicsCounter object at 0x00000255C0760FA0>
<classtemplates.PasadenaBranch object at 0x00000255C0760F10>
<classtemplates.VegasBranch object at 0x00000255C0760FA0>
<classtemplates.VegCounter object at 0x00000255C0760F10>

在本节中,我们使用 ast 模块为多个类生成了代码。这个例子是朝着为应用程序的多个功能或模块自动生成代码的下一步。

生成具有属性的类

在本节中,我们将为包含动态包含在类中的属性列表的类生成代码。仅生成类的代码可以给出模块的初始骨架结构,而如果我们想使类更具体,则需要添加属性。以下流程图表示了本例要遵循的步骤序列:

图 14.5 – 具有多个属性的类的代码生成序列

图 14.5 – 具有多个属性的类的代码生成序列

让我们看看本例的代码:

  1. 我们首先定义一个变量以提供 classname 作为输入,然后定义一个 classtemplate 来创建类声明的模板:

    classname = "VegCounter"
    classtemplate =  '''class ''' +classname+ ''':'''+'\n    '
    
  2. 在下一步中,让我们定义另一个变量以提供属性名称作为输入:

    attributename = ['items', 'countername', 'billamount']
    
  3. 让我们进一步更新 classtemplate,提供生成类代码所需的前述每个属性:

    for attr in attributename:
        classtemplate = classtemplate + attr +''' = 
            None''' + '\n    '
    
  4. 让我们现在解析 classtemplate 并审查抽象语法树:

    class_tree = ast.parse(classtemplate)
    print(ast.dump(class_tree, indent = 4))
    
  5. 上述类模板的语法树如下所示:

    Module(
        body=[
            ClassDef(
                name='VegCounter',
                bases=[],
                keywords=[],
                body=[
                    Assign(
                        targets=[
                            Name(id='items', 
                        ctx=Store())],
                        value=Constant(value=None)),
                    Assign(
                        targets=[
                            Name(id='countername', 
                            ctx=Store())],
                        value=Constant(value=None)),
                    Assign(
                        targets=[
                            Name(id='billamount', 
                            ctx=Store())],
                        value=Constant(value=None))],
                decorator_list=[])],
        type_ignores=[])
    

所有三个变量 – itemscounternamebillamount – 现在都是语法树的一部分。如果我们详细审查树,我们可以在 body | assign | targets | name | id 下查看这些变量。

  1. 我们可以进一步解析树形结构并查看类的代码:

    print(ast.unparse(class_tree))
    

输出如下所示:

class VegCounter:
    items = None
    countername = None
    billamount = None

让我们将代码写入文件并导入:

code = open("classtemplateattr.py", "w")
script = code.write(ast.unparse(class_tree))
code.close()

生成的代码如下所示:

图 14.6 – classtemplateattr.py 文件

图 14.6 – classtemplateattr.py 文件

我们可以导入 classtemplateattr.py 文件,并且可以通过以下方式访问类:

import classtemplateattr as c
c.VegCounter()
vegc = c.VegCounter()
vegc.items = ['onions','tomatoes','carrots','lettuce']
vegc.countername = 'Veg Counter'
vegc.billamount = 200

输出如下所示,所有属性及其对应值都已分配:

['onions', 'tomatoes', 'carrots', 'lettuce']
Veg Counter
200

在本节中,我们生成了一个具有多个属性的类,而没有为类编写代码。相反,我们定义了一个模板,该模板接受类名和属性列表作为输入。有了这个理解,我们可以看看如何生成具有方法的类。

生成具有方法的类

在本节中,我们将为类及其方法生成代码。在本章中,我们的目标是动态生成代码以构建具有特定目的的应用程序。添加属性和方法使类的代码生成更加特定于应用。我们可以查看这个例子的两种变体:

  • 生成具有 init 方法的类

  • 生成具有用户定义方法的类

让我们详细讨论每个部分。

生成具有 init 方法的类

在这个例子中,让我们为类生成代码,并向类中添加一个init方法,并初始化属性。在这个例子中,我们将定义一个用于ABC Megamart蔬菜计数器的类。在init方法中,让我们在这个类中初始化来自ABC Megamart蔬菜计数器的购物车项目:

classname = "VegCounter"
classtemplate =  '''class ''' +classname+ ''':'''+'\n' +''' def __init__(self,*items):
        cartItems = []
        for i in items:
            cartItems.append(i)
        self.items = cartItems'''
class_tree = ast.parse(classtemplate)
print(ast.unparse(class_tree))

解析后的类模板生成了以下代码:

class VegCounter:
    def __init__(self, *items):
        cartItems = []
        for i in items:
            cartItems.append(i)
        self.items = cartItems

该类的抽象语法树是通过函数定义生成的,如下图中所示:

图 14.7 – 初始化方法的函数定义

图 14.7 – 初始化方法的函数定义

通过这个理解,让我们通过生成用户定义方法的代码来看这个类的另一个示例。

生成具有用户定义方法的类

在本节中,让我们通过创建一个模板来生成类的用户定义方法,看看类的变体:

classname = "VegCounter"
methodname = "returnCart"
classtemplate =  '''class ''' +classname+ ''':'''+'\n' +''' def '''+methodname+'''(self,*items):
        cartItems = []
        for i in items:
            cartItems.append(i)
        return cartItems'''
class_tree = ast.parse(classtemplate)
print(ast.unparse(class_tree))

解析后的classtemplate生成了以下代码:

class VegCounter:
    def returnCart(self, *items):
        cartItems = []
        for i in items:
            cartItems.append(i)
        return cartItems

该类的抽象语法树是通过函数定义生成的,如下图中所示:

图 14.8 – 用户定义方法的函数定义

图 14.8 – 用户定义方法的函数定义

当我们想在类级别初始化购物车项目时,我们可以使用init方法,或者稍后使用属性。相比之下,如果我们想保持方法特定的属性并基于方法内的属性执行操作,则可以使用用户定义的方法。

通过这个理解,让我们来看定义自定义类工厂。

定义自定义类工厂

在本节中,让我们定义一个名为classgenerator的函数,该函数使用类模板生成自定义类、属性和方法,如下所示:

def classgenerator(classname, attribute, method):
    classtemplate = '''class ''' +classname+ 
          ''':'''+'\n    ' +attribute+''' = 
          None\n    def '''+method+'''(self,item,status):
        if (status == 'Y'):
            print('Test passed for', item)
        else:
            print('Get another', item)
        '''
    return classtemplate

在本节中,我们通过创建一个函数来使代码生成更加动态,该函数可以生成具有自定义类名、属性名和方法名的代码。这有助于在应用程序中创建针对多个功能的自定义代码。

让我们将自定义类名、属性名和方法名作为输入提供给前面的函数:

class_tree = ast.parse(classgenerator('ElectronicCounter', 'TestItem', 'verifyCart')
actualclass = compile(class_tree, 'elec_tree', 'exec')
print(ast.unparse(class_tree))

生成的类代码如下:

class ElectronicCounter:
    TestItem = None
    def verifyCart(self, item, status):
        if status == 'Y':
            print('Test passed for', item)
        else:
            print('Get another', item)

我们可以在下一节通过开发一个代码生成库来进一步扩展这个示例。

开发一个生成简单库的代码生成器

在本节中,让我们开发一个简单的代码生成器,该生成器为具有getsetdelete属性的类生成代码。本节的目的是通过自动代码生成生成一个完整的库。为了实现这一点,让我们编写以下代码:

  1. 让我们按照以下方式定义代码生成器:

    class CodeGenerator:
        def __init__(self, classname, attribute):
            self.classname = classname
            self.attribute = attribute 
    
  2. 让我们进一步定义一个方法,用于在代码生成器中定义类模板,如下所示:

    def generatecode(self):
            classtemplate = '''class ''' +self.classname+ ''':'''+'''\n    def __init__(self):''' + '\n    '+'''    self._'''+self.attribute+''' = None\n\n    @property
        def test'''+self.attribute+'''(self):\n        return self.test'''+self.attribute+'''\n\n    @test'''+self.attribute+'''.getter
        def test'''+self.attribute+'''(self):\n        print("get test'''+self.attribute+'''")\n        return self._test'''+self.attribute+'''
        @test'''+self.attribute+'''.setter
        def test'''+self.attribute+'''(self, value):
            print("set test'''+self.attribute+'''")
            self._test'''+self.attribute+''' = value
        @test'''+self.attribute+'''.deleter
        def test'''+self.attribute+'''(self):
            print("del test'''+self.attribute+'''")
            del self._test'''+self.attribute+'''
            '''
            class_tree = ast.parse(classtemplate)
            print(ast.unparse(class_tree))
            print('\n')
    
  3. 现在,我们将前面的代码保存到名为codegenerator.py的文件中,并将该文件作为库导入:

    from codegenerator import CodeGenerator as c
    
  4. 让我们定义一个字典对象,并将多个类名及其对应的属性名作为输入:

    classes = {'VegCounter' : 'items',
               'ElectronicCounter' : 'goods',
               'BranchManhattan' : 'Sales',
               'BranchPasadena' : 'Products'
              }
    
  5. 让我们进一步定义一个名为 generatelib 的函数,并将 classes 作为输入参数。此函数接收类名及其属性名作为输入,并从 codegenerator 库的类模板生成代码:

    def generatelib(classes):
        for key, value in classes.items():
            codegen = c(key, value)
            codegen.generatecode()   
    
  6. 在此步骤中,让我们将生成的代码写入文件,以生成一个可进一步使用的自定义库:

    from contextlib import redirect_stdout
    with open('abcmegamartlib.py', 'w') as code:
        with redirect_stdout(code):
            generatelib(classes)
    code.close()
    
  7. 对于每个输入类,生成的代码格式如下:

    class VegCounter:
        def __init__(self):
            self._items = None
        @property
        def testitems(self):
            return self.testitems
        @testitems.getter
        def testitems(self):
            print('get testitems')
            return self._testitems
        @testitems.setter
        def testitems(self, value):
            print('set testitems')
            self._testitems = value
        @testitems.deleter
        def testitems(self):
            print('del testitems')
            del self._testitems
    
  8. 我们可以进一步导入生成的库,并按如下方式定义对象:

    import abcmegamartlib as abc
    abc.BranchManhattan()
    

上述代码返回以下输出:

<abcmegamartlib.BranchManhattan at 0x21c4800c7f0>

这些是使用 Python 的元编程 ast 模块可以实现的代码生成各种示例。

摘要

在本章中,我们探讨了生成自定义类及其自定义属性代码的多个示例。我们还涵盖了生成具有方法和属性的定制类代码的示例。最后,我们开发了一个代码生成器,可以使用 Python 中的抽象语法树概念来开发自定义库。

总体而言,我们看到了各种场景,这些场景可以帮助我们利用 Python 的 ast 模块中的抽象语法树,并使用 Python 元编程生成动态代码。

在下一章中,我们将讨论一个案例研究,我们可以将本书中涵盖的所有元编程概念应用于此案例研究。

第十五章:第十五章:实现案例研究

在本章中,我们将通过应用我们迄今为止学到的元编程概念来实现案例研究。对于这个案例研究,我们将使用Automobile. (1987) UCI 机器学习 仓库数据集。

在本章中,我们将探讨以下主要主题:

  • 解释案例研究

  • 定义基类

  • 开发代码生成库

  • 生成代码

  • 设计执行框架

到本章结束时,你应该了解如何使用 Python 中 ast 库的现有方法来使你的应用程序生成自己的代码。

技术要求

本章中分享的代码示例可在 GitHub 上找到:github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter15

解释案例研究

在我们开始实现之前,我们将查看案例研究的细节。让我们考虑一个专注于销售多个品牌新旧汽车的汽车代理机构,即ABC 汽车代理。这个代理机构希望开发一个应用程序,为每辆汽车生成定制的目录,显示汽车的各项规格和功能。

我们将通过应用本书中学到的概念来查看开发并构建应用程序的细节。需要编目 205 种不同的汽车,构建此案例研究使用的数据来自以下数据集:Automobile. (1987). UCI Machine Learning Repository

有许多方法可以开发一个可以解决这个问题应用程序。我们将探讨如何开发一个可重用的应用程序,该应用程序使用元编程。

汽车数据的整体视图如下:

图 15.1 – The Automobile. (1987). UCI Machine Learning Repository dataset

图 15.1 – The Automobile. (1987). UCI Machine Learning Repository dataset

对于这个案例研究,我们不会使用汽车数据集进行任何详细的数据处理。相反,我们将使用此数据集中的数据来创建应用程序开发的各种组件。本例的设计流程将从开发代码生成库开始,然后创建代码生成框架。然后我们将生成ABC 汽车代理库,最后创建执行框架。所有这些过程都将在本节中详细解释。

为此案例研究开发的 Python 脚本如下:

图 15.2 – ABC 汽车代理案例研究的 Python 脚本

图 15.2 – ABC 汽车代理案例研究的 Python 脚本

将开发以下类来开发汽车销售应用程序:

  • CarSpecs

  • CarMake及其子类

  • CarCatalogue

  • BodyStyle及其子类

  • SaleType及其子类

这些类中的每一个都将在本节中解释。

本应用程序的类结构将如下所示:

图 15.3 – 汽车销售应用程序概述

图 15.3 – 汽车销售应用程序概述

理解这一点后,我们将进一步查看应用程序的基类。

定义基类

我们现在开始构建案例研究所需的代码。

让我们首先开发一个名为CarSpecs的元类。这个类将具有以下结构:

  1. CarSpecs类的__new__方法将执行以下任务:

    1. 如果输入类的属性是整数,则将属性名添加为大写形式的feature,将值以字符串格式添加为info,并将type设置为数值型。

    2. 如果输入类的属性是字符串,则将属性名添加为大写形式的feature,将值以字符串格式添加为info,并将type设置为 varchar。

    3. 如果输入类的属性是布尔值,则将属性名添加为大写形式的feature,将值以字符串格式添加为info,并将type设置为布尔型。

    4. 如果不是,则实际属性将按原样返回。

现在让我们看看CarSpecs的定义:

from abc import ABC, abstractmethod
class CarSpecs(type):
    def __new__(classitself, classname, baseclasses, attributes):  
        newattributes = {}
        for attribute, value in attributes.items():
            if attribute.startswith("__"):
                newattributes[attribute] = value
            elif type(value)==int or type(value)==float:
                newattributes[attribute] = {}
                newattributes[attribute]['feature'] = attribute.title().replace('_', ' ')
                newattributes[attribute]['info'] = str(value)
                newattributes[attribute]['type'] = 'NUMERIC'
            elif type(value)==str:
                newattributes[attribute] = {}
                newattributes[attribute]['feature'] = attribute.title().replace('_', ' ')
                newattributes[attribute]['info'] = value.title()
                newattributes[attribute]['type'] = 'VARCHAR'
            elif type(value)==bool:
                newattributes[attribute] = {}
                newattributes[attribute]['feature'] = attribute.title().replace('_', ' ')
                newattributes[attribute]['info'] = value.title()
                newattributes[attribute]['type'] = 'BOOLEAN'

            else:
                newattributes[attribute] = value                
        return type.__new__(classitself, classname, baseclasses, newattributes)
  1. 在这个例子中,下一个类将是CarCatalogue,它将包含两个抽象方法来定义颜色和打印目录:

    class CarCatalogue(metaclass = CarSpecs):
        @abstractmethod
        def define_color(self):
            pass
        @abstractmethod
        def print_catalogue(self):
            pass
    
  2. 下一个类将是父类或超类,用于捕获汽车规格:

    class CarMake(metaclass = CarSpecs):   
        @abstractmethod
        def define_spec(self):
            pass     
    
  3. 让我们创建另一个名为BodyStyle的超类,它将捕获汽车的车身风格和引擎特性:

    class BodyStyle(metaclass = CarSpecs):
        @abstractmethod
        def body_style_features(self):
            pass   
    
  4. 对于这个案例研究,下一个类将是SaleType,我们将添加一个抽象方法来计算汽车的价格:

    class SaleType(metaclass = CarSpecs):
        @abstractmethod
        def calculate_price(self):
            pass
    
  5. 本类将作为SaleType的子类,用于计算新车的价格:

    class New(SaleType, CarCatalogue,  metaclass = CarSpecs):
        def calculate_price(self, classname):
            car = classname()
            price = float(car.price['info'])
            return price
    
  6. 下一个类将是另一个SaleType的子类,用于计算二手车价格:

    class Resale(SaleType, CarCatalogue,  metaclass = CarSpecs):
        def calculate_price(self, classname, years):
            car = classname()
            depreciation = years * 0.15
            price = float(car.price['info']) * (1 - depreciation)
            return price
    

这些是我们将在下一节中创建模板的主要类,用于生成代码。

开发一个代码生成器库

在本节中,让我们看看开发一个代码生成器,它将被用来为所有基类生成代码——CarSpecsCarMakeCarCatalogueBodyStyleSaleType。详细步骤如下:

  1. 让我们创建一个名为codegenerator.py的文件,并首先定义一个名为CodeGenerator的类:

    class CodeGenerator:
    
  2. 让我们定义一个方法,该方法导入ast库,并添加一个meta_template属性,其值为CarSpecs类的字符串格式。meta_template属性进一步解析和反解析为类代码:

    def generate_meta(self):
            ast = __import__('ast')
            meta_template = '''
    from abc import ABC, abstractmethod, ABCMeta
    class CarSpecs(type, metaclass = ABCMeta):
        def __new__(classitself, classname, baseclasses, attributes):  
            newattributes = {}
            for attribute, value in attributes.items():
                if attribute.startswith("__"):
                    newattributes[attribute] = value
                elif type(value)==int or type(value)==float:
                    newattributes[attribute] = {}
                    newattributes[attribute]['feature'] = attribute.title().replace('_', ' ')
                    newattributes[attribute]['info'] = str(value)
                    newattributes[attribute]['type'] = 'NUMERIC'
                elif type(value)==str:
                    newattributes[attribute] = {}
                    newattributes[attribute]['feature'] = attribute.title().replace('_', ' ')
                    newattributes[attribute]['info'] = value.title()
                    newattributes[attribute]['type'] = 'VARCHAR'
                elif type(value)==bool:
                    newattributes[attribute] = {}
                    newattributes[attribute]['feature'] = attribute.title().replace('_', ' ')
                    newattributes[attribute]['info'] = value.title()
                    newattributes[attribute]['type'] = 'BOOLEAN'
                else:
                    newattributes[attribute] = value                
            return type.__new__(classitself, classname, baseclasses, newattributes)
    '''
            meta_tree = ast.parse(meta_template)
            print(ast.unparse(meta_tree))
            print('\n')
    
  3. 现在让我们定义另一个名为generate_car_catalogue的方法,并添加CarCatalogue类的模板:

    def generate_car_catalogue(self):
            ast = __import__('ast')
            catalogue_template = '''
    class CarCatalogue(metaclass = CarSpecs):
        @abstractmethod
        def define_color(self):
            pass
    
        @abstractmethod
        def print_catalogue(self):
            pass
            '''
            catalogue_tree = ast.parse(catalogue_template)
            print(ast.unparse(catalogue_tree))
            print('\n')
    
  4. 下一步是定义一个名为generate_carmake_code的方法,并添加CarMake类的代码模板:

    def generate_carmake_code(self):
            ast = __import__('ast')
            carmake_template = '''
    class CarMake(metaclass = CarSpecs):   
        @abstractmethod
        def define_spec(self):
            pass     
            '''
            carmake_tree = ast.parse(carmake_template)
            print(ast.unparse(carmake_tree))
            print('\n')
    
  5. 在下一个代码块中,我们将定义另一个名为generate_bodystyle_parent的方法,并添加BodyStyle类的代码模板:

    def generate_bodystyle_parent(self):
            ast = __import__('ast')
            bodystyle_parent_template = '''
    class BodyStyle(metaclass = CarSpecs):
        @abstractmethod
        def body_style_features(self):
            pass  
            '''
            bodystyle_parent_tree = ast.parse(bodystyle_parent_template)
            print(ast.unparse(bodystyle_parent_tree))
            print('\n')
    
  6. 让我们进一步定义 generate_salestype_code 方法,该方法生成 SaleType 类的类代码:

    def generate_salestype_code(self):
            ast = __import__('ast')
            saletype_template = '''
    class SaleType(metaclass = CarSpecs):
        @abstractmethod
        def calculate_price(self):
            pass
            '''
            salestype_tree = ast.parse(saletype_template)
            print(ast.unparse(salestype_tree))
            print('\n')
    
  7. 在此步骤中,让我们定义 generate_newsale_code 方法以生成 New 类的代码:

    def generate_newsale_code(self):
            ast = __import__('ast')
            newsale_template = '''
    class New(SaleType, CarCatalogue,  metaclass = CarSpecs):
        def calculate_price(self, classname):
            car = classname()
            price = float(car.price['info'])
            return price
            '''
            newsale_tree = ast.parse(newsale_template)
            print(ast.unparse(newsale_tree))
            print('\n')
    
  8. 让我们进一步定义 generate_resale_code 方法,该方法生成 Resale 类的代码,并具有计算汽车残值的方法:

        def generate_resale_code(self):
            ast = __import__('ast')
            resale_template = '''
    class Resale(SaleType, CarCatalogue,  metaclass = CarSpecs):
        def calculate_price(self, classname, years):
            car = classname()
            depreciation = years * 0.15
            price = float(car.price['info']) * (1 - depreciation)
            return price
            '''
            resale_tree = ast.parse(resale_template)
            print(ast.unparse(resale_tree))
            print('\n')
    
  9. 在此步骤中,我们将定义一个 generate_car_code 方法;它继承自 CarMake 类,定义单个汽车品牌的颜色和规格,并打印目录:

    def generate_car_code(self, classname, carspecs):
            self.classname = classname
            self.carspecs = carspecs
            ast = __import__('ast')
            car_template = '''
    class '''+self.classname+'''(CarMake, CarCatalogue, metaclass = CarSpecs):
        fuel_type = '''+"'"+self.carspecs['fuel_type']+"'"+'''
        aspiration = '''+"'"+self.carspecs['aspiration']+"'"+'''
        num_of_door = '''+"'"+self.carspecs['num_of_door']+"'"+'''
        drive_wheels = '''+"'"+self.carspecs['drive_wheels']+"'"+'''
        wheel_base = '''+"'"+self.carspecs['wheel_base']+"'"+'''
        length = '''+"'"+self.carspecs['length']+"'"+'''
        width = '''+"'"+self.carspecs['width']+"'"+'''
        height = '''+"'"+self.carspecs['height']+"'"+'''
        curb_weight = '''+"'"+self.carspecs['curb_weight']+"'"+'''
        fuel_system = '''+"'"+self.carspecs['fuel_system']+"'"+'''
        city_mpg = '''+"'"+self.carspecs['city_mpg']+"'"+'''
        highway_mpg = '''+"'"+self.carspecs['highway_mpg']+"'"+'''
        price = '''+"'"+self.carspecs['price']+"'"+'''
        def define_color(self):
                BOLD = '\33[5m'
                BLUE = '\033[94m'
                return BOLD + BLUE
        def define_spec(self):
                specs = [self.fuel_type, self.aspiration, self.num_of_door, self.drive_wheels, 
                         self.wheel_base, self.length, self.width, self.height, self.curb_weight,
                        self.fuel_system, self.city_mpg, self.highway_mpg]
                return specs
        def print_catalogue(self):
                for i in self.define_spec():
                    print(self.define_color() + i['feature'], ": ", self.define_color() + i['info'])   
                    '''
            car_tree = ast.parse(car_template)
            print(ast.unparse(car_tree))
            print('\n')
    
  10. 此代码生成器的最后一个方法是 generate_bodystyle_code,它为不同的车身风格生成类代码,例如轿车和掀背车,定义单个车身风格的颜色和特性,并打印目录:

    def generate_bodystyle_code(self, classname, carfeatures):
            self.classname = classname
            self.carfeatures = carfeatures
            ast = __import__('ast')
            bodystyle_template = '''
    class '''+self.classname+'''(BodyStyle, CarCatalogue,  metaclass = CarSpecs):
        engine_location = '''+"'"+self.carfeatures['engine_location']+"'"+'''
        engine_type = '''+"'"+self.carfeatures['engine_type']+"'"+'''
        num_of_cylinders = '''+"'"+self.carfeatures['num_of_cylinders']+"'"+''' 
        engine_size = '''+"'"+self.carfeatures['engine_size']+"'"+'''
        bore = '''+"'"+self.carfeatures['bore']+"'"+'''
        stroke = '''+"'"+self.carfeatures['stroke']+"'"+'''
        compression_ratio = '''+"'"+self.carfeatures['compression_ratio']+"'"+'''
        horse_power = '''+"'"+self.carfeatures['horse_power']+"'"+'''
        peak_rpm = '''+"'"+self.carfeatures['peak_rpm']+"'"+'''
        def body_style_features(self):
                features = [self.engine_location, self.engine_type, self.num_of_cylinders, self.engine_size,
                         self.bore, self.stroke, self.compression_ratio, self.horse_power, self.peak_rpm]
                return features  
        def define_color(self):
                BOLD = '\33[5m'
                RED = '\033[31m'
                return BOLD + RED
        def print_catalogue(self):
                for i in self.body_style_features():
                    print(self.define_color() + i['feature'], ": ", self.define_color() + i['info'])  
                    '''
            bodystyle_tree = ast.parse(bodystyle_template)
            print(ast.unparse(bodystyle_tree))
            print('\n')
    

使用这些方法,我们已经准备好生成 ABC 汽车代理目录所需的代码。

现在,让我们进一步开发一个代码生成框架,该框架可以生成我们应用程序所需的数百个类。

生成代码

在本节中,我们将利用 codegenerator.py 生成基础类及其相应的子类,这些类为 ABC 汽车代理维护并打印各种目录,如下所示:

  1. 首先,让我们使用汽车数据来生成此应用程序所需的基础类。对于基础数据准备,让我们导入 pandas 库,它有助于处理数据:

    import pandas as pd
    
  2. 让我们加载数据并创建其副本。对于此应用程序,我们需要一组独特的汽车品牌和另一组独特的车身风格:

    auto = pd.read_csv("automobile.csv")
    auto_truncated = auto.copy(deep=True)
    auto_truncated.drop_duplicates(subset = ['make','body-style'], inplace = True)
    auto_truncated.reset_index(inplace = True, drop = True)
    auto_truncated['make'] = auto_truncated['make'].apply(lambda x: x.title().replace('-',''))
    auto_truncated.reset_index(inplace = True)
    auto_truncated['index'] = auto_truncated['index'].astype('str')
    auto_truncated['make'] = auto_truncated['make'] + auto_truncated['index']
    auto_truncated['body-style'] = auto_truncated['body-style'].apply(lambda x: x.title().replace('-',''))
    auto_truncated['body-style'] = auto_truncated['body-style'] + auto_truncated['index']
    

一旦处理了基本数据,让我们创建两个 DataFrames,它们将用于通过代码生成器生成多个类:

auto_specs = auto_truncated[['make', 'fuel-type', 'aspiration', 'num-of-doors', 'drive-wheels', 'wheel-base',  'length', 'width', 'height', 'curb-weight', 'fuel-system', 'city-mpg',  'highway-mpg', 'price']].copy(deep = True)
auto_specs.columns = ['classname', 'fuel_type', 'aspiration', 'num_of_door', 'drive_wheels',                      'wheel_base', 'length', 'width', 'height', 'curb_weight', 'fuel_system', 'city_mpg', 'highway_mpg', 'price' ]
for col in auto_specs.columns:
    auto_specs[col] = auto_specs[col].astype('str')
auto_features = auto_truncated[['body-style', 'engine-location', 'engine-type', 'num-of-cylinders', 'engine-size', 'bore', 'stroke', 'compression-ratio', 'horsepower', 'peak-rpm']].copy(deep = True)
auto_features.columns = ['classname', 'engine_location', 'engine_type', 'num_of_cylinders', 'engine_size', 'bore', 'stroke', 'compression_ratio', 'horse_power', 'peak_rpm']
for col in auto_features.columns:
    auto_features[col] = auto_features[col].astype('str')
  1. 在将数据处理成我们需要提供给代码生成器的格式后,规格的样本数据如下所示:

图 15.4 – 样本规格

图 15.4 – 样本规格

  1. 特性的样本数据如下所示:

图 15.5 – 样本特性

图 15.5 – 样本特性

  1. 现在基础数据已准备好用于生成代码,我们可以开始导入代码生成器:

    from codegenerator import CodeGenerator
    codegen = CodeGenerator()
    
  2. 在此步骤中,现在让我们定义一个函数,通过调用代码生成每个基础类,然后生成 CarMakeBodyStyle 的多个子类来生成库:

    def generatelib():
        codegen.generate_meta()
        codegen.generate_car_catalogue()
        codegen.generate_carmake_code()
        codegen.generate_bodystyle_parent()
        codegen.generate_salestype_code()
        codegen.generate_newsale_code()
        codegen.generate_resale_code()
        for index, row in auto_specs.iterrows():
            carspecs = dict(row)
            classname = carspecs['classname']
            del carspecs['classname']
            codegen.generate_car_code(classname = classname, carspecs = carspecs)
        for index, row in auto_features.iterrows():
            carfeatures = dict(row)
            classname = carfeatures['classname']
            del carfeatures['classname']
            codegen.generate_bodystyle_code(classname = classname, carfeatures = carfeatures)
    
  3. 打开一个名为 abccaragencylib.py 的 Python 文件,并调用 generatelib 函数来编写为所有所需类生成的代码:

    from contextlib import redirect_stdout
    with open('abccaragencylib.py', 'w') as code:
        with redirect_stdout(code):
            generatelib()
    code.close()
    
  4. 以下截图展示了一个示例类自动生成并写入 abccaragencylib.py

图 15.6 – 一个自动生成的汽车品牌类代码

图 15.6 – 一个自动生成的汽车品牌类代码

我们尚未自动生成此示例所需的代码。现在,我们将探讨设计执行框架。

设计执行框架

在本节中,让我们看看设计 ABC 汽车代理应用程序的最后一步,我们将实际运行在此案例研究过程中生成的代码:

  1. 让我们先加载自动生成的库:

    import abccaragencylib as carsales
    
  2. 在这个阶段,我们将通过实现外观设计模式来遵循一系列步骤,以便我们可以打印不同类型汽车的规格和功能:

    class Queue:
        def __init__(self, makeclass, styleclass, age):
            self.makeclass = makeclass
            self.styleclass = styleclass
            self.make = self.makeclass()
            self.style = self.styleclass()
            self.new = carsales.New()
            self.resale = carsales.Resale()
            self.age = age
        def pipeline(self):
            print('*********ABC Car Agency - Catalogue***********')
            self.make.print_catalogue()
            print('\n')
            self.style.print_catalogue()
            print('\n')
            print('New Car Price : ' + str(self.new.calculate_price(self.makeclass)))
            print('Resale Price : ' + str(self.resale.calculate_price(self.makeclass, self.age)))
    
  3. 让我们定义一个运行外观模式的方法:

    def run_facade(makeclass, styleclass, age):
        queue = Queue(makeclass, styleclass, age)
        queue.pipeline()
    
  4. 在这一步,我们将运行一个汽车品牌与车身风格的组合来生成目录:

    run_facade(carsales.AlfaRomero1, carsales.Hatchback28, 3)
    

输出结果如下:

*********ABC Car Agency - Catalogue***********
Fuel Type :  Gas
Aspiration :  Std
Num Of Door :  Two
Drive Wheels :  Rwd
Wheel Base :  94.5
Length :  171.2
Width :  65.5
Height :  52.4
Curb Weight :  2823
Fuel System :  Mpfi
City Mpg :  19
Highway Mpg :  26
Engine Location :  Front
Engine Type :  Ohc
Num Of Cylinders :  Four
Engine Size :  97
Bore :  3.15
Stroke :  3.29
Compression Ratio :  9.4
Horse Power :  69
Peak Rpm :  5200
New Car Price : 16500.0
Resale Price : 9075.0

CarMake生成了 56 个独特的子类,为BodyStyle也生成了 56 个独特的子类。我们可以使用CarMakeBodyStyle的各种组合来打印此应用程序的目录。

  1. 让我们尝试另一种组合:

    run_facade(carsales.Mitsubishi24, carsales.Sedan16, 5)
    

生成的输出如下:

*********ABC Car Agency - Catalogue***********
Fuel Type :  Gas
Aspiration :  Std
Num Of Door :  Two
Drive Wheels :  Fwd
Wheel Base :  93.7
Length :  157.3
Width :  64.4
Height :  50.8
Curb Weight :  1918
Fuel System :  2Bbl
City Mpg :  37
Highway Mpg :  41
Engine Location :  Front
Engine Type :  Dohc
Num Of Cylinders :  Six
Engine Size :  258
Bore :  3.63
Stroke :  4.17
Compression Ratio :  8.1
Horse Power :  176
Peak Rpm :  4750
New Car Price : 5389.0
Resale Price : 1347.25

这是通过在 Python 中应用元编程方法开发应用程序的逐步过程。

摘要

在本章中,我们学习了如何通过应用各种元编程技术来开发应用程序。我们首先解释了案例研究,并定义了此案例研究所需的基类。

我们还学习了如何开发代码生成器以及如何使用它生成代码。我们还设计了一个框架,可用于执行或测试在此案例研究中为应用程序生成的代码。

在下一章中,我们将探讨在设计 Python 和元编程的应用程序时可以遵循的一些最佳实践。

第十六章:第十六章:遵循最佳实践

在本章中,我们将学习一些 Python 编程的最佳实践,这些实践我们可以遵循并将其应用于元编程。Python 增强提案 8(PEP 8)中建议的实践,即 Python 代码的风格指南,也适用于元编程。

PEP 8 背后的概念起源于 Guido van Rossum、Barry Warsaw 和 Nick Coghlan 的文档,并在peps.python.org/pep-0008/中进行了详细解释。本章将涵盖 PEP 8 的一些重要概念,并通过使用ABC Megamart的示例来展示它们如何在元编程以及一般的 Python 编程中实现。

在本章中,我们将探讨以下主要主题:

  • 遵循 PEP 8 标准

  • 编写清晰的注释以供调试和重用

  • 添加文档字符串

  • 命名约定

  • 避免重用名称

  • 避免不必要的元编程

到本章结束时,你将了解执行 Python 元编程的最佳实践。

技术要求

本章中分享的代码示例可在 GitHub 上找到,地址为github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter16

遵循 PEP 8 标准

在本节中,我们将探讨在用 Python 元编程编写应用程序时应遵循的 PEP 8 标准。我们将使用ABC Megamart的示例来应用 PEP 8 文档中的这些标准。

在本节中,我们不会考虑我们遵循的编码标准是否正确,而是将考虑易于维护的编码标准与不易维护的编码标准之间的区别。

缩进

Python 是一种对缩进非常敏感的语言,如果缩进不正确,可能会抛出许多错误。对代码的整体缩进保持纪律有助于避免错误,并使代码更易于阅读。在这个例子中,让我们看看我们如何保持正确的缩进。

要开始查看缩进,让我们从一个大于 10 项的计数器示例开始。我们首先定义一个名为GreaterThan10Counter的类,并包含一个return_cart方法来返回购物车中的商品:

class GreaterThan10Counter():
    def return_cart(self, *items):
        cart_items = []
        for I in items:
            cart_items.append(i)
        return cart_items

让我们再为这个类创建一个对象实例:

greater = GreaterThan10Counter()

接下来,我们创建一个名为cart的变量,它将存储return_cart方法返回的值。鉴于这个类是用于大于 10 项的计数器,购物车返回的商品数量将超过 10,因此代码将不易阅读。

以下截图显示了代码在代码编辑器中的样子:

图 16.1 – 购物车变量赋值

图 16.1 – 购物车变量赋值

难以维护

如果我们将代码的无形部分移到下一行,图 16.1cart 变量的代码将如下所示:

图 16.2 – 未对齐调整的 cart 变量

图 16.2 – 未对齐调整的 cart 变量

上述代码本身并没有错误,因为我们运行它时它仍然会执行而不会出错。唯一的问题是它将很难维护。

易于维护

现在让我们通过将代码与符号对齐来改变缩进,使其易于阅读和维护,如果其他开发者需要接管编辑,代码看起来如下:

图 16.3 – 调整后的 cart 变量

图 16.3 – 调整后的 cart 变量

现在我们已经理解了这一点,让我们看看下一个最佳实践,即以整洁的方式展示代码。

整洁的表示

现在让我们看看在编写代码时如何以及在哪里添加空格。

难以维护

让我们看看以下示例,我们将定义一个名为 signaturedecorator 函数,操作符和它们对应的变量之间没有空格:

def signature(branch):
    def footnote(*args):
        LOGO='\33[43m'
        print(LOGO+'ABC Mega Mart')
        return branch(*args)
    return footnote

让我们进一步在另一个名为 manager_manhattan 的函数上调用 decorator,操作符和变量之间没有空格:

@signature
def manager_manhattan(*args):
    GREEN='\033[92m'
    SELECT='\33[7m'
    for arg in args:
        print(SELECT+GREEN+str(arg))

接下来,让我们按照以下方式调用函数:

manager_manhattan('John M','john.m@abcmegamart.com','40097 5th Main Street','Manhattan','New York City','New York',11007)

上述代码仍然可以正常运行而不会出错,但代码的展示并不整洁,也不容易维护,因为很难区分变量和它的操作符:

ABC Mega Mart
John M
john.m@abcmegamart.com
40097 5th Main Street
Manhattan
New York City
New York
11007

让我们在代码中添加空格。

易于维护

让我们在 signature 函数中添加空格:

def signature(branch):
    def footnote(*args):
        LOGO = '\33[43m'
        print(LOGO + 'ABC Mega Mart')
        return branch(*args)
    return footnote

同样,让我们也在 manager_manhattan 函数中添加空格:

@signature
def manager_manhattan(*args):
    GREEN = '\033[92m'
    SELECT = '\33[7m'
    for arg in args:
        print(SELECT + GREEN + str(arg))

现在让我们调用这个函数:

manager_manhattan('John M', 'john.m@abcmegamart.com', 
                  '40097 5th Main Street', 'Manhattan', 'New York City', 'New York',11007)

运行上述代码会产生以下输出:

ABC Mega Mart
John M
john.m@abcmegamart.com
40097 5th Main Street
Manhattan
New York City
New York
11007

由于添加了空格,上述代码使得区分变量和它们对应的操作符变得更加容易。

通过这个理解,让我们看看下一个最佳实践,即在代码中添加注释。

为调试和重用编写清晰的注释

编写内联注释有助于我们理解为什么编写特定的代码块,并且我们可以随着代码的变化更新注释。我们建议编写注释,以便将来更容易调试代码。然而,请确保注释与代码相关。让我们看看几个内联注释的例子。

冗余注释

让我们看看以下示例,其中我们创建一个元类并从另一个类中调用元类:

class ExampleMetaClass1(type):
    def __new__(classitself, *args):
        print("class itself: ", classitself)
        print("Others: ", args)
        return type.__new__(classitself, *args)
class ExampleClass1(metaclass = ExampleMetaClass1):    
    int1 = 123             # int1 is assigned a value of 123
    str1 = 'test'
    def test():
        print('test')

在前面的代码中,注释清楚地解释了代码所执行的内容,这可以通过简单地查看代码来理解。当我们想要将来调试或修改代码时,这不会很有帮助。

相关注释

让我们看看 Singleton 设计模式并添加相关注释:

class SingletonBilling:         # This code covers an example of Singleton design pattern
    billing_instance = None
    product_name = 'Dark Chocolate'
    unit_price = 6
    quantity = 4
    tax = 0.054    
    def __init__(self):
        if SingletonBilling.billing_instance == None:
            SingletonBilling.billing_instance = self
        else:
            print("Billing can have only one instance")
    def generate_bill(self):
        total = self.unit_price * self.quantity 
        final_total = total + total*self.tax
        print('***********------------------**************')
        print('Product:', self.product_name)
        print('Total:',final_total)
        print('***********------------------**************')

在前面的代码中,注释指定了 SingletonBilling 的用途,而不是提及代码执行的明显任务。

通过这个理解,让我们看看下一个最佳实践,即添加文档字符串。

添加文档字符串

添加文档字符串是为了提供更多关于打算在其他程序或应用中导入和使用的代码的信息。文档字符串将为最终用户提供有关他们将要从程序中调用的代码的信息。这对于最终用户不是库的开发者,而是一个用户来说特别有帮助。让我们看看文档字符串应该在哪里使用的一个例子。

让我们首先创建一个名为 vegcounter.py 的 Python 文件,并添加以下代码:

def return_cart(*items):
    '''
    This function returns the list of items added to the cart.    
    items: input the cart items. Eg: 'pens', 'pencils'
    '''
    cart_items = []
    for i in items:
        cart_items.append(i)
    return cart_items

在前面的代码中,我们通过提供函数及其参数的描述来定义了文档字符串。

Python 文件看起来如下所示:

图 16.4 – 添加到 vegcounter.py 的文档字符串

图 16.4 – 添加到 vegcounter.py 的文档字符串

现在让我们按照以下方式将 vegcounter.py 导入到另一个程序中:

import vegcounter as vc

注意,在这个程序中,vegcounter 内部的函数代码对最终用户不可访问,但 vegcounter 中的函数可以被最终用户的程序调用。

下面的截图演示了文档字符串如何提供本例中所需的信息:

图 16.5 – 文档字符串示例

图 16.5 – 文档字符串示例

在这个例子中,我们在 Python 文件中添加的文档字符串为最终用户提供有关函数及其相应参数以及示例的信息。

元编程的文档字符串

在这个例子中,让我们定义一个名为 BranchMetaClass 的元类,并添加一个文档字符串,说明这是一个元类,不应作为超类或父类继承。将此代码保存到 branch.py

class BranchMetaclass(type):
    '''
    This is a meta class for ABC Megamart branch that adds an additional 
    quality to the attributes of branch classes. 
    Add this as only a meta class.
    There are no methods to inherit this class as a parent class or super class.    
    '''
    def __new__(classitself, classname, baseclasses, attributes):
        import inspect
        newattributes = {}
        for attribute, value in attributes.items():
            if attribute.startswith("__"):
                newattributes[attribute] = value
            elif inspect.isfunction(value):
                newattributes['branch' + attribute.title()] = value
            else:
                newattributes[attribute] = value
        return type.__new__(classitself, classname, baseclasses, newattributes)

现在让我们按照以下方式导入分支及其相应的元类:

from branch import BranchMetaclass

现在让我们调用 BranchMetaclass 来检查文档字符串:

BranchMetaclass

文档字符串在以下屏幕截图中显示:

图 16.6 – BranchMetaclass 的文档字符串

图 16.6 – BranchMetaclass 的文档字符串

这是一个关于如何将文档字符串作为最佳实践包含在内的例子。在类定义中添加文档字符串为最终用户提供正确应用方法或类所需的信息。

通过这个理解,让我们进一步看看在 Python 代码中应遵循的命名约定。

命名约定

Python 中的命名约定是关于如何在 Python 程序中命名各种元素的建议,以确保易于导航和一致性。遵循代码中的统一命名约定可以简化代码导航、连接点和理解流程。这是另一个重要的标准,有助于开发可维护的应用程序。

在本节中,我们将了解如何理想地命名类、变量、函数和方法。

类名

在创建一个新类时,建议以大写字母开头,后面跟小写字母,并在类名中需要区分单词时进行大写。

例如,让我们定义一个用于计费计数器的类。

以下风格不是首选的命名约定:

class billing_counter:
    def __init__(self, productname, unitprice, quantity, tax):
        self.productname = productname
        self.unitprice = unitprice
        self.quantity = quantity
        self.tax = tax

使用前面的命名约定,我们仍然能够执行代码,并且它将按预期工作。但是,使用一个定义良好的命名风格来维护类名将使未来库的管理更容易。首选的类命名风格如下:

class BillingCounter:
    def __init__(self, productname, unitprice, quantity, tax):
        self.productname = productname
        self.unitprice = unitprice
        self.quantity = quantity
        self.tax = tax

驼峰式命名法用于命名类,以便它们可以与变量、方法和函数区分开来。接下来将解释变量的命名约定,然后是方法和函数。

变量

在创建新变量时,建议使用全部小写字母作为变量名,如果相关,后面跟数字。当变量名中有多个单词时,使用下划线操作符分隔它们是一种好习惯。这也帮助我们区分变量和类,因为它们遵循驼峰式命名约定。

让我们看看一个变量不应该如何命名的例子:

class BillingCounter:
    def __init__(self, PRODUCTNAME, UnitPrice, Quantity, TaX):
        self.PRODUCTNAME = PRODUCTNAME
        self.UnitPrice = UnitPrice
        self.Quantity = Quantity
        self.TaX = TaX

现在我们来看一个变量命名首选方法的例子:

class BillingCounter:
    def __init__(self, product, price, quantity, tax):
        self.product = product
        self.price = price
        self.quantity = quantity
        self.tax = tax

让我们进一步看看另一个变量命名的首选方法:

class BillingCounter:
    def __init__(self, product_name, unit_price, quantity, tax):
        self.product_name = product_name
        self.unit_price = unit_price
        self.quantity = quantity
        self.tax = tax

函数和方法

与变量类似,对于函数和方法名称使用小写字母是最佳实践偏好。当变量名中有多个单词时,使用下划线操作符分隔它们是一种好习惯。

让我们看看一个函数或方法不应该如何命名的例子:

class TypeCheck:
    def Intcheck(self,inputvalue):
        if (type(inputvalue) != int) or (len(str(inputvalue)) > 2):
            return False
        else:
            return True
    def STRINGCHECK(self,inputvalue):
        if (type(inputvalue) != str) or (len(str(inputvalue)) > 10):
            return False
        else:
            return True

现在我们来看一个命名方法或函数的首选方法的例子:

class TypeCheck:
    def int_check(self,input_value):
        if (type(input_value) != int) or (len(str(input_value)) > 2):
            return False
        else:
            return True
    def string_check(self,input_value):
        if (type(input_value) != str) or (len(str(input_value)) > 10):
            return False
        else:
            return True

这些命名约定是在从头开发新代码或库时可以遵循的建议。然而,如果代码已经开发并且正在积极维护,建议遵循代码中使用的命名约定。

避免名称重复

在这个例子中,让我们看看如何使用变量或类名以保持代码的可重用性的另一个最佳实践。有时在按顺序编写代码时,可能会觉得重用相同的类或变量名很容易。重用名称将使重用代码中的类、变量、方法或函数变得困难,因为在多个场景中调用它们时,相同的名称被用于不同的元素。

让我们通过一个例子来了解不推荐使用的方法。让我们定义两个名为 Branch 的类,并分别给它们定义一个名为 maintenance_cost 的方法。

第一个 Branch 类的定义如下:

class Branch:
    def maintenance_cost(self, product_type, quantity):
        self.product_type = product_type
        self.quantity = quantity
        cold_storage_cost = 100
        if (product_type == 'FMCG'):
            maintenance_cost = self.quantity * 0.25 + cold_storage_cost    
            return maintenance_cost
        else:
            return "We don't stock this product"

第二个 Branch 类的定义如下:

class Branch:
    def maintenance_cost(self, product_type, quantity):
        self.product_type = product_type
        self.quantity = quantity
        if (product_type == 'Electronics'):
            maintenance_cost = self.quantity * 0.05
            return maintenance_cost
        else:
            return "We don't stock this product"

在前面的代码中,我们有两个执行不同任务的 Branch 类。现在让我们实例化 Branch 类,假设第一个 Branch 类需要在代码的稍后位置执行:

branch = Branch()
branch.maintenance_cost('FMCG', 1)

前面的代码调用了最后定义的 Branch 类,因此最终丢失了第一个 Branch 类的定义:

"We don't stock this product"

为了避免这种混淆,始终为代码中的不同元素提供不同的名称是首选的。

现在我们来看一下推荐的方法。我们将定义一个名为 Brooklyn 的类,其中 FMCG 产品按以下方式存储:

class Brooklyn:
    def maintenance_cost(self, product_type, quantity):
        self.product_type = product_type
        self.quantity = quantity
        cold_storage_cost = 100
        if (product_type == 'FMCG'):
            maintenance_cost = self.quantity * 0.25 + cold_storage_cost    
            return maintenance_cost
        else:
            return "We don't stock this product"

我们将定义另一个名为 Queens 的类,其中电子产品按以下方式存储:

class Queens:
    def maintenance_cost(self, product_type, quantity):
        self.product_type = product_type
        self.quantity = quantity
        if (product_type == 'Electronics'):
            maintenance_cost = self.quantity * 0.05
            return maintenance_cost
        else:
            return "We don't stock this product"

我们现在可以无任何问题地调用这两个类及其方法:

brooklyn = Brooklyn()
brooklyn.maintenance_cost('FMCG', 1)

Brooklyn 的输出如下:

100.25

同样,我们可以单独实例化 Queens 类:

queens = Queens()
queens.maintenance_cost('Electronics', 1)

Queens 的输出如下:

0.05

在了解了为什么我们应该避免重用名称之后,我们可以进一步了解在哪里避免元编程。

避免在不必要的地方使用元编程

仅因为 Python 中有这个特性就写太多的元编程会使整体代码非常复杂且难以处理。在选择为你的应用程序编写元编程时,以下方面应予以考虑:

  • 确定你的用例,并根据你需要修改代码的频率来确定是否需要元编程。

  • 理解你需要多频繁地操作代码的核心元素(如类、方法、变量)之外的部分。

  • 检查你的解决方案是否仅使用面向对象编程即可开发,或者它是否依赖于元类、装饰器和代码生成等元素。

  • 检查你的团队在开发后是否具备维护元编程特性的相关技能。

  • 确认你没有依赖于不支持某些元编程特性的早期版本的 Python。

在应用设计阶段计划应用元编程技术时,以下是一些需要考虑的点。

摘要

在本章中,我们通过各种示例了解了 PEP 8 标准推荐的 Python 最佳实践。我们探讨了缩进的推荐方法和正确使用空白字符。我们还探讨了如何编写有用的注释以及在哪里包含文档字符串。

我们通过一些示例学习了推荐的命名约定。我们还探讨了为什么需要避免重用名称以及在哪里避免元编程。

虽然元编程的概念是高级且复杂的,但我们试图通过本书中的简单、直接的示例来解释它们,以保持内容的趣味性和吸引力。学习 Python 及其特性是一个持续的过程。继续关注 Python 的未来版本,并探索它为元编程提供的新功能。

posted @ 2025-09-22 13:21  绝不原创的飞龙  阅读(9)  评论(0)    收藏  举报