天天看点

《Python游戏编程入门》——1.3 Python中的对象

本节书摘来自异步社区《python游戏编程入门》一书中的第1章,第1.3节,作者[美]jonathan s. harbour ,李强 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。

python是面向对象编程语言,这意味着,它至少支持一些面向对象编程概念。现在,我们将花一些时间来介绍这些概念,因为这是一种编写代码的高效方式。面向对象编程(oop)是一种方法学,也就是做事情的方式。在计算机科学中,有几种较大的、“伞状的”方法学,也就是说,定义了编程语言的功能的方法学。要让我们的技能成为可以传播的,方法学对于这个产业来说很重要。如果每个公司使用他们自己的方法学,那么,为该公司工作的过程中所获取的技能,对于另一个不同的组织来说将会是无用的。软件工程也是一个充满挑战的领域,并且培训的成本很高,因此,对于这个领域相关的每个人(经验丰富的开发者、老板以及教授概念的讲师)来说,方法学都是有益的。

天生的好奇心,这是有天分的程序员的共同特点,如果你也有的话,那么,你肯定会问,在面向对象泛型之前,人们使用的是哪一种编程类型呢。让我们来了解一下这个主题,在我们还没有真正开始使用python之前,先说明一下为什么这个问题如此重要。在编程方法学方面,我们先搞清楚起源在哪里,才能够理解今天位于何处。

结构化编程

在oop之前,人们所采用的方法学叫作过程化编程(procedural programming)或结构化编程(structured programming),这意味着,在这种情况下使用的是过程和结构。过程通常叫作函数,并且,我们如今仍然在使用函数。是的,甚至在oop程序中,仍然有独立的函数,如main()。包含在一个对象中的函数,叫作方法,并且当作为对象的一部分讨论的时候,使用方法这个术语而不是函数。但是,在对象之外,函数仍然存在,并且这是从之前的“时代”(方法学)沿用而来的。

结构是复杂的用户定义类型(user-defined types,udt),它可以将很多的变量包含在一起。最流行的结构化语言是c。然而,结构化编程是一种历史悠久而且颇为成功的方法学,一直延续至今。结构化运动的时间是从20世纪80年代到20世纪90年代,当然,这个时间和其他的方法学的发展有一些重叠。在电子产业中,很多软件开发工具包(sdk)仍然按照结构化的方式来开发,提供了函数库来控制一个电子设备(例如,显卡或嵌入式系统)。可以说,c语言的开发(大概是在20世纪70年代)是以结构化编程为主要方式而进行的。c语言用来创建unix操作系统。

如下是python的结构化程序的一个快速示例。

这段程序产生如下的输出。

函数的定义以def开头,后面跟着函数名、参数和一个冒号。python中没有代码块符号,如c++中的开始花括号({)和结束花括号(})。在python中,函数的结尾是未定义的,假设函数在下一个未缩进的行之前结束。让我们做一点试验,来测试python的行为。如下还是我们的示例,不带任何的注释行。你认为它会输出什么?

输出是:

大多数的python初学者会对此感到惊奇。这里所发生的事情是,print ("end")行向左缩进,因此,它变成了程序的第一行,后面跟着第二行,即printname ("jane doe")。函数定义不被看作是主程序的部分,并且,只有当调用该函数的时候才会运行。如果我们像下面这样,把函数定义放在主程序的下方,会发生什么情况?

这段代码实际上会产生语法错误,因为无法找到printname函数。这就告诉我们,在调用函数之前python必须先解析它。换句话说,函数定义必须位于函数调用的“上方”。

顺序式编程

结构化编程是从早期的顺序式编程方法学发展而来的。这不是正式的教科书的说法,但却是更富有描述性的一种说法。顺序式程序要求在每行代码之前都要有行号。尽管跳转到程序的其他行也是可能的(使用goto或gosub命令),并且这是结构化编程的一个早期的发展方向,但是,顺序式程序倾向于陷入某种程度的复杂性,使得代码变得难以识别或无法修改。这个时候所导致的问题,称为“意大利面条式代码”,这是由于程序似乎要去向每个方向的“流”而导致的。两种最常用的顺序式语言是basic和fortran,并且这些语言的全盛期是20世纪70年代到20世纪80年代。随着开发者对于维护“意大利面条式代码”感到厌烦,人们迫切地需要进行范型迁移。随着诸如pascal和c这样的新的结构化语言的引入,结构化编程应运而生。

助记式编程

在顺序式编程之前,开发者编写的代码更接近于计算机硬件的层级,而他们使用的是汇编语言。有一个“汇编器”程序,就像是编译器一样,但是,它将会把助记式的指令直接转换为对象或二进制文件中的机器代码,准备好让处理器一次一个字节地运行它们。一条汇编式的助记式指令,直接关联到处理器所能够理解的一条机器指令。这就像是在说机器自身的语言,并且很有挑战性。在ms-dos的时代,这些汇编性的指令能够把显示模式转换成分辨率为320×200并且具有256(8位)色的图形模式,这对于20世纪90年代的ibm pc游戏来说已经很好了,因为这会很快。记住,在那个时代,我们没有今天这样的显卡,只有构建到rom bios中的“视频输出”以及操作系统所支持的各种模式。这就是那个时代的所有游戏开发者都喜欢的声名狼藉的“vga mode 13h”。

“ax”是一个16位的处理器寄存器,处理器上的实际的物理电路可以当作一种通用目的的“变量”对待,这里使用了你所熟悉的术语而没有使用电子工程的语言。还有其他3种通用目的寄存器:bx、cx和dx。它们自身都是从8位的intel处理器升级而来的,而后者拥有叫作a、b、c和d的寄存器。当发展到16位的时候,这些寄存器扩展为al/ah、bl/bh、cl/ch和dl/dh,它们分别表示每个16位寄存器的两个8位的部分。乍一听起来,这并不复杂。将一个值放到一个或多个这些变量寄存器之中,然后通过调用一个中断来“加载”一个过程。在vga模式更改的例子中,中断是10h。

我们已经简单地回顾了从过去到现在的编程方法学,以理解和掌握当今所拥有的工具和语言的方法,下面,我们来介绍一下当前的情况以及有些什么发展。如今,面向对象编程仍然是专业程序员所采用的最主要的方法学。它是microsoft的visual studio和.net framework等流行的工具的基础。如今的商业和科学领域中,最主要的编译型oop语言是c++、c#、basic(其现代变体是visual basic)以及java。当然还有其他的语言,但是,这些是最主要的。

python和lua都是脚本编程语言。和c++这样的编译型语言相比,python和lua的处理方式有很大不同,它们是解释型的,而不是编译型的。当你运行一个python程序的时候(扩展名为.py的一个文件),它不会进行编译,而会运行。你可能会在一个python函数中带入语法错误,但是,在调用该函数之前,python不会提示错误。

python或者这段程序中没有一个名为printgobblegobble()的函数,因此,这里应该产生一个错误。输出如下。

但是,如果添加了对errorprone()函数的调用,输出将会如下。

现在,对于python中这一貌似忽略的部分有一些限制。如果你明显错误地定义了一个变量,那么,在运行之前,它才会初次产生错误。在python中,还会因为做了另一件奇怪的事情而把事情搞砸,那就是,使用保留字作为变量:

第一行没问题,但是第二行导致了如下的错误。

这条错误的意思是,print变成了一个变量,确切地说,是一个整数,其值设置为10。然后,我们试图调用旧的print()函数,并且python无法得到它。因为旧的print()函数已经被忽略了。现在,这种奇怪的行为不再适用于python语言中的保留字了,如while、for、if等保留字,而只是适用于函数。当你发现python作为一种脚本语言有着巨大的灵活性的时候,我觉得你会感到惊讶的。

像gcc或visual c++这样的传统的编译器,甚至在考虑运行这样的代码的时候,你就会抓狂。毕竟,它们是编译器。在将程序转换成目标代码之前,它们完整地解析了程序的流程。这么做的缺点就是:编译器无法处理未知的东西,它们只能处理已知的东西,而脚本语言可以很好地处理未知的情况。

顺序式编程演变为结构化编程,结构化编程演变为oop,编程范型从oop开始的下一次演进也将继续保持同样的方式,在范型发生变化之前,当前的编程方法学中将会出现一些明显的改变的迹象。今天,发生在oop上的这些变化,可能会称为自适应编程(adaptive programming)。在当今快节奏的世界中,没有人会像我们以前编程的时候那样,坐在计算机前阅读wordperfect或lotus 1-2-3的200页的手册。还是有人会认为“阅读手册”是解决技术问题的有效方法,但是如今,即便是带有类似手册的产品也很少见了。如今,系统必须具有交互性和自适应性。超越oop的下一次演进,可能是面向实体编程(eop,entity oriented programming)。

想象一下,我们使用实体(使用简单规则来解决复杂问题的自包含对象)来编写代码,而不是使用包含了属性(变量)和方法(函数)的对象来编写代码。这似乎是a.i.的研究方向,而且应该能够与如今已有的oop很好地适应。实际上,已经有了一些早期的迹象出现了。听说过web service吗?web service是寄存在网上的自包含对象,程序可以使用它来执行独特的服务,而程序自身不知道如何进行这些服务。

这些web service可能会只是要求一个库存数据库的参数,并且返回与查询匹配的项目的列表。这种形式的程序交互,一定能够超越编写sql(structured query language,结构化查询语句,这是关系数据库的语言)!那么,将其带入到下一个层级如何?使用某种库或搜索引擎在线查询一个服务,而不是接入一个已知的服务,这会怎么样?

作为另一个可能的示例,假设有一个在线的、可以用于游戏中的游戏实体的库(很可能是由独立开发者或开源团队创建的),其中的实体将会带有其自己的美工素材(2d精灵、3d网状物、材质、音频剪辑等)以及自身的行为(例如一段python脚本)。需要某种格式的素材的一个已有的游戏引擎,可能会使用这种eop的概念来扩展游戏设置。假设你要玩一个游戏,诸如minecraft(www.minecraft.net)这样的某种世界构造游戏,并且,假设你是游戏中的某个新角色。因此,你向游戏提出查询:“我需要一把短的木头椅子”。在查询发出去后的片刻,一把短的木头椅子出现在你的游戏中。假设有一个用于minecraft这样的引擎的在线游戏装备库,我们当然可以想象会发生这种情况。

我们已经进行了足够的历史分析和思考,从而可以触发一些有想象力的思路。现在,让我们来介绍一些具体而实际的内容,即当前的oop方法学及其在python中的实现。或者换句话说,我们用python来创建对象。python确实支持oop特性,但是,它不像是高度特定性的语言c++那样,在各个程度上支持oop。在开始之前,让我们先来了解一些术语。类是一个对象的蓝图。类不能做任何事情,因为它是一个蓝图。只有在运行时创建对象的时候,对象才会存在。因此,当我们编写类代码的时候,它只是一个类的定义,而不是一个对象。只有在运行时,通过类的蓝图来创建对象的时候,它才是真正的对象。类的函数也叫作方法。类的变量通常作为属性来访问(有一种方法用来获取或设置一个变量的值)。当创建一个对象的时候,类实例化为该对象。

让我们来了解python的oop特性的一些具体内容。示例如下。

每个定义的行末,都必须有一个冒号。关键字self描述当前的类,这和它在c++中的作用是相同的。所有的类变量前面必须有一个“self”,以便可以认出这是类的成员;否则,它们将会被当作局部变量。def __init__(self)这一行开始了类的构造函数,这是在类实例化的时候运行的第一个方法。在构造函数之外,可以声明类变量并且在声明的时候进行初始化。

多态

术语多态表示有“多种形式”或“多种形状”,因此,多态是指具备多种形态的能力。在类的环境中,这意味着我们可以使用具有多种形态的方法,也就是说,参数的多种不同的集合。在python中,我们可以使用可选的参数来让方法具备多种功能。新的bug类的构造函数,可以使用可选的参数来进行变换,如下所示:

同样,walk()方法可以升级以支持一个可选的参数:

数据隐藏(封装)

python不允许变量和方法声明为私有的或受保护的,因为python中的所有内容都是公有的。但是,如果你想要让代码像是数据隐藏一样地工作,这也是可以办到的。例如,如下这段代码可以用来访问或修改distance变量(我们假设它是私有的,即便它不是)。

从数据隐藏的角度来看,你可以将distance重命名为p_distance(使其看上去像是私有变量),然后,使用这两个方法来访问它。也就是说,如果数据隐藏对于你的程序来说很重要的话,可以这么做。

继承

python支持基类的继承。当定义一个类的时候,基类包含在圆括号中:

此外,python支持多继承,也就是说,一个子类可以继承自多个父类或基类。例如:

只要每个父类中的变量和方法与其他的变量和方法不冲突,新的子类可以访问它们而毫无问题。但是,如果有任何的冲突,来自父类的冲突变量和方法在继承顺序中具有优先性。

当一个python类继承自一个基类,父类所有的变量和方法都是可用的。变量可以使用,方法可以覆盖。当调用一个基类的构造函数或任何方法的时候,我们可以使用super()来引用基类:

但是,当涉及多继承的时候,当共享相同的变量名或方法名的时候,必须使用父类的名称,以避免混淆。

我们先来看看单继承的示例。如下是一个point类,以及继承自它的一个circle类。

我们可以直接测试这些类:

这会得到如下输出。

我们看到point的功能很简单,但是,circle先调用point的构造函数,然后才调用自己的构造函数,然后复杂地调用point的tostring()并添加自己的新的radius属性。这真的有助于我们了解,为什么所有的类都有一个tostring()方法。

现在,当创建circle类的时候,调用构造函数并传递给它3个参数(100,100,50)。注意,调用了父类(point)的构造函数来处理x和y参数,而radius参数在circle中处理:

super()调用了point类的构造函数,point类是circle类的父类或基类。当使用单继承的时候,这种做法的效果令人惊奇。

尽管多继承是一片沼泽,但至少还是要展示一下它是如何工作的。使用多继承的时候,我们基本上不会使用super()来调用父类中的任何内容,除非每个父类中的变量和方法都是独特的。这里有另一对类,它们构建在前面已经给出的两个类的基础之上。还记得吧,我警告过你,python是一种看上去很奇怪的语言。我们现在来看看。别忘了,python是一种脚本语言,而不是编译型语言。python代码是在运行时解释的。

size类是一个新的辅助类,而rectangle是我们这个示例中真正的焦点。这里,rectangle将继承自point和size:

point是早就定义了的,而size刚刚定义。现在,我们应该可以开始使用point.x、point.y、size.width和size.height,以及每个类中的tostring()方法了。python应该不会抱怨。但是,思路是通过调用父类的构造函数来自动初始化父类。否则,我们会丧失oop的所有优点,并且只是在编写结构化的代码。因此,rectangle构造函数必须按照名称来调用每个父类的构造函数:

注意,x和y传递给了point.__init__(),而width和height传递给了size.__init__()。这些变量在它们各自的类中正确地初始化。当然,我们可以只是在rectangle中定义x、y、width和height,但是,这只是一个演示。通常,为了保持代码简单,我们不建议那么做。在真正的编程中,绝不要以这种方式使用继承。这里只是为了说明多继承。测试一下新的size和rectangle类:

产生如下输出。

现在,这真的有点意思了。size足够简单,很容易理解,但是看一下rectangle的输出。我们调用了point的构造函数和size的构造函数,这完全是按照计划进行的。此外,tostring()方法有效地组合了point.tostring()和size.tostring()各自的输出。