5. 面向对象编程

面向对象编程.md#面向对象编程

面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。

面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序执行。为了简化程序设计,面向过程把函数继续切分为子函数,即把大块函数通过切割成小块函数来降低系统的复杂度。

而面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发来的消息并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。

在Python中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类Class的概念。

我们以一个例子来说明面向过程和面向对象在程序流程上的不同之处。假定我们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个dict表示:

1
2
>>> std1={'name': 'Michael', 'score': 98 }
>>> std2={'name': 'Bob', 'score': 82 }

而处理学生成绩可以通过函数实现,比如打印学生的成绩:

1
2
3
4
5
>>> def print_score(std):
... print('%s: %s' %(std['name'], std['score']))
...
>>> print_score(std1)
Michael: 98

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

1
2
3
4
5
6
7
8
9
>>> class Student(object):
... def __init__(self, name, score):
... self.name=name
... self.score=score
... def print_score(self):
... print('%s: %s' %(self.name, self.score))
...

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

1
2
3
4
5
6
>>> bart=Student('Bart Simpson',59)
>>> lisa=Student('Lisa Simpson',89)
>>> bart.print_score()
Bart Simpson: 59
>>> lisa.print_score()
Lisa Simpson: 89

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

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

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

类和实例

面向对象最重要的概念就是类Class和实例Instance,必须牢记类是抽象的模板,实例是根据类创建出来的一个个具体的对象,每个对象都拥有相同的方法,但是各自的数据可能不同。

仍以Student类为例,在Python中,定义类是通过class关键字:

1
2
3
>>> class Student(object):
... pass
...

class后面紧跟着类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,继承的概念我们后面会讲,通常,如果没有合适的继承类,就是用object类,这是所有类都会继承的类。

定义好了Student类,就可以根据Student类创建出Student实例,创建实例是通过类名+()来实现的:

1
2
3
4
5
>>> bart=Student()
>>> bart
<__main__.Student object at 0x000002C2CE396160>
>>> Student
<class '__main__.Student'>

可以看到,变量bart指向的就是一个Student的实例,后面的0x000002C2CE396160是内存地址,每个object的地址都不一样,而Student本身则是一个类,可以自由的给一个实例变量绑定属性,比如,给实例bart绑定一个name属性:

1
2
3
>>> bart.name='Bart Simpson'
>>> bart.name
'Bart Simpson'

由于类可以起到模板的作用,因此在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__的方法,在创建实例的时候,就把name,score等属性绑定上去:

1
2
3
4
5
>>> class Student(object):
... def __init__(self,name,score):
... self.name=name
... self.score=score
...

注意到__init__方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__方法的内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器会自己把实例变量传进去:

1
2
3
4
5
6
7
>>> bart=Student('Bart Simpson',98)
>>> bart.name
'Bart Simpson'
>>> bart.score
98
>>> bart
<__main__.Student object at 0x000002C2CE396278>

和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数的没有什么区别,仍然可以使用默认参数,可变参数,关键字参数和命名关键字参数。

数据封装

面向对象编程的一个重要特点就是数据封装。在上面的Student类中,每个实例就拥有各自的name和score这些数据。我们可以通过函数来访问这些数据,比如打印一个学生的成绩:

1
2
3
4
5
>>> def print_score(std):
... print('%s:%s ' % (std.name,std.score))
...
>>> print_score(bart)
Bart Simpson:98

但是,既然Student实例本身就拥有这些数据,要访问这些数据就没有必要从外面的函数去访问,可以直接在Student类的内部定义访问数据的函数,这样,就把数据给封装起来了。这些封装数据的函数和Student类本身是关联的,我们称之为类的方法:

1
2
3
4
5
6
7
8
9
10
>>> class Student(object):
... def __init__(self,name,score):
... self.name=name
... self.score=score
... def print_score(self):
... print('%s:%s' %(self.name,self.score))
...
>>> bart=Student('Bart Simpson',98)
>>> bart.print_score()
Bart Simpson:98

这样一来,我们从外部看Student类,就只需要知道,创建实例需要给出namescore,而如何打印,都是在Student类的内部定义的,这些数据和逻辑被封装起来了,调用很容易但是不知道内部实现的细节。

封装的另一个好处是可以给Student类增加新的方法,比如get_grade:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> class Student(object):
... def __init__(self,name,score):
... self.name=name
... self.score=score
... def print_score(self):
... print('%s:%s' %(self.name,self.score))
... def get_grade(self):
... if self.score>=90:
... return 'A'
... elif self.score>=80:
... return 'B'
... else:
... return 'C'
...
>>> bart=Student('Bart Simpson',98)
>>> bart.get_grade()
'A'

get_grade方法可以直接在实例变量上调用不需要知道内部的实现细节。

小结

类是创建实例的模板,而实例则是一个个具体的对象,各个实例拥有的数据都相互独立,互不影响;方法就是与实例绑定的函数和普通函数不同,方法可以直接访问实例的数据;通过在实例上调用 的方法,我们就直接操作了对象内部的数据,也就是说,对于两个实例变量,虽然他们都是同一个类的不同实例,但拥有的变量名称可能不同。

访问限制

在Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。但是,从前面Student来看,外部代码还是可以自由修改一个实例的name、score属性:

1
2
3
4
5
6
7
8
9
>>> bart.score
98
>>> bart.score=69
>>> bart.score
69
>>> bart.get_grade
<bound method Student.get_grade of <__main__.Student object at 0x000002C2CE3966A0>>
>>> bart.get_grade()
'C'

如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在python中,实例的变量名如果以__开头,就变为了一个私有变量private,只有内部可以访问,外部不可以访问,我们把student类改一改:

1
2
3
4
5
6
7
>>> class Student(object):
... def __init__(self,name,score):
... self.__name=name
... self.__score=score
... def print_score(self):
... print('%s: %s' %(self.__name,self.__score))
...

改完后,对于外部代码来说没什么变动,但是已经无法从外部访问实例变量.__name实例变量.__score了:

1
2
3
4
5
>>> bart=Student('Bart Simpson',99)
>>> bart.__name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'

这样就确保了外部代码不能随便修改内部的状态,这样通过访问限制的保护,代码更加稳定。但是如果外部代码要取得name和score怎么办?可以给Student类增加get_name,get_score的方法:

1
2
3
4
5
6
7
8
9
10
11
>>> class Student(object):
... def __init__(self,name,score):
... self.__name=name
... self.__score=score
... def print_score(self):
... print('%s: %s' %(self.__name,self.__score))
... def get_name(self):
... return self.__name
... def get_score(self):
... return self.__score
...

如果要允许外部代码修改score怎么办,需要给Student类增加set_score方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> class Student(object):
... def __init__(self,name,score):
... self.__name=name
... self.__score=score
... def print_score(self):
... print('%s: %s' %(self.__name,self.__score))
... def get_name(self):
... return self.__name
... def get_score(self):
... return self.__score
... def set_score(self,score):
... self.__score=score
...

与原来直接通过bart.score=90相比,在方法中,可以对参数做检查,避免传入无效的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> class Student(object):
... def __init__(self,name,score):
... self.__name=name
... self.__score=score
... def print_score(self):
... print('%s: %s' %(self.__name,self.__score))
... def get_name(self):
... return self.__name
... def get_score(self):
... return self.__score
... def set_score(self,score):
... if 0<=score<=100:
... self.__score=score
... else:
... raise ValueError('bad score')
...

需要注意的是,在Python中,变量名类似__xxx__的,也就是以下划线结尾的,是特殊百年来那个,特殊变量是可以直接访问的,不是private变量,所以,不能用__name__这样的变量名。
而以一个下划线开头的实例变量名,比如_name这样的实例变量外部是可以访问的,但是一般不随便访问此类实例。
双下划线开头的实例变量也不是一定不能从外部访问的,不能直接访问__name是因为Python解释器对外吧__name变量改为了_Student__,所以,仍然可以通过_Student__name来访问__name变量:

1
2
3
>>> bart=Student('Bart Simpson',99)
>>> bart._Student__name
'Bart Simpson'

但是强烈建议你不要这么做,因为不同版本的Python解释器可能会把__name改成不同的变量名。总的来说就是Python本身没有任何机制阻止你干坏事,全靠自觉。

最后注意下面这种错误写法:

1
2
3
4
5
6
7
8
>>> bart=Student('Bart Simpson',99)
>>> bart.get_name()
'Bart Simpson'
>>> bart.__name='fsfd'
>>> bart.__name
'fsfd'
>>> bart.get_name()
'Bart Simpson

表面上看,外部代码‘成功’的设置了__name变量,但是实际上这个__name变量和class内部的__name变量不是一个变量!内部的__name变量已经被Python解释器自动改成了_Student__name,而外部代码给bart增加了一个__name变量。


继承和多态

在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subcalss),而被继承的class称为基类、父类或超类(Base class、Super class)。

比如,我们已经编写了一个名为Animal的class,有一个run()方法可以直接打印:

1
2
3
4
>>> class Animal(object):
... def run(self):
... print('Animal is running...')
...

当我们需要编写DogCat类时,就可以直接从Animal类继承:

1
2
3
4
5
6
>>> class Dog(Animal):
... pass
...
>>> class Cat(Animal):
... pass
...

对于Dog来说,Animal就是它的父类,对于Animal来说,Dog就是它的子类。

继承最大的好处是获得了父类的全部功能。由于Animal实现了run()方法,因此,DogCat作为他的子类,自动拥有了run()方法。

1
2
3
>>> dog=Dog()
>>> dog.run()
Animal is running...

也可以对子类增加一些方法:

1
2
3
4
5
6
7
8
9
10
>>> class Dog(Animal):
... def run(self):
... print('Dog is running...')
... def eat(self):
... print('Dog is eating...')
...
>>> dog=Dog()
>>> dog.eat()
Dog is eating...
>>>

继承的第二个好处需要我们队代码进行一点改进。当子类和父类存在相同的run()方法时,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run()。这样,我们就获得了继承的另一个好处:多态。

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

> a=list() a是list的数据类型
    b=Animal()                       b是Animal的数据类型
    c=Dog()                            c是Dog的数据类型

判断一个变量是否是某个类型可以用isinstance()判断:

1
2
3
4
5
6
7
8
>>> isinstance(a,list)
True
>>> isinstance(b,Animal)
True
>>> isinstance(c,Animal)
True
>>> isinstance(b,Dog)
False

可以看出a,b,c确实对应着listAnimalDog这三种类型。而且c还对应着Animal类型。在继承关系中,如果一个实例的数据类型是某个子类,那他的数据类型可以看做是父类。但是反过来就不行。

为了更好的理解多态,我们还需要再编写一个函数,这个函数可以接受一个Animal类型的变量:

1
2
3
>>> def run_twice(animal):
... animal.run()
... animal.run()

当我们传入Animal的实例时,run_twice()就会打印出:

1
2
3
>>> run_twice(Animal())
Animal is running...
Animal is running...

当我们传入Dog实例时,run_twice()就打印出:

1
2
3
>>> run_twice(Dog())
Dog is running...
Dog is running...

看上去没什么,但是当我们再定义一个Tortoise类型,也从Animal派生:

1
2
3
>>> class Tortoise(Animal):
... def run(self):
... print('Tortoise is running slowly...')

当我们调用run_twice()时,传入Tortoise的实例:

1
2
3
>>> run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...

可以看出,新增一个Animal的子类,不必对run_twice()进行任何修改,实际上,任何依赖Animal作为参数的函数或方法都可以不加修改的正常运行,原因就在于多态。
多态的好处就是当我们需要传入DogCatTortoise……时,我们只需要接收Animal类型就可以,因为前面三者都是Animal类型,然后按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就可以自动调用实际类型的run()方法。
对于一个变量,我们只需要知道他是Animal类型,无需确切的知道他的子类型,就可以放心的调用run()方法,而具体调用的run()方法是作用在DogCatTortoise就是由运行时该对象的确切类型决定,也就是说多态调用中:调用方只管调用,不管细节,而当我们新增一种Aniaml的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调节的,这就是著名的开闭原则:

对扩展开放:允许新增Animal的子类;
对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。
继承还可以一级一级的继承下来,就好比从爷爷到爸爸再到儿子这样的关系。而任何类,最终都可以追溯到根类object,这些继承关系看上去就像一颗倒着的数。
Alt text
静态语言vs动态语言

对于静态语言来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。
对于Python这样的静态语言来说,则不一定需要传入Animal类型,我们只需要保证传入的对象有一个run()方法就可以。

这就是动态语言的‘鸭子类型’,它并不要求严格的继承体系,一个对象只要看起来像押走,走起路来像鸭子,那他就可以被看做是鸭子。
Python的“file-like object”就是一种鸭子类型。对真正的文件对象,它有一个read()的方法,返回其内容。但是,许多对象,只要有read()方法,都可以被视为“file-like object”。许多函数接收的参数就是“file-like object”,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象。


获取对象信息

当我们拿到一个对象的引用时,如何知道这个对象是什么类型,有哪些方法?

type()

首先,我们判断对象类型,使用type()函数,基本类型都可以用type()判断:

1
2
3
4
5
6
>>> type(123)
<class 'int'>
>>> type('123')
<class 'str'>
>>> type(None)
<class 'NoneType'>

如果一个变量指向函数或者类,也可以用type()来判断:

1
2
3
4
5
6
>>> type(abs)
<class 'builtin_function_or_method'>
>>> type(a)
<class 'list'>
>>> type(c)
<class '__main__.Dog'>

但是type()函数返回的是什么类型呢?它返回对应的Class类型。如果我们要在if语句中判断,就需要比较两个变量的type类型是否相同:

1
2
3
4
5
6
>>> type(123)==type(456)
True
>>> type(123)==int
True
>>> type('123')==type('abc')
True

判断基本数据类型可以直接写intstr等,但如果要判断一个对象是否是函数时,可以使用types模块中定义的常量:

1
2
3
4
5
6
7
8
9
10
11
12
>>> import types
>>> def fn():
... pass
...
>>> type(fn)==types.FunctionType
True
>>> type(abs)==types.BuiltinFunctionType
True
>>> type(lambda x:x)==types.LambdaType
True
>>> type((x for x in range(10)))==types.GeneratorType
True

isinstance()

对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()函数。
继承关系为:object->Animal->Dog->Husky,那么isinstance()就可以告诉我们一个对象是否是某种类型

1
2
3
4
5
6
7
8
9
>>> a=Animal()
>>> b=Dog()
>>> c=Husky()
>>> isinstance(c,Husky)
True
>>> isinstance(c,Dog)
True
>>> isinstance(c,Animal)
True

可以看出虽然c自身是Husky类型,但由于Husky是从Dog上继承下来的,所以c还是Dog类型。也就是说,isinstance()判断的是一个对象是否是该类型本身或者位于该类型的父继承链上。能用type()判断的类型都可以用isinstance()判断,并且还可以判断一个变量是否是某些类型中的一种:

1
2
3
4
5
6
7
8
>>> isinstance('a',str)
True
>>> isinstance(123,int)
True
>>> isinstance([1,2,3],(list,tuple))
True
>>> isinstance((1,2,3),(list,tuple))
True

dir()

如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:

1
2
>>> dir('123')
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动调用该对象的__len__方法,所以下面代码是等价的:

1
2
3
4
>>> len('1234')
4
>>> '1234'.__len__()
4

我们自己写的类,如果也想用len(myObj)的话,就自己写一个__len__()方法:

1
2
3
4
5
6
7
>>> class MyDog(object):
... def __len__(self):
... return 100
...
>>> dog=MyDog()
>>> len(dog)
100

剩下的都是普通属性或方法,比如lower()返回小写的字符串:

1
2
>>> 'ADSAFD'.lower()
'adsafd'

仅仅把属性和方法列出来是不够的,配合getattr()``setattr()以及hasattr(),我们可以直接操作一个对象的状态:

1
2
3
4
5
6
7
>>> class MyObject(object):
... def __init__(self):
... self.x=9
... def power(self):
... return self.x*self.x
...
>>> obj=MyObject()

测试该对象的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> hasattr(obj,'x')
True
>>> obj.x
9
>>> hasattr(obj,'y')
False
>>> setattr(obj,'y',19)
>>> hasattr(obj,'y')
True
>>> getattr(obj,'y')
19
>>> obj.y
19

如果试图获取不存在的属性,会抛出AttributeError的错误:

1
2
3
4
>>> getattr(obj,'g')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyObject' object has no attribute 'g'

可以传入一个default参数,如果属性不存在,就返回默认值:

1
2
>>> getattr(obj,'g',404)
404

通过内置的一系列函数,我们可以对任意一个Python对象进行剖析,拿到其内部的数据。要注意的是,只有在不知道对象信息的时候,我们才会去获取对象的信息。

如果可以直接写:
sum=obj.x+obj.y
就不要写:
sum=getattr(obj,’x’)+getattr(obj,’y’)

一个正确的用法例子如下:

1
2
3
4
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None

如果我们要从文件流fp中读取图像,首先要判断该fp是否存在read方法,如果存在,则该对象是一个流,如果不存在,则无法读取。


实例属性和类属性

由于Python是动态语言,根据类创建的实例可以任意绑定属性。
给实例绑定属性的方法是通过实例变量,或者通过self变量:

1
2
3
4
5
6
>>> class Student(object):
... def __init__(self,name):
... self.name=name
...
>>> s=Student('Bob')
>>> s.score=90

但是,如果Student类本身需要绑定一个属性的时候可以直接在class中定义属性,这种属性是类属性,归Student所有:

1
2
3
>>> class Student(object):
... name='sad'
...

当我们定义了一个类属性,这个属性虽然归类所有,但类的所有实例都可以访问到:

1
2
3
4
5
6
7
8
9
10
11
>>> s=Student()
>>> print(s.name)
sad
>>> s.name='Mick'
>>> print(s.name)
Mick
>>> print(Student.name)
sad
>>> del s.name
>>> print(s.name)
sad

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

Donate comment here