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

简介:

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

1.3 Python中的对象

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

1.3.1 在面向对象之前是什么

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

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

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

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

# Structured program
# Function definition
def PrintName(name):
    print("The name is " + name + ".")
# Start of program
PrintName("Jane Doe")

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

The name is Jane Doe.
Pyhon中的注释行都是以#字符开头的。

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

def PrintName(name):
    print("The name is " + name + ".")
print("END")
PrintName("Jane Doe")

输出是:

END
The name is Jane Doe.

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

PrintName("Jane Doe")
def PrintName(name):
    print("The name is " + name + ".")

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

Traceback (most recent call last):
  File "FunctionDemo.py", line 4, in <module>
    PrintName("Jane Doe")
NameError: name 'PrintName' is not defined
当使用IDLE保存源代码的时候,确保要包含扩展名.PY,因为IDLE不会自动添加扩展名。

顺序式编程

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

10 print "I am freaking out!"
20 goto 10
你是否真正认为这段顺序式代码很有趣呢?我是这么认为的。它把我带回到了几年之前。有一款叫作QB64(www.qb64.net)的不错的编译器(并且是免费的),它支持BASIC、QBASIC以及QuickBasic(它是结构式的,但不是顺序式的)的所有老式的风格。此外,QB64支持OpenGL,因此,它潜在性地支持高级图形和游戏设置,并且支持BASIC的老式变体。

助记式编程

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

mov ax, 13h
int 10h
有一个有趣的历史性站点,专门介绍了VGA mode 13h编程: http://www.delorie.com/djgpp/doc/ug/graphics/vga.html。

“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。

现实世界

如果你喜欢电子工程和汇编语言这个主题,那么有一个和老式的工作对应的现代工种,这就是设备驱动编程。如今,这已经成为一种魔法,专门为那些真正理解硬件的工程师而保留。因此,你可以看到,如果你对这个工作感兴趣,学习汇编语言对此是非常有益处的。

1.3.2 接下来是什么

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

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

# Funny syntax error example
# Bad function!
def ErrorProne():
printgobblegobble("Hello there!")
print("See, nothing bad happened. You worry too much!")

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

See, nothing bad happened. You worry too much!

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

Traceback (most recent call last):
  File "ErrorProne.py", line 9, in <module>
    ErrorProne()
  File "ErrorProne.py", line 5, in ErrorProne
    printgobblegobble("Hello there!")
NameError: global name 'printgobblegobble' is not defined

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

Behold:
print = 10
print(print)

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

Traceback (most recent call last):
  File "ErrorProne.py", line 8, in <module>
    print(print)
TypeError: 'int' object is not callable

这条错误的意思是,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这样的引擎的在线游戏装备库,我们当然可以想象会发生这种情况。

1.3.3 OOP:Python的方式

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

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

class Bug(object):
    legs = 0
    distance = 0
    def __init__(self, name, legs):
        self.name = name
        self.legs = legs
    def Walk(self):
        self.distance += 1
    def ToString(self):
        return self.name + " has " + str(self.legs) + " legs" + \
               " and taken " + str(self.distance) + " steps."

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

多态

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

def __init__(self, name="Bug", legs=6):
    self.name = name
    self.legs = legs

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

def Walk(self,distance=1):
    self.distance += distance

数据隐藏(封装)

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

def GetDistance(self):
    return p_distance
def SetDistance(self, value):
    p_distance = value

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

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

class Car(Vehicle):

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

class Car(Body,Engine,Suspension,Interior):

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

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

return super().ToString()

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

1.3.4 单继承

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

class Point():
    x = 0.0
    y = 0.0
    def __init__(self, x, y):
        self.x = x
        self.y = y
        print("Point constructor")
    def ToString(self):
        return "{X:" + str(self.x) + ",Y:" + str(self.y) + "}"
class Circle(Point):
    radius = 0.0
    def __init__(self, x, y, radius):
        super().__init__(x,y)
        self.radius = radius
        print("Circle constructor")
    def ToString(self):
        return super().ToString() + \
               ",{RADIUS=" + str(self.radius) + "}"

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

p = Point(10,20)
print(p.ToString())
c = Circle(100,100,50)
print(c.ToString())

这会得到如下输出。

Point constructor
{X:10,Y:20}
Point constructor
Circle constructor
{X:100,Y:100},{RADIUS=50}

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

多继承是一片沼泽。我建议尽可能避免使用它,并且尽可能保持类的简单和直接,大多数情况下,可能只有一个层级的继承。尽可能地给你的类众多的功能,而不是将它们划分到多个类中。

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

def __init__(self, x, y, radius):
    super().__init__(x,y)
    self.radius = radius

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

1.3.5 多继承

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

class Size():
    width = 0.0
    height = 0.0
    def __init__(self,width,height):
        self.width = width
        self.height = height
        print("Size constructor")
    def ToString(self):
        return "{WIDTH=" + str(self.width) + \
               ",HEIGHT=" + str(self.height) + "}"
class Rectangle(Point,Size):
    def __init__(self, x, y, width, height):
        Point.__init__(self,x,y)
        Size.__init__(self,width,height)
        print("Rectangle constructor")
    def ToString(self):
        return Point.ToString(self) + "," + Size.ToString(self)

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

class Rectangle(Point,Size):

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

def __init__(self, x, y, width, height):
    Point.__init__(self,x,y)
    Size.__init__(self,width,height)

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

s = Size(80,70)
print(s.ToString())
r = Rectangle(200,250,40,50)
print(r.ToString())

产生如下输出。

Size constructor
{WIDTH=80,HEIGHT=70}
Point constructor
Size constructor
Rectangle constructor
{X:200,Y:250},{WIDTH=40,HEIGHT=50}

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

相关文章
|
30天前
|
Java Python 开发者
Python 学习之路 01基础入门---【Python安装,Python程序基本组成】
线程池详解与异步任务编排使用案例-xian-cheng-chi-xiang-jie-yu-yi-bu-ren-wu-bian-pai-shi-yong-an-li
78 2
Python 学习之路 01基础入门---【Python安装,Python程序基本组成】
|
2天前
|
Python
Python从入门到精通:深入学习面向对象编程——2.1.2继承、封装和多态的概念
Python从入门到精通:深入学习面向对象编程——2.1.2继承、封装和多态的概念
|
2天前
|
存储 索引 Python
Python从入门到精通——1.3.1练习编写简单程序
Python从入门到精通——1.3.1练习编写简单程序
|
2天前
|
开发框架 前端开发 数据库
Python从入门到精通:3.3.2 深入学习Python库和框架:Web开发框架的探索与实践
Python从入门到精通:3.3.2 深入学习Python库和框架:Web开发框架的探索与实践
|
2天前
|
数据采集 数据可视化 数据处理
Python从入门到精通的文章3.3.1 深入学习Python库和框架:数据处理与可视化的利器
Python从入门到精通的文章3.3.1 深入学习Python库和框架:数据处理与可视化的利器
|
2天前
|
Java 数据库连接 数据处理
Python从入门到精通:3.1.2多线程与多进程编程
Python从入门到精通:3.1.2多线程与多进程编程
|
2天前
|
存储 网络协议 关系型数据库
Python从入门到精通:2.3.2数据库操作与网络编程——学习socket编程,实现简单的TCP/UDP通信
Python从入门到精通:2.3.2数据库操作与网络编程——学习socket编程,实现简单的TCP/UDP通信
|
8天前
|
机器学习/深度学习 数据可视化 数据挖掘
《Python 简易速速上手小册》第9章:数据科学和机器学习入门(2024 最新版)
《Python 简易速速上手小册》第9章:数据科学和机器学习入门(2024 最新版)
19 1
|
8天前
|
人工智能 数据挖掘 程序员
《Python 简易速速上手小册》第1章:Python 编程入门(2024 最新版)
《Python 简易速速上手小册》第1章:Python 编程入门(2024 最新版)
35 0
|
9天前
|
机器学习/深度学习 数据采集 算法
scikit-learn入门指南:从基础到实践
【4月更文挑战第17天】这篇指南介绍了scikit-learn,一个Python数据分析和机器学习的重要库。内容涵盖安装、数据加载与预处理、模型训练(如KNN分类器)、评估、调参优化及高级应用,如降维和聚类。通过实例展示了scikit-learn在分类任务中的使用,强调其在数据科学中的重要性。要深入了解,可参考官方文档和实践案例。