面向对象编程


面向对象编程

在开始详细探讨面向对象编程(OOP)在 Processing 中如何工作之前,让我们先对"对象"本身进行一个简短的概念性讨论。想象一下,你不是在 Processing 中编程,而是在写一个关于你一天的程序,一份指令清单。它可能是这样的:

醒来。
吃早餐。
上班或上学。
……

这里涉及什么?具体来说,涉及哪些事物?首先,虽然从我们写上述指令的方式来看可能不明显,但主要的事物是,一个人类,一个人。你表现出某些属性。你看起来有某种样子;也许你有黑色头发,戴眼镜,看起来有点书呆子气。你也有能力做事情,比如醒来(想必你也能睡觉)、吃饭。对象就像你一样,是一个有属性并能做事情的事物。

那么这与编程有什么关系?对象的属性就是变量;对象能做的事情就是函数。面向对象编程是所有编程基础的结合:数据和功能。

让我们为一个非常简单的人对象绘制数据和函数:

人类数据

  • 身高
  • 体重
  • 性别
  • 肤色
  • 发色

人类函数

  • 睡觉
  • 醒来
  • 吃饭
  • 上班或上学

现在,在我们深入讨论之前,需要先进行一个简短的形而上学讨论。上述结构并不是人类本身,它只是描述了人类背后的想法或概念。它描述了成为人类意味着什么。成为人类就是要有身高、头发、要睡觉、要吃饭,等等。这对编程对象来说是一个关键的区别。这个人类模板被称为类。类与对象不同。你是一个对象。我是一个对象。地铁上的那个人是一个对象。阿尔伯特·爱因斯坦是一个对象。我们都是人,是人类这个想法的真实世界实例。

想象一个月饼模具。月饼模具可以制作月饼,但它本身不是月饼。月饼模具是类,月饼是对象。

使用对象

在查看类本身的实际编写之前,让我们先简要看看在主程序(即 setup()draw())中使用对象如何让代码变得更清晰。

考虑一个简单草图的伪代码,该草图在窗口中水平移动一个矩形(我们将这个矩形视为"小汽车")。

数据(全局变量):

  • 汽车颜色
  • 汽车 x 位置
  • 汽车 y 位置
  • 汽车 x 速度

设置:

  • 初始化汽车颜色
  • 初始化汽车位置到起始点
  • 初始化汽车速度

绘制:

  • 填充背景
  • 在位置显示汽车及其颜色
  • 按速度递增汽车位置

要实现上述伪代码,我们会在程序顶部定义全局变量,在 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 中使用对象如何使代码变得清晰、可读。艰苦的工作在于编写对象模板,即类本身。当你第一次学习面向对象编程时,通常有用的练习是采用一个没有对象编写的程序,在不改变功能的情况下,使用对象重写它。我们将用小汽车示例做这件事,以面向对象的方式重新创建完全相同的外观和行为。

所有类都必须包含四个元素:名称、数据初始化和方法。(从技术上讲,唯一实际必需的元素是类名,但做面向对象编程的目的是包含所有这些。)

以下是我们如何将简单非面向对象草图中的元素放入小汽车类中,从中我们将能够制作小汽车对象:

简单非 OOP 小汽车

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) 指定名称。然后我们在缩进块中封装类的所有代码。类名传统上是大写的(以区别于变量名,变量名传统上是小写的)。
  • 数据初始化:Python 会在你从类创建对象时自动调用名为 __init__ 的方法。你应该在类中定义这个方法并在这里初始化其数据。当你在方法内部编写代码时,有一个特殊的词 self,它允许你设置定义在类每个单独实例上的变量。以这种方式定义的变量通常被称为"实例变量"。(__init__ 方法类似于其他面向对象语言中的"构造函数",如 Java 和 C++。)
  • 功能:我们可以通过编写方法为对象添加功能。每个方法(包括 __init__)的第一个参数应该是 self。Python 在你调用方法时自动将此参数传递给方法;它允许你在方法内部访问每个对象的实例变量。

注意,类的代码作为自己的块存在,可以放在 setup()draw() 之外的任何地方,只要在你使用它之前定义即可。

代码对比说明:

  • 左侧:非面向对象方式,使用全局变量和独立函数
  • 右侧:面向对象方式,将数据和功能封装在类中
  • 主要区别:右侧代码使用 self 访问实例变量,代码结构更清晰

使用对象:详细说明

早些时候,我们快速了解了对象如何大大简化 Processing 草图的主要部分(即 setup()draw())。让我们看看上述步骤背后的详细信息,说明如何在草图中使用对象。

步骤 1. 实例化对象变量

为了初始化变量(即给它一个起始值),我们使用赋值操作——变量等于某物。对于其他 Python 数据类型,它看起来像这样:

# 变量初始化   
var = 10   # var 等于 10

初始化对象有点复杂。与整数或浮点数不同,我们不只是简单地给它赋值,我们必须实例化对象。我们通过像函数一样调用类名来做到这一点。

# 对象实例化
myCar = Car()

在上面的例子中,"myCar" 是对象变量名,"=" 表示我们将其设置为等于某物,那个某物是汽车对象的新实例。我们真正在做的是初始化一个汽车对象。当你初始化一个原始变量时,比如整数,你只是将它设置为等于一个数字。但对象可能包含多个数据片段。回想汽车类,我们看到这行代码调用了 __init__ 方法,一个初始化对象所有变量并确保汽车对象准备就绪的特殊方法。

步骤 2. 使用对象

一旦我们成功实例化了一个对象,我们就可以使用它。使用对象涉及调用内置在该对象中的方法。人类对象可以吃饭,汽车可以驾驶,狗可以吠叫。通过点语法调用对象内部的函数:variableName.objectFunction(Function Arguments)

在汽车的情况下,可用的函数都没有参数,所以它看起来像这样:

# 函数通过"点语法"调用。 
myCar.drive()
myCar.display()

__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__ 方法中添加一个参数?"

尽管如此,这是学习的重要技能,最终,这是使面向对象编程强大的事情之一。但现在,这可能感觉很痛苦。让我们看看参数在这种情况下的工作原理。

__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()
   # 确定子弹是否击中敌人的代码