Python的非程序员教程3
面向对象编程 (OOP)
到目前为止,你所进行的编程是过程式编程。然而,今天的许多程序都是面向对象的。了解这两种编程方式,并知道它们之间的区别是非常重要的。许多计算机科学中的重要语言,如 C++ 和 Java,通常使用面向对象编程(OOP)方法。
对于初学者和非程序员来说,OOP 的概念常常让人感到困惑和复杂。这是正常的。如果你感到困惑或无法理解,不要气馁。如果这一章没有帮助你,有很多其他的资源可以帮助你克服可能遇到的问题。
本章将分为不同的课程。每节课将以不同的方式解释 OOP,确保 OOP 被尽可能全面地讲解,因为它非常重要。在课程之前,有一个介绍部分,解释了 OOP 的关键概念、术语和其他重要的领域,理解这些是理解每节课的基础。
简介
将过程视为一个函数。函数有一个特定的目的。这个目的可能是获取输入、执行数学计算、显示数据,或处理文件中的数据。通常,过程使用与代码分离的数据进行操作。这些数据常常在过程之间传递。当程序变得更大、更复杂时,这可能会引发问题。例如,你设计了一个程序,使用变量存储关于产品的信息。当客户请求产品信息时,这些变量会被传递给不同的函数以完成不同的任务。随着更多的数据存储在这些产品中,你决定将信息存储在列表或字典中。为了让程序继续正常运行,你现在必须编辑每个接收变量的函数,使它们接受并操作列表或字典。想象一下,如果程序有数百兆字节大,且包含数百个文件,这将需要多长时间来编辑!这将让你崩溃!更不用说,代码中几乎肯定会出错,仅仅因为大量的工作和输入错误的可能性。这显然不是最优的。过程式编程以过程或函数为中心。但 OOP 以创建对象为中心。记得过程式编程将数据和代码分开吗?记得那个庞大的程序需要花费多长时间来编辑吗?那么,想象一下,将这些文件和数据“合并”成一个“实体”的对象。在技术上,对象是一个包含数据和过程(代码、函数等)的实体。
对象中的数据称为数据属性。
对象中的函数或过程称为方法。
可以将数据属性看作是变量。
可以将方法看作是函数或过程。
让我们看一个简单的日常例子。假设你卧室里的灯和开关。数据属性如下所示:
light_on
(True 或 False)switch_position
(Up 或 Down)electricity_flow
(True 或 False)
方法如下所示:
move_switch
change_electricity_flow
数据属性可能是不可见的。例如,你不能直接看到电流流向灯泡。你知道灯亮着是因为电流在流动,但你无法直接看到电流流动。然而,你可以看到开关的位置(switch_position
),也可以看到灯是否亮着(light_on
)。一些方法是私有的,意味着你不能直接更改它们。例如,除非你切断灯具中的电线(为了本示例假设你不知道电线的存在),否则你无法直接改变电流的流动。你也不能直接更改灯是否亮着(而且不,不能拧下灯泡!)。然而,你可以通过使用对象中的方法间接地改变这些属性。如果你没有缴纳电费,change_electricity_flow
方法将会把 electricity_flow
属性的值更改为 False
。如果你翻动开关,move_switch
方法将会改变 light_on
属性的值。
到这里,你可能会想,“这和 Python 有什么关系?”或者,“我明白了,但是如何在代码中实现对象呢?”好吧,我们差不多要到达那个点了!在我们深入代码之前,还有一个概念需要解释。
在 Python 中,对象的数据属性和方法是由类来定义的。可以把类看作是对象的蓝图。例如,你的家——你居住的物体——你也可以称之为你的住所、平房、单元房等等——它是根据一套蓝图建造的;这些蓝图就相当于设计你家、住所或平房的类。
再次强调,类告诉我们如何创建一个对象。在技术术语中,这一点非常重要:类定义了对象内部的数据属性和方法。
为了创建一个类,我们需要编写一个类定义。类定义是一组语句,定义了对象的数据属性和方法。
第一课
以下是一个过程式编程示例,执行简单的数学运算,针对用户输入的单个数字。
# 程序由 Mitchell Aikens 编写
# 无版权声明
# 2012
# 程序1
def main():
try:
# 获取要操作的数字
num = float(input("请输入一个数字进行操作。\n"))
# 通过程序2操作后的结果
addednum = addfive(num)
# 通过程序3操作后的结果
multipliednum = multiply(addednum)
# 将值传递给程序4
display(multipliednum)
# 处理来自用户输入非数字的异常
except ValueError:
print("请输入一个有效的数字。\n")
# 重置 num 的值,清除非数字数据。
num = 0
# 重新调用 main。
main()
# 程序2
def addfive(num):
return num + 5
# 程序3
def multiply(addednum):
return addednum * 2.452
# 程序4
def display(multi):
# 显示最终值
print("最终值是 ", multi)
# 调用程序1
main()
如果我们输入数字 "5",输出将如下所示:
请输入一个数字进行操作。
5
最终值是 24.52
如果我们输入 "g" 然后修正输入并输入 "8",输出将如下所示:
请输入一个数字进行操作。
g
请输入一个有效的数字。
请输入一个数字进行操作。
8
最终值是 31.875999999999998
下面是一个类和使用该类的程序。这个面向对象编程程序与上述过程式编程程序执行相同的功能。在我们深入类和程序之前,先介绍一些重要的 OOP 编码概念。要创建一个类,我们使用 class
关键字。关键字后面是你希望为类命名的名称。按照惯例,类名通常使用驼峰命名法(CapWords)。例如,如果我想创建一个名为 dirtysocks
的类,代码如下:
class DirtySocks:
首先展示类的定义,然后是使用该类的程序。
# 文件名:oopexample.py
# Mitchell Aikens
# 无版权声明
# 2012
# OOP 演示 - 类
class NumChange:
def __init__(self):
self.__number = 0
def addfive(self, num):
self.__number = num
return self.__number + 5
def multiply(self, added):
self.__added = added
return self.__added * 2.452
这个类定义了一个 NumChange
类,它具有两个方法:addfive
和 multiply
。addfive
方法接受一个数字并将其加 5,multiply
方法接受一个已加 5 的数字,并将其乘以 2.452。
使用上述类的程序如下:
# 文件名:oopexampleprog.py
# Mitchell Aikens
# 无版权声明
# 2012
# OOP 演示 - 程序
import oopexample
maths = oopexample.NumChange()
def main():
num = float(input("请输入一个数字。\n"))
added = maths.addfive(num)
multip = maths.multiply(added)
print("操作后的值是 ", multip)
main()
在查看这个程序后,你可能有些困惑。这没关系。我们从解析类开始。类的名称是 NumChange
,它包含三个方法:
__init__
addfive
multiply
这三个方法的代码结构是相似的:
def __init__(self):
def addfive(self, num):
def multiply(self, added):
注意每个方法都有一个名为 self
的参数。这个参数必须出现在类的每个方法中。虽然这个参数不一定必须叫做 self
,但这是标准做法,所以你应该遵循这个习惯。这个参数之所以必需,是因为当方法执行时,它必须知道在哪个对象上操作其属性。即使只有一个对象,我们仍然需要确保解释器知道我们要使用该类中的数据属性。因此,我们通常使用 self
参数。
让我们来看第一个方法:
def __init__(self):
在 Python 中,大多数类都有一个 __init__
方法,当类的实例(对象)在内存中创建时,它会自动执行。(当我们引用一个类时,类的一个实例 [或对象] 被创建。)这个方法通常被称为初始化方法。当这个方法执行时,self
参数会自动被分配给对象。这个方法之所以叫做初始化方法,是因为它 "初始化" 数据属性。在 __init__
方法下,我们将 number
属性的值初始化为 0。我们通过点符号来引用对象的属性:
def __init__(self):
self.__number = 0
self.__number = 0
这一行意味着:"对象的属性 number 的值为 0"
。
接下来是第二个方法:
def addfive(self, num):
self.__number = num
return self.__number + 5
这个方法叫做 addfive
。它接受一个名为 num
的参数,这个参数来自使用该类的程序。方法将这个参数的值赋给对象内的 number
属性,然后返回 number
的值加上 5,返回的结果会传回调用该方法的语句。
接下来是第三个方法:
def multiply(self, added):
self.__added = added
return self.__added * 2.453
这个方法叫做 multiply
。它接受一个名为 added
的参数。方法将该参数的值赋给 added
属性,并返回 added
属性的值乘以 2.452,返回结果传回调用该方法的语句。
注意每个方法的名称都以两个下划线开始吗?我们来解释一下。之前我们提到过,对象通过方法操作其内部的数据属性。理想情况下,这些数据属性应该只能通过对象内的方法来操作。外部代码可以操作数据属性的做法是可以避免的。为了 "隐藏" 属性,只允许对象内的方法来操作它们,我们在属性名之前使用了两个下划线(正如我们展示的那样)。如果省略这两个下划线,外部代码也有可能操作这些属性。
接下来我们看看使用刚才分析的类的程序。
注意代码中的第一行非注释代码:
import oopexample
这一行代码导入了我们保存在单独文件(模块)中的类。类不一定要放在单独的文件中,但通常都是这样,所以现在习惯性地进行模块导入是一个好习惯。
接下来这一行:
maths = oopexample.NumChange()
这一行创建了 NumChange
类的实例,该类存储在名为 oopexample
的模块中,并将该实例存储在变量 maths
中。语法是:模块名.类名()
。接下来我们定义了 main
函数,并获取用户输入的数字。
下一行:
added = maths.addfive(num)
这行代码将 num
变量的值传递给 addfive
方法,它是我们存储在 maths
变量中的类的一部分,然后将返回的值存储在 added
变量中。
下一行:
multip = maths.multiply(added)
这行代码将 added
变量的值传递给 multiply
方法,它也是我们存储在 maths
变量中的类的一部分,并将返回的值存储在 multip
变量中。
接下来这一行:
print("操作后的值是 ", multip)
这行代码打印 "操作后的值是 <multip 的值>"。最后一行调用 main
函数,执行上述步骤。