在开始详细探讨面向对象编程(OOP)在 Processing 中如何工作之前,让我们先对"对象"本身进行一个简短的概念性讨论。想象一下,你不是在 Processing 中编程,而是在写一个关于你一天的程序,一份指令清单。它可能是这样的:
这里涉及什么?具体来说,涉及哪些事物?首先,虽然从我们写上述指令的方式来看可能不明显,但主要的事物是你,一个人类,一个人。你表现出某些属性。你看起来有某种样子;也许你有黑色头发,戴眼镜,看起来有点书呆子气。你也有能力做事情,比如醒来(想必你也能睡觉)、吃饭。对象就像你一样,是一个有属性并能做事情的事物。
那么这与编程有什么关系?对象的属性就是变量;对象能做的事情就是函数。面向对象编程是所有编程基础的结合:数据和功能。
让我们为一个非常简单的人对象绘制数据和函数:
现在,在我们深入讨论之前,需要先进行一个简短的形而上学讨论。上述结构并不是人类本身,它只是描述了人类背后的想法或概念。它描述了成为人类意味着什么。成为人类就是要有身高、头发、要睡觉、要吃饭,等等。这对编程对象来说是一个关键的区别。这个人类模板被称为类。类与对象不同。你是一个对象。我是一个对象。地铁上的那个人是一个对象。阿尔伯特·爱因斯坦是一个对象。我们都是人,是人类这个想法的真实世界实例。
想象一个月饼模具。月饼模具可以制作月饼,但它本身不是月饼。月饼模具是类,月饼是对象。
在查看类本身的实际编写之前,让我们先简要看看在主程序(即 setup()
和 draw()
)中使用对象如何让代码变得更清晰。
考虑一个简单草图的伪代码,该草图在窗口中水平移动一个矩形(我们将这个矩形视为"小汽车")。
要实现上述伪代码,我们会在程序顶部定义全局变量,在 setup()
中初始化它们,并在 draw()
中调用函数来移动和显示汽车。像这样:
c = color(0)
x = 0.0
y = 100.0
speed = 1.0
def setup():
size(200, 200)
def draw():
background(255)
move()
display()
def move():
global x
x = x + speed
if x > width:
x = 0
def display():
fill(c)
rect(x, y, 30, 10)
面向对象编程允许我们将所有变量和函数从主程序中取出,并将它们存储在汽车对象内部。汽车对象将了解其数据——颜色、位置、速度。对象还将了解它能做的事情,即方法(对象内部的函数)——汽车可以驾驶,也可以被显示。
使用面向对象设计,伪代码改进为这样:
注意我们从第一个示例中移除了所有全局变量。我们没有为小汽车颜色、小汽车位置和小汽车速度设置单独的变量,现在我们只有一个变量,一个小汽车变量!我们没有初始化这三个变量,而是初始化一个东西,小汽车对象。那些变量去哪里了?它们仍然存在,只是现在它们生活在小汽车对象内部(并且将在小汽车类中定义,我们稍后会讲到)。
超越伪代码,草图的实际主体可能看起来像这样:
myCar = Car()
def draw():
background(255)
myCar.drive()
myCar.display()
我们稍后会详细讨论上述代码,但在此之前,让我们看看小汽车类本身是如何编写的。
上面的简单小汽车示例演示了在 Processing 中使用对象如何使代码变得清晰、可读。艰苦的工作在于编写对象模板,即类本身。当你第一次学习面向对象编程时,通常有用的练习是采用一个没有对象编写的程序,在不改变功能的情况下,使用对象重写它。我们将用小汽车示例做这件事,以面向对象的方式重新创建完全相同的外观和行为。
所有类都必须包含四个元素:名称、数据初始化和方法。(从技术上讲,唯一实际必需的元素是类名,但做面向对象编程的目的是包含所有这些。)
以下是我们如何将简单非面向对象草图中的元素放入小汽车类中,从中我们将能够制作小汽车对象:
c = color(255)
xpos = 100
ypos = 100
xspeed = 1
def setup():
size(200, 200)
def draw():
background(0)
display()
drive()
def display():
rectMode(CENTER)
fill(c)
rect(xpos, ypos, 20, 10)
def drive():
xpos = xpos + xspeed
if xpos > width:
xpos = 0
class Car(object):
def __init__(self):
self.c = color(255)
self.xpos = 100
self.ypos = 100
self.xspeed = 1
def display(self):
rectMode(CENTER)
fill(self.c)
rect(self.xpos, self.ypos, 20, 10)
def drive(self):
self.xpos += self.xspeed
if self.xpos > width:
self.xpos = 0
class WhateverNameYouChoose(object)
指定名称。然后我们在缩进块中封装类的所有代码。类名传统上是大写的(以区别于变量名,变量名传统上是小写的)。__init__
的方法。你应该在类中定义这个方法并在这里初始化其数据。当你在方法内部编写代码时,有一个特殊的词 self
,它允许你设置定义在类每个单独实例上的变量。以这种方式定义的变量通常被称为"实例变量"。(__init__
方法类似于其他面向对象语言中的"构造函数",如 Java 和 C++。)__init__
)的第一个参数应该是 self
。Python 在你调用方法时自动将此参数传递给方法;它允许你在方法内部访问每个对象的实例变量。注意,类的代码作为自己的块存在,可以放在 setup()
和 draw()
之外的任何地方,只要在你使用它之前定义即可。
代码对比说明:
self
访问实例变量,代码结构更清晰早些时候,我们快速了解了对象如何大大简化 Processing 草图的主要部分(即 setup()
和 draw()
)。让我们看看上述步骤背后的详细信息,说明如何在草图中使用对象。
为了初始化变量(即给它一个起始值),我们使用赋值操作——变量等于某物。对于其他 Python 数据类型,它看起来像这样:
# 变量初始化
var = 10 # var 等于 10
初始化对象有点复杂。与整数或浮点数不同,我们不只是简单地给它赋值,我们必须实例化对象。我们通过像函数一样调用类名来做到这一点。
# 对象实例化
myCar = Car()
在上面的例子中,"myCar" 是对象变量名,"=" 表示我们将其设置为等于某物,那个某物是汽车对象的新实例。我们真正在做的是初始化一个汽车对象。当你初始化一个原始变量时,比如整数,你只是将它设置为等于一个数字。但对象可能包含多个数据片段。回想汽车类,我们看到这行代码调用了 __init__
方法,一个初始化对象所有变量并确保汽车对象准备就绪的特殊方法。
一旦我们成功实例化了一个对象,我们就可以使用它。使用对象涉及调用内置在该对象中的方法。人类对象可以吃饭,汽车可以驾驶,狗可以吠叫。通过点语法调用对象内部的函数:variableName.objectFunction(Function Arguments)
在汽车的情况下,可用的函数都没有参数,所以它看起来像这样:
# 函数通过"点语法"调用。
myCar.drive()
myCar.display()
在上面的例子中,汽车对象是这样初始化的:
myCar = Car()
这是我们在学习 OOP 基础时的一个有用的简化。尽管如此,上面的代码有一个相当严重的问题。如果我们想写一个有两个汽车对象的程序怎么办?
# 创建两个汽车对象
myCar1 = Car()
myCar2 = Car()
这实现了我们的目标;代码将产生两个汽车对象,一个存储在变量 myCar1 中,一个在 myCar2 中。但是,如果你研究汽车类,你会注意到这两辆汽车将是相同的:每辆都是白色的,从屏幕中间开始,速度为 1。用英语来说,上面读作:
制作一辆新车。
我们想要说的是:
制作一辆新的红色汽车,位置在 (0,10),速度为 1。
这样我们也可以说:
制作一辆新的蓝色汽车,位置在 (0,100),速度为 2。
我们可以通过在括号内放置参数来做到这一点:
myCar = Car(color(255,0,0), 0, 100, 2)
__init__
方法必须重写以包含这些参数:
def __init__(self, c, xpos, ypos, xspeed):
self.c = c
self.xpos = xpos
self.ypos = ypos
self.xspeed = xspeed
根据我的经验,使用 __init__
方法参数来初始化对象变量可能有点令人困惑。请不要责怪自己。代码看起来很陌生,可能看起来非常冗余:"对于我想要的每个变量,我都必须在 __init__
方法中添加一个参数?"
尽管如此,这是学习的重要技能,最终,这是使面向对象编程强大的事情之一。但现在,这可能感觉很痛苦。让我们看看参数在这种情况下的工作原理。
在上面的例子中,汽车对象是这样初始化的:
myCar = Car()
这是我们在学习 OOP 基础时的一个有用的简化。尽管如此,上面的代码有一个相当严重的问题。如果我们想写一个有两个汽车对象的程序怎么办?
# 创建两个汽车对象
myCar1 = Car()
myCar2 = Car()
这实现了我们的目标;代码将产生两个汽车对象,一个存储在变量 myCar1 中,一个在 myCar2 中。但是,如果你研究汽车类,你会注意到这两辆汽车将是相同的:每辆都是白色的,从屏幕中间开始,速度为 1。用英语来说,上面读作:
制作一辆新车。
我们想要说的是:
制作一辆新的红色汽车,位置在 (0,10),速度为 1。
这样我们也可以说:
制作一辆新的蓝色汽车,位置在 (0,100),速度为 2。
我们可以通过在括号内放置参数来做到这一点:
myCar = Car(color(255,0,0), 0, 100, 2)
__init__
方法必须重写以包含这些参数:
def __init__(self, c, xpos, ypos, xspeed):
self.c = c
self.xpos = xpos
self.ypos = ypos
self.xspeed = xspeed
根据我的经验,使用 __init__
方法参数来初始化对象变量可能有点令人困惑。请不要责怪自己。代码看起来很陌生,可能看起来非常冗余:"对于我想要的每个变量,我都必须在 __init__
方法中添加一个参数?"
尽管如此,这是学习的重要技能,最终,这是使面向对象编程强大的事情之一。但现在,这可能感觉很痛苦。让我们看看参数在这种情况下的工作原理。
参数是在函数体内使用的局部变量,当函数被调用时会被值填充。在例子中,它们只有一个目的,就是初始化对象内部的变量。这些是重要的变量,汽车的实际颜色,汽车的实际 x 位置,等等。__init__
方法的参数只是临时的,仅存在于将值从对象制作的地方传递到对象本身。当然,你可以将这些函数参数命名为任何你想要的——它们不必与实例变量具有相同的名称。但是,建议选择一个对你有意义的名称,并且保持一致。
假设这是你第一次体验面向对象编程,重要的是要慢慢来。这里的例子只使用一个类,最多从该类制作两三个对象。尽管如此,没有实际的限制。Processing 草图可以包含你愿意编写的任意多个类。
例如,如果你在编程太空侵略者游戏,你可能会创建一个太空船类、一个敌人类和一个子弹类,为游戏中的每个实体使用一个对象。
此外,虽然不是原始的,但类就像整数和浮点数一样是数据类型。由于类是由数据组成的,对象因此可以包含其他对象!例如,让我们假设你刚刚完成了叉子和勺子类的编程。继续到餐具设置类,你可能会在该类本身中包含叉子对象和勺子对象的变量。这在面向对象编程中是完全合理的,也很常见。
class PlaceSetting(object):
def __init__(self):
fork = Fork()
spoon = Spoon()
对象,就像任何数据类型一样,也可以作为参数传递给函数。在太空侵略者游戏例子中,如果太空船向敌人射击子弹,我们可能想在敌人类内部编写一个函数来确定敌人是否被子弹击中。
def hit(self, bullet):
bulletX = bullet.getX()
bulletY = bullet.getY()
# 确定子弹是否击中敌人的代码