Python面向对象编程指南(txt+pdf+epub+mobi电子书下载)


发布时间:2021-04-24 03:34:52

点击下载

作者:[美] Steven F. Lott 洛特

出版社:人民邮电出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Python面向对象编程指南

Python面向对象编程指南试读:

前言

本书主要介绍Python语言的高级特性,特别是如何编写高质量的Python程序。这通常意味着编写高性能且拥有良好可维护性的程序。同时,我们也会探究不同的设计方案并确定究竟是哪种方案提供了最佳性能。而对于一些正在寻找解决方案的问题,这也是一种很好的方式。

本书的大部分内容将介绍一种给定设计的不同替代方案。一些方案性能更好,另一些方案更加简单或者更加适合于特定领域的问题。最重要的是,找到最好的算法和最优的数据结构,以最少的开销换取最大的价值。时间就是金钱,高效的程序会为它们的用户创造更多的价值。

Python的很多内部特性都可以直接被应用程序所使用。这意味着,我们的程序可以与Python现有的特性非常好地整合。充分利用这些Python特性,可以让我们的面向对象设计整合得很好。

我们经常会为一个问题寻找多种不同的解决方案。当你评估不同的算法和数据结构时,通常会设计几种不同的方案,它们在性能和内存的使用上不尽相同。通过评估不同的方案,最终合理地优化应用程序,这是一种重要的面向对象设计技巧。

本书一个更为重要的主题是,对于任何问题,没有所谓的唯一且最好的方法。相反,会有许多不同的方案,而这些方案也各有优劣。

关于编程风格的主题非常有趣。敏锐的读者会注意到,在一些非常细微的部分,例如在名称选择和符号的使用上,并非所有的例子都完全符合PEP-8。

随着你能够越来越熟练地以面向对象的方式使用Python,也将不得不花大量的时间去阅读各种Python源码。你会发现,甚至在Python标准库的模块中,都有很大的可变性。相比于展示完全一致的例子,我们更倾向于去关注那些不一致的部分,正如我们在各种开源项目中所看到的,一致性的缺乏,正是对代码更好的认可。本书涵盖的内容

我们会用一些章节深入讲解Python的3个高级主题。● 一些预备知识,主要讲解一些基本的主题,例如unittest、

doctest、docstrings以及一些特殊的函数名。

第1部分“用特殊方法实现Python风格的类”,这个部分着重讲解面向对象编程以及如何更好地将Python内置的特性和我们的类进行集成,这个部分包括以下8章。● 第1章“__init()__方法”,详细讲解了__init()__的功能和实现,我

们会用不同的方式初始化一些简单的对象。接着,我们会尝试初

始化更加复杂的对象,例如集合和容器。● 第2章“与Python无缝集成——基本特殊方法”,讲解如何通过

加入特殊函数来扩展一个简单的类。我们需要了解继承的默认行

为,以便理解怎样的重写是必需的,以及什么时候应该使用重写。● 第3章“属性访问、特性和修饰符”,主要讲解了默认情况下它

们是如何工作的。我们需要决定在什么时候在什么地方重写默认

行为。我们还将探讨描述符的细节,以便更好地理解Python的内

部工作机制。● 第4章“抽象基类设计的一致性”,主要关注collections.abc模块

中的抽象基类。我们会探讨collections和containers的基本概念,

主要关注那些常被扩展和修改的部分。类似地,我们还会探讨

numbers的基本概念,主要关注那些常被实现的部分。● 第5章“可调用对象和上下文的使用”,主要讲述了使用

contextlib提供的方法以不同的方式来创建上下文管理器。我们会

讲解可调用对象的一系列不同设计以及为什么有时候一个有状态

的可调用对象会比一个简单的函数更加有用。在我们定制自己的

上下文管理器之前,我们还会探讨如何使用Python中内置的上下

文管理器。● 第6章“创建容器和集合”,关注container类的基本使用。我们会

探讨在创建容器过程中会调用的各种特殊函数。同时,我们也会

探讨如何扩展内置容器以添加新特性。最后,我们将封装内置容

器,然后通过委托方法让基础容器可以使用这些封装。● 第7章“创建数值类型”,涵盖了这些基本的运算符:+、−、

*、/、//、%和**。同时,我们也会介绍比较运算符,包括<、>、

<=、>=、==和!=。最后,我们会总结一些在扩展和定制自己的

数值类型时需要注意的设计要点。● 第8章“装饰器和mixin——横切方面”,涵盖了简单函数描述符,

带参数的函数修饰符、类修饰符和方法修饰符。

第2部分“持久化和序列化”介绍一个序列化到存储介质的持久化对象,它可能是转换为JSON后写入文件系统的,也可能是通过ORM存储到数据库的。这个部分会着重探讨持久化的不同方法,包括以下5章。● 第9章“序列化和保存——JSON、YAML、Pickle、CSV和

XML”,涵盖了对不同数据格式做简单的持久化时可使用的现有

的库,例如JSON、YAML、Pickle、XML和CSV。● 第10章“用Shelve保存和获取对象”,探讨了使用Python模块进

行简单的数据库操作,例如Shelve和dbm。● 第11章“用SQLite保存和获取对象”,进入更加复杂的SQL和关

系数据库的世界。因为SQL的特性并不符合面向对象设计的原则,

我们会遇到“阻抗不匹配”问题。一个通用的解决方案是使用

ORM存储大量的领域对象。● 第12章“传输和共享对象”,探讨HTTP协议以及使用JSON、

YAML和XML来表示要传输的对象。● 第13章“配置文件和持久化”,涵盖了Python应用程序使用配置

文件的不同方法。

第3部分“测试、调试、部署和维护”,我们会展示如何收集数据来支持和调试高性能程序。其中包括创建尽可能完善的文档——减少技术支持的难度。这个部分包括最后5章。● 第14章“Logging和Warning模块”,探讨了如何使用logging和

warning模块来记录审计和调试信息。相比于使用print()函数,这

将是巨大的进步。● 第15章“可测试性的设计”,涵盖了如何设计可测试的程序,以

及如何使用unittest和doctest。● 第16章“使用命令行”,探讨如何使用argparse模块解析选项和

参数。接着,我们会使用命令模式来编写易于整合和扩展的程序

模块,而不是使用纯粹的shell脚本。● 第17章“模块和包的设计”,涵盖了如何设计模块和包。这是一

个更高级的主题,我们会探讨如何将相关的类组织在一个模块

中,以及如何将相关的模块组合成一个包。● 第18章“质量和文档”,涵盖了我们应该如何将设计文档化,以

便让用户相信我们的软件是可靠的,并且是以正确的方式实现

的。阅读本书你需要准备什么

你需要下面的软件来编译和运行本书中的示例。● 安装带有标准库的Python 3.2或者更高版本。我们会使用Python

3.3,但是Python 3.2和Python 3.3之间的差别很小。● 我们还会使用一些第三方的包,包括PyYaml、SQLAlchemy和

Jinja2。● http://pyyaml.org。● http://www.sqlalchemy.org。安装的时候,对照安装指南,http://docs.sqlalchemy.org/en/rel_0_9/intro.html#installation。用--without-cextensions可以简化安装过程。● http://jinja.pocoo.org/。● 根据需要,你也可以选择安装Sphinx或者Docutils,因为我们也

会介绍它们。● http://sphinx-doc.org。● http://docutils.sourceforge.net。本书的目标读者

本书主要讲述Python的高级主题,所以要求读者熟悉Python 3。通过解决大型的复杂问题,你将会获益良多。

如果你非常熟悉其他的编程语言,但是想切换到Python,那么你可能会发现本书对你很有帮助。本书不会介绍诸如语法之类的基本概念。

对于熟悉Python 2的程序员,本书可以帮助你切换到Python 3。我们不会涉及任何版本切换的工具(例如,从版本2升级到版本3),以及任何共用的库(例如six)。书中重点讲述Python 3带来的新开发方式。约定

在本书中,你会发现我们使用不同样式的文字来区分不同类别的信息,下面是这些样式的一些例子。

文本中涉及源码的单词会用以下这种样式:“我们可以通过import来使用Python的其他模块”。

代码块的样式如下所示:class Friend(Contact):   def __init__(self, name, email, phone):     self.name = name     self.email = email     self.phone = phone

当我们想提醒你注意一个代码块的特定部分时,我们对该部分使用粗体:class Friend(Contact):   def __init__(self, name, email, phone):     self.name = name     self.email = email     self.phone = phone

以下是一个从命令行进行输入和输出的例子:>>> e = EmailableContact("John Smith", "jsmith@example.net")>>>Contact.all_contacts

新术语和重要的文字以粗体显示。在屏幕上看到的字,例如在菜单或对话框中出现的文字显示效果为:“我们通过这个功能来实现在每次单击Roll按钮时,在标签中显示一个新的随机值”。警告或重要信息会以这样的形式显示。提示和技巧会以这样的形式显示。读者反馈

非常欢迎读者对本书提供反馈和建议。让我们知道你关于本书的看法——你所喜欢和不喜欢的部分。哪部分内容使读者获得了最大收获,了解这点对于我们是重要的。

如果你要为我们提供反馈的话,只需要发送邮件到feedback@packpub.com,并在消息标题中包含书名。

如果你有推荐让我们出版的书,请发送邮件至suggest@packtpub.com。

如果有一个你所擅长的主题并有兴趣写书,可以联系www.packtpub.com/authors。客服支持

作为Packt图书的主人,我们会尽力为你提供帮助。下载本书的示例代码

你可以从此处下载所有你通过 Packt 账号支付的 Packt 图书的示例代码:http://www.PacktPub.com。如果你是通过其他方式支付的,可以访问http://www.PacktPub.com/support并进行注册,我们将通过邮件的方式发送给你。勘误

我们已经尽力保证本书内容的质量并避免错误的发生。如果你发现了本书的任何错误——可能是在文字描述上或代码中——我们将非常感谢你能联系我们。这样做可以为其他读者提供帮助并有助于我们提高本书后续的内容质量。如果你发现了任何勘误,请通过访问http://www.packtpub.com/support来联系我们,选择你的书并单击let us know链接,然后勘误会被上传到我们的网站,或是添加到该书名下的勘误列表。任何已有的勘误都可以通过在http://www.packtpub.com/support选择书名来进行查看。版权

对各种媒体而言,互联网上受版权保护的各种材料都长期面临非法复制的问题。Packt非常重视对版权和许可的保护。如果你在网络上发现了任何对我们的内容进行非法复制的情形,请立即为我们提供网址或网站名称,这样我们可以采取相应措施。

你也可以通过copyright@packtpub.com联系我们,提供盗版材料的链接。

我们感谢你能协助保护作者版权并帮助我们为你提供更有价值的内容。其他问题

如果你对本书任何一方面存在疑问,可以通过questions@packtpub.com和我们联系,我们会尽力提供答复。审阅者简介

Mike Driscoll从2006年开始使用Python语言,他很喜欢在自己的博客中分享有关Python的使用技巧(博客地址:http://www.blog.pythonlibrary.org/)。他也是DZone出版的Core Python refcard一书的作者之一,同时也在Packt出版社出版过多本个人著作,例如Python 3 Object Oriented Programming、Python 2.6 Graphics Cookbook和Tkinter GUI Application Development Hotshot。Mike最近在写Python 101这本书。

我要感谢我的妻子Evangeline对我长期以来的支持,感谢我的朋友和家庭成员为我所做的一切。

Róman Joost从1997年就开始从事开源项目的开发。作为GIMP用户文档的项目经理,他已经为GIMP和Python/Zope的开源项目做了8年的贡献。目前Róman在澳大利亚布里斯班的红帽公司工作。

SakisKasampalis来自新西兰,目前在基于位置服务的B2B提供商担任软件开发工程师。他不赞成对编程语言和开发工具持过度追捧的态度。他的原则是把正确的技术应用在适合的问题上。Python是他最喜爱的工具之一,他很看重Python的高效。在FOSS的项目工作期间,Kasampalis维护GitHub上的一个与Python设计模式相关的项目,相关资料可以从https://github.com/faif/python-patterns下载。同时,他也是Packt出版的Learning Python Design Patterns一书的审阅者。

Albert LukaszewskiPh.D目前是苏格兰南部的Lukaszewski咨询服务中心的首席顾问。他已经有30多年的编程经验,现在是系统设计与实现方面的顾问。他之前还在爱可信欧洲有限公司担任过首席工程师。他的大部分经验都与文本处理、数据库系统和自然语言处理(Natural Language Processing,NLP)相关。同时,他还写过MySQL for Python一书,该书由Packt出版。除此之外,他之前还曾为纽约时报子公司About.com写过Python专栏。

Hugo Solis是哥斯达黎加大学物理系的教授助理。他目前主要研究计算宇宙学、复杂性和氢对材料特性的影响。他在使用C/C++和Python进行科研和可视化方面有着丰富的编程经验。他是自由软件基金会的成员之一,也做过一些开源项目。目前他主要负责管理IFT,这是哥斯达黎加的一个非营利的科研机构,致力于物理学科的实践(http://iftucr.org)。

我要向我亲爱的母亲Katty Sanchez表示感谢,感谢她的支持以及诸多不错的创意。一些预备知识

为了使本书接下来的内容更清晰,我们先来看一些关心的问题。其中一项是21点游戏。我们将重点关注21点游戏的模拟,但并不赞成赌博。

然而,对于面向对象编程来说,模拟是最早的问题之一。这也是能够体现出面向对象编程优雅的一个情形。有关更多信息,可参见http://en.wikipedia.org/wiki/Simula,以及Rob Pooley写的An Introduction to Programming。

本章会介绍一些工具的背景,它们是编写出完整的Python程序和包的基础。在接下来的章中会使用它们。

我们会使用timeit模块将面向对象设计进行对比,找出性能更好的那个。在很多有关如何更好地写出适用于问题模型代码的主观考虑中,使用客观事实来进行说明是非常重要的。

我们将介绍如何在面向对象中使用unittest和doctest模块,它们是在开发过程中核对实际工作的基本工具。

一个良好的面向对象设计应当是清晰的并且可读性很强。为了确保良好的可读性,编写Python风格的文档是必要的。Docstrings在模块、类和方法中都很重要。我们会在这里简单概括RST标记并会在第18章“质量和文档”中详细介绍。

此外,我们还要解决集成开发环境(Integrated Development Environment,IDE)的问题。常见的问题是Python开发最好的IDE。

最后,我们会介绍Python中特殊基本方法的概念。关于特殊方法,在前7章都有介绍。在这里,我们会介绍一些有助于理解第1部分“用特殊方法实现Python风格的类”的背景知识。

在讨论Python面向对象编程过程中,将尽量避免一些题外话。我们会假设你已经读了Python 3 Object Oriented Programming这本书。我们不会重复在其他地方已经讲得很清楚的内容。在本书中,会完全关注Python 3的内容。

我们会引用很多常见的面向对象设计模式,也不会重复在Learning Python Design Patterns书中出现的内容。关于21点游戏

如果你还不熟悉21点游戏,以下是大致的介绍。

游戏的最终目标是,从庄家手中拿到牌,将手中的牌组成和为在庄家点总数与21之间的数字。

在纸牌中数字牌(2到10)包含了牌的点数值。而非数字牌(J、Q、K)等同于10点。而A等于11点或1点。当把A当作11点使用时,手中牌的值被称为软手。当将A当作1点使用时,手中牌的值称为硬手。

如果手中牌中包含了A和7,就可以当作8点硬手或18点软手。

有4种两张牌的组合可以构成21点。它们都称为21点,尽管其中一种组合包含了J。玩21点游戏

21点游戏在不同的场合会有所不同,但主要流程类似,包含如下几点。● 首先,玩家和庄家各自发两张牌,玩家会知道自己手中的牌面值。

在有些场合中会需要将牌面朝上。● 庄家的牌一张朝上一张朝下。因而玩家会了解一点庄家的牌,但

不完全知道。● 如果庄家朝上的那张牌是A,那么另一张牌有4/13的概率为10,

因而构成总数21。玩家可以选择做一个额外的保险下注。● 下一步,玩家可以选择拿牌或停止拿牌。这两种常见的选择称为

叫或停牌。● 还会有更多选择。如果玩家手中的牌匹配,可以选择分牌。这算

是额外的下注,分后的两手牌将分开进行游戏。● 最终,玩家可以在拿最后一张牌前选择双倍下注,称为天生21

点。如果玩家的牌总数为10或11,这就是一种常见的下注方

式。

手牌的最终评判如下。● 如果玩家的牌大于21点,称为超出21,玩家输并且庄家朝下的

那张牌将不做考虑。● 如果玩家的牌小于等于21点,那么根据一种简单的规则庄家拿

牌。之后庄家手中的牌必须小于18点。手中牌的总数高出18,

庄家必须停叫。关于这点会略有不同,现在暂时忽略。● 如果庄家超出21,玩家赢。● 如果庄家和玩家都小于等于21点,则比较他们手中牌的大小。

现在暂时不关心最终的收益。对不同玩法和下注策略的模拟过程来说,总收益关系不大。21点游戏策略

对于21点游戏来说,玩家必须使用以下两种策略。● 一种策略用于决定玩法:保险、叫、停叫、分牌或双倍。● 另一种策略用于决定下注大小。一种常见的谬误统计可以引导玩

家提高或降低下注,进而最大限度地保证胜的概率并减少输的概

率。任何模拟游戏的软件也必须对复杂下注策略进行模拟。它们

是一些有趣的算法,通常包含状态,需要学习一些高级的Python

编程技巧来完成。

这两种策略是介绍策略模式不错的例子。21点游戏模拟器对象的设计

我们将使用游戏中的元素,例如玩家手中的牌作为对象模型的例子。然而,不会对整个过程进行模拟。我们会重点关注游戏中的元素,因为它们会有细微的差别但不是特别复杂。

使用一个简单的容器:存放手中的牌对象,可以包含0个或多个。

介绍Card的子类:NumberCard,FaceCard和Ace。

介绍几种不同的方式来定义这种简单的类层次结构。由于层次结构很小(并且简单),可以简单对几种不同的实现方式进行尝试。

介绍几种实现玩家手中牌的方式。这只是一个简单的纸牌集合,包含了一些额外的功能。

从全局的视角来看玩家对象,玩家会有几手牌和下注策略以及21点游戏策略。这是一个复杂的组合对象。

我们也会对洗牌和发牌进行快速介绍。性能——timeit模块

我们会使用timeit模块来将不同面向对象设计和Python结构进行对比,timeit模块包含了很多函数。重点关注的是timeit,这个函数会为一些语句创建一个Timer对象,也会包含一些预备环境的安装代码,然后调用Timer的timeit()方法来执行一次安装过程并重复执行目标语句。返回值为运行语句所需的时间。

默认计数为100000次。这提供了一个有意义的平均时间值,来自其他计算机上OS级别活动的统计。对于复杂的或长时间运行的语句,需要谨慎使用小计数值。

以下是与timeit简单交互的示例代码:>>> timeit.timeit( "obj.method()", """... class SomeClass:...   def method(self):...    pass... obj= SomeClass()""")0.1980541350058047下载示例代码你可以通过自己的帐号下载所有从Packt出版社所购买的书籍中的示例代码:http://www.packtpub.com。如果你是从其他地方购买的,可以访问http://www.packtpub.com/support并注册,我们会通过邮件形式发送给你。

obj.method()语句以字符串的形式提供给timeit(),安装为类定义并且也由字符串的形式提供。语句中所需要的任何东西都必须在安装中提供,它包括所有的导入和所有的变量定义以及对象创建。

可能会需要多尝试几次来完成安装过程。当使用交互式Python时,经常会由于命令行窗口的翻屏导致无法追踪全局变量和导入信息。有一个例子是,10000次空方法的调用,花了0.198秒。

以下是另一个使用timeit的例子:>>> timeit.timeit( "f()","""... def f():...  pass... """ )0.13721893899491988

这个例子说明了,空函数的调用会比空方法的调用略快一些,在这个例子中差不多为44%。

在一些情况下,OS的开销可以作为性能的测量组件,它们通常源自难以控制的因素。在这个模块中可以使用repeat()函数来替代timeit()函数。它会收集基本定时的多个样本,对OS在性能上的影响做进一步分析。

对于我们而言,timeit()函数会提供所有反馈信息,我们可用于在客观上对不同面向对象涉及的要素进行评估。测试——unittest和doctest

单元测试当然是基本的。如果没有用于展示某个功能的单元测试,那么这个功能就不是真的存在。换句话说,对于一个功能来说,直到有测试可以说明它已经完成才算是完成。

我们只会对测试进行少量介绍。如果对每个面向对象设计功能的测试都进行深入介绍,那么这本书的厚度应该是现在的两倍。在忽略测试内容的细节上会存在一个误区,好的单元测试似乎只是可选的。当然不是,它们是必需的。单元测试是必需的如果有疑问,可以先设计测试用例,再对代码进行修改,满足测试用例。

Python提供了两种内置的测试框架。大部分应用和库会同时使用两者。对于所有测试来说,普遍的一种封装为unittest模块。另外,在许多公共API docstrings中可以找到一些例子,都使用了doctest模块。而且,在unittest中可以包含doctest中的一些模块。

好一点的做法是,每个类和函数都至少有一个单元测试。更重要的是,可见的类、函数和模块也要包含doctest。还有更好的做法:100%的代码覆盖,100%的逻辑分支覆盖等。

实际上,一些类不需要测试。例如由namedtuple()创建的类不需要单元测试,除非首先去怀疑namedtuple()的实现。如果不相信Python的实现,就无法基于它来写程序。

一般地,我们会先设计测试用例再编写可以通过测试用例的代码。测试用例会凸显出代码中API的形态。本书会介绍几种写代码的方式,它们的接口是相同的,这点很重要。一旦我们定义了接口,会有几种不同的实现方式。一组测试应该能够适应几种不同面向对象的设计。

一种常见的方式是使用unittest工具为项目创建至少有以下3种平行的目录。● myproject:这个目录会需要安装在lib/site-packages中,作为应

用最终的包。它会有一个__init__.py包,而且会放在每个模块

中。 -test:这个目录包含测试脚本。对于一些情形,这些脚本

在模块中是平行存在的。在一些情况下,脚本可能会很大并且比

模块自身更复杂。● doc:这个目录中会包含其他文档。我们会在下一节以及第18章“质量和文档”中对它进行介绍。

在一些情况下,你会希望在多个类上运行同样的测试组件,这样就能够确保每个类是工作的。但在根本不工作的类上使用timeit进行比较是没有意义的。单元测试与技术探究

作为面向对象设计的一部分,通常会创建一个类似本节代码中所演示的技术探究模块,我们会把它分为3个部分。首先,是以下这个全局的抽象类。import typesimport unittestclass TestAccess( unittest.TestCase ):   def test_should_add_and_get_attribute( self ):     self.object.new_attribute= True     self.assertTrue( self.object.new_attribute )   def test_should_fail_on_missing( self ):     self.assertRaises( AttributeError, lambda: self.object.undefined )

抽象类TestCase的子类中定义了一些希望类可以通过的测试。实际被测试的对象被忽略了。它通过self.object被引用,但是没有提供定义,使得TestCase子类保持抽象。每个具体类都会需要setUp()方法。

以下是3个具体的TestAccess子类,会包含以下3种不同对象的测试。class SomeClass:   passclass Test_EmptyClass( TestAccess ):   def setUp( self ):     self.object= SomeClass()class Test_Namespace( TestAccess ):   def setUp( self ):     self.object= types.SimpleNamespace()class Test_Object( TestAccess ):   def setUp( self ):     self.object= object()

TestAccess类的每个子类都提供了所需要的setUp()方法。每个方法创建了一种不同的被测试对象。第1个是空类的实例。第2个是types.SimpleNamespace的实例。第3个是object的实例。

为了运行这些测试,需要创建一个组件,来阻止我们运行TestAccess抽象类的测试。

以下是探究的其余部分。def suite():   s= unittest.TestSuite()   s.addTests( unittest.defaultTestLoader.loadTestsFromTestCase(Test_EmptyClass) )   s.addTests( unittest.defaultTestLoader.loadTestsFromTestCase(Test_Namespace) )   s.addTests( unittest.defaultTestLoader.loadTestsFromTestCase(Test_Object) )   return sif __name__ == "__main__":   t= unittest.TextTestRunner()   t.run( suite() )

现在我们得到了具体的证据,object类的使用方式与其他类是不同的。进一步说,我们有了一个可以用于演示其他可行(或不可行)设计的测试。例如,用于演示types. SimpleNamespace作为空类行为的测试。

我们跳过了很多单元测试用例的细节,会在第15章“可测试性的设计”中进行详细介绍。Docstring——RST标记和文档工具

所有的Python代码都应该在模块、类和方法级别包含docstrings。不是每个方法都需要docstring,有一些方法名已经很好了,不需要进一步说明。而大多数情况下,文档的说明是基本的。

Python文档通常使用ReStructured Text(RST)标记来写。

然而,在本书的示例代码中,为了限制本书内容在合理的范围内,没有使用docstrings。这样的缺点是,docstrings看起来是可选的,可它们是必需的。

再次强调,docstrings是必需的。

docstrings在Python中通过以下3种方式使用。● 内部的help()函数用于显示docstrings。● doctest工具可以在docstrings中查找示例并把它们当作测试用例

运行。● 类似Sphinx和epydoc这样的外部工具可以帮助文档的提取。

由于RST相对简单,编写好的docstrings相对非常简单。我们会在第18章“质量和文档”中对文档以及预计标记进行详细介绍。现在通过一个例子来看一下docstring的形式。def factorial( n ):   """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)

以上代码展示了RST标记的参数和返回值,还包括了关于限制的一段说明。所包括的doctest输出可用于验证使用doctest工具完成的实现。有很多标记功能可用于提供更多的结构和语义方面的信息。IDE的选择

关于Python开发的IDE常见问题是最好的IDE是什么。简单的回答是IDE的选择根本不重要,支持Python的开发环境实在太多了。

本书的所有实例都通过Python的>>>提示来演示交互的过程。运行能够交互的例子是非常有意义的。精心编写的Python代码应该很简单,并能够从命令行运行。我们应该能够在>>>提示中展示一个设计。

从>>>提示来运行代码是对Python设计复杂度的一个重要的质量测试。如果类或函数过于复杂,那么就没有办法从>>>提示运行。对于一些复杂的类,应该提供模仿对象来模拟简单的交互过程。关于特殊方法名

Python有多层的实现,但我们只关心其中两层。

从表面上看,我们有Python的源代码。源代码是传统面向对象与过程式函数调用的混合体。面向对象符号的后缀中通常包括object.method()或object.attribute这样的结构。而前缀中包括了function(object)的调用,是典型的过程式设计。此外还包含了插入符,例如object+other。另外还有其他语句,例如for和调用对象方法的with语句。

function(object)前缀的出现会导致一些程序员产生疑问,是否进行纯面向对象的Python编程。认为严格的遵守面向对象(object.method())的设计方式是有必要或有帮助的,这种说法是不够明确的。Python混合使用了前缀和后缀的编程方式,前缀符号代表了特殊方法的后缀符号。前缀、中缀和后缀符号的选择要基于表达力和优雅程度。良好Python代码的目标之一是,它看起来应该像英文。在底层,语法变化是由Python特殊方法实现的。

在Python中的任何事物都是对象。这点与Java或C++不同,它们会有“原始”类型来避免对象范型。每个Python对象都提供了一个特殊方法的数组,其中包含了语言最上层功能的实现细节。例如,可以在应用程序中写str(x)。这个前缀符号在底层的实现为x.__str__()。

类似a+b这样的结构会被实现为a.__add__(b)或b.__radd__(a),取决于对象a和b所属的类定义中所提供的类型兼容性规则。

需要强调的是,在外部语法与特殊方法内部实现之间的映射不只是把function(x)重写为x.__function__()。在许多语言功能中,包含了一些特殊方法支持这项功能。一些特殊方法包含了从基类、object所继承的默认实现,另一些特殊方法则没有默认实现而会直接抛出异常。

第1部分“用特殊方法实现Python风格的类”将会介绍这些特殊方法并会演示如何实现这些特殊方法,以使得我们的类定义能够与Python无缝结合。总结

我们介绍了示例的问题域:21点游戏。选择这个例子是因为它包含了一定的算法复杂度但又不是过于复杂或者难懂。另外也介绍了在本书中会用到的3个重要模块。● timeit模块,我们会用于对比不同实现的性能。● unittest和doctest模块,我们会用于确保软件能够正确运行。

书中也介绍了几种向Python程序中添加文档的方式。我们会在模块、类和函数中使用docstrings。为了节省空间,不是每个例子都会展示docstrings,但它们都是最基本的。

集成开发环境(Integrated Development Environment,IDE)的使用不是基本的,任何有效的IDE或文本编辑器对于高级Python开发都应该是可以选择的。

在后续的8章中,我们将对特殊方法名进行分类介绍,内容主要包括如何能够创建出与内置模块无缝集成的Python程序。

在第1章中,我们会重点关注__init__()方法以及使用它的不同方式。__init__()方法很重要,因为初始化是对象生命周期的第1个大步骤;每个对象必须正确地初始化才能很好地工作。更重要的是,__init__()参数值的形式有很多种。我们会介绍几种不同设计__init__()的方式。▶▶第1部分用特殊方法实现Python风格的类__init__()方法与Python无缝集成——基本特殊方法属性访问、特性和修饰符抽象基类设计的一致性可调用对象和上下文的使用创建容器和集合创建数值类型装饰器和Mixins——横切方面用特殊方法实现

Python风格的类 通过重写特殊方法来完成对Python内部机制的调用,在Python中是很普遍的。例如len()函数就可以重写一个类的__len__()方法。

这意味着对于像(len(x))这样的通用公共接口,任何类(例如,声明一个类叫tidy)都可以实现它。任何类都可以重写一个像__len()__这样的特殊方法,这样一种机制构成了Python多态机制的一部分;任何实现了__len()__函数的类都会响应通用公共接口(len(x))中的len()函数。

每当定义一个类,可以(而且应该)提供这些特殊方法的实现来与Python语言更好地结合。本书的第1部分“用特殊方法实现Python风格的类”是对传统面向对象设计的一种延伸,可以使创建的Python类更具Python风格。任何一个类都应当与Python语言其余的任何原生部分很好地结合。这样一来,既可以重用很多其他语言现有的功能和标准库,而且编写的包和模块也将更容易维护和扩展。

在某种程度上,创建的类都可以作为Python扩展的形式来实现。开发者都希望自己的类更接近Python语言的原生类。这样一来,在语言之间、标准库之间以及应用程序之间的代码区别就能够最小化。

为了实现更好的可扩展性,Python语言提供了大量的特殊方法,它们大致分为以下几类。● 特性访问(Attribute Access):这类特殊方法实现了对象的特性

访问,使用方式为object.attribute,既可以用来赋值,也可以在

del语句中执行删除操作。● 可调用对象(Callables):这个方法的适用对象为参数,就像

Python内部的len ()函数。(也是应用于参数。)● 集合(Collections):这类方法提供了很多集合操作的功能。类

似这类方法的使用有sequence[index]、mapping[key]和some_set

| another_set。● 数字(Numbers):这类方法提供了大量的数学运算符和比较运

算符。可以使用这些方法来扩展Python的数字部分。● 上下文(Context):这类函数通常使用with语句来实现上下文的

管理。● 迭代器(Iterator):可以使用这类方法定义迭代器,通常不需要

考虑这部分的扩展,因为生成器(Generator)已经提供了非常

优雅的实现。然而,我们仍会探究如何创建自己的迭代器。

在Python 3 Object Oriented Programming一书中已经介绍了这些特殊方法中的一部分,以下我们将重新回顾这些主题并对其他属于基本范畴的特殊方法进行深入介绍。

尽管是基础的范畴,仍可以针对其他比较深入的主题进行讨论。这里将会以基础的几个特殊方法作为开始,后续会讨论一些高级的特殊方法。

__init__()函数为对象的初始化操作提供了很大的自由度,对于不可变(每次操作都会产生一个新实例)的对象而言,声明和定义是非常重要的。在第1章中,我们会讨论一些关于这个函数设计的方案。第1章 __init__()方法

__init__()方法的重要性体现在两点。首先,初始化既是对象生命周期的开始,也是非常重要的一个步骤,每个对象都必须正确地执行了初始化才能够正常地工作。其次,__init()__方法的参数可以多种形式来完成赋值。

因为__init()__方法传参方式的多样化,意味着对象的初始化过程也会有多种。关于这一点我们将使用一些有代表性的例子对此进行详细说明。

在深入讨论__init__()函数之前,需要看一下Python语言的类层次结构。简单地说,所有的类都可以继承object类,在自定义类中可以提供比较操作的默认实现。

本章会演示简单对象初始化的不同形式(例如,打牌)。随后将深入探讨复杂对象的初始化过程,涉及集合以及使用策略和状态模式实现的玩家类。1.1 隐式的基类——object

每个Python类的定义都会隐式继承自object类,它的定义非常简单,几乎什么行为都不包括。我们可以创建一个object实例,但很多事情无法完成,因为很多特殊方法的调用程序都会抛出异常。

对于任何自定义类,都会隐式继承object。以下是一个类定义的示例(隐式继承了object类)。class X:  pass

下面是对自定义类进行交互的代码。>>> X.__class__>>> X.__class__.__base__

可以看到类定义就是对type类的一个对象的类型声明,基类为object。

相应地,派生自object类中的对象方法也将继承各自相应的默认实现。在某些情况下,基类中一些特殊方法的默认行为也正是我们想要的。对于一些特殊情况,就需要重写这些方法。1.2 基类中的__init__()方法

对象的生命周期主要包括了创建、初始化和销毁。后面章节会详细讨论对象的创建和销毁,本章专注于对象的初始化。

object作为所有类的基类,已经为__init__()方法提供了默认实现,一般情况下不需要重写这个函数。如果没有对它进行重写,那么在创建对象时将不会产生其他变量的实例。在某些情况下,这种默认行为是可以接受的。

对于继承自object的子类,总可以对它的属性进行扩展。例如,对于下面这个类,实例化就不对函数(area)所需要的变量(width和length)进行初始化。class Rectangle:  def area( self ):    return self.length * self.width

Rectangle类的area函数在返回值时使用了两个属性,可并没有在任何地方对其赋值。在Python中,这种看似奇怪的调用尚未赋值属性的操作却是合法的。

下面这段代码演示如何使用刚定义的Rectangle类。>>> r= Rectangle()>>> r.length, r.width = 13, 8>>>r.area()104

虽然这种延迟赋值的实现方式在Python中是合法的,但是却给调用者带来了潜在的困惑,因此要尽量避免这样的用法。

然而,这样的设计看似又提供了灵活性,意味着在__init__()方法被调用时不必为所有的属性赋值。这看似是不错的选择,一个可选属性即可以看作是某子类中的成员,且无须对这个子类进行显式地定义就可以完成对原生机制的扩展。然而这种多态机制不但给程序带来了隐藏的不确定性,也会相应产生很多令人费解的if语句。

因此,延迟初始化属性的设计在某种情形下可能会有用,可是这样也可能会导致非常糟糕的设计。

在Zen of python poem一书中曾提出过这样的建议:“显式而非隐式”。

对于每个__init__()方法,都应当显式地指定要初始化的变量。糟糕的多态 在灵活性与糟糕之间有一个临界。 一旦发觉书写了这样的代码,我们就已经丧失了灵活性并开始了糟糕的设计。  if 'x' in self.__dict__:或:  try:    self.x except AttributeError:这时就要考虑添加一个公共函数或属性来重构这个API,相比于添加if语句,重构将是更好的选择。1.3 在基类中实现init()方法

通过实现__init()__方法来初始化一个对象。每当创建一个对象,Python会先创建一个空对象,然后调用该对象的__init()__函数。这个方法提供了对象内部变量以及其他一些一次性过程的初始化操作。

以下是关于一个Card类层次结构定义的一些例子。这里定义了一个基类和3个子类来描述Card类的基本信息。有两个变量是参数直接赋值的,另外两个参数是通过初始化方法计算来完成初始化的。class Card:  def __init__( self, rank, suit ):    self.suit= suit    self.rank= rank    self.hard, self.soft = self._points()class NumberCard( Card ):  def _points( self ):    return int(self.rank), int(self.rank)class AceCard( Card ):  def _points( self ):    return 1, 11class FaceCard( Card ):  def _points( self ):    return 10, 10

在以上代码段中,__init()__把公共初始化方法引入到了基类Card中,这样3个子类NumberCard、AceCard和FaceCard都能够共享公共的初始化逻辑。

这是一个常见的多态设计,每个子类为_points()方法提供特有的实现。所有的子类有相同的方法名和属性。这3个子类在使用时可以通过互换对象来更换实现方式。

如果只是简单地使用字母来定义花色,就可以使用如下的代码段来创建Card对象。cards = [ AceCard('A', '♠'), NumberCard('2','♠'), NumberCard('3','♠'), ]

这里枚举了Card集合中的几个Card对象,把牌面值(rank)和花色(suit)作为参数传入来实例化。从长远来看,需要一个更智能的工厂函数来创建Card对象,因为枚举所有52张牌非常麻烦而且容易出错。在介绍工厂函数前,先看一些其他的问题。1.4 使用__init()__方法创建常量清单

我们可以为所有卡片的花色单独创建一个类。可在21点应用中,花色不是很重要,用一个字母来代替就可以。

这里使用花色的初始化作为创建常量对象的一个实例。很多情况下,应用会包括一个常量集合。静态常量也正构成了策略(Strategy)或状态(State)模式的一部分。

有些情况下,常量会在应用或配置文件的初始化阶段被创建。或者创建变量的行为是基于命令行参数的。我们会在第16章“使用命令行”中介绍应用初始化和启动的详细设计过程。

Python中并没有提供简单而直接的方式来定义一个不可变对象。我们会在第3章“属性访问、特性和修饰符”中介绍如何创建可靠的不可变对象。这个例子中,把花色这个属性定义为不可变是有意义的。

如下代码定义了一个花色类,可以用来创建4个花色常量。class Suit:  def __init__( self, name, symbol ):    self.name= name    self.symbol= symbol

如下代码是对这个类的调用。Club, Diamond, Heart, Spade = Suit('Club','♣'), Suit('Diamond','♦'), Suit('Heart','♥'), Suit('Spade','♠')

现在就可以使用如下代码创建Card对象了。cards = [ AceCard('A', Spade), NumberCard('2', Spade), NumberCard('3', Spade), ]

对于以上的这个小例子来说,这样的方式相比于简单地使用一个字母来代替花色的实现方式并没有太大的优势。可在更复杂的情况下,可能会需要创建一组策略或状态模式对象的集合。如果把创建好的花色对象做缓存,构成一个常量池,使得在调用时对象可被重用,那么性能将得到显著的提升。

我们不得不承认在Python中这些对象只是在概念上是常量,它们仍然是可变的。使用额外的代码实现使得这些对象成为完全不可变的可能会更好。

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载