参考原文
引言:面向对象编程(Object Oriented Programming)是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包括了数据和操作数据函数。数据封装、继承和多态是面向对象的三大特点。
类和实例
面向对象最重要的概念就是类和实例。要牢记类是抽象的模板,实例是根据类创建出来的一个个具体的"对象"。
下面以Student类为例,说一说在Python中类的基本用法,首先定义Student类:
class Student(object): pass
定义类时,用关键字class,然后跟上类名Student(大写单词开头),再紧接(object)说明继承于哪个类,object是所有类最终都会继承的类。
现在已经定义好类了,就应该基于Student类创建实例了:
>>> bart = Student() >>> bart <__main__.Student object at 0x00000179C0DAF0F0> >>> Student <class '__main__.Student'>
创建实例是通过类名+()实现的。可以看到变量bart指向的就是一个Student的实例,后面的0x00000179C0DAF0F0是内存地址,而Student本身则是一个类。
在Python中,可以自由的给一个实例变量绑定属性,如给实例bart绑定一个name属性:
>>> bart.name = 'Bart Simpson' >>> bart.name 'Bart Simpson
因为类是模板,因此可以在创建实例的时候,把我们认为一些必须绑定的属性强制填写进去(类似于C#中的构造函数)。可以通过定义一个特殊的方法__init__方法,强制添加一些属性。如把name,score属性绑定上去:
class Student(object): def __init__(self, name, score): self.name = name self.score = score
注意到__init__方法的第一个参数永远是self,表示创建的实例本身,因此在__init__方法内部就可以把各种属性绑定到self。这样创建实例时,就必须传入相应的参数(self不需要我们传,Python解释器自己会把实例变量传进去):
>>> bart = Student() Traceback (most recent call last): File "<pyshell#9>", line 1, in <module> bart = Student() TypeError: __init__() missing 2 required positional arguments: 'name' and 'score' >>> bart = Student('Alice', 100) >>> bart.name 'Alice' >>> bart.score 100
Tips:在类中定义的函数,与普通函数相比,只有一个不同:第一个参数永远是实例变量self,并且在调用时,不用传递该函数。
数据封装
我们可以再在类中添加一些函数用于操作类中的属性和数据,如添加一个打印学生成绩的函数:
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('Alice', 100) >>> bart.print_score() Alice: 100
这样我们从外部看Student类,就只需知道,创建实例需要给出name和score,而如何打印,都是在Student类内部定义的,这些数据和逻辑被封装了,调用时,不必知道内部实现的细节。
Tips:和静态语言不同,Python允许对实例变量绑定任何的属性,也就是说对于两个实例变量,虽然它们都是同一个类的不同的实例,但拥有的属性名称却有可能不同。
访问限制
上面已经提过了Python允许自由地修改实例的属性,我们应该对一些属性加以限制,保护使外部的代码不能随意修改对象内部的状态,从而使代码更加的健壮。所以把上面的Student类修改一下:
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了:
>>> bart = Student('Bart Simpson', 59) >>> bart.__name Traceback (most recent call last): File "<pyshell#22>", line 1, in <module> bart.__name AttributeError: 'Student' object has no attribute '__name'
如果要允许外部的代码获取、修改类中变量,可以增加get、set方法。这样做可以对参数进行检查,避免传入无效的参数或不安全的参数。So:
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_score(self): return self.__score def set_score(self,score): if 0 <= score <= 100: self.__score = score else: raise ValueError('Invalid score')
试试:
>>> bart = Student('Bart Simpson', 59) >>> bart.get_score() 59 >>> bart.set_score(60) >>> bart.get_score() 60 >>> bart.set_score(102) Traceback (most recent call last): File "<pyshell#29>", line 1, in <module> bart.set_score(102) File "<pyshell#24>", line 17, in set_score raise ValueError('Invalid score') ValueError: Invalid score
Tips:在Python中变量名以双下划线开头,以双下划线结尾的如:__name__是特殊变量;以一个下划线开头的是可以访问的,如_name,它的含义是“虽然我是可以被访问的,但请尽量将我视为私有变量,不要随意访问”;以双下划线开头的实例变量虽然是私有变量,其实是因为Python解释器把它改成了_类名__变量名,所以你仍然可以通过后者访问,但最好不要这样干(不同版本Python解释器改成的名字不同)
注意下面错误:
>>> bart = Student('Bart Simpson', 59) >>> bart.get_name() 'Bart Simpson' >>> bart.__name = 'New Name' # 设置__name变量! >>> bart.__name 'New Name'
表面上,外部代码成功的设置了私有变量__name,但其实不然,这个__name和class内部的变量不是同一个变量,内部的变量已经被Python解释器自动改成了_Student__name,这是一个新的变量:
>>> bart.get_name() # get_name()内部返回self.__name 'Bart Simpson'
继承和多态
继承
前面已经说过了封装,那么什么又是继承呢?在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),被继承的类称为基类、父类或超类(Base class、Super class)。
为什么要继承?继承有什么好处?继承的好处之一,也是最大的好处就是子类获得父类的全部功能。如父类Animal:
class Animal(object): def run(self): print('Animal is running')
编写一个Dog类继承于Animal:
class Dog(Animal): pass
此时Dog类就自动获得了父类的run方法:
dog = Dog() dog.run() # reuslt: Animal is running
再来说继承的第二个好多态。
多态
父类有自己的方法,子类也可以有自己的方法,当子类和父类存在同名的方法时,子类的方法会覆盖父类的方法,在代码运行时总会调用子类的方法,这就是多态。
例如上面的Dog类也有一个run方法:
class Dog(Animal): def run(self): print('Dog is running')
此时运行Dog实例的run方法:
dog = Dog() dog.run() # reuslt: Dog is running
此时dog变量即是Dog类,又是Animal类:
dog = Dog() print(isinstance(dog, Dog),isinstance(dog, Animal)) #result:True True
那么多态的好处呢?要理解多态的好处,我们还需再编写一个函数,参数接受一个Animal类型的变量:
def run_twice(animal): animal.run() animal.run()
当我们传入Animal实例时:
run_twice(Animal()) ''' Animal is running Animal is running '''
因为Dog的实例也是Animal类所以:
run_twice(Dog()) ''' Dog is running Dog is running '''
你会发现,新增一个Animal的子类,不必对run_twice做任何修改,任何依赖Animal作为参数的函数或者方法都可以不加修改便可运行正常,原因就在于多态。
对于一个变量,我们只需知道它的父类类型,无需知道它的子类类型,就可以调用父类中有的方法,而具体调用的是父类的方法,还是旗下多个子类中有的同名方法,由运行时该对象的确定。这就是著名的开闭原则:对扩展开放--允许增加子类 对修改封闭--不需修改父类的方法。
静态语言vs动态语言
对于静态语言(如Java,C#)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者其子类,否则无法调用run()方法。而对于Python这种动态语言来说,则不一定需要传入Animal类型,只需保证传入的对象有同名的run方法就可以了。
这就是动态语言的"鸭子类型",它并不严格要求继承的对象,一个对象只要“看起来像鸭子”,那它就可以看成是鸭子。Python中的“file-like object”就是一种鸭子类型,只要这个对象有read()方法,就被视为“file-like objec”。
Tips:继承可以把父类所有功能都拿过来,子类只需增加自己特有的方法,也可以重写父类的方法。动态语言的鸭子类型决定了继承不像静态语言那么必需。