类和对象的基本概念
什么是类
在编程时,往往需要通过程序修改变量。但有时候多个变量间可能有很强的关联,是一个整体。此时,可以使用结构体描述这一个整体。然而,结构体只能包含数据成员,却不包含对这些数据操作所需要的函数。
如果把需要描述并操作的某些变量和函数看成一个整体,这个整体就称为类(type)。类定义了该集合中每个对象所共有的属性和支持的操作。例如,对于每一种数据结构如链表、栈、二叉树等,每个集合可能都具有一些相同的属性例如后向指针、栈顶位置、树结点值,但还有一些操作如增(add)、删(remove)、改(update)、查(find),不同的数据结构执行的操作也不一样。这种将一系列属性与方法作为一个整体封装成一个类的编程思路,称为 面向对象编程 (object-oriented programming)。面向对象编程是一种编程思路而不是具体的编程语言,使用 C 语言照样可以编写具有面向对象编程思想的代码,只是实现起来比较麻烦。
Python 是一种原生支持 面向对象编程 的编程语言,可以轻松实现面向对象编程的大多数要求。Python 提供了许多内置的类,例如列表就是其中之一。list
类的属性有各个元素(暂且可以这么认为),方法有修改,排序数据等。除此之外还可以实现自己的类,例如向量类,这个类的属性有方向和大小,它的方法有获取模长、标量放缩等。
在 Python 中,实现一个最简单的类是这样的:
...
类的定义以关键字 class
开头,之后跟着一个类名,并且以冒号和缩进确定类的主体。
类名是一个标识符,必须符合 Python 标识符的命名规则。一般建议类的命名应该遵循大驼峰式标记,即类的任意一个英文单词都应该以大写字母开头。
紧跟类的定义后是类的内容,并且需要缩进。由于第一个类目前并没有实际完成一些事情,因此使用了 pass
关键字表示类的主体暂时忽略。
可以使用内置函数 type()
来查看刚刚定义的类的类型:
结果为:
结果表明,SomeType
的类型是“类”
什么是实例
有了类以后,还需要一个具体的实例(instance)来完成具体的行为。对于内置的 list
类,需要有一个存储了具体数据的列表来完成对列表的操作。同样,对于上述的自定义类向量,也需要一个具体的向量实例来完成取模、计算等操作。
对于以上定义的类,获得一个类的实例的方法如下:
通过输入类名并紧跟一对小括号,便可以获得一个类的对象。这种由类创建出的具体对象称为实例(instance)。类是抽象的概念,而实例是具体的概念。
例如,假设有一个类 Fruit(水果),显然它有一些属性例如 taste(味道)、color(颜色) 等,但只有获得了一个实例 apple(苹果) 后,这些属性才能具体描述。
回到上述语句,获得一个实例有点像调用一个函数,当然这实际上是根据类名生成了一个类的实例。并且由于这个类缺乏具体描述,因此小括号中暂时不需要传入参数。
可以将实例赋值给一个变量,并且每次使用括号,都会为该类生成一个新的实例。例如,以下代码生成了两个类,并使用 type()
函数查看它们的类型:
结果为:
观察上述结果,打印实例变量时,它输出了一个地址,这表明每一个实例之间都是独一无二的。另外,结果显示实例 another_instance
的类型是“当前文件的 SomeType
类”。
什么是属性
以上创建的类和实例看起来没有任何作用,它不包含任何数据,也不做任何事情。对于一个已经创建的类,最首要的就是明确类的属性(attribute)。
前文说过,属性用来描述一个类有哪些特性。对于一个类,它的每个实例属性具体的值可能不太一样,但是它们都存在这个属性。
假设将平面上的一个点看出一个类,那么可以这样定义它:
这个类 Point
需要描述它的坐标,可以通过点记法给一个实例赋予任意的属性。例如,可以这样描述它的属性:
以上描述为实例添加属性的语法为:
这种语法称为点记法,它的赋值过程和变量的赋值是一样的,值可以是任意的,甚至是一个函数或者另一个类。
通过在实例后面加上一个点,就可以表示它的属性,这种方式就和结构体访问字段是一样的。运行以上代码,即可打印属性值:
这几行代码表明,one_point
实例被添加了两个属性,一个是代表横坐标的 x
,一个是代表纵坐标的 y
。并且它们的属性都被赋予了一个具体的整数值。
什么是方法
之前说过,类是属性和操作的集合。获得一个带属性的类以后,这个类还需要完成一些事情,来改变这些属性。改变属性的行为称为方法(method)。
例如,对于内置类 list
,它有一些方法,比如增加数据(append
、insert
、extend
)、移除数据(remove
)等,每个具体的方法实现的效果都不一样。对于自定义的向量类,可以使用方法对它做一些放缩、运算、取模等操作。对于抽象一些的类 Fruit(水果) 来说,它也可以定义一些方法,例如 cut(把一个具体的水果切成小块) 、flavor(给水果加上调料改变味道) 等。
从之前实现的 Point
类开始,可以给它添加一个叫做 move_to_origin
的方法,该方法用来将这个点移动到原点。包含该方法后,完整的类的定义为:
Python 中类的方法和定义一个函数类似,都以关键字 def
开头,然后是一个函数/方法名和一对小括号,括号内包含一些参数,以冒号引出函数/方法的主体。一般推荐方法名遵循蛇形命名规则,即名称中的单词字母全部小写并以下划线连接。
注意方法和函数有一点不一样,所有针对实例的方法都必须包含一个参数,这个参数通常被称为 self
,它必须是第一个位置参数,不过它的名称不一定要是 self
,也可以改成 this
或者 Hello
。一般情况下,定义方法的语法为:
...
一个方法中的 self
参数,是对调用这个方法对象的一个引用。
不过无需手动传入这个 self
参数,调用一个实例方法的方式是实例名后面跟上一个点,再跟上一个方法名,后面像调用函数一样加上一个括号,即:
Python 会自动将一个具体的实例作为第一个参数传给 self
。此时,self.x
实际上就是 instance.x
,替换为实际参数后可以看出形式参数 self
用来代表某个具体的实例,而执行方法时将某个具体的实例对象传给这个形式参数。
例如,以下尝试调用该方法:
结果为:
可以看出执行了 .move_to_origin()
方法后,该点确实被移到了原点。如果在定义方法时忘记了 self
参数,那么通过实例调用方法时,会产生 TypeError
,错误信息是某方法需要 0 个参数,却传入了 1 个,这就是因为调用方法时需要参数 self
来代表一个具体的实例,才能通过参数去获取、更改它的属性。
实际上也可以在类中调用这个函数,并且将类的一个实例作为实际参数传给方法:
两者是完全一样的。许多面向对象编程的语言都使用关键字 this
表示对实例的一个引用,而 Python 使用这种参数的形式来获取实例,加强了类与实例间的联系。
以下定义了一个更复杂的方法 scale
,用来在坐标轴上按相对位置缩放一个点:
通过这个方法,可以看出方法和函数一样,都可以有多个参数,都可以有返回值,甚至可以拥有类型标注。通过实例调用一个方法就类似于直接执行一个函数,只不过第一个位置参数是自动传入的。
类的初始化
在上一节中,定义了一个类 Point
,并使用 .x
和 .y
属性来描述它的坐标,.move_to_origin()
和 .scale()
方法来多点执行一些操作。然而,在每次实例化一个类后,都需要给它的 .x
和 .y
属性赋一个具体值,否则会访问不到它们的属性,产生 AttributeError
。
最好能在实例化类时,就能给实例的属性赋予一个具体值,这样即使后续忘记赋值,也不影响程序的执行。然而,为了给实例添加上属性,只能通过 self.
的格式从形式参数中获取类的实例,然而只有在有实例时,该方法才能调用,也就是说还是只能在生成实例后手动为每一个实例添加属性。
事实上,类有一个特殊的方法 .__init__()
,它是一个初始化方法,用来初始化类的属性。类还有很多类似的特殊方法(special method, or magic method),它们都以双下划线开头,双下划线结尾,有时也被称为双下方法(dunder method)。所有的特殊方法在以后会逐一介绍。
在创建自己的方法时,尽量不要以双下划线开头结尾,否则一旦创建了一些特殊的方法,可能会在不必要的时刻被调用,从而招致莫名其妙的错误。
.__init__()
方法类似别的面向对象编程语言的构造方法,不过 Python 的构造方法另有它用,.__init__()
的实际效果等效于构造方法。
一旦类拥有了这样一个初始化方法,在实例化该类时,使用类名加上一对圆括号,实际上就是在调用这个方法。假设下面有一个类 Vector
:
在初始化时,调用 Vector()
实际上就是在调用它的初始化方法。由于 .__init__()
方法有两个参数,因此实例化时,也需要传入两个参数:
这样,在初始化时,就向实例传入了两个参数,这样确保了只要成功生成了一个实例,它就自动执行一些代码,生成了一些属性,无需后续再手动添加属性,减少了代码量和出错率。
试着调用一下实例的属性:
可以看出在初始化时,它成功地创建了一些属性。
当然,初始化方法也不一定要添加参数,它也可以仅包含 self
参数,但这样就不能在创建时通过传参为每个实例添加不同的属性了。仅包含 self
参数的 .__init__()
构造方法,又称为类的默认构造方法。在有些时候只需要确定的实例属性,当然可以这么做。
本节介绍了面向对象编程的基础和使用 Python 编写面向对象程序的基本语法。面向对象编程是一种非常实用,也应用非常广泛的编程思想,许多编程语言都提供了面向对象编程的语法支持。本节对面向对象编程的思想介绍并不深刻,有兴趣的读者可以自行去 stackoverflow 、知乎等网站搜索,许多人都对其有自己独到的理解。
参考资料/延伸阅读
https://docs.python.org/3/tutorial/classes.html
Python3 官方文档对 Python 面向对象编程的简介,目前仍是 Python 面向对象编程入门的最好教程。