面向对象编程

面向对象编程–Object Oriented Programming,简称OOP,是一种程序设计思想。OOP把对象最为程序的基本单元,一个对象包含了数据和操作数据的函数
面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序进行。为了简化程序设计,面向过程是把函数继续切分为子函数,即把大块函数通过切割成小块函数来降低系统的复杂度。而面向对象的程序设计是把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的信息,并处理这些信息,计算机程序的执行就是一系列信息在各个对象之间的传递。
在Python中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class)的概念。

下面以一个例子说明面向对象和面向过程在程序流程上的不同之处。
假设我们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个dict表示:
(六)【重拾Python】面向对象编程_python学习
而处理学生成绩可以通过函数实现,比如打印学生的成绩。

如果采用面向对象的程序设计思想,我们首选思考的不是程序的执行流程,而是Student这种数据类型应该被视为一个对象,这个对象拥有name和score这两个属性(Property)。如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,对对象发一个print_score消息,让对象自己把自己的数据打印出来。

(六)【重拾Python】面向对象编程_python学习_02

给对象发信息实际上就是调用对象对应的关联函数,我们称之为对象的方法(Method)。面向·对象的程序写出来就像这样:

(六)【重拾Python】面向对象编程_python学习_03

面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。Class是一种抽象概念,比如我们定义的Student类,是指学生这个群体,而实例则是一个个具体的Student,比如bart和Lisa是两个具体的Student。

所以,面向对象的设计思想是抽象出Class,根据Class创建Instance。

面向对象的抽象程度又比函数要高,因为一个Class既包含数据,又包含操作数据的方法。

数据封装、继承和多态是面向对象的三大特点。

1 类和实例

面向对象最重要的概念是类(Class)和实例(Instance),必须牢记类是抽象的模板,比如Student类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。
仍以Student类为例,在Python中,定义类是通过class关键字:
(六)【重拾Python】面向对象编程_python学习_04

class后面紧跟着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的。通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。
定义好了Student类,就可以创建实例:类名+()
(六)【重拾Python】面向对象编程_python学习_05
可以看到,变量bart指向的就是一个Student实例,后面的0x10a67a590是内存地址,每个object的地址都不一样,而Student本身就是一个类。

可以自由地给一个实例变量绑定属性,比如,给实例bart绑定一个name属性:
(六)【重拾Python】面向对象编程_python学习_06
由于类起到模板作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__方法,在创建实例时,就把name,score等属性绑上去:
(六)【重拾Python】面向对象编程_python学习_07
注意到__init__方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__方法内部,就可以把各种属性绑定到self,因此self就指向创建的实例本身。

有了__init__方法,在创建实例时,就不能传入空的参数,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去。
(六)【重拾Python】面向对象编程_python学习_08

数据封装
面向对象编程一个重要特点就是数据封装。在上面的Student类中,每个实例就拥有各自的name和score这些数据。我们可以通过函数来访问这些数据,比如打印一个学生的数据:
(六)【重拾Python】面向对象编程_python学习_09
但是,既然Student实例本身就拥有这些数据,要访问这些数据,就没必要从外面的函数去访问,可以直接在Student类的内部定义访问数据的函数,这样,就把“数据”给封装起来了。这些封装数据的函数是和Student类本身是关联起来的,我门称之为类的方法:
(六)【重拾Python】面向对象编程_python学习_10
要定义一个方法,除了第一个参数self外,其他和普通函数一样。要调用一个方法,只需要在实例变量上直接调用,除了self不用传递,其他参数正常传入:
(六)【重拾Python】面向对象编程_python学习_11
这样一来,我们从外部看Student类,就只需要知道,创建实例需要给出name和score,而如何打印,都是在Student类的内部定义的,这些数据和逻辑被“封装“起来了,但却不用知道内部实现的细节。

封装的另一个好处是可以给Student类增加新的方法,如:
(六)【重拾Python】面向对象编程_python学习_12

与静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的实例,但拥有的变量名称都可能不同:
(六)【重拾Python】面向对象编程_python学习_13


2 访问限制

在Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。但是,外部代码还是可以自由地修改一个实例的属性。
如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在Python中,实例的变量如果以__开头,就变成一个私有变量,只有内部可以访问,外部不能访问,所以,我们把上面的Student类改一改:
(六)【重拾Python】面向对象编程_python学习_14
改变后,对于外部代码来说,没什么变动,但是已经无法从外部访问实例变量了:
(六)【重拾Python】面向对象编程_python学习_15
这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。

但是只是不让外部修改内部的属性,怎么办?可以给Student类增加get_name和get_score这样的方法:
(六)【重拾Python】面向对象编程_python学习_16

如果又要允许外部代码修改scoe怎么办?可以增加set_score方法:
(六)【重拾Python】面向对象编程_python学习_17

你也许会问,原先那种直接通过bart.score=00也可以修改啊,为什么要定义一个方法大费周折?因为在方法中,可以对参数做检查,避免传入无效的参数:
(六)【重拾Python】面向对象编程_python学习_18
需要注意的是,变量名类似__xxx__的,也就是以双下划线开头和结尾的是特殊变量,特殊变量是可以直接访问的,不是private变量。

有些时候,你会看到以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是,按照约定,这样的变量意思是:虽然我可以被访问,但请把我看成私有的,不要随意访问。

双划线也可以从外部访问:
(六)【重拾Python】面向对象编程_python学习_19
但是强烈建议不要这样做,因为不同版本Python可能会把__name改成不同的变量名。

最后注意下面这种错误写法:
(六)【重拾Python】面向对象编程_python学习_20
表面上看,外部代码成功设置了__name变量,但实际上这个__name变量和class内部的变量不是同一个,内部的变量已经被Python解释器改成_Student__name了,而外部代码给bart新增加了一个__name变量。
(六)【重拾Python】面向对象编程_python学习_21


3 继承和多态

在OOP程序设计中,当我们定义一个class时,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class 、super class)
比如,我们已经编写了一个名为Animal的class,有一个run()方法可以直接打印:
(六)【重拾Python】面向对象编程_python学习_22

当我们需要编写Dog和Cat类时,就可以直接从Animal类继承:
(六)【重拾Python】面向对象编程_python学习_23
对于Dog来说,Animal就是它的父类,对于Animal来说,Dog和Cat就是它的子类。

继承最大的好处就是子类可以获得父类的全部功能
(六)【重拾Python】面向对象编程_python学习_24
运行结果如下;
(六)【重拾Python】面向对象编程_python学习_25
当然,也可以对子类增加一些方法,比如Dog类:
(六)【重拾Python】面向对象编程_python学习_26

**继承的第二个好处是可以对父类的代码做改进:
(六)【重拾Python】面向对象编程_python学习_27
结果如下:
(六)【重拾Python】面向对象编程_python学习_28

当子类和父类都存在相同的run()时,我们说,子类的run()覆盖了父类的run(),在代码运行时,总是会首先调用子类的run()。这样,我们就获得继承的另一个好处:多态.。

要理解什么是多态,我们首先要对数据类型再作一点说明。当我们定义一个class时,我们实际上就定义了一种数据类型。我们定义的数据类型和Python自带的数据类型,比如str、list、dict没什么两样:

(六)【重拾Python】面向对象编程_python学习_29
判断一个变量是否时某个类型可以用isinstance()判断:
(六)【重拾Python】面向对象编程_python学习_30
看来a、b、c确实对应着list、Animal、Dog这三种类型。
(六)【重拾Python】面向对象编程_python学习_31
看来,c不仅仅是Dog,还是Animal。

所以,在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看作是父类。但是反过来就不行:
(六)【重拾Python】面向对象编程_python学习_32
要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Animal类型的变量:
(六)【重拾Python】面向对象编程_python学习_33
当我们传入Animal的实例时,run_twice()就打印出:
(六)【重拾Python】面向对象编程_python学习_34
当我们传入cat实例时,run_twice()就打印出:
(六)【重拾Python】面向对象编程_python学习_35
看上去没啥意思,但是仔细想想,现在,如果我们再定义一个Tortoise类型,也从Animal派生:
(六)【重拾Python】面向对象编程_python学习_36
当我们调用run_twice()时,传入Tortoise的实例:
(六)【重拾Python】面向对象编程_python学习_37
你会发现,新增一个Animal的子类,不必对run_twice做任何修改,实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因在于多态。

多态的好处就是,当我们需要传入Dog、Cat、Tortoise…时,我们只需要接收Animal类型就可以了,因为Dog、Cat、Tortoise…时,我们只需要接收Animal类型就可以了,因为Dog、Cat、Tortoise…就是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或子类,就会自动调用实际类型的run()方法,这就是多态的意思:
对于一个变量,我们只需要知道它是Animal类型,无需确切地知道他的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在Animal、Dog、Cat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态的正真威力:调用方只管调用,不管细节,而当我们新增加一种Animal子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:
对扩展开放:允许新增Animal子类
对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。

继承还可以一级一级地继承下去,就好比从爷爷到爸爸再到儿子这样的关系,而任何类,最终都可以追溯到object,这些继承关系看上去就像一棵树。比如下面的继承树:
(六)【重拾Python】面向对象编程_python学习_38

静态语言vs动态语言

对于静态语言(例如java),如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。
对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了。
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子”,走起路来像鸭子,那它就可以被看作鸭子。


4 获取对象信息

type()

基本类型都可以用tupe()判断:
(六)【重拾Python】面向对象编程_python学习_39

如果一个变量指向函数或者类,也可以用tupe()判断:
(六)【重拾Python】面向对象编程_python学习_40
type()返回的是对应的Class类型。如果我们要在if语句中判断,就需要比较两个变量的tupe类型是否相同:
(六)【重拾Python】面向对象编程_python学习_41
判断基本数据类型可以直接写int,str等,但如果要判断一个对象是否是函数怎么办?可以使用tupes模块中定义的常量:
(六)【重拾Python】面向对象编程_python学习_42

使用isinstance()
对于class的继承关系来说,使用tupe()就很不方便。我们要判断class的类型,可以使用isinstance()函数。
如果继承关系是:
(六)【重拾Python】面向对象编程_python学习_43
先创建三种类型的对象:
(六)【重拾Python】面向对象编程_python学习_44
isinstance可以判断一个对象是否是该类型本身,或者位于该类型的父继承链上。
h是Husky、Dog、Animal类型。
d不是Husky类型

能用type()类型判断的基本类型也可以用isinstance()判断。
还可以判断一个变量是否是某些类型中的一种,比如下面的代码就可以判断是否是list或者tuple:
(六)【重拾Python】面向对象编程_python学习_45总之优先使用isinstance()

使用dir()
如果获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如获得一个str对象的所有属性和方法:
(六)【重拾Python】面向对象编程_python学习_46
类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用对象的__len__()方法,比如,下面的代码是等价的:
(六)【重拾Python】面向对象编程_python学习_47
我们自己写类,如果也想用len(myObj)的话,就自己写一个__len__方法:
(六)【重拾Python】面向对象编程_python学习_48
(六)【重拾Python】面向对象编程_python学习_49

仅仅把属性和方法列出来是不够的,配合getattr()setattr()hasattr()我们可以直接操作一个对象的状态:
(六)【重拾Python】面向对象编程_python学习_50
紧接着,可以测试该对象的属性:
(六)【重拾Python】面向对象编程_python学习_51

可以传入一个默认参数,如果属性不存在,就返回默认值:
(六)【重拾Python】面向对象编程_python学习_52
也可以获得对象的方法:
(六)【重拾Python】面向对象编程_python学习_53

通过内置的一系列函数,我们可以对任意一个Python对象进行剖析,拿到其内部的数据。要注意的是,只有在不知道对象信息的时候,我们才会去获取对象信息。如果可以写:
(六)【重拾Python】面向对象编程_python学习_54
就不要写:
(六)【重拾Python】面向对象编程_python学习_55
一个正确的用法如下:
(六)【重拾Python】面向对象编程_python学习_56


5 实例属性和类属性

由于Python是动态语言,根据类创建的实例可以任意绑定属性。
给实例绑定属性的方法是通过实例变量,或者通过self变量:
(六)【重拾Python】面向对象编程_python学习_57
但是,如果Student类本身需要绑定一个属性呢?可以直接在class中定义属性,这种属性是类属性,归Student类所有:
(六)【重拾Python】面向对象编程_python学习_58

在编写程序时,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽类属性,但是当你删除实例属性后,再使用相同的名称,访问的将是类属性。

(六)【重拾Python】面向对象编程_python学习_59


廖雪峰 python