书名:Python面向对象编程:构建游戏和GUI
ISBN:978-7-115-60231-2
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 [美]艾维·卡尔布(Irv Kalb)
译 赵利通
责任编辑 谢晓芳
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
读者服务:
微信扫码关注【异步社区】微信公众号,回复“e60231”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。
本书首先介绍构建类和创建对象的基础知识,并结合代码讲述如何将理论付诸实践;然后讨论面向对象编程的关键概念—— 封装、多态性和继承,包括如何使用对象管理器创建并管理多个对象,如何通过封装对客户端代码隐藏对象的内部细节,如何使用多态性定义一个接口并在多个类中实现它,如何应用继承构建现有代码;最后讲述如何构建一款带完整的动画和声音的视频游戏,从而将所有内容整合在一起。本书涵盖了两个功能齐全的Python代码包,它们将加速Python中图形用户界面程序的开发。
本书不仅适合Python开发人员阅读,还适合计算机相关专业的师生阅读。
Irv Kalb是加州大学圣克鲁斯硅谷分校和硅谷大学的客座教授,负责“Python入门”与“Python面向对象编程”课程的教学。Irv拥有计算机科学的学士和硕士学位,使用多种计算机语言进行面向对象编程已超过30年,并且到现在已经有超过10年的教学经验。他有几十年的软件开发经验,主要关注教育软件的开发。在Furry Pants Productions公司,他和妻子以Darby the Dalmatian这个角色为原型,制作并发布了两张寓教于乐的CD-ROM。Irv还撰写了Learn to Program with Python 3: A Step-by-step Guide to Programming(Apress)一书。
Irv深入参与了极限飞盘(Ultimate Frisbee)这项运动的早期开发。他主持编写了多个版本的官方规则手册,并与人合著了关于这项运动的第一本图书—— Ultimate: Fundamentals of the Sport。
Monte Davidoff是一名独立的软件开发顾问。他的研究方向是DevOps和Linux。Monte使用Python编程已经超过20年。他使用Python开发过多种软件,包括业务关键型应用程序和嵌入式软件。
本书介绍一种称为面向对象编程(Object-Oriented Programming,OOP)的编程技术,以及如何在Python中使用这种技术。在OOP出现之前,程序员使用所谓的过程式编程技术(也称为结构化编程),构建一组函数(过程),并通过调用这些函数来传递数据。OOP范式为程序员提供了一种高效的编程方式,将代码和数据组合成内聚的单元,并且这种单元常常是高度可重用的。
在准备撰写本书时,我深入研究了现有的文献和视频,特别关注其他人如何解释这个重要的、内容广泛的主题。我发现,讲师和作者通常首先定义一些关键的术语—— 类、实例变量、方法、封装、继承、多态性等。
虽然这些都是重要的概念,本书也将深入介绍它们,但是我将采用一种不同的方式,首先考虑这个问题:“我们要解决什么问题?”即,如果OOP是解决方案,那么什么是问题?为了回答这个问题,本书首先展示一些使用过程式编程方式编写的程序示例,指出这种编程风格存在的问题。然后,本书将展示面向对象的编程方法如何让构建这种程序变得更加简单,也让程序本身变得更容易维护。
本书针对的是了解Python并使用过Python标准库中基本函数的读者。假定你了解Python的基本语法,并且能够使用变量、赋值语句、if/elif/else语句、while循环、for循环、函数、函数调用、列表、字典等编写小程序到中等规模的程序。如果你不熟悉这些概念,建议你先阅读我撰写的Learn to Program with Python 3:A Step-by-step Guide to Programming(Apress)一书。
本书是一本面向中等程度读者的图书,所以不会介绍一些更加高级的主题。例如,为了保证内容的适用性,本书在很多时候不会详细介绍Python的内部实现。为了简单和清晰起见,也为了让本书的关注点一直保持在如何掌握OOP技术上,本书的示例只使用Python语言的一个子集。在编写Python代码时,还有更加高级、更加简洁的方式,但那些内容不在本书的讨论范围内。
本书尽量以与语言无关的方式介绍OOP的底层细节,但会指出Python和其他OOP语言不同的地方。通过本书学习OOP风格的代码的基础知识后,如果愿意,你应该能够轻松地在其他OOP语言中应用这些技术。
本书的所有示例代码都使用Python 3.6~3.9编写和测试。所有示例都应该能够在Python 3.6及更高版本上运行。
从Python官网可以免费获得Python。如果你还没有安装Python,或者想升级到最新版本,可以访问该网站,选择Download标签页,然后单击Download按钮。这将把一个可安装的文件下载到你的计算机上。双击下载的文件来安装Python。
在Windows系统中安装Python
如果在Windows系统上安装Python,需要正确设置一个重要的选项。使用向导进行安装时,应该会看到如下界面。
在对话框底部,有一个Add Python 3.9 to PATH复选框。请一定要选中这个复选框(它默认是未选中的)。这个设置能够让安装的pygame包(本书后面将会介绍)正确工作。
注意:
我知道“PEP 8 — Style Guide for Python Code”,也知道它推荐为变量和函数名称使用蛇形命名约定(snake_case)。但是,在PEP 8文档问世之前,我已经使用驼峰命名约定(camelCase)许多年了,在我的职业生涯中已经习惯了这种约定。因此,本书中的所有变量和函数名称都将采用驼峰命名约定。
本书前几章的示例使用基于文本的Python。这些示例程序以文本形式从用户那里获得输入,然后以文本形式把信息输出给用户。本书将展示如何开发基于文本的程序,通过在代码中模拟物体来介绍OOP。我们首先将创建电灯开关、调光开关和电视机遥控器对象,然后介绍如何使用OOP来模拟银行账户,以及管理银行。
在介绍了OOP的基础知识后,本书将介绍pygame模块,它使程序员能够编写具有图形用户界面(Graphical User Interface,GUI)的游戏和应用程序。在基于GUI的程序中,用户与按钮、复选框、文本输入和输出字段,以及其他对用户友好的小部件进行直观的交互。
我选择在Python中使用pygame,因为这种组合使我能够利用屏幕上的元素,以高度可视化的方式演示OOP概念。pygame的可移植性很强,能够在几乎所有平台和操作系统上运行。我使用pygame 2.0测试了本书中所有使用pygame包的示例程序。
我创建了一个叫作pygwidgets的包,它能够与pygame一起使用,并实现了许多基本的小部件,这些小部件都是使用OOP方法创建的。本书后面将介绍这个包,并提供一些可以运行和修改的示例代码。这种方法使你能够看到关于关键的面向对象概念的真实示例,同时利用这些技术来创建有趣的、有可玩性的游戏。本书还将介绍我开发的pyghelpers包,它提供的代码对编写更加复杂的游戏和应用程序有帮助。
本书的所有示例代码可从No Starch网站下载(请搜索“object-oriented-python”)。
这些示例代码也可以从我的GitHub仓库中逐章获取(请搜索“IrvKalb/Object-Oriented- Python-Code”)。
本书分为4部分。第一部分介绍面向对象编程。
● 第1章回顾过程式编程风格。该章展示如何实现一个基于文本的纸牌游戏,我们编写程序模拟一家管理一个或多个账户的银行。在这个过程中,该章讨论了过程式方法的常见问题。
● 第2章介绍类和对象,并展示如何在Python中使用类来代表现实世界的物体,如电灯的开关或电视机的遥控器。你将看到如何使用面向对象的方法解决第1章介绍的问题。
● 第3章介绍两种思维模型,帮助你思考在Python中创建对象时,底层发生了什么。我们将使用Python Tutor一步步执行代码,查看对象是如何创建的。
● 第4章通过介绍对象管理器对象的概念,演示处理相同类型的多个对象的标准方式。我们将使用类扩展银行账户模拟程序,并将展示如何使用异常处理错误。
第二部分重点讨论如何使用pygame构建GUI。
● 第5章介绍pygame包,以及事件驱动的编程模型。我们将构建一些简单的程序,使你了解如何在窗口中添加图片,以及如何处理键盘和鼠标输入,然后将开发一款更加复杂的弹球游戏。
● 第6章更详细地介绍如何在pygame程序中使用OOP。我们将使用OOP风格重写弹球游戏,并开发一些简单的GUI元素。
● 第7章介绍pygwidgets模块,它包含许多标准GUI元素(按钮、复选框等)的完整实现,每个元素都作为一个类实现。
第三部分深入介绍OOP的主要信条。
● 第8章讨论封装,即向外部代码隐藏实现细节,并将所有相关方法放在类中。
● 第9章介绍多态性,即多个类可以有名称相同的方法,并展示多态性如何使你能够调用多个对象中的方法,并不需要知道每个对象的类型。我们将创建一个Shapes程序来演示这个概念。
● 第10章介绍继承,它允许你创建一组子类,让这些子类都使用基类中的公共代码,而不是对相似的类重复造轮子。我们将介绍通过继承简化编程的一些现实示例,例如,实现一个只接受数字的输入字段,然后将使用继承来重写Shapes示例。
● 第11章讨论另外一些重要的OOP概念,它们主要与内存管理有关。我们将介绍对象的生存期,并且作为一个示例,将创建一款戳气球小游戏。
第四部分探讨与在游戏开发中使用OOP有关的一些主题。
● 第12章不仅演示如何把第1章开发的纸牌游戏改为一个基于pygame的GUI程序,还展示如何创建可重用的Deck类和Card类,使你能够在自己创建的其他纸牌游戏中使用它们。
● 第13章介绍定时功能。我们将开发不同的定时器类,使程序在保持运行的同时检查指定的时限。
● 第14章解释可以用来显示图片序列的动画类。我们将介绍两种动画技术—— 从单独图片文件的一个集合创建动画,以及提取和使用单个精灵表文件中包含的多张图片。
● 第15章解释状态机和场景管理器的概念。状态机代表和控制程序的流程,而场景管理器可以用于创建包含多个场景的程序。为了演示它们的用法,我们创建了Rock,Paper,Scissors游戏的两个版本。
● 第16章讨论不同类型的模态对话框,这是另外一个重要的用户交互功能。我们创建一款功能完整的、基于OOP的视频游戏,叫作Dodger,它演示了本书介绍的许多技术。
● 第17章介绍设计模式的概念,重点讨论模型-视图-控制器模式,然后展示一个掷色子程序,该程序使用模型-视图-控制器模式,允许用户以不同的可视化方式查看数据。该章最后对全书内容做一个简单的总结。
在本书中,并不需要大量使用命令行。在本书中使用的命令行只用于安装软件。本书清晰地列出了安装指令,所以你不需要学习任何额外的命令行语法。
相比使用命令行进行开发,我坚信应该使用一个交互式开发环境(Interactive Development Environment,IDE)。IDE自动处理底层操作系统的许多细节,并使你能够只使用一个程序来编写、编辑和运行代码。IDE通常是跨平台的,允许程序员轻松地从Mac计算机切换到Windows计算机(反之亦然)。
当你安装Python时,会安装一个IDLE开发环境,本书中的所有小示例程序均可以在这个环境中运行。IDLE使用起来很简单,对于能够在一个文件中写出来的程序,它使用起来很方便。当开发使用多个Python文件的复杂程序时,建议使用一个更加复杂的环境。我使用JetBrains PyCharm开发环境,它能够更加轻松地处理包含多个文件的项目。该环境的社区版可以免费从JetBrains网站获取,我强烈建议使用它。PyCharm还集成了一个调试器,这在编写较大的程序时很有用。关于如何使用这个调试器的更多信息,请观看我的YouTube视频Debugging Python 3 with PyCharm。
本书介绍并提供了两个Python包—— pygwidgets和pyghelpers。通过使用这两个包,你应该能够构建GUI程序,但更重要的是,你应该能够理解如何使用类来编写每个小部件,把它们作为对象使用。
通过包含各种小部件,本书中的示例游戏一开始相对简单,后来逐渐变得更加复杂。第16章展示如何开发和实现一个功能完整的视频游戏,它还包含一个保存在文件中的高分表。
到本书结束时,你应该能够编写自己的游戏,可能是纸牌游戏,也可能是Pong、Hangman、Breakout、Space Invaders等风格的视频游戏。当使用面向对象编程方法时,你能够使程序轻松地显示和控制相同类型的多个对象,当构建用户界面以及开发游戏时,常常需要这么做。
面向对象编程是一种通用的风格,可以用在编程的各个方面。希望你喜欢这种学习OOP的方法。
接下来就进入正题!
我想感谢使本书得以出版的下列人士。
Al Sweigart让我开始使用pygame(特别是他的“Pygbutton”代码),并允许我使用他的Dodger游戏的概念。
Monte Davidoff通过使用GitHub、Sphinx和ReadTheDocsd,帮助我正确创建源代码和代码的文档。他使用各种各样的工具整理文件。
Monte Davidoff(没错,是同一个人)是一位出色的技术审校者,他为整本书提出了优秀的技术和写作建议。在他的建议下,本书的许多代码示例更加符合Python的习惯和OOP的实践。
Tep Sathya Khieu为本书绘制了所有原始图。我不是艺术家,甚至在电视上也不会扮演艺术家。在我用铅笔草绘出需要的草图后,Tep把它们变成了清晰的、一致的作品。
Harrison Yung、Kevin Ly和Emily Allis为一些游戏提供了美工作品。
早期审校者Illya Katsyuk、Jamie Kalb、Gergana Angelova和Joe Langmuir发现并纠正了许多拼写错误,并为内容的修改和阐释提供了好的建议。
感谢为本书做出贡献的所有编辑—— Liz Chadwick(策划编辑)、Rachel Head(文字编辑)和Kate Kaminski(责任编辑)。她们常常提出疑问,并帮助我梳理文字,或者调整我对一些概念的解释,为本书做出了巨大贡献。她们还帮助我在合适的地方加上或者去掉逗号,并帮助我断句,就像这里这句这样,以便确保我能够清晰地表达我的意图。恐怕我永远理解不了什么时候使用which,什么时候使用that,或者什么时候使用逗号,什么时候使用破折号,不过幸运的是,她们知道!还要感谢Maureen Forys(排版人员),她为本书的版式设计做出了重要的贡献。
感谢这么多年来参加我的课程的所有学生。他们的反馈、建议以及在课堂上的表现对塑造本书的内容和我的教学风格很有用。
最后,撰写、编辑本书与调试本书配套的代码是一个漫长的过程,感谢我的家人在这个过程中一直支持我。没有他们的支持,我是无法完成本书的。
本书由异步社区出品,社区(https://www.epubit.com/)为您提供后续服务。
您还可以扫码右侧二维码, 关注【异步社区】微信公众号,回复“e60231”直接获取,同时可以获得异步社区15天VIP会员卡,近千本电子书免费畅读。
作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“发表勘误”,输入勘误信息,单击“提交勘误”按钮即可,如下图所示。本书的作者和编辑会对您提交的勘误信息进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区投稿(直接访问www.epubit.com/contribute即可)。
如果您所在的学校、培训机构或企业想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发送给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、人工智能、测试、前端、网络技术等。
异步社区
微信服务号
本书第一部分介绍面向对象编程。我们将讨论过程式代码的固有问题,然后介绍面向对象编程如何解决这些问题。用包含状态和行为的对象来思考问题,为编写代码带来了一种新的视角。
第1章将回顾过程式Python。本章首先展示一个基于文本的纸牌游戏,命名为Higher or Lower,然后使用Python完成一个越来越复杂的银行账户,帮助你理解过程式编码中常见的问题。
第2章将介绍在Python中如何使用类来表示现实世界的物体。我们将编写一个程序来模拟电灯开关,然后修改它来包含使灯光变暗的功能,最后开发一个更加复杂的电视机遥控器模拟程序。
第3章将介绍面向对象编程中的两种思维模型。
第4章将演示处理相同类型的多个对象的标准方式(例如,考虑一个需要跟踪许多相似的游戏元素的简单游戏,如跳棋)。我们将扩展第1章的银行账户程序,探讨如何处理错误。
编程入门课程和图书常常采用过程式编程风格来讲解软件开发,这种编程风格将一个程序拆分为许多函数(也称为过程或子例程)。把数据传递给函数,然后函数执行一个或多个计算,并且通常会传回结果。本书则介绍一种不同的编程范式——面向对象编程(Object-Oriented Programming,OOP)。面向对象编程允许程序员以一种不同的思维方式来思考如何构建软件。面向对象编程使程序员能够将代码和数据合并为内聚的单元,从而避免过程式编程固有的一些复杂的问题。
在本章中,我们通过创建两个使用多种Python结构的小程序,回顾Python的一些基本概念。第1个程序对应一款名为Higher or Lower的小纸牌游戏,第2个程序模拟银行系统,对一个、两个和多个账户执行操作。这两个程序都是使用过程式编程开发的,即使用了标准的数据和函数技术。后面将使用OOP技术重写这两个程序。本章的目的是演示过程式编程的一些固有的关键问题。了解了这些问题后,后面的各章将解释OOP如何解决这些问题。
第1个示例是一个名为Higher or Lower的简单纸牌游戏。在这款游戏中,从一副牌中随机取出8张牌。第一张牌亮出来。游戏要求玩家预测在选出的纸牌中,下一张牌比当前亮出的牌点数更大还是更小。例如,假设亮出的牌的点数是3。玩家选择“更大”,就显示下一张牌。如果这张牌的点数更大,玩家就是正确的。在这个示例中,如果玩家选择了“更小”,就是错误的。
如果玩家的猜测正确,就得到20分;如果不正确,就失去15分。如果要翻开的下一张牌的点数与当前亮出的牌的点数相同,玩家是不正确的。
程序需要表示包含52张牌的牌堆,这里将使用一个列表来表示这个牌堆。列表的这52个元素中的每个元素都是一个字典(键值对的一个集合)。为了表示任意牌,每个字典将包含3个键值对'rank’ ‘suit’和'value’。rank是牌面大小(Ace,2,3,…,10,Jack,Queen,King),但value是用于比较牌的整数(1,2,3,…,10,11,12,13)。例如,方块11用下面的字典来表示。
{‘rank’: ‘Jack’, ‘suit’: ‘Clubs’, ‘value’: 11}
在玩家玩一局游戏之前,创建代表牌堆的列表并洗牌,使纸牌随机排列。程序中没有使用图片显示纸牌,所以每一次用户选择“更大”或“更小”时,程序将从牌堆中获取一个纸牌字典,输出它的牌面大小和花色。然后,程序比较新牌的值和上一张牌的值,根据用户的回答正确与否给出反馈。
代码清单1-1显示了Higher or Lower游戏的代码。
注意:
本书中的代码可从No Starch网站下载(搜索“object-oriented-python”)或GitHub网站下载(搜索“IrvKalb/Object-Oriented-Python-Code/”)。你可以下载并运行代码,也可以自己输入代码。
代码清单1-1:使用过程式Python的Higher or Lower游戏(文件: HigherOrLower- Procedural.py)
# HigherOrLower
import random
# Card constants
SUIT_TUPLE = (‘Spades’, ‘Hearts’, ‘Clubs’, ‘Diamonds’)
RANK_TUPLE = (‘Ace’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’, ‘10’, ‘Jack’,
‘Queen’, ‘King’)
NCARDS = 8
# Pass in a deck and this function returns a random card from the deck
def getCard(deckListIn):
thisCard = deckListIn.pop() # pop one off the top of the deck and return
return thisCard
# Pass in a deck and this function returns a shuffled copy of the deck
def shuffle(deckListIn):
deckListOut = deckListIn.copy() # make a copy of the starting deck
random.shuffle(deckListOut)
return deckListOut
# Main code
print(‘Welcome to Higher or Lower.’)
print(‘You have to choose whether the next card to be shown will be higher or
lower than the current card.’)
print(‘Getting it right adds 20 points; get it wrong and you lose 15 points.’)
print(‘You have 50 points to start.’)
print()
startingDeckList = []
❶ for suit in SUIT_TUPLE:
for thisValue, rank in enumerate(RANK_TUPLE):
cardDict = {‘rank’:rank, ‘suit’:suit, ‘value’:thisValue + 1}
startingDeckList.append(cardDict)
score = 50
while True: # play multiple games
print()
gameDeckList = shuffle(startingDeckList)
❷ currentCardDict = getCard(gameDeckList)
currentCardRank = currentCardDict[‘rank’]
currentCardValue = currentCardDict[‘value’]
currentCardSuit = currentCardDict[‘suit’]
print(‘Starting card is:’, currentCardRank + ‘ of ‘ + currentCardSuit)
print()
❸ for cardNumber in range(0, NCARDS): # play one game of this many cards
answer = input(‘Will the next card be higher or lower than the ‘ +
currentCardRank + ‘ of ‘ +
currentCardSuit + ‘? (enter h or l): ‘)
answer = answer.casefold() # force lowercase
❹ nextCardDict = getCard(gameDeckList)
nextCardRank = nextCardDict[‘rank’]
nextCardSuit = nextCardDict[‘suit’]
nextCardValue = nextCardDict[‘value’]
print(‘Next card is:’, nextCardRank + ‘ of ‘ + nextCardSuit)
❺ if answer == ‘h’:
if nextCardValue > currentCardValue:
print(‘You got it right, it was higher’)
score = score + 20
else:
print(‘Sorry, it was not higher’)
score = score - 15
elif answer == ‘l’:
if nextCardValue < currentCardValue:
score = score + 20
print(‘You got it right, it was lower’)
else:
score = score - 15
print(‘Sorry, it was not lower’)
print(‘Your score is:’, score)
print()
currentCardRank = nextCardRank
currentCardValue = nextCardValue # don’t need current suit
❻ goAgain = input(‘To play again, press ENTER, or “q” to quit: ‘)
if goAgain == ‘q’:
break
print(‘OK bye’)
程序首先将牌堆创建为一个列表(❶)。每张牌是由牌面大小、花色和值构成的一个字典。对于每局游戏,从牌堆中取出第一张牌,将其元素保存到变量中(❷)。对于接下来的7张牌,要求用户预测下一张牌比刚刚展示的牌的点数更大还是更小(❸)。从牌堆中取出下一张牌,将其元素保存到另一组变量中(❹)。游戏比较用户的回答和取出的牌,并根据比较结果给用户提供反馈,分配分数(❺)。当用户对选择的全部7张牌做出预测后,我们询问他们是否想再玩一次(❻)。
这个程序演示了编程,尤其是Python编程的许多元素——变量、赋值语句、函数和函数调用、if/else语句、输出语句、while循环、列表、字符串和字典。本书假定你已经熟悉本例中使用的所有元素。如果这个程序中有你不熟悉或不清楚的地方,最好先了解相关的知识,然后再继续学习本书。
这是一个基于纸牌的游戏,所以代码显然创建并操纵一副模拟的纸牌。如果我们想编写另外一个基于纸牌的游戏,那么重用关于牌堆和纸牌的代码会非常有帮助。
在过程式程序中,通常很难识别与程序的某个部分(在本例中对应牌堆和纸牌)相关的所有代码。在代码清单1-1中,牌堆的代码包含两个元组常量、两个函数和一些主代码,这些主代码构建了两个全局列表,一个全局列表代表包含52张牌的起始牌堆,另一个全局列表代表在游戏过程中使用的牌堆。另外要注意,即使在这样一个小程序中,数据和操纵数据的代码也不一定紧密地放在一起。
因此,在另外一个程序中重用牌堆或者纸牌的代码并没有那么容易或者直观。第12章将回顾这个程序,展示OOP解决方案如何使得重用这个程序的代码变得更加容易。
银行账户模拟是过程式编码的第2个示例。这个程序模拟一个银行的运作,本节将给出这个程序的几个变体。在程序的每个新版本中,将添加更多功能。注意,这些程序并没有达到发布的质量标准,无效的用户输入或者错误的用法会导致错误。这里的目的是让你关注于代码如何与一个或多个银行账户的数据进行交互。
首先,思考客户想要对银行账户做什么操作,以及需要什么数据来表示账户。
客户想要对银行账户做的操作包括:
● 创建账户;
● 存款;
● 取款;
● 查询余额。
下面则列出了代表一个银行账户至少需要的数据列表:
● 客户姓名;
● 密码;
● 余额。
注意,所有的操作都是动宾结构,所有的数据项都是名词。真实的银行账户不仅能够支持多得多的操作,还会包含其他数据(如账户持有人的地址、电话号码和社保号),但是为了让我们的讨论清晰易懂,一开始只支持这4个操作和3条数据。另外,为了保持内容简单,不偏离主题,我们只使用整数美元。还需要指出的是,在真实的银行应用程序中,不会像这里的示例这样使用明文(未加密的文本)来保存密码。
在代码清单1-2显示的最初版本中,只有一个账户。
代码清单1-2:只包含一个账户的银行模拟程序(文件: Bank1_OneAccount.py)
# Non-OOP
# Bank Version 1
# Single account
❶ accountName = ‘Joe’
accountBalance = 100
accountPassword = ‘soup’
while True:
❷ print()
print(‘Press b to get the balance’)
print(‘Press d to make a deposit’)
print(‘Press w to make a withdrawal’)
print(‘Press s to show the account’)
print(‘Press q to quit’)
print()
action = input(‘What do you want to do? ‘)
action = action.lower() # force lowercase
action = action[0] # just use first letter
print()
if action == ‘b’:
print(‘Get Balance:’)
userPassword = input(‘Please enter the password: ‘)
if userPassword != accountPassword:
print(‘Incorrect password’)
else:
print(‘Your balance is:’, accountBalance)
elif action == ‘d’:
print(‘Deposit:’)
userDepositAmount = input(‘Please enter amount to deposit: ‘)
userDepositAmount = int(userDepositAmount)
userPassword = input(‘Please enter the password: ‘)
if userDepositAmount < 0:
print(‘You cannot deposit a negative amount!’)
elif userPassword != accountPassword:
print(‘Incorrect password’)
else: # OK
accountBalance = accountBalance + userDepositAmount
print(‘Your new balance is:’, accountBalance)
elif action == ‘s’: # show
print(‘Show:’)
print(‘ Name’, accountName)
print(‘ Balance:’, accountBalance)
print(‘ Password:’, accountPassword)
print()
elif action == ‘q’:
break
elif action == ‘w’:
print(‘Withdraw:’)
userWithdrawAmount = input(‘Please enter the amount to withdraw: ‘)
userWithdrawAmount = int(userWithdrawAmount)
userPassword = input(‘Please enter the password: ‘)
if userWithdrawAmount < 0:
print(‘You cannot withdraw a negative amount’)
elif userPassword != accountPassword:
print(‘Incorrect password for this account’)
elif userWithdrawAmount > accountBalance:
print(‘You cannot withdraw more than you have in your account’)
else: #OK
accountBalance = accountBalance - userWithdrawAmount
print(‘Your new balance is:’, accountBalance)
print(‘Done’)
程序首先初始化了3个变量,用于代表一个账户的数据(❶)。然后,显示了一个菜单,用于选择操作(❷)。程序的主代码直接操作全局账户变量。
在本例中,所有操作都在主代码级别;代码中没有使用函数。程序可以正确运行,但看起来有点长。为了使很长的程序变得更加清晰,通常采用的方法是将相关代码放到函数中,然后调用那些函数。我们在银行程序的下一个实现中将探讨这种方法。
在代码清单1-3对应的版本中,将代码拆分为单独的函数,每个函数对应一种操作。这里仍然只模拟了一个账户。
代码清单1-3:使用函数的只包含一个账户的银行模拟程序(文件: Bank2_OneAccount WithFunctions.py)
# Non-OOP
# Bank 2
# Single account
accountName = ‘’
accountBalance = 0
accountPassword = ‘’
❶ def newAccount(name, balance, password):
global accountName, accountBalance, accountPassword
accountName = name
accountBalance = balance
accountPassword = password
def show():
global accountName, accountBalance, accountPassword
print(‘ Name’, accountName)
print(‘ Balance:’, accountBalance)
print(‘ Password:’, accountPassword)
print()
❷ def getBalance(password):
global accountName, accountBalance, accountPassword
if password != accountPassword:
print(‘Incorrect password’)
return None
return accountBalance
❸ def deposit(amountToDeposit, password):
global accountName, accountBalance, accountPassword
if amountToDeposit < 0:
print(‘You cannot deposit a negative amount!’)
return None
if password != accountPassword:
print(‘Incorrect password’)
return None
accountBalance = accountBalance + amountToDeposit
return accountBalance
❹ def withdraw(amountToWithdraw, password):
❺ global accountName, accountBalance, accountPassword
if amountToWithdraw < 0:
print(‘You cannot withdraw a negative amount’)
return None
if password != accountPassword:
print(‘Incorrect password for this account’)
return None
if amountToWithdraw > accountBalance:
print(‘You cannot withdraw more than you have in your account’)
return None
❻ accountBalance = accountBalance - amountToWithdraw
return accountBalance
newAccount(“Joe”, 100, ‘soup’) # create an account
while True:
print()
print(‘Press b to get the balance’)
print(‘Press d to make a deposit’)
print(‘Press w to make a withdrawal’)
print(‘Press s to show the account’)
print(‘Press q to quit’)
print()
action = input(‘What do you want to do? ‘)
action = action.lower() # force lowercase
action = action[0] # just use first letter
print()
if action == ‘b’:
print(‘Get Balance:’)
userPassword = input(‘Please enter the password: ‘)
theBalance = getBalance(userPassword)
if theBalance is not None:
print(‘Your balance is:’, theBalance)
❼ elif action == ‘d’:
print(‘Deposit:’)
userDepositAmount = input(‘Please enter amount to deposit: ‘)
userDepositAmount = int(userDepositAmount)
userPassword = input(‘Please enter the password: ‘)
❽ newBalance = deposit(userDepositAmount, userPassword)
if newBalance is not None:
print(‘Your new balance is:’, newBalance)
--- snip calls to appropriate functions ---
print(‘Done’)
在这个版本中,为银行账户的每个操作(创建账户(❶)、查询余额(❷)、存款(❸)和取款(❹))分别创建了一个函数,并重新组织代码,使主代码调用不同的函数。
这样一来,主程序变得易读了许多。例如,如果用户输入d,表示他们想要存款(❼),主代码现在会调用一个名为deposit()的函数(❸),并传入存款数目,以及用户输入的账户密码。
但是,如果查看这里的任何函数(如withdraw()函数)的定义,就会发现代码使用了global语句(❺)来访问(获取或设置)代表账户的变量。在Python中,只有当想要在函数中修改一个全局变量的值时,才需要使用global语句。但是,这里使用它们,只是为了清楚地表明,这些函数引用了全局变量(尽管只获取它们的值)。
作为一般编程原则,函数绝不应该修改全局变量。函数只应该使用传递给它的数据,基于这些数据进行计算,然后返回结果(当然,不必返回结果)。这个程序中的withdraw()函数确实可以正常运行,但违反了这个原则,它不仅修改了全局变量accountBalance的值(❻),还访问了全局变量accountPassword的值。
代码清单1-4中的银行模拟程序采用了与代码清单1-3相同的方法,但添加了支持两个账户的功能。
代码清单1-4:使用函数且支持两个账户的银行模拟程序(文件: Bank3_TwoAccounts.py)
# Non-OOP
# Bank 3
# Two accounts
account0Name = ‘’
account0Balance = 0
account0Password = ‘’
account1Name = ‘’
account1Balance = 0
account1Password = ‘’
nAccounts = 0
def newAccount(accountNumber, name, balance, password):
❶ global account0Name, account0Balance, account0Password
global account1Name, account1Balance, account1Password
if accountNumber == 0:
account0Name = name
account0Balance = balance
account0Password = password
if accountNumber == 1:
account1Name = name
account1Balance = balance
account1Password = password
def show():
❷ global account0Name, account0Balance, account0Password
global account1Name, account1Balance, account1Password
if account0Name != ‘’:
print(‘Account 0’)
print(‘ Name’, account0Name)
print(‘ Balance:’, account0Balance)
print(‘ Password:’, account0Password)
print()
if account1Name != ‘’:
print(‘Account 1’)
print(‘ Name’, account1Name)
print(‘ Balance:’, account1Balance)
print(‘ Password:’, account1Password)
print()
def getBalance(accountNumber, password):
❸ global account0Name, account0Balance, account0Password
global account1Name, account1Balance, account1Password
if accountNumber == 0:
if password != account0Password:
print(‘Incorrect password’)
return None
return account0Balance
if accountNumber == 1:
if password != account1Password:
print(‘Incorrect password’)
return None
return account1Balance
--- snipped additional deposit() and withdraw() functions ---
--- snipped main code that calls functions above ---
print(‘Done’)
即使只有两个账户,也可以看出来,这种方法很快会变得难以处理。首先,我们在❶❷和❸那里为每个账户设置了3个全局变量。另外,每个函数现在有一个if语句,用于选择访问或修改哪组全局变量。每当我们想要添加另外一个账户时,就需要添加另外一组全局变量,并在每个函数中添加更多if语句。这并不是一种可行的方法。要用一种不同的方式来处理任意数量的账户。
为了更方便支持多个账户,在代码清单1-5中,我们将使用列表表示数据。程序的这个版本使用了3个列表——accountNamesList、accountPasswordsList和accountBalancesList。
代码清单1-5:使用并行列表的银行模拟程序(文件: Bank4_N_Accounts.py)
# Non-OOP Bank
# Version 4
# Any number of accounts - with lists
❶ accountNamesList = []
accountBalancesList = []
accountPasswordsList = []
def newAccount(name, balance, password):
global accountNamesList, accountBalancesList, accountPasswordsList
❷ accountNamesList.append(name)
accountBalancesList.append(balance)
accountPasswordsList.append(password)
def show(accountNumber):
global accountNamesList, accountBalancesList, accountPasswordsList
print(‘Account’, accountNumber)
print(‘ Name’, accountNamesList[accountNumber])
print(‘ Balance:’, accountBalancesList[accountNumber])
print(‘ Password:’, accountPasswordsList[accountNumber])
print()
def getBalance(accountNumber, password):
global accountNamesList, accountBalancesList, accountPasswordsList
if password != accountPasswordsList[accountNumber]:
print(‘Incorrect password’)
return None
return accountBalancesList[accountNumber]
--- snipped additional functions ---
# Create two sample accounts
❸ print(“Joe’s account is account number:”, len(accountNamesList))
newAccount(“Joe”, 100, ‘soup’)
❹ print(“Mary’s account is account number:”, len(accountNamesList))
newAccount(“Mary”, 12345, ‘nuts’)
while True:
print()
print(‘Press b to get the balance’)
print(‘Press d to make a deposit’)
print(‘Press n to create a new account’)
print(‘Press w to make a withdrawal’)
print(‘Press s to show all accounts’)
print(‘Press q to quit’)
print()
action = input(‘What do you want to do? ‘)
action = action.lower() # force lowercase
action = action[0] # just use first letter
print()
if action == ‘b’:
print(‘Get Balance:’)
❺ userAccountNumber = input(‘Please enter your account number: ‘)
userAccountNumber = int(userAccountNumber)
userPassword = input(‘Please enter the password: ‘)
theBalance = getBalance(userAccountNumber, userPassword)
if theBalance is not None:
print(‘Your balance is:’, theBalance)
--- snipped additional user interface ---
print(‘Done’)
在程序的开头,将3个列表设置为空列表(❶)。为了创建一个新账户,将合适的值追加到每个列表(❷)。
因为现在要处理多个账户,所以使用了银行账户号码这个基本概念。每当用户创建账户的时候,代码就对一个列表使用len()函数,并返回该数字,作为该用户的账号(❸❹)。为第1个用户创建账户时,accountNamesList的长度是0。因此,第1个账户的账号是0,第2个账户的账号是1,以此类推。和真正的银行一样,在创建账户后,要执行任何操作(如存款或取款),用户必须提供自己的账号(❺)。
但是,这里的代码仍然在使用全局数据,现在有3个全局数据列表。
想象一下在电子表格中查看这些数据,如表1-1所示。
表1-1 数据表
账户号码 |
姓名 |
密码 |
余额 |
---|---|---|---|
0 |
Joe |
soup |
100 |
1 |
Mary |
nuts |
3550 |
2 |
Bill |
frisbee |
1000 |
3 |
Sue |
xxyyzz |
750 |
4 |
Henry |
PW |
10000 |
这些数据作为3个全局Python列表进行维护,每个列表代表这个表格中的一列。例如,从突出显示的列中可以看到,全部密码作为一个列表放到一起。用户的姓名放到另外一个列表中,余额也放到一个列表中。当采用这种方法时,要获取关于一个账户的信息,需要使用一个公共的索引值访问这些列表。
虽然这种方法可行,但非常麻烦。数据没有以一种符合逻辑的方式进行分组。例如,把所有用户的密码放到一起并不合理。另外,每次为一个账户添加一个新特性(如地址或电话号码)时,就需要创建并访问另外一个全局列表。
与这种方法相反,我们真正想要的分组是代表这个电子表格中的一行的分组,如表 1-2所示。
表1-2 数据表
账户号码 |
姓名 |
密码 |
余额 |
---|---|---|---|
0 |
Joe |
soup |
100 |
1 |
Mary |
nuts |
3550 |
2 |
Bill |
frisbee |
1000 |
3 |
Sue |
xxyyzz |
750 |
4 |
Henry |
PW |
10000 |
这样一来,每一行代表与一个银行账户关联的数据。虽然数据一样,但这种分组是代表账户的一种更加自然的方式。
为了实现最后这种方法,我们将使用一种稍微复杂一些的数据结构。在这个版本中,创建一个账户列表,其中每个账户(列表中的每个元素)是如下所示的一个字典。
{‘name’:<someName>, ‘password’:<somePassword>, ‘balance’:<someBalance>}
注意:
在本书中,每当我在尖括号(<>)中给出一个值的时候,意味着你应该使用自己选择的值来替换该项(以及尖括号)。例如,在上面的代码行中,<someName>、<somePassword>和<someBalance>是占位符,应该用实际值替换它们。
最后,这个实现的代码如代码清单1-6所示。
代码清单1-6:使用字典列表的银行模拟程序(文件: Bank5_Dictionary.py)
# Non-OOP Bank
# Version 5
# Any number of accounts - with a list of dictionaries
accountsList = [] ❶
def newAccount(aName, aBalance, aPassword):
global accountsList
newAccountDict = {‘name’:aName, ‘balance’:aBalance, ‘password’:aPassword}
accountsList.append(newAccountDict) ❷
def show(accountNumber):
global accountsList
print(‘Account’, accountNumber)
thisAccountDict = accountsList[accountNumber]
print(‘ Name’, thisAccountDict[‘name’])
print(‘ Balance:’, thisAccountDict[‘balance’])
print(‘ Password:’, thisAccountDict[‘password’])
print()
def getBalance(accountNumber, password):
global accountsList
thisAccountDict = accountsList[accountNumber] ❸
if password != thisAccountDict[‘password’]:
print(‘Incorrect password’)
return None
return thisAccountDict[‘balance’]
--- snipped additional deposit() and withdraw() functions ---
# Create two sample accounts
print(“Joe’s account is account number:”, len(accountsList))
newAccount(“Joe”, 100, ‘soup’)
print(“Mary’s account is account number:”, len(accountsList))
newAccount(“Mary”, 12345, ‘nuts’)
while True:
print()
print(‘Press b to get the balance’)
print(‘Press d to make a deposit’)
print(‘Press n to create a new account’)
print(‘Press w to make a withdrawal’)
print(‘Press s to show all accounts’)
print(‘Press q to quit’)
print()
action = input(‘What do you want to do? ‘)
action = action.lower() # force lowercase
action = action[0] # just use first letter
print()
if action == ‘b’:
print(‘Get Balance:’)
userAccountNumber = input(‘Please enter your account number: ‘)
userAccountNumber = int(userAccountNumber)
userPassword = input(‘Please enter the password: ‘)
theBalance = getBalance(userAccountNumber, userPassword)
if theBalance is not None:
print(‘Your balance is:’, theBalance)
elif action == ‘d’:
print(‘Deposit:’)
userAccountNumber= input(‘Please enter the account number: ‘)
userAccountNumber = int(userAccountNumber)
userDepositAmount = input(‘Please enter amount to deposit: ‘)
userDepositAmount = int(userDepositAmount)
userPassword = input(‘Please enter the password: ‘)
newBalance = deposit(userAccountNumber, userDepositAmount, userPassword)
if newBalance is not None:
print(‘Your new balance is:’, newBalance)
elif action == ‘n’:
print(‘New Account:’)
userName = input(‘What is your name? ‘)
userStartingAmount = input(‘What is the amount of your initial deposit? ‘)
userStartingAmount = int(userStartingAmount)
userPassword = input(‘What password would you like to use for this account? ‘)
userAccountNumber = len(accountsList)
newAccount(userName, userStartingAmount, userPassword)
print(‘Your new account number is:’, userAccountNumber)
--- snipped additional user interface ---
print(‘Done’)
使用这种方法,可以在一个字典中找到与一个账户相关的所有数据(❶)。要创建一个新账户,我们创建一个新的字典,将其追加到账户列表中(❷)。为每个账户分配一个数字(一个简单的整数),对账户执行任何操作时,都必须提供这个账号。例如,当用户存款时,需要提供自己的账号,getBalance()函数会使用该账号作为账户列表中的索引(❸)。
这种方法让代码整洁了许多,使数据的组织更加符合逻辑。但是,程序中的每个函数仍然必须访问全局账户列表。让函数能够访问所有账户数据会带来潜在的安全风险。理想情况下,每个函数只应该能够影响一个账户的数据。
本章展示的示例有一个共同的问题:函数操作的所有数据存储在一个或多个全局变量中。出于下面的原因,在过程式编程中大量使用全局数据不是好的做法。
(1)如果函数使用或者修改全局数据,则很难在其他程序中重用该函数。访问全局数据的函数在操作与函数代码本身处于不同(更高)级别的数据。该函数将需要使用global语句来访问全局数据。你不能直接在另外一个程序中重用一个依赖全局数据的函数,而只能在具有类似全局数据的程序中重用它。
(2)许多过程式程序有大量全局变量。按照定义,全局变量可被程序中任意地方的任何代码使用或修改。过程式程序中常常散布着对全局变量赋值的语句,可能包含在主代码中,也可能包含在函数内。因为变量值可能在任何地方改变,所以极难调试和维护采用这种方式编写的程序。
(3)使用全局数据的函数常常访问过多数据。当函数使用一个全局列表、字典或其他任何全局数据结构时,它能够访问该数据结构中的所有数据。但是,函数通常只应该操作该数据结构中的一部分数据(或少量数据)。能够读写大型数据结构中的任何数据,可能导致出现错误,如不小心使用或者重写该函数不应该访问的数据。
代码清单1-7展示了一种面向对象方法,该方法将一个账户的所有代码和关联数据组合到一起。这里有许多新概念,从下一章开始将详细介绍所有细节。尽管你现在可能没有完全理解这个示例,但是注意,这里把代码和数据合并到一个脚本(称为类)中。下面是你在本书中第一次接触到的面向对象代码。
代码清单1-7:第1个Python类示例(文件: Account.py)
# Account class
class Account():
def __init__(self, name, balance, password):
self.name = name
self.balance = int(balance)
self.password = password
def deposit(self, amountToDeposit, password):
if password != self.password:
print(‘Sorry, incorrect password’)
return None
if amountToDeposit < 0:
print(‘You cannot deposit a negative amount’)
return None
self.balance = self.balance + amountToDeposit
return self.balance
def withdraw(self, amountToWithdraw, password):
if password != self.password:
print(‘Incorrect password for this account’)
return None
if amountToWithdraw < 0:
print(‘You cannot withdraw a negative amount’)
return None
if amountToWithdraw > self.balance:
print(‘You cannot withdraw more than you have in your account’)
return None
self.balance = self.balance - amountToWithdraw
return self.balance
def getBalance(self, password):
if password != self.password:
print(‘Sorry, incorrect password’)
return None
return self.balance
# Added for debugging
def show(self):
print(‘ Name:’, self.name)
print(‘ Balance:’, self.balance)
print(‘ Password:’, self.password)
print()
现在,思考这里的函数与前面的过程式编程示例有什么相似之处。函数的名称与之前相同(show()、getBalance()、deposit()和withdraw()),但这里还有一些使用了self(或self.)的代码。后面的章节将介绍它的含义。
本章首先采用过程式编程实现了一个叫作Higher or Lower的纸牌游戏的代码。第12章将展示如何创建这个游戏的面向对象版本,并添加图形用户界面。
之后,本章介绍了模拟银行系统的问题,先让它支持一个账户,然后让它支持多个账户。本章讨论了使用过程式编程来实现模拟程序的几种不同的方式,并说明了这种方法造成的一些问题。最后,本章展示了使用类的银行账户代码。
读者服务:
微信扫码关注【异步社区】微信公众号,回复“e60231”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。