为了使本书的平衡中的设计问题更加清晰,我们需要研究一些作为动机的问题。其中之一是使用面向对象编程(OOP)进行仿真。仿真是 OOP 的早期问题领域之一。这是一个 OOP 工作起来特别优雅的领域。
我们选择了一个相对简单的问题域:玩二十一点游戏的策略。我们不想支持赌博;事实上,一点研究将表明,这场比赛对玩家来说是一场沉重的比赛。这应该表明,大多数赌场赌博只不过是对不计其数的人征税。
本章第一节将回顾二十一点的游戏规则。在阅读了纸牌游戏之后,本章的大部分内容将提供编写完整 Python 程序和包所必需的工具的一些背景知识。我们将了解以下概念:
- Python 运行时环境以及特殊方法名称如何实现语言特性
- 集成开发环境(IDEs)
- 使用
pylint或black工具创建统一样式 - 使用类型提示和
mypy工具建立函数、类和变量的正确使用 - 使用
timeit进行性能测试 - 使用
unittest、doctest和pytest进行单元测试 - 使用
sphinx和基于 RST 的标记创建可用文档
虽然其中一些工具是 Python 标准库的一部分,但大多数工具都在库之外。在讨论 Python 运行时时,我们将讨论工具的安装。
本书将尽量避免脱离 Python OOP 的基础。我们假设您已经阅读过 Packt 的Python 3 面向对象编程。我们不想重复其他地方很好地陈述的事情。我们将重点介绍 Python3。
我们将参考一些常见的面向对象设计模式,并尽量避免重复 Packt 的学习 Python 设计模式中的演示。
本章将介绍以下主题:
- 关于 21 点游戏
- Python 运行时和特殊方法
- 交互、脚本和工具
- 选择 IDE
- 一致性和风格
- 输入提示和
mypy程序 - 性能
timeit模块 - 测试–
unittest和doctest - 文档–
sphinx和 RST 标记 - 安装组件
本章的代码文件可在中找到 https://git.io/fj2UB 。
本书中的许多示例将集中于模拟具有许多中等复杂状态变化的过程。二十一点的纸牌游戏在游戏过程中涉及一些规则和一些状态变化。如果你不熟悉二十一点的游戏,这里有一个概述。
游戏的目标是接受庄家的牌,以创建一手牌,该手牌的总分介于庄家的总分和 21 分之间。发牌人的牌只露出一部分,迫使玩家在不知道发牌人的总牌数或牌组中的后续牌数的情况下做出决定。
数字卡(2 到 10)的点值等于数字。脸牌(杰克、女王和国王)值 10 分。这张王牌值十一分或一分。当使用 ace 作为 11 点时,手的值为软。当使用 ace 作为一个点时,该值为硬。
因此,一手有 A 和 7 的牌,硬牌总数为 8,软牌总数为 18。这会导致玩家选择额外的牌。如果庄家出示一张脸牌,很可能庄家持有 20 分,玩家可能不想冒险再拿一张牌。
每套西装有四张两张卡片组合,总共 21 张。这些都被称为二十一点,,尽管四种组合中只有一种涉及到一个杰克。这些组合通常提供奖金支付,因为只有四种组合可用。
游戏的大部分内容都是关于如何正确选择牌。当然,这里有一个打赌的因素。由于规定将一只手分为两手,赌博和赌博之间的区别变得更加复杂。当玩家的两张牌的等级相同时,这是允许的。下一节将详细介绍如何玩游戏。
游戏的机制一般如下。细节可能会有所不同,但大纲类似:
- 首先,玩家和庄家每人得到两张牌。当然,玩家知道他们两张牌的价值。他们在赌场面对面交易。
- 庄家的一张牌被透露给玩家。它正面朝上展示。因此,玩家对庄家的牌略知一二,但不是全部。这是典型的更复杂的模拟,其中部分信息可用,并且需要统计建模来做出适当的决策。
- 如果庄家有一张王牌,玩家将有机会进行额外的保险赌注。这是一种特殊情况,是存在例外情况的更复杂模拟的典型情况。
- 对于游戏的平衡,玩家可以选择接收卡或停止接收卡。有四种选择:
- 玩家可以点击,这意味着可以再拿一张牌。
- 他们可以或站着或站着处理所发的牌。
- 如果玩家的牌匹配,手牌可以分开。这需要额外下注,两手牌分开进行
- 玩家可以在最后一张牌之前加倍下注。这被称为加倍。
手工作品的最终评估如下:
- 如果玩家超过 21,手牌是半身像,玩家输了,庄家的脸朝下牌是不相关的。这为经销商提供了一个优势。
- 如果玩家的总数是 21 或以下,那么庄家根据一个简单的固定规则取牌。经销商必须击中总数小于 18 的手牌;经销商必须站在总共 18 只或更多的手上。
- 如果庄家破产,玩家获胜。
- 如果庄家和牌手都在 21 岁或以下,则比较手牌。总得分越高,则获胜。在平局的情况下,比赛是一场推,既不是赢也不是输。如果玩家赢了 21,他们会赢得更大的奖金,通常是赌注的 1.5 倍。
规则可能会有很大的不同。我们将省略这些细节,重点关注模拟所需的 Python 代码。
在二十一点的情况下,玩家实际上必须使用两种策略:
- 决定玩什么游戏的策略:采取保险、打击、站立、分割或双重下降。
- 决定下注金额的策略。一个常见的统计谬误导致玩家提高和降低赌注,试图保持他们的赢款和减少他们的损失。尽管存在潜在的谬误,但这些都是有趣的、有状态的算法。
当然,这两组策略是策略设计模式的主要示例。
我们将使用游戏的元素,例如玩家、手和牌,作为对象建模的示例。我们不会设计整个模拟。我们将关注这个游戏的元素,因为它们有一些细微差别,但并不十分复杂。
卡片是相对简单、不变的对象。有多种建模技术可用。卡片分为数字卡、脸卡和王牌的简单类层次结构。有简单的容器,包括卡片实例的手和卡片组。这些是有状态的集合,可以添加和删除卡片。在 Python 中有许多方法可以实现这一点,我们将研究许多替代方法。我们还需要从整体上研究播放器。玩家将拥有一系列手牌,以及一个下注策略和一个21 点游戏策略。这是一个相当复杂的复合对象。
掌握面向对象 Python 的一个基本概念是理解对象方法是如何实现的。让我们看一个相对简单的 Python 交互:
>>> f = [1, 1, 2, 3]
>>> f += [f[-1] + f[-2]]
>>> f [1, 1, 2, 3, 5]我们创建了一个列表f,其中包含一系列值。然后,我们使用+=操作符对该列表进行变异,以附加一个新值。f[-1] + f[-2]表达式计算要追加的新值。
f[-1]的值使用列表对象的__getitem__()方法实现。这是 Python 的核心模式:简单的运算符式语法是通过特殊方法实现的。特殊方法的名称周围有__以使其与众不同。对于简单的前缀和后缀语法,对象是明显的;f[-1]作为f.__getitem__(-1)实施。
附加操作同样通过__add__()特殊方法实现。对于二进制运算符,Python 将尝试两个操作数,以确定哪一个操作数提供了特殊方法。在本例中,两个操作数都是整数,都将提供合适的实现。在混合类型的情况下,二进制运算符的实现可能会将一个值强制转换为另一个类型。然后,将f[-1] + f[-2]实现为f.__getitem__(-1).__add__(f.__getitem__(-2))。
+=操作符对f的更新是通过__iadd__()特殊方法实现的。因此,f += [x]被实现为f.__iadd__([x])。
在前八章中,我们将非常仔细地研究这些特殊方法,以及如何设计类,使其与 Python 的内置语言功能紧密集成。掌握特殊方法是掌握面向对象 Python 的精髓。
Python 通常被描述为包含编程的电池。所需的一切都可以直接作为单个下载的一部分提供。这提供了运行时、标准库和空闲编辑器作为一个简单的开发环境。
下载和安装 Python3.7 并开始在桌面上以交互方式运行它非常容易。上一节中的示例包括交互式 Python 中的>>>提示符
如果您正在使用 Iron Python(IPython实现),那么交互将如下所示:
In [ 1 ]: f = [ 1 , 1 , 2 , 3 ]
In [ 3 ]: f += [f[- 1 ] + f[- 2 ]]
In [ 4 ]: f
Out[ 4 ]: [1, 1, 2, 3, 5] 提示略有不同,但语言相同。每个语句在呈现给 Python 时都会进行求值
这对于一些实验来说很方便。我们的目标是构建工具、框架和应用程序。虽然许多示例将以交互方式显示,但大多数实际编程将通过脚本文件进行。
以交互方式运行示例可以得出一个深刻的结论。编写良好的 Python 代码应该足够简单,可以从命令行运行。
**Good Python is simple. ** We should be able to demonstrate a design at the >>> prompt.
交互使用不是我们的目标。从>>>提示符中执行代码是对复杂性的质量测试。如果代码太复杂,无法在>>>提示符下执行,则需要重构。
本书的重点是创建完整的脚本、模块、包和应用程序。尽管有些示例以交互模式显示,但目标是创建 Python 文件。这些文件可能像脚本一样简单,也可能像包含用于创建 web 应用程序的文件的目录一样复杂。
像mypy、pytest和pylint这样的工具可以处理 Python 文件。准备脚本文件几乎可以用任何文本编辑器来完成。但是,最好使用 IDE,在 IDE 中可以提供许多工具来帮助开发应用程序和脚本。
一个常见的问题是,“做 Python 开发的
最好的
IDE 是什么?”
这个问题的简短回答是 IDE 的选择并不重要。支持 Python 的开发环境数量众多,而且都非常易于使用。长答案需要一个对话,讨论哪些属性将 IDE 列为最佳。
Spyder IDE 是 Anaconda 发行版的一部分。这使得下载了 Anaconda 的开发人员可以轻松访问它。空闲编辑器是 Python 发行版的一部分,它为使用 Python 和构建脚本提供了一个简单的环境。PyCharm 拥有商业许可证和社区版,它提供了大量功能,并用于编写本书中的所有示例。
作者利用了编辑器、集成的 Python 提示符和单元测试结果,这些都是现成的。PyCharm 在conda环境中运行良好,避免了对安装哪些软件包的混淆
互联网上的搜索将提供一长串其他工具。请参见 IDE Python wiki 页面,了解许多备选方案(https://wiki.python.org/moin/IntegratedDevelopmentEnvironments )。
本书中的所有示例都是使用black工具编写的,以提供一致的格式。进行了一些额外的手动调整,以使代码保持在印刷材料的窄尺寸范围内。
使用black的常见替代方法是使用pylint来识别格式问题。然后可以纠正这些错误。除了详细分析代码质量外,pylint工具还提供了一个数字质量分数。对于本书,需要禁用一些pylint规则。例如,模块通常具有不符合首选顺序的导入;一些模块还具有与doctest示例相关的导入,并且似乎未使用;一些例子使用全局变量;有些类定义仅仅是框架,没有适当的方法定义
使用pylint定位潜在问题至关重要。使pylint警告保持沉默通常是有帮助的。在下面的示例中,我们需要消除关于test_list变量名作为全局变量无效的pylint警告:
# pylint: disable=invalid-name test_list = """
>>> f = [1, 1, 2, 3]
>>> f += [f[-1] + f[-2]]
>>> f
[1, 1, 2, 3, 5]
"""
if __name__ == "__main__" :
import doctest
__test__ = {name: value
for name, value in locals ().items()
if name.startswith( "test_" )}
doctest.testmod( verbose = False ) pylint警告除了有助于保持一致的风格外,还有助于识别拼写错误和常见错误列表。例如,实例变量通常为self。pylint会发现一个意外的拼写错误sefl
Python3 允许使用类型提示。提示出现在赋值语句、函数和类定义中。当程序运行时,Python 不会直接使用它们。相反,它们被外部工具用来检查代码是否正确使用了类型、变量和函数。下面是一个带有类型提示的简单函数:
def F(n: int ) -> int :
if n in ( 0 , 1 ):
return 1
else :
return F(n- 1 ) + F(n- 2 )
print ( "Good Use" , F( 8 ))
print ( "Bad Use" , F( 355 / 113 ))当我们运行mypy程序时,我们会看到如下错误:
Chapter_1/ch01_ex3.py:23: error: Argument 1 to "F" has incompatible type "float"; expected "int"此消息通知我们错误的位置:文件为Chapter_1/ch01_ex3.py,即文件的 23第行。细节告诉我们函数F的参数值不正确。这种问题可能很难看出。在某些情况下,单元测试可能无法很好地涵盖这种情况,并且程序可能会隐藏一些微妙的错误,因为可能会使用不正确类型的数据。
我们将使用timeit模块来比较不同面向对象设计和 Python 构造的实际性能。我们将重点介绍本模块中的timeit()函数。此函数创建一个Timer对象,用于测量给定代码块的执行情况。我们还可以提供一些创建环境的预备代码。此函数的返回值是运行给定代码块所需的时间。
默认计数为 100000。这提供了一个有意义的时间,用于计算计算机上进行测量的其他操作系统级活动的平均值。对于复杂或长期运行的语句,较低的计数可能是明智的。
以下是与 timeit 的简单互动:
>>> timeit.timeit("obj.method()",
... """
... class SomeClass:
... def method(self):
... pass
... obj= SomeClass()
... """)
0.1980541350058047 待测代码为 obj.method() 。作为字符串提供给timeit()。设置代码块是类定义和对象构造。这个代码块也是作为字符串提供的。需要注意的是,语句所需的所有内容都必须在设置中。这包括所有导入,以及所有变量定义和对象创建。
这个例子显示 100000 个不做任何事情的方法调用花费 0.198 秒。
单元测试是绝对必要的。
如果没有自动测试来显示特定元素的功能,那么该功能实际上并不存在。换句话说,直到有一个测试表明它完成了,它才完成。
我们将在测试时进行切面接触。如果我们深入测试每一个面向对象的设计特性,这本书的长度将是它的两倍。省略测试细节的缺点是使良好的单元测试看起来是可选的。它们显然不是可选的。
Unit testing is essential. When in doubt, design the tests first. Fit the code to the test cases.
Python 提供了两个内置测试框架。大多数应用程序和库都将使用这两种方法。一个用于测试的通用包装器是unittest模块。此外,许多公共 API docstring 都有可供doctest模块找到和使用的示例。另外,unittest可以合并 doctest 。
pytest工具可以定位测试用例并执行它们。这是一个非常有用的工具,但必须与 Python 的其余部分分开安装。
一个崇高的理想是每个类和函数至少有一个单元测试。重要的、可见的类和函数通常也会有doctest。还有其他崇高理想:100%的代码覆盖率;100%逻辑路径覆盖率,依此类推。
实际上,有些类不需要测试。例如,扩展 typing.NamedTuple 的类实际上不需要复杂的单元测试。重要的是测试您编写的类的独特特性,而不是从标准库继承的特性。
通常,我们希望首先开发测试用例,然后编写适合测试用例的代码。测试用例将代码的 API 形式化。这本书将揭示许多编写具有相同接口的代码的方法。一旦我们定义了一个接口,仍然有许多适合该接口的候选实现。一组测试将应用于几个不同的面向对象设计。
使用unittest和pytest工具的一种通用方法是为您的项目创建至少三个并行目录:
myproject:此目录是将在lib/site-packages中为您的软件包或应用程序安装的最终软件包。它有一个__init__.py文件。我们将把每个模块的文件放在这里。tests:此目录下有测试脚本。在某些情况下,脚本将与模块并行。在某些情况下,脚本可能比模块本身更大、更复杂。docs:这有其他文件。我们将在下一节以及第三部分的一章中讨论这个问题。
在某些情况下,我们希望在多个候选类上运行相同的测试套件,以便确保每个候选类都能工作。对实际上不起作用的代码进行timeit比较是没有意义的。
所有 Python 代码在模块、类和方法级别都应该有 docstring。并非每个方法都需要 docstring。有些方法名称选择得很好,不需要多说了。然而,大多数情况下,为了清晰起见,文档是必不可少的。
Python 文档通常使用重构文本(RST标记)编写。
然而,在本书的代码示例中,我们将省略 docstring。这一遗漏使这本书的大小保持在合理的范围内。这种差距的缺点是使 docstring 看起来是可选的。它们显然不是可选的。
This point is so important, we'll emphasize it again: docstrings are essential.
Python 以三种方式使用 docstring 材质:
- 内部
help()功能显示文档字符串。 doctest工具可以在 docstring 中找到示例,并将它们作为测试用例运行。- 外部工具,如
sphinx和pydoc可以从这些字符串中生成优雅的文档摘要。
由于 RST 相对简单,编写好的 docstring 非常容易。我们将在第 18 章处理命令行中详细介绍文档和预期标记。但是,现在,我们将提供一个 docstring 可能是什么样子的快速示例:
def factorial(n: int) -> int:
"""
Compute n! recursively.
:param n: an integer >= 0
:returns: n!
Because of Python's stack limitation, this won't compute a value larger than about 1000!.
>>> factorial(5)
120
"""
if n == 0:
return 1
return n*factorial(n-1) 这显示了n参数的 RST 标记和返回值。它包括关于限制的附加说明。它还包括一个doctest示例,可用于使用doctest工具验证实现。使用:param n:和:return:识别sphinx工具将使用的文本,以提供信息的正确格式和索引。
所需的大多数工具都必须添加到 Python 3.7 环境中。常用的方法有两种:
- 使用
pip安装所有设备。 - 使用
conda创建一个环境。本书中描述的大多数工具都是 Anaconda 发行版的一部分。
pip安装使用单个命令:
python3 -m pip install pyyaml sqlalchemy jinja2 pytest sphinx mypy pylint black 这将在当前 Python 环境中安装所有必需的包和工具。
conda装置创造了一个conda环境,使本书的材料与任何其他项目分开:
-
安装
conda。如果您已经安装了 Anaconda,那么您就有了 Conda 工具,无需再做任何事情。如果你还没有水蟒,那么安装miniconda,这是开始的理想方式。访问https://conda.io/miniconda.html 下载适合您平台的conda版本。 -
使用
conda构建并激活新环境。 -
然后升级
pip。这是必要的,因为 Python 3.7 环境中的默认pip安装通常稍微过时。 -
最后,安装
black。这是必需的,因为black目前不在任何conda分销渠道中。
以下是命令:
$ conda create --name mastering python=3.7 pyyaml sqlalchemy jinja2
pytest sphinx mypy pylint
$ conda activate mastering
$ python3 -m pip install --upgrade pip
$ python3 -m pip install black 这套工具(pytest、sphinx、mypy、pylint和black对于创建高质量、可靠的 Python 程序至关重要。其他组件pyyaml、sqlalchemy和jinja2有助于构建有用的应用程序。
在本章中,我们考察了二十一点的游戏。这些规则具有中等程度的复杂性,为创建模拟提供了一个框架。模拟是 OOP 的最初用途之一,并且仍然是说明语言和库优势的编程问题的丰富来源。
本章介绍 Python 运行时使用特殊方法实现各种运算符的方式。本书的大部分内容将展示如何使用特殊的方法名来创建与其他 Python 特性无缝交互的对象。
我们还研究了构建好的 Python 应用程序所需的许多工具。这包括 IDE、用于检查类型提示的mypy程序,以及用于获得一致样式的black和pylint程序。我们还研究了用于进行基本性能和功能测试的timeit、unittest和doctest模块。对于项目的最终文档,安装sphinx很有帮助。这些额外组件的安装可通过pip或conda完成。pip工具是 Python 的一部分,conda工具需要再次下载才能使用。
在下一章中,我们将从类定义开始探索 Python。我们将特别关注如何使用__init__()特殊方法初始化对象。