易学Python

978-7-115-33522-7
作者: 【澳】Anthony Briggs
译者: 王威袁国忠
编辑: 傅道坤
分类: Python

图书目录:

详情

本书采用简洁有趣的方式对Python 3编程语言进行了讲解,并使用卡通人物对知识要点进行注释,读者可以从中学习到从Python编程语言的基础知识和实用技巧,从而开发出包括多用户冒险游戏和MP3播放器在内的简单Python应用程序。

图书摘要

版权信息

书名:易学Python

ISBN:978-7-115-33522-7

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [澳]Anthony Briggs

  译    王 威 袁国忠

  责任编辑 傅道坤

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Original English language edition, entitled Hello! Python by Anthony Briggs, published by Manning Publications Co., 209 Bruce Park Avenue, Greenwich, CT 06830.

Copyright ©2012 by Manning Publications Co.

Simplified Chinese-language edition copyright ©2013 by Posts & Telecom Press. All rights reserved.

本书中文简体字版由Manning Publications Co.授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

版权所有,侵权必究。


本书采用简洁、有趣、易学的方式对Python编程语言进行了讲解,其风格与通篇介绍编程特性、罗列语言功能的大多数编程图书不同,而是引导读者带着好奇,带着问题去学习、掌握Python编程语言,继而编写真实而有用的程序。

本书总共分为3部分,共12章,第1部分介绍了为何使用Python来编程。从第2部分起,通过编写Hunt the Wumpus游戏带领读者认识并解决编程中的一些实际问题。例如,如何管理复杂的程序,确保其清晰易懂;如何在程序中使用Python标准库,以节省编程时间,同时让程序更容易理解;如何对编写好的程序进行测试;如何进行面向对象的程序设计。第3部分则使用框架对书中的程序进行完善,让读者对Python的强大功能有一个更深入的认识。本书最后还提供了一些Python资源,供读者深入学习Python时参考。

无论您是零基础的Python初学人员,还是具有其他语言编程经验,但是想从事Python开发的人员,本书都将带领您踏上有趣的Python学习之路。


前言

受邀编写本书时,我无意再编写一本介绍性图书,而想采取不同的做法。我阅读过的编程图书无不罗列功能:列表可用于存储信息,您可调用len(mylist)来获悉它包含多少项信息,调用pop()来删除末尾的元素,调用append()来添加元素,等等。这些就是您需要知道的有关列表的全部知识,接下来介绍下一项功能。在这样的图书中,即便有示例程序,也要么只包含寥寥数行代码,要么出现在最后几章,作为对全书内容的总结。

我自己在最初学习编程时,并不是先从头到尾阅读整本编程图书,等对一切都熟悉后再开始编写程序。相反,我带着问题(要做的事情)去阅读,并在阅读过程中搞懂这些问题。我经常阅读编程图书,但都是旨在搞清楚我遇到的问题。程序编写出来后,它们也许不是特别优雅,运行速度也可能不是特别快,但它们毕竟出自我的手——我知道其工作原理,还解决了我面临的实际问题。

就这样锻炼到今天,我编写的程序在很大程度上都可以说既优雅,速度也快。在我认识的优秀程序员中,大多也这样学习编程。在本书中,我竭尽所能地重现这个过程,但步伐更快,涵盖了我学到的所有编程知识,还有我曾遇到过的陷阱。除第1章和第12章外,每章都将一个实用程序作为核心,旨在演示特定Python功能或库——通常是多个。有些章节很有趣,有些很实用,但不再有乏味的铺垫章节,详尽地讲述烦人的细节——列表或字典的各种功能,甚至是如何使用Python将数字相加。

相反,您将编写一个个程序,并在需要时学习相关的Python功能,而不是预先学习它们。有几章建立在前几章的基础之上,因此您将学习如何扩展既有程序,以添加新功能并避免设计失控——无论您要编写的程序规模如何,都必须这样做。本书还探讨了多种编程风格:从简单脚本到面向对象程序,再到基于事件的游戏。

我旨在向您呈现一部与众不同的作品,让您从第1章开始就编写程序,并在实际应用中学习Python功能的用法。但愿这种写作方式能助您真正明白如何使用Python。


本书旨在帮助读者学习Python和如何使用Python编程。无论您在编程方面完全是门外汉还是有一定经验,本书都将引领您踏上编程之路,最终编写出网络游戏和Web应用程序。

本书的风格不同于大多数编程图书:不详尽地列举各种功能,而向读者展示一幅更真实的画卷。从第2章起,您就将跟随笔者的脚步,编写真实而实用的程序—既说优点也谈缺点。编程语言的每项功能都有其用途,但如果不见识因误用导致的bug、有问题的代码和糟糕的程序,就很难对其用途有真正的认识。

在本书中,将逐步改进和扩展一些程序,让读者明白函数、类和模块等Python功能有助于您控制规模不断增大的代码。需要扩展程序时,这些功能还有助于减轻您的负担。

虽然没有明确指出,但本书实际上大致分三部分。第1章介绍Python基本语法、库的用法和一些常见概念,还有为理解程序的工作原理而需要知道的各种知识;第二部分介绍高级功能以及让您能够直接用来实现更多功能的库;最后一部分使用框架编写了一些完整的程序,这对您学习Python帮助更大。

阅读完本书后,您依然将受惠于它。本书的所有程序都是可扩展的,您可在编写自己的程序时重用它们。大多数经验丰富的程序员都有代码库,其中包含他以前编写的所有代码;本书的代码为您开发自己的项目打下了基础。

第1章简要地介绍了Python、编程以及编程的根本目的:为何编写程序、可使用程序做什么。本章还详细地阐述了如何在Windows、Mac和Linux系统上安装Python,以及在安装过程中可能遇到的一些常见问题。

第2章介绍程序的基本组成部分,而您将编写第一个程序——Hunt the Wumpus。在此过程中,您将亲身体验程序员面临的一些问题。例如,如何管理复杂的程序,确保它们清晰易懂。

第3章介绍著名的Python标准库,还有如何导入标准库代码以及其他程序员为执行常见任务而编写的代码。您将学习如何在程序中使用这些代码,以节省大量时间并让程序更容易理解。

第4章介绍如何测试程序,包括单元测试和系统测试以及一些常见的测试问题及其解决方案。在此过程中,您将编写一个简单且易于扩展的待办事项清单应用程序。

第5章介绍如何使用Python进行面向业务的编程:下载网页、分析其中的信息并使用这些信息来创建电子邮件和CSV文件。本章还介绍了如何让程序更健壮,能够应对错误的信息格式和其他错误。

第6章将编写一个冒险游戏,其中有洞穴,有怪物,还有财宝。在此过程中,您将学习类的工作原理以及如何设计面向对象的程序。

第7章介绍如何使用混合类、_getattribute_和特性等高级功能来扩展类。本章还简要地介绍了Python的其他高级功能,如迭代器、生成器、正则表达式和函数式编程。

第8章介绍Django并帮助您建立一个待办事项网站。您将学习Django模板、数据库处理功能、表单和管理功能。本章还介绍了一些常见的Web开发模式,包括REST式URL设计以及使用正确的HTTP方法。

第9章介绍如何使用Pyglet库编写类似于《行星撞击地球》和《月球着陆器》的街机游戏,您将学习几何学、基于事件的编程和定时器。

第10章对第6章编写的冒险游戏进行扩展,让您和朋友能够使用Telnet通过网络玩这款游戏。为此,我们将使用Python网络库Twisted来处理连接、定义协议以及添加日志功能。

第11章修改第8章编写的待办事项清单应用程序,让每位用户都有自己的待办事项清单。您将学习如何处理登录、使用Django创建用户、使用Django通用视图、确保Web应用程序的安全、将Web应用程序部署到Apache或Nginx等服务器。

最后,第12章提供了一些资源,供您继续学习Python时参考,这包括邮件列表和用户组、可供您阅读和探索的程序以及您可能想研究的其他库。


从2000年初开始,Anthony Briggs就一直在使用Python开发程序,当前正为墨尔本的Ramble Communications开发Web发布系统。他还曾为澳大利亚和加拿大的一家旅游公司开发预订系统,并最终成为首席开发人员,负责整个项目。


Anthony邀请我为本书作序时,我的第一反应是,“又是负担,我可不干”。然而,我总得先瞄一眼吧。这一看不打紧,我很快发现了点缀其中的User Friendly卡通人物(我敢肯定,像我一样从穿孔卡和磁带时代起就一直从事计算机工作的人,都对User Friendly钟爱有加)。我想我得看一看手稿,结果发现它包含短短12章,可让您对Python及其最常见的应用有足够认识,进而踏入编程殿堂,或让您确定做程序员并非您想要的生活。

即便最终的结论是编程并非您想要的生活,为购买本书所做的投资也物超所值。如果您连使用Python进行编程都不喜欢,那您很可能根本就不喜欢编程。在这种情况下,阅读本书可避免您入错行,白白浪费数年光阴。

本书到处都是实用建议,作者从不装大尾巴狼,试图发表一些不可信的看法。我深信,这部脚踏实地的著作将帮助大量非程序员读者熟悉Python语言。

但愿本书能让广大读者对编程和迷人的信息技术有新的认识。当前,美国很多州教育资金都捉衿见肘,难以在中学开展丰富的计算机教育,而本书足以吸引中学生,让他们迷恋编程。等政府洞悉教育的目的后,学生们将被编程课本深深地吸引,很快消化全书的内容。

Steve Holden

The Open Bastion总裁


首先,感谢我美丽的妻子Lyndall给予支持,让我能抽出时间编写本书。本书的编写时间比预想的长得多,但她的热情始终如一,即便无数个周末我为了本书而深居简出。

其次,感谢Manning出版社的团队:感谢编辑Sebastian Stirling的建议和经验;感谢June Eding和Tiffany Taylor做最后的编辑和校对,确保本书按时付梓;感谢Karen Tegtmeyer的协调和组织工作;感谢Michael Stephens帮助制订最初的写作计划。

第三,感谢J.D. Fraser允许Manning出版社在Hello系列丛书中使用User Friendly中的卡通形象,并允许我在本书中给这些卡通形象撰写对白。

第四,感谢本书beta版本的所有测试人员帮助找出错误,他们是Daniel Hadson、Eldar Marcussen、William Taylor、David Hepworth和Tony Haig;还要感谢MEAP项目的所有成员,感谢他们提出建议和批评以及指正错误。

最后,要感谢以下评审人员在各个阶段提供宝贵的反馈:Tray Skates、Curtis Miller、Joe Hoover、Michael R. Bain、Francesco Goggi、Mike Stok、Michael R. Head、Cheryl M. Davis、Daniel Bretoi、Amos Bannister、Rob Allen、Dr. John Grayson、William Z. Taylor、Munch Paulson、David Hepworth、Eldar Marcussen、Daniel Had-son、Tony Niemann、Paolo Corti、Edmon Begoli、Lester Lobo、Robby O’Connor和Sopan Shewale。特别感谢Marion Newlevant对终稿做详尽的技术审阅,还要特别感谢Steve Holden欣然为本书作序。


本章介绍如下内容:

既然购买了本书,您很可能想学习编程技术。祝贺您!打算学编程的人不是很多,但编程很有趣,自学编程的回报也很高。编程是衡量您是不是文盲的新标准;如果不会编写简单程序(如批处理文件、邮件过滤器、电子表格公式),将在会这样做的人面前处于劣势。编程还是工具,可帮助您将点子付诸实施。

我10岁左右开始涉足编程,当时使用的是Commodore 64。在那个时候,除游戏和简单字处理软件外,现成的软件不多。Commodore等计算机自带了BASIC,编程很容易,无需学很多知识就能很快得到结果。

然而,这样的情况一去不复返了。当前,要编写程序必须先安装软件;但学会编程后,就可创建各种神奇的程序,替您完成繁琐的工作,向您提供信息,供您娱乐。最重要的是,编程很有趣,每个人都应尝试。

您将发现,本书有一些卡通人物点缀其中。这旨在以有趣的方式提供一些背景知识,让您知道接下来要讲的内容,或指出一些常见问题。这些卡通人物都来自User Friendly,但内容都是我编写的,如果您不喜欢,就责怪我吧。

下面开始介绍有关编程的基本知识。

这是一本编程书,在详细探讨编程之前,有必要介绍一些基本知识:什么是编程?如何编程?编程的定义很简单:

编程就是告诉计算机做什么。

但与大多数定义一样,这种定义太过简单。与国际象棋一样,学习编程的基本规则很容易,但要精通这些规则并结合使用它们要难得多。编程与人类活动的很多方面相关:从某种意义上说,如果不进行编程,就很难让计算机从事有意义的工作;编程不仅关乎数字和计算,还关乎设计、创意和个人表达。

下面将前面的编程定义分解为几部分,并分别审视它们。要明白前面的编程定义,需要知道计算机是什么以及“告诉”和“做什么”的准确含义。

1.计算机

计算机是速度很快的计算器,可按您的指令做简单决策。计算机指令很简单,通常包含计算机要完成的任务,如相加和比较。然而,结合使用一系列指令可创建大型程序,让您能够完成复杂的工作,如撰写文档、玩游戏、计算账户余额以及控制核反应堆。

计算机看似很聪明,实际上很傻,一根筋且缺乏基本常识。计算机毕竟只是机器,您让它做什么,它就做什么,根本不考虑后果。就拿删除整个硬盘的命令来说吧,大多数人明白其后果很严重,可能在遵命行事前向您核实,但计算机什么都不问,直接将硬盘上所有的数据都删除。

注意:


您让它做什么它就做什么,这既是计算机的优点,也是计算机的缺点。

如果使用(或编写)的程序行为怪异或无缘无故地崩溃,这不是它的错——它只是按指令行事。

2.告诉

使用Python时,您通常这样向它发出命令:在文本文件中输入程序代码,再让程序Python运行该文件,这将在本章后面介绍。您输入的指令可复杂可简单,执行的任务各种各样:将数字相加、打开其他文件、在屏幕上显示内容等。简单的Python程序类似于下面这样:

number = "42"
print "Guess my number..."
guess = raw_input(">")
if guess == number:
  print "Yes! that's it!"
else:
  print "No - it's", number

raw_input("hit enter to continue")

如果您不明白这个程序的含义,不用太担心;这个示例旨在提供一些背景知识。

3.做什么

现在事情开始变得有趣起来了。大多数现代计算机都是“图灵完备(Turing complete)”的,即什么都能做——您想得到的任何事情计算机都能做。至少从理论上说如此:所需的时间或复杂程度可能超乎预期,或者需要特殊硬件(如果您希望以特定方式交互),但只要编写的程序没有问题,并提供了足够的数据,计算机就什么都能做。下面是计算机用于完成的一些任务。

您可能没有将机器人探测器送到其他星球所需的硬件,但至少从理论上说,您依然能够运行这些程序。例如,就拿用于控制Spirit和Opportunity的计算机来说吧,其计算能力根本无法与您的台式机、笔记本电脑和手机相媲美,真是难以置信。

大家很容易将重点放在计算机编程的具体方面,如指令、加法、网络、硬件等,但编程的核心是创意:具体地说就是在程序中实现创意,供他人使用。自人类使用长矛以来,工具被人们用来探索新事物的情况层出不穷,编程也不例外。计算机自面世以来帮助人们想出了很多新点子,其中包括互联网、电子表格、交互式游戏以及桌面出版。

尽管我无法帮您想出新的创意,但我可以告诉您一些来自别人的创意,您可以用来激发自己的创意灵感。

本书涉及的大多数编程方面都与设计相关。设计通常指的是特定问题的常用解决方案。例如,建筑图描绘了建筑物及其占据的空间,解决了一些常见的建筑问题:如何进出建筑物以及如何在建筑物内移动;如何使用建筑物;建筑物如何巧妙利用材质让人感到愉悦等。

是否有效地解决了问题决定了设计的好坏,也决定了它是否优于其他设计。这意味着设计不可能面面俱到,总是存在其他更佳的问题解决方案。对于您的设计方案,别忘了提出如下问题:这种解决方案是否正确无误?它是否只解决了问题的一部分?将设计付诸实施有多容易?如果它只是好10%,但付诸实施的难度增加了一倍,就应选择更简单的设计方案。

既然编程是实现创意的方案,它解决了哪些问题呢?下面是您可能遇到的一些问题。

编程需要完成的重要工作是,以尽可能简单、清晰的方式将创意表达出来。开发计算机语言时,面临的一个常见主题是管理复杂性。即便编写的程序简单明了,一不小心就会只见树木不见森林。修改程序时,您可能错误地理解程序最初的意图,进而引入错误或不一致。优秀的编程语言提供了帮助处理细节的功能,让您在必要时能够深入或避开细节。

另一个重要因素是,使用特定语言编写的程序有多灵活。在实现创意方面,探索式编程是个很有用的工具,本书将大量采用这种方式;但如果编程语言没有管理复杂性和隐藏细节的强大工具,就很难修改程序,导致探索式编程的很多优点无法显现出来。

对编程有了基本认识后,该说说本书介绍的语言Python了。

本书介绍Python,这也是我最喜欢的编程语言。对刚开始学习编程的初学者来说,Python是理想选择,其中的原因很多。

如果将Python与其他编程语言进行比较,您首先将注意到它易于理解。Python的语法清晰得不能再清晰。下述特征让Python对用户非常友好。

Python开发小组竭力沿正确的方向前行,让编程尽可能简单明了。为找到提供功能的最佳方式,Python开发小组时常推迟功能的发布,甚至将其取消。在程序外观和行为方面,Python甚至有其独特的理念;要获悉这些理念,请在本章后面安装Python后输入import this。

虽然Python简单易用,但也是货真价实的语言。语言通常分两种:一种易于使用,它们带辅助轮,适合用于教授编程;另一种更难,但功能更多,让您能够完成实际工作。学习编程时,您有两种选择。

Python消除了这两种语言的缺点,将它们的优点融于一身。Python易于使用和学习,但随着编程技能的不断提高,您依然可继续使用它,因为它的速度很快,还提供了大量很有帮助的功能。最重要的是,与一步一个脚印地学习编程相比,一开始就真刀真枪地干更容易实现目标。

Python自带了很多库,您还可下载并安装很多其他的库。库是其他程序员编写好的程序代码,您可轻松地重用它们。库让您能够读取文件、处理数据、通过互联网连接到其他计算机、提供网页、生成随机数以及执行众多其他类型的基本操作。Python非常适合用于编写下述程序。

编写程序时,通常最难的部分已经有人替您完成了,您只需结合使用多个库,就能完成工作。第3章将更详细地介绍Python库及其用法。

Python是一种深受欢迎的语言,其社区庞大而友好,大家都乐于向新的Python开发人员伸出援手。您可通过主邮件列表提问,但还有一个专为新手提供帮助的邮件列表。网上还有大量教程和示例代码。

提示:


能工摹其形,巧匠摄其魂(Good artists borrow, great artists steal)。Python社区规模庞大,不管编写哪种类型的程序,都有大量程序供您借鉴和模仿。有一定Python编程经验后,阅读他人编写的程序将是一种深入学习的绝佳途径。

社区庞大的另一个优点是,很多人踊跃地推动Python向前发展,这使得bug很快能够得到修复,新功能得以定期添加。Python一直在稳步改善。

至此,您对编程有大致认识,知道为何Python是不错的选择。下面在计算机上安装Python,让您能够运行自己的程序。如果您使用的是Linux,请跳过下一节;如果使用的是Mac,请跳过下两节。

在接下来的三小节中,我将引导您循序渐进地安装Python,创建一个简单程序以核实Python在系统上运行正常,并介绍运行程序的基本步骤。现在核实Python运行正常可避免您以后气馁。

我们将使用Python 2版本,这是因为本书使用的大多数库都不支持Python 3。在本书编写期间,标准版是Python 2.7。要安装Python,需要从Python网站下载一个程序并运行它。这个程序包含Python、Python库以及运行Python程序所需的一切。

首先访问http://python.org/并单击Download,在打开的页面中,列出了用于各种操作系统的Python版本。单击Windows版本,并将其保存到桌面,如图1.1所示。

图1.1 Python.org网站的下载页面

下载完毕后,双击安装程序图标以运行它。如果出现如图1.2所示的对话框,单击“运行”以运行Python安装程序。

图1.2 您确定要运行这个从网上下载的程序吗?当然!

接下来将出现一系列对话框,让您能够指定Python安装选项。通常,除非您的计算机硬盘空间很小,需要安装到其他驱动器,否则使用默认选项(已经为您指定的选项)即可。在每个对话框中,如果您对选项设置满意,请单击Next按钮进入下一个对话框。

图1.3 为所有用户安装Python

图1.4 指定Python的安装位置

最后一个阶段可能需要一段时间,这取决于您的计算机的速度,当看见图1.7所示的对话框后,就说明已安装完毕。

图1.5 选择要安装的Python组件

图1.6 正在安装Python

图1.7 安装完毕

在系统上安装Python后,来创建一个简单的程序。这将让您确信正确地安装了Python,并了解如何创建和运行程序。

Python程序通常存储为文本文件,并由Python解释器运行。首先,您将使用“记事本”创建文件,但如果您喜欢其他文本编辑器,也可使用它。不要使用Microsoft Word或“写字板”来创建程序,因为它们会插入额外的格式设置字符,而Python无法理解这些字符。要打开“记事本”,可单击“开始”按钮,再选择“所有程序”>“附件”>“记事本”,如图1.8所示。

图1.8 “记事本”所处的位置

在打开的“记事本”中输入下述代码,如图1.9所示。不要管这些代码的功能,当前您只是想测试Python,确信能够运行程序。

print "Hello World!"
raw_input("hit enter to continue")

图1.9 Python测试程序

输入完毕后,将文件保存到桌面,并将其命名为hello_world.py,如图1.10所示。扩展名.py很重要,它让Windows知道这是一个Python程序。

图1.10 将测试程序保存到桌面

如果您查看桌面,将看到您刚编写的程序,它有一个蓝黄相间的Python图标。双击该图标将运行这个程序,如图1.11所示。

图1.11 双击您的程序以运行它

祝贺您!在您的计算机上,Python安装正确且运行正常!请继续阅读下面的内容,了解如何从命令行运行Python——出现问题时这是一个重要的故障排除工具。如果您没有看到图1.11所示的输出,也不用担心,后面的“排除故障”一节将介绍一些常见问题及其解决方案。

还可以从命令行运行Python程序。如果程序主要处理文本输入输出、作为操作系统脚本运行或需要大量输入,从命令行运行通常更容易,因为使用命令行选项比使用自定义设置窗口更容易。

注意:


访问并运行程序的方式很多,一种是双击,另一种是使用命令行。在本书中,您还将学习其他几种运行程序的方式。

程序存在bug时,从命令行运行也更容易,因为这让您能够看到错误消息,而不是什么都没有或者窗口打开后立即关闭。

要打开Windows命令行窗口,可单击“开始”按钮,再选择“所有程序”>“附件”>“命令提示符”,如图1.12所示。

图1.12 Windows命令提示符所处的位置

这将打开一个黑色窗口,其中显示了一些白色文本。输入“cd Desktop”切换到目录“桌面”,再输入python hello_world.py,以启动Python并让它运行前面创建的脚本文件。

当您这样做时,可能出现下面两种情况之一:运行指定的程序,如果这样就万事大吉了;出现一条错误消息,指出找不到Python程序,如图1.13所示。如果出现后一种情况,也不用难受,您只需告诉Windows到哪里去查找Python即可。

图1.13 Windows不知道Python在什么地方!

您需要修改Windows的路径设置。路径是一个位置列表,当您要求Windows运行程序时,它将在这些地方查找。首先右击图标“我的电脑”,并选择“属性”,如图1.14所示。

图1.14 查看计算机的属性

接下来,切换到“高级”选项卡,并单击对话框底部的“环境变量”按钮,如图1.15所示。您将看到一个环境变量列表,如图1.16所示。

图1.15 编辑系统属性 

图1.16 编辑变量Path

在对话框的下半部分,找到变量Path并双击。在出现的“编辑系统变量”对话框中,在行尾添加“;c:\python27”,再单击“确定”按钮,如图1.17所示。

图1.17 在变量Path中添加Python的安装位置

注意:


Windows在指定的路径中查找文件;计算机中的每个文件都有路径。在第3章,您将更详细的学习路径以及如何使用它们。

接下来,在打开的所有对话框中都单击“确定”按钮,直到回到桌面。再打开一个命令提示符窗口(原来的窗口仍将使用以前的路径设置),并输入python hello_world.py,您将看到程序的输出,如图1.18所示。

图1.18 成功了!现在Windows能够找到Python

祝贺您为开始编程做好了准备!但您可能应该先阅读“排除故障”一节,并了解另一个更适合用于编写Python程序的工具。

下面介绍如何在Linux系统上安装Python。

如何在Linux系统上使用Python呢?这介绍起来比较麻烦,因为有很多不同的Linux版本,它们的工作方式存在细微差别。这里以Gnome和Ubuntu为例,其他Linux版本的情况与之类似。

在Linux系统上,并非必须手动安装Python,这取决于您使用的是哪种版本。大多数Linux版本都默认安装了Python,但通常不是最新版本。要获悉当前安装的是哪个版本,可使用命令python -V。

在Linux系统上安装Python的主要方法有两种:使用现成包或从源代码编译。

包管理器用起来简单明了,它将为您处理大部分依赖关系和编译问题。在Debian apt-get系统中,只需输入sudo apt-get install python,就可自动安装最新版本的Python;您还可使用apt-cache search python来查找其他包,因为通常您还想安装大量其他的包(python-dev或python-docs)。

另一种选择是从源代码编译,但这超出了本书的范围。从源代码编译可能很复杂,如果您要使用所有的Python功能,还需安装其他几个库(如gnu-readlines和OpenSSL)。通常,使用包来安装更容易,但如果您一定要从源代码编译,可访问www.python.org/download/source/,这里提供了有关如何编译Python的更详细信息。

一般而言,Linux用户对命令行更得心应手,这将稍后介绍。然而,您也可以从Gnome等GUI运行Python程序,虽然相比于Windows系统,这更复杂些。在文本编辑器中输入下述程序,并将其存盘:

#!/usr/bin/python
print "hello world!"
ignored = raw_input("Hit enter to continue")

您还需编辑该文件的权限,将其设置为可执行的,以便能够直接运行它,如图1.19所示。

图1.19 编辑文件hello_world.py的权限

编辑权限后,就可双击这个程序文件,再单击Run in Terminal按钮,以运行它,如图1.20所示。

图1.20 选择如何处理程序

如果能看到图1.21所示的窗口,就说明您成功运行了该程序。虽然从GUI运行Python程序时,这种方法最简单,但还可使用其他方式来运行脚本,且无需指定要显示还是运行程序。在Gnome中,可创建一个程序启动器。这将打开如图1.22所示的对话框。

图1.21 在Ubuntu Linux系统上,在终端窗口中运行的测试程序

图1.22 在启动器中设置命令

别忘了,对于基于终端的程序(如这里的测试脚本),需要使用类似于下面的命令在终端窗口中运行:

gnome-terminal -e '/usr/bin/python /home/anthony/Desktop/hello_world.py'

虽然该命令是针对Gnome的,但其他Linux版本和窗口管理器也支持类似的选项。

很多Linux程序都可从命令行运行,Python也不例外。为此,您需要打开一个终端窗口。如果您使用的是Gnome,可通过菜单Applications > Accessories来打开它。

打开终端窗口后,将看到一个命令提示符。要执行脚本,可输入类似于下面的命令:

python path/to/your/script

如果脚本保存在桌面,可这样做:

python ~/Desktop/hello_world.py

如果要让脚本看起来更像系统命令,可省略文件扩展名.py,将其保存到指定的路径中(大多数系统都包含文件夹~/bin),并使用类似于chmod 755 path/to/script.py这样的命令将其设置为可执行的。只要脚本的第一行为#!/usr/bin/python,就应该能够从命令行使用脚本名来运行它,而不管当前处于什么位置。

至此,介绍了如何在Windows和Linux系统上安装Python,下面来看看如何在Mac系统上安装Python。

在Mac和Linxu系统上使用Python的方式很像,明显的差别只是图形用户界面不同。Mac OS 10.5预装了Python 2.5,而Snow Leopard(Mac OS 10.6)预装了Python 2.6,这两个版本都支持本书涉及的代码。

如果您要安装更新的Python版本,可从Python网站下载标准.dmg映像文件,但为确保程序正常运行,需要注意几个细节。

从终端运行程序时,您首先需要让Mac OS X使用新的Python版本;否则它将继续使用自带版本。所幸的是,Python提供了一个脚本,可用于替您完成这项工作。只要切换到Applications下的文件夹Python,并运行应用程序Update Shell Profile(如图1.23所示),以后Shell窗口就将使用正确的Python版本。

图1.23 正确设置新的Python路径

第二步是指定您双击Python程序时,将如何处理它们。默认情况下,将在Python自带的编辑器IDLE中打开它们,但我更喜欢让它们运行Python程序,这样它们看起来就像是真正的应用程序一样。右击(或按住Control键并单击)扩展名为.py的Python文件,将弹出一个快捷菜单,如图1.24所示。

图1.24 设置处理Python文件的默认方式

这让您能够选择这次使用哪个程序来运行Python脚本;如果要指定以后都使用哪个程序来运行.py文件,可选择Other(其他),这将打开如图1.25所示的对话框。

图1.25 将默认应用程序设置为Python Launcher

在目录Applications下,选择Python文件夹中的Python Launcher,再选中复选框Always Open With,然后单击Open(打开)按钮。这样,每当您双击.py脚本时,都将运行该脚本,而不是在IDLE中打开它。如果要核实能否从命令行执行Python程序,可运行应用程序Terminal,并在其中执行前一节列出的命令。

在系统上安装Python后,该介绍一下如何排除可能出现的故障了。

如果运行Python程序时没有看到窗口,问题可能出在几个地方。在您学习编程的过程中,可能经常遇到类似这样的错误。要解决问题,一个不错的做法是,将运行程序时出现的错误消息或症状作为关键字,在网上进行搜索。另外,深陷困境时,要大胆地寻求帮助,如通过Python邮件列表。下面介绍一些常见问题。

如果您输入的程序代码有误,可能看到窗口出现后迅速消失。请核查输入的代码,确保所有代码都准确无误后,再次运行程序。如果程序依然不能正确运行,可尝试从命令行运行它,这让您能够知道Python的所作所为以及程序是否有错。

如果程序没有蓝黄相间的图标,就意味着Windows不知道它是Python程序。请核实其扩展名是否是.py;如果扩展名是.py,则意味着可能没有正确安装Python,可尝试卸载并重新安装Python。

在Linux系统中,程序开头的#!行告诉Shell,应使用哪个程序来运行该脚本。如果指定的程序不存在,从命令行运行脚本时出现类似于下面的错误消息:

bash: ./hello_world.py: /usr/local/bin/python: bad interpreter:
  No such file or directory

要修复这种问题,需要找出Python的安装位置,并相应修改脚本的第一行代码。为此,最简单的方式是在命令行输入which python,它将指出Python的安装位置。另一种方法是使用代码#!/usr/bin/env python(而不是直接指定Python的位置),这将使用程序env来查找Python。

最后,来看看文本编辑器和IDE是如何简化编程工作的。

要创建程序,您需要使用文本编辑器来编辑供Python读取的文件,但Microsoft Word和“写字板”等编辑器都是糟糕的选择,因为它们使用的复杂格式与Python(以及其他编程语言)不兼容。相反,您应使用直接编辑文本,且不支持粗体和分页等格式的程序。

如果您使用的是Windows PC,总是可以使用“记事本”,而Linux和Mac OS X也提供了类似的应用程序。然而,这种编辑器的功能很少,无助于您发现众多常见的编程错误,如未正确缩进代码、字符串缺少右引号等。

一种更好的选择是使用Python自带的IDLE编辑器,或下载稍后列出的编辑器之一,这些编辑器是专为编程而设计的。用于编程的编辑器通常提供了额外功能,可大大简化编程工作。

在Python网站上,列出了一大串适合用于编写Python程序的编辑器,其网址为http://wiki.python.org/moin/PythonEditors。下面是其中一些较常用的编辑器。

有些编辑器也是集成开发环境(IDE)。IDE提供了除文本编辑外的其他服务,可节省编程时间。通常,它们让您能够使用Python解释器、自动完成功能以及高级代码导航(例如,直接跳转到程序中有错的源代码处),还提供了交互式调试工具,让您能够以步进方式运行代码以及在程序运行是查看变量。Python wiki也列举了一系列Python IDE,其网址为http://wiki.python.org/moin/IntegratedDevelopmentEnvironments。下面是一些您该考虑使用的IDE。

该使用IDE还是编辑器呢?该使用哪个编辑器或IDE呢?这通常取决于个人喜好以及项目的规模。从事的编程项目较大时,值得花时间学习功能强大的编辑器或IDE。最佳的做法是,尝试使用大量编辑器,看看哪些是您最喜欢的。

本章介绍了一些基本知识,要使用Python进行编程,您必须掌握这些知识。您大致了解一些知识:编程的定义、编程哲学、程序员经常面临的各种问题;还了解了一些细节:如何安装和运行Python、如何创建程序、如何通过图形用户界面和命令行运行程序。

长期而言,应对可能出现的错误是最重要的编程技能之一。错误发生后,要找出导致错误的源代码并从根本上修复错误,需要您持之以恒并做一些探测工作,因此知道有哪些资源可供使用至关重要。本书后面将介绍如何处理错误。

下一章介绍Python基本语句,并使用这些语句编写游戏Hunt the Wumpus,届时您在本章学到的所有知识(尤其是如何运行Python程序)都将派上用场。


本章介绍如下内容:

在本书前面,始终未涉及Python中组织程序的基本方式之一:类。类和面向对象编程通常被认为是一个庞大而吓人的主题,仅供真正的程序员用来编写程序,因此您可能认为,要正确地使用它们,需要大量的理论知识。没有比这种看法更离谱的了。在Python中,使用类和面向编程易如反掌。

在本章中,您将首先复习第2章为游戏Hunt the Wumpus编写的洞穴生成代码,并了解到使用类编写这些代码容易得多;然后,您将以此为基础,参照Adventure和Zork编写一个功能齐备的冒险游戏。在此期间,您将全面学习Python类以及如何充分利用它们。

如果您回顾第2章,可能还记得您编写了一系列函数,用于处理玩家进入和wumpus所处的洞穴。有创建洞穴的函数,有使用通道将洞穴连通的函数,有确保所有洞穴都连在一起的函数,不一而足。在第2章编写程序时,您完全依靠函数来处理洞穴;另一种方式是创建一个类来标识这些函数,并将它们之间的关系固定下来。

看待类的另一种方式是,将其视为容器或包装器,包含要在程序中使用的数据。您可将执行特定任务所需的数据都封装起来,并提供处理它们的函数。在数据特别复杂、难以处理或需要保持一致(如跟踪银行账户余额的程序)时,尤其适合这样做。

类类似于抽象数据类型,包含一组数据以及可对这些数据执行的所有操作。您无需在类中指定可对数据执行的各种可能操作,而只根据具体情况指定有用的操作。然而,设计类时,考虑其各种可能用途并包含合理的操作常常大有裨益。

可将类视为橡皮图章,图章刻好后,就可用它轻松地盖章,想盖多少次都行。类的工作原理与此类似。您通常不直接使用类,而使用其实例——使用类创建的对象。

类的一个优点是,如果需要稍微不同的图案,只需复制原来的类,再稍作修改。图6.1以橡皮图章的方式展示了本章将用到的类。

图6.1 怪物类似于玩家,只是头上长角,还生就一张苦瓜脸

实例和类本身都可以有方法和数据供您调用和访问。在很大程度上说,这些都将在您创建实例时设置,但Python允许您动态地更新它们,甚至重新绑定方法。

注意:


有大量面向对象编程术语,其中很多都让初学者感到迷惑。您将听人提及类、对象、实例、方法、类方法、获取函数、设置函数等。如果您不确定对方的意思,请务必搞清楚他说的是橡皮图章,还是盖在纸上的章。

程序清单6.1是一个类,您应感到眼熟,虽然有些部分与以前截然不同。这是您在第2章编写的洞穴列表和方法,但将它们封装在类中了。

程序清单6.1 用于存储洞穴的对象

from random import choice

class Caves(object):                 ❶
  def __init__(self, number_of_caves):
    self.number_of_caves = number_of_caves   ❷
    self.cave_list = range(number_of_caves)  ❷
  self.unvisited = range(number_of_caves)[1:]  ❷
  self.visited = [0]                 ❷
  self.caves = []                   ❷
  self.setup_caves(number_of_caves)        ❷
  self.link_caves()                  ❷

def setup_caves(self, cave_numbers):        ❸
  """ Create the starting list of caves """
  for cave in range(cave_numbers):
    self.caves.append([])

def link_caves(self):                 ❸
  """ Make sure all of the caves are connected
  with two-way tunnels """
  while self.unvisited != []:
    this_cave = self.choose_cave(self.visited)
    next_cave = self.choose_cave(self.unvisited)
    self.create_tunnel(this_cave, next_cave)
    self.visit_cave(next_cave)

def create_tunnel(self, cave_from, cave_to):    ❸
  """ Create a tunnel between cave_from
  and cave_to """
  self.caves[cave_from].append(cave_to)
  self.caves[cave_to].append(cave_from)

def visit_cave(self, cave_number):         ❸
  """ Mark a cave as visited """
  self.visited.append(cave_number)
  self.unvisited.remove(cave_number)

def choose_cave(self, cave_list):          ❸
  """ Pick a cave from a list, provided
  that the cave has less than 3 tunnels."""
  cave_number = choice(cave_list)
  while len(self.caves[cave_number]) >= 3:
    cave_number = choice(cave_list)
  return cave_number

def print_caves(self):                ❸
  """ Print out the current cave structure """
  for number in self.cave_list:
    print number, ":", self.caves[number]
if __name__ == '__main__':              ❹
  caves = Caves(20)                 ❹
  caves.print_caves()                ❹

❶ 创建类

❷ 设置类的变量

❸ 将函数转换为方法

❹ 通过创建实例进行测试

先来看创建类的Python语法❶。这与函数创建语法类似,但根据约定,类名的首字母大小(毕竟类很重要)。括号内是继承的类,这里是Python通用类object,因为您什么都不想继承。

大多数Python类都有__init__方法,它负责在创建实例时对其进行设置❷。这个方法包含参数self,您注意到了吗?这个参数让方法能够访问变量和共享状态。这里包含您在第2章使用的所有列表,但访问它们是使用了前缀self,这表明访问的是实例中的变量。

这里包含创建洞穴的函数,它们也都将self作为参数❸。除此之外,这些函数没有太大变化,这符合您的预期,因为它们不过是接受参数self的函数。

定义类后,您创建一个实例并调用方法print_caves(),对其进行测试❹。Python将执行这个类的方法__init__,而这个方法调用setup_caves()和link_caves()来创建洞穴网络——从caves.print_caves()的结果可以知道这一点。

将所有函数都放在类中有什么好处呢?主要好处是所有有关洞穴的细节都包含在您创建的实例中。这样,您可以同时创建其他洞穴系统,而不用担心它们相互冲突。您还可以扩展这个类(包含洞穴名和其他函数)或添加扩展洞穴系统的方法。

注意:


类提供了另一种您可在程序中使用的分而治之机制。创建实例后,您无需考虑其工作原理,而只需考虑可用它来做什么。

然而,这个Caves类存在一个问题。虽然您创建了类且其效果不错,但这依然不是面向对象设计,您只是将既有功能放到了类中。如果以后要添加额外功能,如在洞穴中捡宝、更多怪物等,将很难。在第2章,添加函数改变了程序的设计,同样,为妥善地使用类,必须调整设计重点。

很多人都喜欢面向对象编程,其中的原因之一是,对象通常对应于现实世界的东西,这让您在开发程序期间很容易想见它们将如何交互。编写管理财务的程序时,您可能创建Account、Expense、Income和Transaction类;编写车间控制程序时,您可能创建Component、ConveyorBelt、Assembly(即组装在一起的多个部件)和AssemblyLine。

咱们回过头去更深入地思考一下冒险游戏。这种游戏都包含什么呢?传统上,玩家是无畏的冒险家,为财宝、名声和荣誉深入地牢或充斥怪物的洞穴。图6.2以草图方式描绘了本章要编写的游戏,您可以使用它向朋友说明这个游戏。

图6.2 冒险游戏草图

其中的基本元素是洞穴,而不是整个洞穴系统。您可能受第2章介绍的列表和函数的误导,错误地认为洞穴系统是重要的组成部分,但从正确层面(洞穴及其内部的东西)着眼,可让设计更整洁得多。程序清单6.2演示了如何编写洞穴类,您应将其放在文件caves.py中,否则本章后面的有些代码将不能正确运行。

程序清单6.2 面向对象程度更高的设计

from random import choice, shuffle

class Cave(object):                   ❶
    def __init__(self, name, description):    ❶
    self.name = name                 ❶
    self.description = description         ❶
    self.here = []                  ❶
    self.tunnels = []                ❷

  def tunnel_to(self, cave):             ❷
    """Create a two-way tunnel"""          ❷
    self.tunnels.append(cave)            ❷
    cave.tunnels.append(self)            ❷

  def __repr__(self):                 ❸
    return "<Cave " + self.name + ">"       ❸

cave_names = [
  "Arched cavern",
  ... ]

def create_caves():
  shuffle(cave_names)                  ❹
  caves = [Cave(cave_names[0])]
  for name in cave_names[1:]:
    new_cave = Cave(name, name)
    eligible_caves = [cave for cave in caves    ❹
              if len(cave.tunnels) < 3]   ❹
    new_cave.tunnel_to(choice(eligible_caves))
    caves.append(new_cave)
  return caves

if __name__ == '__main__':
  for cave in create_caves():              ❺
    print cave.name, "=>", cave.tunnels       ❺

❶ 新的Cave 类

❷ 每个洞穴都“知道”自己与哪些洞穴相通

❸ 添加方法__repr__

❹ 创建洞穴系统的函数

❺ 为测试打印洞穴列表

首先定义了新类Cave❶。这里没有使用三个列表,分别存储洞穴名、连接在一起的洞穴和未连接的洞穴,而将有关洞穴的信息存储在对象中。以后创建洞穴列表时,可轻松地根据这些属性进行筛选。您添加了一个self.here列表,用于存储洞穴中的其他对象,如玩家、怪物和财宝;还添加了字符串description,玩家进入洞穴时,将使用它向玩家描绘洞穴。当前,暂时未使用这两个新增的值。

由于您可轻松地获悉洞穴与哪些洞穴相通❷,因此添加到其他洞穴的通道很容易:将另一个洞穴和self(当前洞穴)加入通道列表即可。另外,您还在实例层级处理洞穴,这让程序清晰得多。

最后,您在类中添加了方法__repr__❸。在基类object中,方法__repr__返回的结果(类似于<__ main__.Cave object at 0x00B38EF0>)不好理解;通过新增这个方法,可让打印洞穴时的程序输出明确得多。

余下的工作是将洞穴连接起来。这里使用了第2章的洞穴名列表,为每个洞穴创建一个Cave实例,将其连接到既有洞穴,再加入到caves列表❹。这里唯一比较棘手的地方是,如何找到可连接到的洞穴。这很容易,只需在列表解析中检查每个洞穴的通道列表长度。另外,注意到Python没有限制您解决问题的方式——您可根据需要随意使用函数、类和代码。

为让您相信这可行,打印了完整的洞穴列表❺。

这里的重点是,程序清单6.1的多少代码被替换掉了。Caves类有6个彼此调用的方法,这里用一个类和一个外部函数取而代之。第2章说过,如果代码更短,更简单,通常说明选择的道路正确;也就是说,这个面向对象设计非常适合本章的冒险游戏。下面着手解决程序的下一部分:处理玩家输入。

大多数冒险游戏的玩法都是这样的:玩家输入指令,如往北走、拾剑、杀怪物和检宝,游戏再显示操作结果,以及有关玩家所处房间和房间中物件的描述。这里采取同样的方法,并利用一些对象的属性简化程序的扩展工作。别忘了,您还需让代码易于测试,因此将把处理用户输入的代码放在独立的函数中。

您将首先寻找一种不错的方式,在类结构中编写将名词转换为动词的接口。通常情况下,对象为名词,而操作对象的方法为动词,因此命令GET SWORD应在当前房间找到Sword对象,并调用其Get接口。这样设计意味着不用编写一个什么都会做的大型Player类,而可编写大量更容易理解、修改和扩展的小型类。

程序清单6.3是该程序的核心代码:Player类。它负责读取玩家的输入,并寻找合适的对象来解读命令。您应将这些代码保存在文件player.py中。

程序清单6.3 Player类

import shlex

class Player(object):
  def __init__(self, location):        ❶
    self.location = location         ❶
    self.location.here.append(self)     ❶
    self.playing = True             ❶

  def get_input(self):
    return raw_input(">")
def process_input(self, input):         ❷
  parts = shlex.split(input)          ❷
  if len(parts) == 0:              ❷
    return []                  ❷
  if len(parts) == 1:              ❷
    parts.append("")              ❷
  verb = parts[0]                 ❷
  noun = " ".join(parts[1:])           ❷

  handler = self.find_handler(verb, noun)   ❸
  if handler is None:               ❸
    return [input +               ❸
        "? I don't know how to do that!"] ❸
  return handler(self, noun)           ❸

def find_handler(self, verb, noun):        ❹
  if noun != "":                   ❹
    object = [x for x in self.location.here  ❹
         if x is not self and         ❹
           x.name == noun and        ❹
           verb in x.actions]        ❹
    if len(object) > 0:              ❹
      return getattr(object[0], verb)     ❹
  if verb.lower() in self.actions:        ❺
    return getattr(self, verb)          ❺
  elif verb.lower() in self.location.actions:  ❺
    return getattr(self.location, verb)     ❺

def look(self, player, noun):            ❻
  return [self.location.name,
       self.location.description]

def quit(self, player, noun):             ❻
  self.playing = False
  return ["bye bye!"]

actions = ['look', 'quit']              ❻

❶ Player 的变量

❷ 处理玩家输入

❸ 执行命令

❹ 查找处理命令的方法

❺ 查找洞穴或玩家的方法

❻ 一些示例命令

当前,Player类需要包含的变量很简单❶。您将它放到指定洞穴,并指出它正在玩游戏。

您将命令划分为几部分,并设置变量verb和noun的值❷。您使用了shlex.split()来划分命令,因为它比常规函数split更擅长处理引号。例如,如果玩家输入GET“GOLD KEY”,shlex.split()将把GOLD KEY视为一部分。您将动词后面的所有内容都视为名词部分,因此玩家输入GET GOLD KEY也没问题。

将命令转换为易于处理的格式后,您尝试查找应调用的方法❸。如果find_handler返回None,就说明没有处理该命令的方法,因此显示一条错误消息(如果您玩过冒险游戏,就熟悉这种错误消息)。

有了命令后,您尝试查找处理它的方法❹。如果命令包含名词(如GET SWORD中的SWORD),您就查找与之匹配的对象,看它能否处理这种命令。函数getattr()非常适合做这项工作,它在类中查找指定名称的属性或方法。如果找不到,就将命令交给洞穴或Player去处理(这有助于您在本章后面提供更佳的接口)。

如果玩家没有指定名词,可能意味着命令比较通用(如LOOK或QUIT),因此您在当前洞穴和Player对象中查找方法❺。如果它们都没有处理该命令的方法,方法将到此结束,这意味着返回None,进而导致程序显示错误消息。

Player包含两个基本命令(LOOK和QUIT)❻,让您感受一下命令的工作原理。您需要在Cave类中添加一个空的actions列表;否则,如果Player不能处理指定的命令,将引发错误。

编写Player类后,您需要从玩家那里读取输入,并执行相应的命令。程序清单6.4是一个示例框架,它创建一个简单的洞穴,让玩家置身中,然后开始循环:不断地读取输入并执行相应的命令,直到玩家结束游戏。以后您可能将这些代码封装在Game类中,但由于Python非常灵活,您可将其保留为函数,并编写和测试其他类。

程序清单6.4 使用Player类

def test():
  import cave                  ❶
  empty_cave = cave.Cave(            ❶
    "Empty Cave",               ❶
    "A desolate, empty cave, "        ❶
    "waiting for someone to fill it.")   ❶
  player = Player(empty_cave)         ❶

  print player.location.name          ❷
  print player.location.description      ❷
  while player.playing:             ❷
    input = player.get_input()        ❷
    result = player.process_input(input)  ❷
    print "\n".join(result)          ❷

if __name__ == '__main__':
  test()

❶ 设置环境

❷ 主循环

创建Player对象时,需要传入一个Cave对象,因此您创建一个测试洞穴,并让玩家置身其中❶。创建洞穴时,只需指定简单的名称和描述即可。

创建Player对象后,您从玩家那里获取输入,并将其传递给方法process_input(),后者以字符串列表的方式返回结果❷。接下来,您使用字符串"\n"的方法join()在一行中打印结果。玩家发出quit命令时,变量player.playing将为false,因此程序到此结束。

如果您现在运行这个冒险游戏,应该能够指定命令LOOK和QUIT。这个程序很简单,下一节将介绍如何扩展接口。

咱们现在在游戏中添加一些更有趣的元素。首先,您希望在游戏开始时给玩家一些装备或财宝,以吸引玩家玩下去,进而不能自拔。没有宝剑和财宝,游戏就不是冒险游戏,因此咱们先来添加这些物品。然而,这样做之前需要更深入地思考一下游戏设计。

显然,您需要与这些物品交互,这意味着您至少需要能够执行GET SWORD、LOOK SWORD和DROP SWORD等命令。按您当前的行事方式,这意味着需要有处理这些命令的方法。

一种选择是将这个方法放在Player类中——毕竟,执行捡拾和放下操作的是玩家。您一不小心就会这样想,但进行面向对象编程时,应尽可能放权。例如,以后您可能添加玩家无法捡拾的物品,如笨重的箱子或雕像。这好办,只需检查对象,看其immovable标记是否被设置。如果仅当箱子非常坚固时,玩家才能捡拾,该如何办呢?再进行检查。您完全能够想见这样做的后果:等到游戏编写好时,Player的get()方法可能包含五六个甚至二十个条件。

注意:


刚开始设计类时,可能感觉很棘手。需要牢记的要点是,经验很重要,因此您会越做越顺手。另外,别忘了您可尝试不同的设计,并从中挑选最佳的设计。

一种更佳的方式是,让物品自己负责判断玩家能否捡拾它。箱子“知道”自己的重量,还能通过检查确保仅当玩家有权获取其中的物品时,才能捡拾箱子。这听起来怪怪的,但Player对象不应负责检查物品的重要以及怪物如何搏斗,因为将这些物品加入Player对象将导致它过于复杂。下面来看看如何实现可供玩家寻找的对象,然后修改这些对象,使其可供玩家捡拾。

程序清单6.5 一个可供玩家寻找的对象

class Item(object):
  def __init__(self, name, description, location):
    self.name = name
    self.description = description
    self.location = location
    location.here.append(self)

  actions = ['look']

  def look(self, player, noun):
    return [self.description]

Item对象需要知道的事情与对象Player和Cave很像:名称和所处的位置。您可以在创建Item实例时提供所有这些信息。

当前,Item对象只对一个命令做出响应:LOOK。玩家发出命令LOOK,并将Item对象的名称作为命令中的名词时,将调用方法look,而这个方法只是返回指定Item对象的描述。

另外,您还需修改洞穴的描述,让玩家知道洞穴中都有些什么。在这里,您将效仿前面的做法,将Player类的方法look()删除,并在Cave类中添加方法look()。

程序清单6.6 修改命令look()

def look(self, player, noun):
  if noun == "":                  ❷
    result = [self.name,
          self.description]
    if len(self.here) > 0:            ❶
      result += ["Items here:"]        ❶
      result += [x.name for x in self.here  ❶
            if 'name' in dir(x)]     ❶
  else:
    result = [noun + "? I can't see that."]  ❷
  return result
actions = ['look']                   ❸

❶ 列举物品

❷ 错误处理

❸ 修改actions

主要的修改是,列举洞穴中的物品,并将其放在描述后面❶。如果不这样做,玩家将不知道洞穴中有宝剑,除非瞎猜。

如果玩家试图寻找洞穴中没有的东西(如执行命令LOOK AARDVARK),也将调用这个函数。如果洞穴中没有土豚(aardvark),就需返回一条错误消息❷。

别忘了更新Cave类的actions列表❸,并将look从Player类的actions列表中删除,否则Player将依然会试图去处理LOOK命令。

程序清单6.7 更新环境设置代码

  ...
  "A desolate, empty cave, "
  "waiting for someone to fill it.")

import item
sword = item.Item("sword",
  "A pointy sword.", empty_cave)
coin = item.Item("coin", "A shiny gold coin. "
  "Your first piece of treasure!", empty_cave)

player = Player(empty_cave)
...

在冒险游戏中添加物品,设置其名称、描述和位置,Item对象将负责处理其他的事情。

如果您现在运行这个冒险游戏,应该能够寻找财宝和闪亮的宝剑,但不能捡拾,因此接下来需添加捡拾功能。

现在,需要让物品对捡拾命令做出响应。为此,需要做两方面的修改:一是给Item类添加方法get()和drop();二是修改Player类,使其能够携带物品。程序清单6.8演示了如何在游戏中添加这些命令。

程序清单6.8 物品自己让玩家能够捡拾它

actions = ['look', 'get', 'drop']

def get(self, player, noun):                ❶
  if self.location is player:              ❷
    return ["You already have the " + self.name]  ❷

  self.location.here.remove(self)           ❶
  self.location = player                 ❶
  player.inventory.append(self)            ❶
  return ["You get the " + self.name]         ❶

def drop(self, player, noun):              ❸
  if self not in player.inventory:           ❸
    return ["You don't have the " + self.name]   ❸
  player.inventory.remove(self)            ❸
  player.location.here.append(self)          ❸
  self.location = player.location           ❸
  return ["You drop the " + self.name]        ❸

❶ get 方法

❷ 检查物品是否已加入财产目录

❸ Drop 方法

方法get()本身很简单❶。Item对象需要将自己从当前洞穴的here数组中删除,加入到玩家的财产目录(稍后将创建该列表)中,并设置当前所处的位置。执行这些操作后,您返回一条消息,让玩家知道他捡到了物品。

如果没有❷所示的检查,上述代码也能正常运行,但如果玩家再次捡拾物品,程序将崩溃,因为不能从列表中删除原本就没有的物品。

方法drop()与方法get()很像,但效果相反❸。与前面的方法get()一样,将物品移到当前洞穴之前,需要检查它包含在玩家的财产目录中。

程序清单6.9演示了如何更新Player类,使其能够存储物品;还添加了一些命令,让Player对象能够显示财产目录和错误消息;最后,在查找与命令对应的方法时,还检查了财产目录。

程序清单6.9 更新Player类

class Player(object):
  def __init__(self, location):
    self.location = location
  self.location.here.append(self)
  self.playing = True
  self.inventory = []               ❶

...

actions = ['quit', 'inv', 'get', 'drop']

def get(self, player, noun):             ❷
  return [noun + "? I can't see that here."]   ❷

def drop(self, player, noun):            ❷
  return [noun + "? I don't have that!"]     ❷

def inv(self, player, noun):             ❸
  result = ["You have:"]               ❸
  if self.inventory:                 ❸
    result += [x.name for x in self.inventory] ❸
  else:                         ❸
    result += ["nothing!"]             ❸
  return result                    ❸

...
def find_handler(self, verb, noun):
  # Try and find the object
  if noun != "":
    object = [x for x in
      self.location.here + self.inventory    ❹
      if x is not self and
      ...

❶ 玩家的财产目录

❷ 错误处理程序

❸ 显示财产目录

❹ 查找方法时检查财产目录

首先,玩家需要使用财产目录存储物品❶。为此,最简单的方式是在Player类中添加一个列表。玩家捡拾物品时,物品将被加入到该列表末尾。

这些错误处理程序❷与您为Cave类编写的错误处理程序类似。如果玩家试图捡拾当前洞穴中没有的物品或丢弃财产目录中没有的物品,将调用这些方法来处理命令GET和DROP。

应该让玩家知道他都捡拾了哪些物品,因此提供了一个方法❸,它列出玩家随身携带的所有物品。

最后的修改是,在查找处理命令的方法时检查财产目录❹,让玩家能够寻找物品或丢弃其财产目录中的物品。

至此,玩家可寻找和捡拾宝剑和闪闪发光的钱币,还可丢弃它们(虽然这样做不存在什么冒险性)。有了宝剑和财宝在手,该在洞穴迷宫中继续前行了。

如果没有探险,冒险游戏就不成其为冒险游戏。在大多数游戏中,玩家发出类似于GO NORTH(简写为NORTH或N)的命令来移动。玩家移动时,游戏将更新描述,让玩家知道自己刚穿过的区域。您将像添加其他命令那样添加移动命令,但同时添加一些缩写,让玩家输入移动命令更容易。

首先,来看看如何在Cave类中添加方向,然后创建让玩家能够移动的命令。

程序清单6.10 在Cave类中添加方向

class Cave(object):
  directions = {                  ❶
    'north' : 'south',              ❶
    'east' : 'west',               ❶
    'south' : 'north',              ❶
    'west' : 'east' }               ❶

  def __init__(self, name="Cave", description=""):
    ...
    self.tunnels = {}               ❷
    for direction in self.directions.keys():  ❷
      self.tunnels[direction] = None      ❷

def exits(self):                    ❸
  return [direction for direction, cave     ❸
          in self.tunnels.items()      ❸
          if cave is not None]        ❸

def look(self, player, noun):
  if noun == "":
    ...
    if len(self.exits()) > 0:           ❹
      result += ['Exits:']            ❹
      for direction in self.exits():       ❹
        result += [direction + ": " +     ❹
          self.tunnels[direction].name]   ❹
    else:                       ❹
      result += ['Exits:', 'none.']       ❹

def tunnel_to(self, direction, cave):         ❺
  """Create a two-way tunnel"""
  if direction not in self.directions:        ❺
    raise ValueError(direction +
      " is not a valid direction!")         ❻
  reverse_direction = self.directions[direction]   ❻
  if cave.tunnels[reverse_direction] is not None:   ❻
    raise ValueError("Cave " + str(cave) +      ❻
      " already has a cave to the " +        ❻
      reverse_direction + "!")            ❻
  self.tunnels[direction] = cave            ❺
  cave.tunnels[reverse_direction] = self        ❺

❶ 方向列表

❷ 添加到其他洞穴的通道

❸ 列出所有出口

❹ 在look 函数中显示出口

❺ 创建通道

❻ 处理错误行为的异常

首先,在Cave类中添加可移动的方向❶,让玩家知道可沿哪些方向移动。您还可据此找出相反的方向(稍后就需要这样做)。

创建每个洞穴时,都需设置一个基本数据结构,用于存储沿各个方向可进入的洞穴❷。当前,它们都是None,这意味着沿相应方向无法前往其他洞穴。

接下来是一个便利方法,它列出当前洞穴的所有出口❸。这个方法很简单——对self.tunnels进行列表解析,当让您能够使用cave.exits(),从而让代码更容易理解。不要被函数的编写顺序欺骗,这个方法的代码是从look方法中提取出  来的。

玩家需要知道他可沿哪些方向移动,因此look方法列出了所有出口❹。即便没有任何出口,也需让玩家知道这一点。

您还需要在洞穴之间建立通道。建立单向通道很容易,只需将洞穴加入self.tunnels,并将其与正确方向相关联。但您希望通道是双向的,因此您在列表中搜索相反的方向,并建立从目标洞穴到当前洞穴的通道❺。

如果说这个方法看起来有点怪异,那是因为它包含引发通道建立错误的异常❻。方法raise()引发异常,这些隐藏与本书前面介绍过的异常类似,如输入错误引发的异常。这里您创建了类似于整数和字符串的自定义对象,因此像它们那样引发异常(而不是打印错误消息或忽略输入)要好得多。这样,如果程序崩溃,您将知道问题的根源,进而显示明确的错误消息,这使得排除程序故障容易得多。

提示:


设计类似于Cave这样的类(它们相当于库类)时,尽可能捕获错误并引发异常都是不错的主意。这样,以后使用这些类时如果犯错,错误将显而易见。

至此,Cave类能够存储方向以及到其他洞穴的通道,还能向玩家描述这些方向。下面来添加一些命令,让玩家能够在洞穴之间移动,如程序清单6.11所示。

程序清单6.11 在洞穴之间移动的命令

def go(self, player, noun):               ❶
  if noun not in self.directions:           ❶
    return [noun + "? "                ❶
      "I don't know that direction!"]       ❶
  if self.tunnels[noun] is None:           ❶
    return ["Can't go " + noun + " from here!"]  ❶
  self.here.remove(player)                ❷
  self.tunnels[noun].here.append(player)       ❷
  player.location = self.tunnels[noun]        ❷
  return (['You go ' + noun] +             ❷
      self.tunnels[noun].look(player, ''))    ❷

def north(self, player, noun):     ❸
  return self.go(player, 'north')  ❸
n = north                 ❸
def east(self, player, noun):     ❸
  return self.go(player, 'east')   ❸
e = east                  ❸
def south(self, player, noun):     ❸
  return self.go(player, 'south')  ❸
s = south                 ❸
def west(self, player, noun):     ❸
  return self.go(player, 'west')   ❸
w = west                  ❸
l = look                  ❸

actions = ['look', 'l', 'go',              ❹
      'north', 'east', 'south', 'west',      ❹
      'n', 'e', 's', 'w']              ❹

❶ 检查玩家的输入

❷ 移动玩家

❸ 添加一些快捷键

❹ 更新Cave 类的actions 列表

这里添加的是基本命令GO(如GO NORTH)。首先,需要检查玩家输入的方向,确保方向有效,可通往其他洞穴❶。

确定方向有效后,就可接着移动玩家了❷。这很简单:将玩家从当前洞穴移走,将其加入新洞穴,并更新玩家的位置。您还在该命令的结果末尾添加了新洞穴的描述,让游戏更容易玩,并减少玩家的输入量。

为方便玩家玩游戏,您还可给常见命令提供快捷键❸。反复输入GO NORTH让人烦,因此您允许玩家使用NORTH或N,其他各个方向也与此类似。在幕后,这些命令调用方法go(),因此它们的行为完全相同。

最后,需要更新Cave类的列表actions❹,还给look()命令添加了快捷键。

至此,就大功告成了。注意到这里是如何在玩家和洞穴间划分职责的吗?这是面向对象设计常见的特征——对象的职责非常明确。在这里,洞穴负责记录自己的出口及其通往哪些洞穴,而玩家可在其命令go()中使用这些信息。如果以后有其他对象需要使用方向信息,您无需从Player类或其他地方提取相应的代码,就能让新功能正常运行。

然而,玩家需要在洞穴网络中移动,为此程序清单6.12扩展了前面的洞穴网络生成函数。

程序清单6.12 创建洞穴网络

Class Cave(object):
    ...
    def can_tunnel_to(self):            ❶
    return [v for v in self.tunnels.values()   ❶
        if v is None] != []           ❶

cave_names = [
  "Arched cavern",
  ...
  "Spooky Chasm",
]

def create_caves():                    ❷
  shuffle(cave_names)
  caves = [Cave(cave_names[0])]
  for name in cave_names[1:]:
    new_cave = Cave(name)
    print caves
    eligible_caves = [cave for cave in caves     ❸
              if cave.can_tunnel_to()]    ❸
    old_cave = choice(eligible_caves)         ❸
    directions = [direction for direction, cave   ❹
            in old_cave.tunnels.items()     ❹
            if cave is None]           ❹
    direction = choice(directions)           ❹
    old_cave.tunnel_to(direction, new_cave)      ❺
    caves.append(new_cave)                 ❺
  return caves
player.py:
if __name__ == '__main__':                  ❻
  import cave                         ❻
  caves = cave.create_caves()               ❻

  cave1 = caves[0]                      ❻
  import item                         ❻
  sword = item.Item("sword", "A pointy sword.", cave1) ❻
  coin = item.Item("coin", "A shiny gold coin. "     ❻
    "Your first piece of treasure!", cave1)       ❻

  player = Player(cave1)                   ❻
  print '\n'.join(player.location.look(player, ''))
  while player.playing:
    input = player.get_input()
    result = player.process_input(input)
    print "\n".join(result)

❶ 另一个便利方法

❷ 修改原来的函数

❸ 从列表中挑选洞穴

❹ 挑选进入洞穴的方向

❺ 建立到新洞穴的通道

❻ 更新players.py 中的游戏环境设置

首先,您再创建了一个便利方法❶。稍后您将明白如何使用它,它指出可否连通到其他洞穴(如果不能,说明全部4个方向都被占用了)。

接下来的函数❷是程序清单6.2的create_caves()的修改版。主要差别在于,这个版本除选择洞穴外,还选择方向,但创建的也是标准的连通洞穴结构。

为选择要连接到的下一个洞穴,您在列表解析中使用了便利方法can_tunnel_to()❸。

您还需要挑选一个未占用的方向,用于通往选定洞穴❹。如果所有方向都被占用了,函数choice将失败,但您不用太担心这样的情况发生,因为方法can_tunnel_to()已经指出至少有一个方向未被占用。

选择空闲的方向后,建立到新洞穴的通道就很容易了❺。您还将新洞穴添加到洞穴系统中,让其他新洞穴可以连接到它。

最后,您在player.py中更新游戏环境设置,以使用新的洞穴系统❻。现在,游戏不再只有一个荒凉的空洞穴,且将所有东西(包括玩家)都放到了第一个洞穴中。除此之外,游戏与以前没有其他不同。

如果此时运行players.py,玩家将身处第一个洞穴,并能够看到一些出口以及常规描述和物品。玩家可捡拾宝剑、在洞穴间移动、将宝剑丢在其他地方以及返回原来的洞穴。

祝贺您建造了一个独立王国!请前往这个王国探险,等您回来后再添加其他一些内容。

至此,您创建了玩家、物品以及可捡拾的财宝。在这个冒险游戏中,还未添加的元素是意外的痛苦死亡,这也被称为危险和刺激。下面在游侠中添加一些怪物,它们在洞穴迷宫中四处游荡,受到威胁时可能攻击与它身处一个洞穴的玩家,还可能捡拾洞穴中的财宝。玩家也可攻击怪物及抢夺怪物的财宝。

咱们先想一想。您对怪物不是非常熟悉吗?下表对怪物和玩家做了比较。

 怪物              

玩家              

 在洞穴迷宫中四处游荡

在洞穴迷宫中四处游荡

 捡拾财宝          

捡拾财宝          

 攻击玩家          

攻击怪物          

怪物和玩家看起来有很多相同之处。在基于函数的程序中,您看到这一点后将意识到需要避免重复,但在面向对象的程序中,如何处理这种问题呢?答案是创建Player的子类,这通常被称为继承。

继承相当于这样说:复制一个类并稍作修改,使其以不同的方式完成这个类的功能。在这个游戏中,怪物的行为与玩家很像,但不是由玩家告诉它接下来如何做,而由怪物自己决定。怪物也需要有名称和描述,这样玩家才能寻找它们。这意味着怪物的方法__init__和get_input将不同,但您将保留Player类的其他方面不变。程序清单6.13是新类Monster的初稿。

程序清单6.13 在游戏中添加怪物

import random
import player                        ❶

class Monster(player.Player):              ❶

  def __init__(self, location, name, description):
    player.Player.__init__(self, location)     ❷
    self.name = name
    self.description = description
     
  def get_input(self):                  ❸
    if random.choice((0, 1)):             ❸
      return ("go " +                 ❸
        random.choice(self.location.exits()))  ❸
    else:                         ❸
      return ""                    ❸

  def look(self, player, noun):             ❹
    return [self.name, self.description]       ❹

  def get(self, player, noun):              ❹
    return ["The " + self.name +            ❹
      " growls at you."]                ❹

❶ 导入player 模块并继承Player

❷ 调用父类的方法__init__

❸ 怪物的大脑

❹ 让玩家能够与怪物交互的函数

您首先需要做的是导入模块player❶,以便创建类时继承player.Player而不是object。这样,需要查找Monster类未定义的属性或方法时,Python将在player.Player而不是object中查找。

初始化Monster类时,还需初始化其父类,确保一切都准备就绪❷。在这里,需要设置的主要内容是怪物所处的洞穴。

接下来是Player和Monster类的相似之处。怪物的AI是一个不同版本的get_input方法,它生成命令,而不是让玩家输入命令❸。当前,生成命令的方式比较简单:要么返回一个空字符串(即什么也不做),要么沿一个随机方向移动(利用Cave类的方法exits())。

玩家将试图与怪物交互,因此需要提供交互机制❹。look()是从Item类中复制而来的,它返回怪物的描述;而get()返回一条有趣的错误消息。

至此,您编写了Monster类,它与外部世界交互的能力与玩家相同,看到的信息也与玩家相同。这很重要,其原因有几个。下面就来深入介绍这些原因及其与面向对象设计的关系。

使用继承的第一个原因是,可将通用功能放在基类中,这减少了让程序运行需要处理的特殊情况。您不需要两个独立的游戏循环—分别用于玩家和怪物,也不需要编写类似于下面的代码:if player then: ...else if monster then: ....,相反您可以相同的方式对待玩家和 怪物。

使用继承的第二个原因是,它让程序更容易扩展;就这里而言,这相当于打造了一个接口,怪物、玩家以及您能想到的其他任何东西都可通过它来与外部世界交互。如果需要在游戏中添加第三类参与者,只需编写这类参与者特有的部分。

最后一个原因是,使用继承可极大地减少您必须编写的代码量,并让程序更容易理解得多;不管程序是不是面向对象的,更容易理解总是件好事。

需要指出的另一点是,这并非设计类的唯一方式。另一种可能更佳的方式是,创建第三个类(称之为Mobile或Actor),将玩家和怪物的通用功能都放在其中,再让玩家和怪物继承这个类。在面向对象设计中,这样的类通常被称为抽象类:您不创建其实例,而继承它并添加缺失的功能,再创建子类的实例。

注意:


面向对象术语可能令人迷惑,但见过几个示例后,您就会发现它们非常简单。只需将其与您非常熟悉的东西关联起来即可,如本章的Cave、Player和Monster类。

在这里,使用抽象类的优势并不那么明显,因为您只有两个类。然而,如果以后您发现有些功能Player类需要,而Monster类不应该有(或Monster类需要,而Player类不应该有),就可使用一个抽象类。

另一个设计要点是,到目前未知,您一直倾向于使用组合而不是继承。继承通常被称为“是一个”关系:玩家是一个参与者(actor),怪物也是。而组合是“有一个”关系:洞穴中有一个玩家,而玩家有大量物品(item)。在组合关系中,对象之间的耦合程度通常更低些:要相互交互,它们必须通过方法调用并查看对方的值;而不像继承那样自动将一个对象的方法插入到另一个对象中。在大多数情况下,都应使用组合,但在正确的情况下使用继承时,结果将大不相同。图6.3说明了本章游戏中的各种组合和继承关系。

图6.3 本章游戏中的一些继承和组合关系

编写好Player和Monster类后,需要对游戏处理玩家的方式做些修改。您不希望只从玩家那里获取输入——也应从怪物那里获取输入!您还希望将前面使用的函数都放在一个类中,让它们更容易正确地交互。为此,编写了程序清单6.14所示的类,您可使用它来设置游戏、创建洞穴系统以及获取输入(直到游戏结束)。

程序清单6.14 Game类

import random
import item, player, monster, cave
import random

class Game(object):
  def __init__(self):         ❶
    self.caves = self.create_caves()
    cave1 = self.caves[0]
    sword = item.Item("sword", "A pointy sword.", cave1)
    coin = item.Item("coin", "A shiny gold coin. "
      "Your first piece of treasure!", cave1)
    orc = monster.Monster(cave1, 'orc',
      'A generic dungeon monster')
  self.player = player.Player(cave1)

cave_names = [              ❶
  "Arched cavern",
  ...
  "Spooky Chasm",
]

def create_caves(self):         ❶
  random.shuffle(self.cave_names)
  caves = [cave.Cave(self.cave_names[0])]
  for name in self.cave_names[1:]:
    new_cave = cave.Cave(name)
    eligible_caves = [each_cave for each_cave in caves
              if each_cave.can_tunnel_to()]
    old_cave = random.choice(eligible_caves)
    directions = [direction for direction, each_cave
            in old_cave.tunnels.items()
            if each_cave is None]
    direction = random.choice(directions)
    old_cave.tunnel_to(direction, new_cave)
    caves.append(new_cave)
  return caves

def do_input(self):           ❷
  get_input_from = [thing for cave in self.caves
  for thing in cave.here
  if 'get_input' in dir(thing)]
for thing in get_input_from:
  thing.events = []
  thing.input = thing.get_input()

def do_update(self):           ❸
  things_to_update = [thing for cave in self.caves
    for thing in cave.here
    if 'update' in dir(thing)]
  for thing in things_to_update:
    thing.update(

def run(self):               ❹
  while self.player.playing:
    self.do_input()
    self.do_update()
if __name__ == '__main__':         ❺
  game = Game()
  game.run()

❶ 将游戏初始化代码放在__init__中

❷ 从玩家和怪物那里获取输入

❸ 根据输入采取相应的措施

❹ 新的游戏循环

❺ main( )部分简单得多

函数__init__包含以前都通过Player类运行的设置代码:创建物品、怪物和玩家。将这些代码放在__init__中合理得多❶。

然后,您询问每个游戏参与者想接着做什么❷。请注意,函数dir()可用于自定义类,也可用于Python内置对象。这里将输入与处理分离,可防止一个参与者根据其他参与者的后续行为做出决策,这让游戏对您和玩家来说都更容易理解。

您收集所有输入后,各个参与者将依次行动❸。这里的机制与以前相同:创建一个参与者列表,再迭代它们。如果要保证公平,可能应该打乱这个列表,让参与者的行动顺序是随机的,但怪物不在乎是否不公平。

接下来是主游戏循环❹。它与以前的主游戏循环相同,但调用do_input和do_update,且玩家和怪物将其输出存储在结果列表中。您还创建了一个独立的事件列表,用于存储每一轮发生的事情。

最终的好处是,__main__干净整洁得多❺。这里使用的所有棘手代码都被划分成块,并放到方法中。

您需要做的最后一项工作是,在Player类中添加一个update函数。代码检查游戏中的所有对象时,将认为玩家以及从Player派生而来的对象都需要更新:

Class Player(object):
  ...
  def __init__(self, location):
    self.name = "Player"
    self.description = "The Player"
...
def update(self):
  self.result = self.process_input(self.input)

这个方法调用process_input,传入玩家的输入,并将返回值(一个字符串列表)存储到self.result中。您还需要给Player类添加一个name属性,因为Monster试图执行命令时,将对Player调用process_input。

如果您现在运行这个程序,将在当前洞穴中看到一个兽人(orc)。按回车键多次以模拟等待一段时间,而兽人将离开当前洞穴。如果您四处搜索,将发现兽人像无头苍蝇一样在洞穴迷宫中游荡。然而,除非您希望这个游戏像欧洲艺术剧院那样做徒劳的探索,最好再添加一些有趣的游戏元素。

本章游戏的最后一部分是让玩家和怪物相互攻击。对游戏来说,竞争氛围必不可少:格斗能力、速度、谁建造的城市最大、谁建造的房子最好等。在这里,您编写的是一个地下城冒险游戏,因此格斗必不可少:《龙与地下城》玩家都希望能够痛击兽人。鉴于格斗将在玩家和怪物之间展开,您首先给Player类添加一个attack方法,如程序清单6.15所示。

程序清单6.15 攻击其他参与者

class Player(object):

   def __init__(self, location):
  self.name = "Player"               ❶
  self.description = "The Player"         ❶
  self.hit_points = 3                ❶
  self.events = []                  ❶
...

def attack(self, player, noun):
  if player == self:                 ❻
    return ["You can't attack yourself!"]    ❻
  hit_chance = 2                    ❷
  has_sword = [i for i in player.inventory     ❷
         if i.name == 'sword']         ❷
  if has_sword:                     ❷
    hit_chance += 2                  ❷

  roll = random.choice([1,2,3,4,5,6])        ❸
  if roll > hit_chance:                 ❸
    self.events.append("The " +           ❸
      player.name + " misses you!")        ❸
    return ["You miss the " + self.name]      ❸

  self.hit_points -= 1                  ❹
  if self.hit_points <= 0:                ❹
    return_value = ["You kill the " + self.name]  ❹
    self.events.append("The " +             ❹
      player.name + " has killed you!")       ❹
    self.die()                      ❹
    return return_value                 ❹

  self.events.append("The " +              ❺
    player.name + " hits you!")            ❺
  return ["You hit the " + self.name]          ❺

def die(self):                        ❻
  self.playing = False                  ❻
  self.input = ""                     ❻
  self.name = "A dead " + self.name           ❻

❶ 额外的玩家属性

❷ 计算玩家的命中点数

❸ 命中条件

❹ 报告死亡

❺ 报告攻击

❻ 处理死亡

您首先添加了方法attack()需要的一些属性❶。hit_points的含义显而易见,而events用于存储发生在玩家身上(或玩家看到)的事情。之前您已经添加了名称和描述,因此您可以轻松地处理格斗,无论攻击目标是怪物还是玩家。

您将使用一种简单的格斗机制:计算命中点数并掷骰子,如果摇出的点数未超过命中点数,玩家或怪物将被击中❷。命中点数通常为2,但如果攻击者有宝剑,命中点数将为4。别忘了,将对被攻击者(而不是攻击者)调用方法attack()。

接下来,您通过摇骰子来确定是否命中❸,摇出的点数为1~6的随机数。如果摇出的点数大于命中点数,则不能命中。然而,在方法结束前,您将发生的情况告知攻击者和被攻击者。

如果命中,被攻击者的生命值将减1。如果生命值降低到零,被攻击者就会死亡❹。如果玩家死亡,游戏将结束。但无论死的是玩家还是怪物,您都报告情况,但在调用函数die()前生成消息,因为这个函数可能修改消息(将消息改成You kill the A dead orc!)。

如果被攻击者没有死亡,结果与未命中很像:将发生的情况报告给攻击者和被攻击者,游戏继续❺。

鉴于怪物被打死时可能需要做大量清理工作,您将处理死亡的代码放在一个独立的方法中❻:将被攻击者的playing属性设置为False,撤销余下的命令,并修改其名称以指出它已死亡。您还进行合理性检查,以免参与者攻击自己。

这就是在游戏中支持格斗所需做的全部工作!鉴于妥善地封装了所有类,因此不需要对Cave、Item和Game类做任何修改。准确地说是不完全这样;如果您运行该程序,将发现一个显而易见的问题:怪物不会反击;更糟糕的是,怪物被玩家杀死后,还在四处乱窜!这两个问题都很容易解决,只需简单地升级怪物的AI即可,如程序清单6.16所示。

程序清单6.16 更新怪物的AI

def get_input(self):
  if not self.playing:                  ❶
    return ""                      ❶
  player_present = [x for x in self.location.here  ❷
            if x.name == "Player"]       ❷
  if player_present:                   ❷
    return "attack " + player_present[0].name    ❷
  if random.choice((0, 1)):
    return "go " + random.choice(self.location.exits())
  else:
    return ""

❶ 怪物不知道自己死了

❷ 怪物仇恨玩家

怪物死了不会说话。怪物死后,就不应再生成任何输入❶;如果没有这段代码,即便被玩家杀死,兽人也会在洞穴迷宫中乱窜。

接下来,您可以让怪物对玩家还以颜色❷。如果怪物在洞穴中看到玩家,您就让它发出攻击命令,就像玩家看到怪物时一样。玩家,吃我一拳!如果看不到玩家,怪物将在洞穴迷宫中随机漫游。

至此,您添加了冒险游戏的所有元素:可供玩家探索的洞穴迷宫、攻击玩家的怪物、可供玩家收集以帮助他实现目标的物品和财宝。有了本章的代码,再加上想象力,您几乎能够根据喜好创建任何冒险游戏。

本章介绍了一些类和方法,这些只是可加入到游戏中的类和方法的冰山一角。您可根据喜好,沿任何方向进一步开发这个游戏,下面是一些有关如何扩展该游戏的想法。

当前,只有一个兽人和两个物品。您可添加其他类型的怪物和财宝(威力更大的武器或以不同方式影响怪物的武器)。您还可以存储得分(该得分可通过Player类的score方法进行访问),并在玩家结束游戏或死亡时将得分打印出来。

您可以扩展Item类或方法Player.attack(),以添加其他有用的物品(如装甲或绳索)以及可将这些物品用在它们身上的东西。如果您要添加大量命中点数乘数不同的武器或装甲,可能需要采取一些办法,以简化计算打击力度(受损程度)增强(降低)的方式。

有些冒险游戏的重点是探险而非杀死怪物。您可以在设置阶段添加合适的描述,使用预先生成(而非随机生成的)洞穴迷宫,并添加处理特定事件的方法,如扬帆启航、升降城堡吊桥。

您可能想尝试给Item类添加各种方法,看看通过重写内置方法可做些什么。例如,您可给物品添加移动方法go()、north()等,并创建仅当玩家有合适的钥匙才能通过的门。还可以添加不动的怪物,玩家必须有合适的魔剑或密码才能通过;另外,还可在游戏找不到原始对象时,让其他物品来处理特定命令。

必须指出的是,本章并未介绍类的全部功能,而只设计95%的程序都要用到的类功能。还有其他高级类功能,如方法和属性缺失处理、特性(property)和混合类,本章后面将在适当的时候介绍它们。然而,如果您熟悉其他语言中的类,可能应该浏览Python文档,看看使用Python类还能做什么。

本章介绍了大量的面向对象主题和问题,探讨了类是如何让程序更清晰、更容易理解的。具体地说,我们介绍了以下内容。

本章远未介绍Python类系统的所有功能,但您已经牢固地掌握了有关如何使用类的基本知识,更重要的是如何使用类来解决程序面临的问题。在本书后面,您将进一步使用类及其高级功能。但是,下一章将介绍与函数紧密相关的另一项Python功能:生成器。


相关图书

深度学习的数学——使用Python语言
深度学习的数学——使用Python语言
动手学自然语言处理
动手学自然语言处理
Web应用安全
Web应用安全
Python高性能编程(第2版)
Python高性能编程(第2版)
图像处理与计算机视觉实践——基于OpenCV和Python
图像处理与计算机视觉实践——基于OpenCV和Python
Python数据科学实战
Python数据科学实战

相关文章

相关课程