模块化编程是现代开发人员必不可少的工具。过去的日子已经一去不复返了,你可以把东西拼凑起来,希望它能起作用。为了构建持久的健壮系统,您需要了解如何组织您的程序,以便它们能够随着时间的推移而增长和发展。意大利面编码不是选项。模块化编程技术,特别是 Python 模块和包的使用,将为您提供在快速变化的编程环境中作为专业人员取得成功所需的工具。
在本章中,我们将:
- 看看模块化编程的基本方面
- 了解如何使用 Python 模块和包来组织代码
- 了解未使用模块化编程技术时会发生什么情况
- 了解模块化编程如何帮助您掌握开发过程
- 以 Python 标准库作为模块化编程的示例
- 创建一个使用模块化技术构建的简单程序,看看它在实践中是如何工作的
让我们从学习模块及其工作原理开始。
对于大多数初学者来说,他们的第一个 Python 程序是著名的Hello World程序的某个版本。这个程序看起来像这样:
print("Hello World!")该单行程序将保存在磁盘上的文件中,通常命名为类似于hello.py的文件,并通过在终端或命令行窗口中键入以下命令来执行:
python hello.pyPython 解释器随后会尽职尽责地打印出您要求它执行的消息:
Hello World!此hello.py文件称为Python 源文件。当您刚开始时,将所有程序代码放入单个源文件是组织程序的一种好方法。您可以定义函数和类,并将指令放在底部,以便在使用 Python 解释器运行程序时启动程序。将程序代码存储在 Python 源文件中可以避免每次需要告诉 Python 解释器要做什么时都需要重新键入它。
然而,随着程序变得越来越复杂,您会发现跟踪您定义的各种函数和类变得越来越困难。您将忘记将特定代码放在何处,并发现越来越难以记住所有不同的代码是如何组合在一起的。
模块化编程是一种组织程序的方法,因为程序变得越来越复杂。您可以创建一个 Python模块,一个包含 Python 源代码的源文件,用来做一些有用的事情,然后将这个模块导入到您的程序中,以便您可以使用它。例如,您的程序可能需要跟踪程序运行时发生的事件的各种统计信息。最后,您可能想知道每种类型的事件发生了多少。为此,您可以创建一个名为stats.py的 Python 源文件,其中包含以下 Python 代码:
def init():
global _stats
_stats = {}
def event_occurred(event):
global _stats
try:
_stats[event] = _stats[event] + 1
except KeyError:
_stats[event] = 1
def get_stats():
global _stats
return sorted(_stats.items())stats.pyPython 源文件定义了一个名为stats的模块——如您所见,模块的名称只是源文件的名称,没有.py后缀。您的主程序可以通过导入该模块,然后根据需要调用您定义的各种函数来使用该模块。下面这个简单的示例演示了如何使用stats模块收集和显示事件的统计信息:
import stats
stats.init()
stats.event_occurred("meal_eaten")
stats.event_occurred("snack_eaten")
stats.event_occurred("meal_eaten")
stats.event_occurred("snack_eaten")
stats.event_occurred("meal_eaten")
stats.event_occurred("diet_started")
stats.event_occurred("meal_eaten")
stats.event_occurred("meal_eaten")
stats.event_occurred("meal_eaten")
stats.event_occurred("diet_abandoned")
stats.event_occurred("snack_eaten")
for event,num_times in stats.get_stats():
print("{} occurred {} times".format(event, num_times))我们对记录膳食和零食不感兴趣,当然这只是一个例子,但这里需要注意的重要事项是如何导入stats模块,以及如何使用您在stats.py文件中定义的各种函数。例如,考虑下面的代码行:
stats.event_occurred("snack_eaten")因为event_occurred()函数是在stats模块中定义的,所以每当您引用此函数时,都需要包含模块的名称。
有几种方法可以导入模块,因此不需要每次都包含模块的名称。我们将在第 3 章中使用模块和包来了解这一点,我们将更详细地了解名称空间和import命令的工作方式。
如您所见,import语句用于加载模块,任何时候您看到模块名称后面有一个句点时,您都可以判断程序引用的是该模块中定义的某个内容(例如,函数或类)。
正如 Python 模块允许您将函数和类组织到单独的 Python 源文件中一样,Python包允许您将多个模块组合在一起。
Python 包是具有某些特征的目录。例如,考虑下面的 Python 源文件目录:
这个名为animals的 Python 包包含五个 Python 模块:cat、cow、dog、horse和sheep。还有一个特殊的文件名为 __init__.py。该文件称为包初始化文件;该文件的存在告诉 Python 系统该目录包含一个包。包初始化文件还可用于初始化包(因此而得名),还可用于简化包的导入。
从 Python 版本 3.3 开始,包并不总是需要包含初始化文件。但是,没有初始化文件的包(称为命名空间包)仍然非常少见,并且只在非常特定的情况下使用。为了简单起见,我们将在本书中使用常规包(带有__init__.py文件)。
就像我们在调用模块中的函数时使用模块名一样,我们在引用包中的模块时使用包名。例如,考虑下面的代码:
import animals.cow
animals.cow.speak()在这个示例中,speak()函数在cow.py模块中定义,该模块本身是animals包的一部分。
包是组织更复杂 Python 程序的一种很好的方式。您可以使用它们将相关模块分组在一起,甚至可以在包中定义包(称为嵌套包),以保持程序的超级组织。
请注意,import语句(以及相关的from...import语句)可以以多种方式用于将包和模块加载到程序中。我们在这里只触及了表面,向您展示了 Python 中的模块和包是什么样子的,以便您在程序中看到它们时能够识别它们。我们将在第 3 章、使用模块和包中更深入地了解模块和包的定义和导入方式。
下载示例代码
该书的代码包也托管在 GitHub 上的https://github.com/PacktPublishing/Modular-Programming-with-Python 。我们在上还提供了丰富的书籍和视频目录中的其他代码包 https://github.com/PacktPublishing/ 。看看他们!
模块和包不仅仅是为了在多个源文件和目录中传播 Python 代码,它们允许您组织您的代码,以反映您的程序尝试执行的逻辑结构。例如,假设您被要求创建一个 web 应用程序来存储和报告大学考试结果。考虑到您所获得的业务需求,您为应用程序提出了以下总体结构:
该程序分为两个主要部分:一个web 界面,与用户交互(并通过 API 与其他计算机程序交互);一个后端,处理在数据库中存储信息、生成报告的内部逻辑,并将结果通过电子邮件发送给学生。如您所见,web 界面本身被分解为四个部分:
- 用户身份验证部分,用于处理用户注册、登录和注销
- 用于查看和输入考试结果的 web 界面
- 生成报告的 web 界面
- API,允许其他系统根据请求检索考试结果
当您考虑应用程序的每个逻辑组件(即,前面说明中的每个框)时,您也开始思考每个组件将提供的功能。当你这么做的时候,你已经在用模块化的术语思考了。实际上,应用程序的每个逻辑组件都可以直接实现为 Python 模块或包。例如,您可以选择将程序分为两个主要包,分别命名为web和backend,其中:
web包包含名为authentication、results、reports和api的模块backend包包含名为database、reportgenerator和emailer的模块
如您所见,上图中的每个阴影框都成为一个 Python 模块,每个框组都成为一个 Python 包。
一旦决定了要定义的包和模块的集合,就可以通过在每个模块中编写适当的函数集来开始实现每个组件。例如,backend.database模块可能有一个名为get_students_results()的函数,该函数返回单个学生在给定科目和年份的考试结果。
在真实的 web 应用程序中,您的模块化结构实际上可能有些不同。这是因为您通常使用 Django 之类的 web 应用程序框架创建 web 应用程序,Django 将自己的结构强加给您的程序。然而,在本例中,我们将模块化结构保持尽可能简单,以显示业务功能如何直接转换为包和模块。
显然,这个例子是虚构的,但它展示了如何用模块化的术语来思考一个复杂的程序,将其分解为各个组件,然后使用 Python 模块和包依次实现这些组件。
使用模块化设计技术的一大好处是,它们迫使您思考程序的结构方式,并让您定义一种随着程序的发展而不断发展的结构,而不仅仅是跳进和编写代码。您的程序将是健壮的、易于理解的、随着程序范围的扩大而易于重组的,并且也便于其他人使用。
木工们有一句格言同样适用于模块化编程:万物皆有其位,万物皆有其位。这是高质量代码的标志之一,就像它是组织良好的木工车间的标志一样。
要了解模块化编程为何是一项如此重要的技能,请想象一下,如果在编写程序时没有应用模块化技术,将会发生什么。如果您将所有 Python 代码放在一个源文件中,不尝试在逻辑上排列函数和类,而只是在文件末尾随机添加新代码,那么您将得到一堆令人费解的代码。以下是在没有任何模块化组织的情况下编写的程序示例:
import configparser
def load_config():
config = configparser.ConfigParser()
config.read("config.ini")
return config['config']
def get_data_from_user():
config = load_config()
data = []
for n in range(config.getint('num_data_points')):
value = input("Data point {}: ".format(n+1))
data.append(value)
return data
def print_results(results):
for value,num_times in results:
print("{} = {}".format(value, num_times))
def analyze_data():
data = get_data_from_user()
results = {}
config = load_config()
for value in data:
if config.getboolean('allow_duplicates'):
try:
results[value] = results[value] + 1
except KeyError:
results[value] = 1
else:
results[value] = 1
return results
def sort_results(results):
sorted_results = []
for value in results.keys():
sorted_results.append((value, results[value]))
sorted_results.sort()
return sorted_results
if __name__ == "__main__":
results = analyze_data()
sorted_results = sort_results(results)
print_results(sorted_results)此程序旨在提示用户输入多个数据点,并计算每个数据点出现的频率。它确实有效,函数和变量名确实有助于解释程序的每个部分都做了些什么,但它仍然很混乱。仅仅看一下源代码,就很难理解这个程序是做什么的。函数只是在作者决定实现它们时添加到文件的末尾,即使对于一个相对较小的程序,也很难跟踪各个部分。想象一下,如果一个程序有 10000 行长,那么尝试调试或维护这样的程序!
这个程序是意大利面编码编程的一个例子,其中所有内容都混杂在一起,源代码没有整体组织。不幸的是,意大利面编码经常与其他编程习惯相结合,这使得程序更难理解。一些更常见的问题包括:
- 选择不当的变量和函数名,不能提示每个变量或函数的用途。一个典型的例子是一个使用变量名的程序,例如
a、b、c和d。 - 完全没有解释代码应该做什么的文档。
- 具有意外副作用的函数。例如,假设我们示例程序中的
print_results()函数在打印时修改了results数组。如果您想打印两次结果,或者在打印后使用结果,您的程序将以一种非常神秘的方式失败。
虽然模块化编程不能治愈所有这些弊病,但它迫使您考虑程序的逻辑组织这一事实将帮助您避免这些弊病。将代码组织成逻辑片段将有助于您构建程序,以便您知道每个部分所属的位置。思考软件包和模块,以及每个模块包含的内容,将鼓励您为程序的各个部分选择清晰、适当的名称。使用模块和软件包也可以很自然地包括文档字符串到来解释程序中每个部分的功能。最后,使用逻辑结构鼓励程序的每个部分执行一项特定任务,从而降低副作用潜入代码的可能性。
当然,与任何编程技术一样,模块化编程也可能被滥用,但如果使用得当,它将大大提高您编写的程序的质量。
想象一下您正在编写一个程序来计算海外采购的价格。您的公司位于英国,您需要计算以美元购买的物品的当地价格。其他人已经编写了一个 Python 模块来下载汇率,因此您的程序开始时看起来如下所示:
def calc_local_price(us_dollar_amount):
exchange_rate = get_exchange_rate("USD", "EUR")
local_amount = us_dollar_amount * exchange_rate
return local_amount到现在为止,一直都还不错。您的程序包含在公司的在线订购系统中,代码将投入生产。然而,两个月后,您的公司不仅开始从美国订购产品,还开始从中国、德国和澳大利亚订购产品。您争先恐后地更新程序以支持这些替代货币,并编写如下内容:
def calc_local_price(foreign_amount, from_country):
if from_country == "United States":
exchange_rate = get_exchange_rate("USD", "EUR")
elif from_country == "China":
exchange_rate = get_exchange_rate("CHN", "EUR")
elif from_country == "Germany":
exchange_rate = get_exchange_rate("EUR", "EUR")
elif from_country = "Australia":
exchange_rate = get_exchange_rate("AUS", "EUR")
else:
raise RuntimeError("Unsupported country: " + from_country)
local_amount = us_dollar_amount * exchange_rate
return local_amount该节目再次投入生产。六个月后,又增加了 14 个国家,项目经理还决定添加一个新功能,用户可以看到产品价格随时间的变化。作为负责此代码的程序员,您现在必须添加对这 14 个国家的支持,并添加对历史汇率追溯的支持。
当然,这是一个精心设计的例子,但它确实展示了程序通常是如何演变的。程序代码不是你写一次就永远离开的东西。您的程序不断变化和发展,以响应新的需求、新发现的错误和意外的后果。有时候,一个看似简单的改变可能根本就不是。例如,考虑在前一个例子中编写了 Type T0-Ty 函数的可怜程序员。该函数现在不仅必须支持任何给定货币对的当前汇率,还必须返回返回到任何所需时间点的历史汇率。如果此函数从不支持历史汇率的源获取其信息,则可能需要从头重写整个函数以支持替代数据源。
有时,程序员和 IT 经理试图抑制更改,例如编写详细的规范,然后一次实现程序的一部分(所谓的瀑布编程方法)。但是改变是编程不可或缺的一部分,试图抑制它就像试图阻止风吹一样。最好接受你的程序将改变,并学会如何尽可能好地管理这个过程。
模块化技术是管理程序变更的极好方法。例如,随着程序的增长和发展,您可能会发现特定的更改需要在程序中添加新模块:
然后,您可以在需要使用此新功能的程序的其他部分中导入并使用该模块。
或者,您可能会发现新功能只需要更改模块的内容:
这是模块化编程的主要好处之一,因为特定功能如何实现的细节在模块内部,所以您通常可以在不影响程序任何其他部分的情况下更改模块的内部结构。程序的其余部分继续导入和使用模块,就像以前一样,只有模块的内部实现发生了更改。
最后,您可能会发现您需要重构您的程序。在这里,您必须更改代码的模块化组织,以改进程序的工作方式:
重构可能涉及在模块之间移动代码、创建新模块、删除旧模块以及更改模块的工作方式。从本质上讲,重构是对程序进行重新思考的过程,以使其更好地工作。
在所有这些更改中,使用模块和包可以帮助您管理所做的更改。由于各个模块和包都执行定义良好的任务,因此您确切地知道程序的哪些部分需要更改,并且您可以将更改的影响限制为仅受影响的模块和使用它们的系统部分。
模块化编程不会让变化消失,但它将帮助您以最佳方式处理变化和正在进行的编程过程。
用来描述 Python 的流行语之一是它是一种包含语言的电池,也就是说,它附带了丰富的内置模块和包集合,称为Python 标准库。如果您编写过任何非平凡的 Python 程序,那么几乎可以肯定您使用了 Python 标准库中的模块来实现这一点。为了了解 Python 标准库有多大,下面是该库中的几个示例模块:
|单元
|
描述
|
| --- | --- |
| datetime | 定义使用日期和时间值存储和执行计算的类 |
| tempfile | 定义一系列用于处理临时文件和目录的函数 |
| csv | 支持读取和写入 CSV 格式文件 |
| hashlib | 实现加密安全哈希 |
| logging | 允许您写入日志消息和管理日志文件 |
| threading | 支持多线程编程 |
| html | 用于解析和生成 HTML 文档的模块集合(即包) |
| unittest | 用于创建和运行单元测试的框架 |
| urllib | 从 URL 读取数据的模块集合 |
这些只是 Python 标准库中 300 多个可用模块中的一小部分。如您所见,提供了大量功能,所有这些都内置于每个 Python 发行版中。
由于提供了大量的功能,Python 标准库是模块化编程的一个很好的例子。例如,math标准库模块提供了一系列数学函数,使处理整数和浮点数更容易。如果您查看本模块的文档(http://docs.python.org/3/library/math.html ),你会发现大量的函数和常数集合,它们都是在math模块中定义的,几乎可以执行你想象得到的任何数学运算。在本例中,各种函数和常量都是在单个模块中定义的,因此在需要时很容易引用它们。
相反,xmlrpc包允许您进行和响应使用 XML 协议发送和接收数据的远程过程调用。xmlrpc包由两个模块组成:xmlrpc.server和xmlrpc.client,其中server模块允许您创建 XML-RPC 服务器,client模块包含访问和使用 XML-RPC 服务器的代码。这是一个使用模块层次结构对相关功能进行逻辑分组的示例(在本例中,在xmlrpc包内),同时使用子模块分离包的特定部分。
如果您还没有这样做,那么花一些时间来查看 Python 标准库的文档是值得的。这可以在找到 https://docs.python.org/3/library/ 。值得研究本文档,以了解 Python 如何将如此庞大的功能集合组织到模块和包中。
Python 标准库并不完美,但随着时间的推移,它已经得到了改进,今天的库是一个应用于综合库的模块化编程技术的好例子,涵盖了广泛的特性和函数。
现在我们已经了解了什么是模块以及如何使用它们,让我们实现第一个真正的 Python 模块。虽然这个模块很简单,但您可能会发现它是对您编写的程序的一个有用的补充。
在计算机编程中,缓存是存储先前计算结果的一种方式,以便更快地检索。例如,假设您的程序必须根据三个参数计算运输成本:
- 订购物品的重量
- 订购物品的尺寸
- 客户的位置
根据客户的位置计算运输成本可能会涉及很多问题。例如,您可能会对您所在城市内的配送收取固定费用,但对于外地订单,您会根据客户的距离收取额外费用。您甚至可能需要向货运公司的 API 发送一个查询,以查看装运给定项目将收取多少费用。
由于计算运输成本的过程可能非常复杂和耗时,因此使用缓存存储以前计算的结果是有意义的。这允许您使用以前计算的结果,而不必每次都重新计算运输成本。要做到这一点,您需要将calc_shipping_cost()函数的结构设置为如下所示:
def calc_shipping_cost(params):
if params in cache:
shipping_cost = cache[params]
else:
...calculate the shipping cost.
cache[params] = shipping_cost
return shipping_cost如您所见,我们获取提供的参数(在本例中为重量、尺寸和客户的位置),并检查缓存中是否已经存在这些参数的条目。如果是这样,我们将从缓存中检索先前计算的运输成本。否则,我们将经历一个可能非常耗时的过程:计算运输成本,使用提供的参数将其存储在缓存中,然后将运输成本返回给调用者。
注意前面伪代码中的cache变量看起来非常像 Python 字典,您可以基于给定的键在字典中存储条目,然后使用该键检索条目。然而,字典和缓存之间有一个至关重要的区别:缓存通常对它可以包含的条目数量有限制,而字典没有这样的限制。这意味着字典将永远增长,如果程序运行很长时间,可能会占用计算机的所有内存,而缓存永远不会占用太多内存,因为条目数量有限。
缓存达到最大大小后,每次添加新条目时都必须删除现有条目,以便缓存不会继续增长:
选择要删除的条目有多种方法,最常用的方法是删除最近使用最少的条目,即最长时间未使用的条目。
缓存在计算机程序中非常常用。事实上,即使您尚未在编写的程序中使用缓存,您也几乎肯定曾经遇到过它们。是否有人建议您清除浏览器的缓存以解决 web 浏览器的问题?是的,web 浏览器使用缓存来保存以前下载的图像和网页,这样就不必再次检索它们,而清除浏览器缓存的内容是修复行为不正常的 web 浏览器的常见方法。
现在让我们编写我们自己的 Python 模块来实现缓存。在编写之前,让我们考虑一下缓存模块所需的功能:
- 我们将把缓存的大小限制为 100 个条目。
- 我们需要一个
init()函数来初始化缓存。 - 我们将有一个
set(key, value)函数在缓存中存储条目。 get(key)函数将从缓存中检索条目。如果该键没有输入,则此函数应返回None。- 我们还需要一个
contains(key)函数来检查给定的条目是否在缓存中。 - 最后,我们将实现一个返回缓存中条目数的
size()函数。
我们有意使这个模块的实现非常简单。真正的缓存将使用Cache类来允许您同时使用多个缓存。它还允许根据需要配置缓存的大小。然而,为了保持简单,我们将直接在模块内实现这些功能,因为我们希望专注于模块化编程,而不是将其与面向对象编程和其他技术相结合。
继续并创建一个名为cache.py的新 Python 源文件。这个文件将保存我们新模块的 Python 源代码。在本模块顶部,输入以下 Python 代码:
import datetime
MAX_CACHE_SIZE = 100我们将使用datetime标准库模块来计算缓存中最近使用最少的条目。第二条语句定义了MAX_CACHE_SIZE,设置了缓存的最大大小。
请注意,我们遵循使用大写字母定义常量的标准 Python 约定。这使它们更容易在源代码中看到。
我们现在想要为缓存实现init()函数。为此,请在模块末尾添加以下内容:
def init():
global _cache
_cache = {} # Maps key to (datetime, value) tuple.如您所见,我们创建了一个名为init()的新函数。此函数中的第一条语句global _cache定义了一个名为_cache的新变量。global语句将该变量作为模块级全局变量使用,即该变量可以被cache.py模块的所有部分共享。
请注意变量名开头的下划线字符。在 Python 中,前导下划线是一种表示名称是私有的约定。换句话说,_cache全局变量旨在用作cache.py模块的内部部分。下划线告诉您不需要在cache.py模块之外使用此变量。
init()函数中的第二条语句将_cache全局设置为空字典。请注意,我们添加了一条注释,解释如何使用词典;将这样的注释添加到代码中是一种很好的做法,这样其他人(以及您,当您在长时间处理其他内容后查看此代码时)可以很容易地看到此变量的用途。
在摘要中,调用init()函数的作用是在模块内创建一个私有_cache变量,并将其设置为空字典。现在让我们编写set()函数,它将使用这个变量在缓存中存储一个条目。
将以下内容添加到模块末尾:
def set(key, value):
global _cache
if key not in _cache and len(_cache) >= MAX_CACHE_SIZE:
_remove_oldest_entry()
_cache[key] = [datetime.datetime.now(), value]set()函数再次以global _cache语句开始。这使得_cache模块级全局变量可供函数使用。
if语句检查缓存是否将超过允许的最大大小。如果是这样,我们调用一个名为_remove_oldest_entry()的新函数,从缓存中删除最旧的条目。请注意,此函数名如何再次以下划线开头,这表明此函数是私有的,只应由模块本身中的代码使用。
最后,我们将条目存储在_cache字典中。请注意,我们将当前日期和时间以及值存储在缓存中;这将让我们知道缓存项上次使用的时间,这在我们必须删除最早的项时非常重要。
现在让我们实现get()函数。将以下内容添加到模块末尾:
def get(key):
global _cache
if key in _cache:
_cache[key][0] = datetime.datetime.now()
return _cache[key][1]
else:
return None您应该能够理解这段代码的作用。唯一值得注意的是,我们在返回相关值之前更新了缓存项的日期和时间。这让我们知道缓存项上次使用的时间。
实现了这些功能后,剩下的两个功能也应该很容易理解。将以下内容添加到模块末尾:
def contains(key):
global _cache
return key in _cache
def size():
global _cache
return len(_cache)这里不应该有任何惊喜。
还有一个功能需要实现:我们的私有_remove_oldest_entry()功能。将以下内容添加到模块末尾:
def _remove_oldest_entry():
global _cache
oldest = None
for key in _cache.keys():
if oldest == None:
oldest = key
elif _cache[key][0] < _cache[oldest][0]:
oldest = key
if oldest != None:
del _cache[oldest]这就完成了cache.py模块本身的实现,包括我们前面描述的五个主要功能,以及一个私有函数和一个私有全局变量,它们在内部用于帮助实现我们的公共功能。
现在,让我们编写一个简单的测试程序来使用这个cache模块,并验证它是否正常工作。创建一个新的 Python 源文件,我们称之为test_cache.py,并将以下内容添加到此文件:
import random
import string
import cache
def random_string(length):
s = ''
for i in range(length):
s = s + random.choice(string.ascii_letters)
return s
cache.init()
for n in range(1000):
while True:
key = random_string(20)
if cache.contains(key):
continue
else:
break
value = random_string(20)
cache.set(key, value)
print("After {} iterations, cache has {} entries".format(n+1, cache.size()))这个程序首先导入三个模块:两个来自 Python 标准库,另一个是我们刚刚编写的cache模块。然后,我们定义一个名为random_string()的实用函数,它生成给定长度的随机字母字符串。在此之后,我们通过调用cache.init()初始化缓存,然后生成 1000 个随机条目添加到缓存中。添加每个缓存条目后,我们打印出已添加的条目数以及当前缓存大小。
如果运行此程序,您可以看到它正在按预期工作:
$ python test_cache.py
After 1 iterations, cache has 1 entries
After 2 iterations, cache has 2 entries
After 3 iterations, cache has 3 entries
...
After 98 iterations, cache has 98 entries
After 99 iterations, cache has 99 entries
After 100 iterations, cache has
100 entries
After 101 iterations, cache has 100 entries
After 102 iterations, cache has 100 entries
...
After 998 iterations, cache has 100 entries
After 999 iterations, cache has 100 entries
After 1000 iterations, cache has 100 entries缓存继续增长,直到达到 100 个条目,此时最旧的条目将被删除,以便为新条目腾出空间。这可以确保缓存保持相同的大小,无论添加了多少个新条目。
虽然我们可以用cache.py模块做更多的事情,但这足以演示如何创建一个有用的 Python 模块,然后在另一个程序中使用它。当然,您不仅限于在主程序中导入模块,模块还可以导入其他模块。
在本章中,我们介绍了 Python 模块的概念,并了解了 Python 模块如何只是 Python 源文件,由另一个源文件导入和使用。然后我们查看了 Python 包,发现这些是由名为__init__.py的包初始化文件标识的模块集合。
我们探讨了如何使用模块和包来组织程序的源代码,以及为什么使用这些模块化技术对开发大型系统如此重要。我们还探索了意大利面代码的外观,并发现了如果不模块化程序可能出现的一些其他陷阱。
接下来,我们将编程视为一个不断变化和进化的过程,以及模块化编程如何以最佳方式帮助处理不断变化的代码库。然后,我们了解到 Python 标准库是大量模块和包集合的一个优秀示例,并通过创建我们自己的简单 Python 模块来完成,该模块演示了有效的模块编程技术。在实现该模块的过程中,我们了解了模块如何在变量和函数名中使用前导下划线,将它们标记为模块的专用,同时使其余函数和其他定义可供系统的其他部分使用。
在下一章中,我们将应用模块化技术开发更复杂的程序,该程序由多个模块组成,共同解决更复杂的编程问题。





