Python面向对象编程-OOP
Python面向对象编程-OOP
20141216 Chenxin整理
OOP的3大特性: 封装,继承,多态
一.封装
OOP目的,OOP为了代码重用 :分解代码 ,最小化代码的冗余以及对现在的代码进行定制来编写程序 ,而不是实地修改代码或从头开始.
一.类定义
在进行python面向对象编程之前,先来了解几个术语:类,类对象,实例对象,属性,函数和方法。
类是对现实世界中一些事物的封装,定义一个类可以采用下面的方式来定义:
class className:
block
注意类名后面有个冒号,在block块里面就可以定义属性和方法了。当一个类定义完之后,就产生了一个类对象。
类对象支持两种操作:引用和实例化。引用操作是通过类对象去调用类中的属性或者方法,而实例化是产生出一个类对象的实例,称作实例对象。比如定义了一个people类:
class people: #people就是个全局的类对象
name = 'jack' #定义了一个属性
def printName(self): #定义了一个方法
print self.name
people类定义完成之后就产生了一个全局的类对象,可以通过类对象来访问类中的属性和方法了。当通过people.name来访问时,people.name中的people称为类对象。
当然还可以进行实例化操作,p=people( ),这样就产生了一个people的实例对象,此时也可以通过实例对象p来访问属性或者方法了(p.name).
在上面代码中注释的很清楚了,name是一个属性,printName()是一个方法,与某个对象进行绑定的函数称作为方法。一般在类里面定义的函数与类对象或者实例对象绑定了,所以称为方法;而在类外定义的函数一般没有同对象进行绑定,就称为函数。
class 语句说明
一般形式
class
data=value
def mothod(self,...):
self.member=value
1.就像函数一样 .class语句是作用域 ,由内嵌的赋值语句建立变量名 ,就存在这个本地作用域内 .
2.就像模块内的变量名 ,在 class语句内赋值的变量名会变成类对象中的属性 (对象的成员变量 ).
扩展阅读 命名空间
命名空间
点号和无点号的变量 ,会用不同的方式处理 .
无点号运算的变量名(例如 ,X)与作用域相对应.
点号的属性名(如 object.X)使用的是对象的命名空间 .
有些作用域会对对象的命名空间进行初始化(模块和类)
以下实例总结了命名空间的概念
X=11 #模块属性,全局
def f():
print X # 函数(本地)作用域内没有 X,嵌套函数没有 X变量 ,当前全局作用域(模块的命名空间内)有 ,显示全局
def g():
X=22 # 定义本地作用域变量 X
print X # 搜索函数(本地)作用域内变量 X,有打印
class C:
X=33 # 定义的类属性,类的命名空间
def m(self ):
X=44 # 貌似在这里没有什么意义
self.X=55 # 定义类实例的属性 ,实例的命名空间
if name=='main':
print X # 打印模块属性 结果 11
f() # 调用f(),f() 返回模块全局变量的 X 11
g() # 调用g(),g() 返回函数内局部变量 X 22
print X # 打印 模块全局变量的里变量 ,模块的属性 11
obj=C() # 调用类的方法产生实例
print obj.X # 打印实例的属性X X继承类的属性 ,所以为 33
obj.m() # 实例调用类的m方法
print obj.X # 显示这个X 属性 因为上一步m方法设置了实例的属性 X,为 55
命名空间字典
模块的命名空间实际上是以字典的形式实现的 ,并且可以由内置属性 __dict__显示这一点 .类和实例对象也是如此:属性点号运算
其内部就是字典的索引运算 ,而属性继承其实就是搜索链接的字典而已 .
实际上 ,实例和类对象就是 Python中带有链接的字典而已 ,
class Super():
... def hello(self):
... self.data1='diege'
...
class Sub(Super):
... def hola(self):
... self.data2='eggs'
...
制作子类的实例时 ,该实例一开始会是空的命名空间字典 ,但是有链接会指向它的类 ,让继承搜索能顺着寻找 .
实际上,继承树可在特殊的属性中看到 ,你可以进行查看 .实例中有个 __class__属性链接到了它的类 ,而类有个 __base__属性(就是元组,其中包含了通往更高的超类的连接) .
X=Sub()
X.dict
{}
X.class
<class main.Sub at 0x2850353c>
Y=Super()
Y.dict
{}
Y.class
<class main.Super at 0x285034ac>
Sub.bases
(<class main.Super at 0x285034ac>,)
Super.bases
()
当类为self属性赋值时 ,会填入实例对象 .
实例对象的命名空间保存了数据 ,会随实例的不同而不同 ,而 self正是进入其命名空间的钩子 .
Y=Sub()
X.hello()
X.dict
{'data1': 'diege'}
X.hola()
X.dict
{'data1': 'diege', 'data2': 'eggs'}
Sub.dict
{'module': 'main', 'doc': None, 'hola': <function hola at 0x284954c4>}
Super.dict
{'module': 'main', 'hello': <function hello at 0x28495f0c>, 'doc': None}
Sub.dict.keys(),Super.dict.keys()
(['module', 'doc', 'hola'], ['module', 'hello', 'doc'])
Y.dict
{}
Y是这个类的第 2个实例 .即时 X的字典已由方法内的赋值语句做了填充 ,Y还是空的命名空间字典 .每个实例都有独立的命名空间字典 ,一开始是空的 ,可以记录和相同类的其他实例命名空间字典中属性 ,完全不同的属性 .
因为属性实际上是 python的字典键 ,其实有两种方式可以读取并对其进行赋值:通过点号运算 ,或通过键索引运算 .
X.data1,X.dict['data1']
('diege', 'diege')
X.data3='lily'
X.dict
{'data1': 'diege', 'data3': 'lily', 'data2': 'eggs'}
dir(X)
['doc', 'module', 'data1', 'data2', 'data3', 'hello', 'hola']
dir(Sub)
['doc', 'module', 'hello', 'hola']
dir(Super)
['doc', 'module', 'hello']
对实例赋值 ,只影响实例 ,不会影响实例的类和超类
命名空间连接
__class__和 __bases__这些属性可以在程序代码内查看继承层次 .可以用他来显示类树
二.属性
在类中我们可以定义一些属性,比如:
class people: #称为类对象people
name = 'jack' #name属性
age = 12 #age属性
p = people() #实例化了一个对象p,称为实例对象p
print p.name,p.age
p = people( )实例化了一个对象p,然后就可以通过p来读取属性了。这里的name和age都是公有的,可以直接在类外通过对象名访问,如果想定义成私有的,则需在前面加2个下划线 ' __'。
class people:
__name = 'jack'
__age = 12
p = people()
print p.__name,p.__age #不可以在类外访问私有变量
这段程序运行会报错.提示找不到该属性,因为私有属性是不能够在类外通过对象名来进行访问的.方法也是一样,方法名前面加了2个下划线的话表示该方法是私有的,否则为公有的.
三.方法
方法调用需要通过实例 ,一般是通过实例调用的 instance.method(arg...)
这会自动翻译成以下形式的类方法函数调用:class.method(instance,args...)
在类中可以根据需要定义一些方法,定义方法采用def关键字,在类中定义的方法至少会有一个参数,一般以名为'self'的变量作为该参数(用其他名称也可以),而且需要作为第一个参数。下面看个例子:
class people:
__name = 'jack'
__age = 12
def getName(self):
return self.__name #在类的内部来访问类对象的私有变量
def getAge(self):
return self.__age
p = people()
print p.getName(),p.getAge()
如果对self不好理解的话,可以把它当做C++中类里面的this指针一样理解,就是对象自身的意思,在用某个对象调用该方法时,就将该对象作为第一个参数传递给self。
self 说明
Python的类的方法和普通的函数有一个很明显的区别,类的方法必须有个额外的第一个参数 (self ),但在调用这个方法的时候不必为这个参数赋值 .
Python的类的方法的这个特别的参数 self指代的是对象本身,按照 Python的惯例,它用 self来表示 .
为何Python 给self 赋值而你不必给self赋值?
例子说明:创建了一个类 MyClass,实例化 MyClass得到了 MyObject这个对象,然后调用这个对象的方法 MyObject.method(arg1,arg2) ,这个过程中, Python会自动转为 Myclass.method(MyObject,arg1,arg2)
这就是Python的 self的原理了。即使你的类的方法不需要任何参数,但还是得给这个方法定义一个 self参数,虽然我们在实例化调用的时候不用理会这个参数不用给它赋值。
示例:
class Python:
def selfDemo(self):
print 'Python,why self?'
p = Python()
p.selfDemo()
输出:Python,why self?
把p.selfDemo() 带个参数如:p.selfDemo(p),得到同样的输出结果.
如果把self去掉的话,
这样就报错了: TypeError: selfDemo() takes no arguments (1 given)
继承
像class 语句这样的命名空间工具的重点就是支持变量名继承 .
在Python 中, 当对对象进行点号运算时 ,就会发生继承 ,而且涉及到搜索属性定义树(一或多个命名空间) .每次使用 obecj.attr形式的表达式时( objecj是实例或类对象) ,Python会从头到尾搜索命名空间树 ,先从对象开始 ,找到第一个 attr为止 .这包括在方法中对 self属性的引用 .因为树中较低的定义会覆盖较高的定义.
属性树的构造
1实例属性 是由对方法内 self属性进行赋值运算而生成的
2类属性 是通过 class语句内顶层的语句(赋值语句)而生成的
3超类链接 通过 class语句首行的括号内列出类而生成的
继承方法的专有化 - 重载 -回调
继承树搜索模式.继承会先在子类寻找变量名 ,然后才查找超类 ,子类就可以对超类的属性重新定义来取代默认的行为 .
把系统做成类的层次 ,再新增外部的子类来对其进行扩展 ,而不是在原处修改已存在的逻辑 .
当子类重新定义一个方法时又调用了超类同样的方法来实现这个子类方法的部分功能 ,这里就用到回调技术 .
这样实现了默认的行为 ,换句话说 ,Sub.mothod只是扩展了 Super.mothod的行为 ,而不是完全取代他 .
抽象类
抽象类就是会调用方法的类 ,但没有继承或定义该方法 ,而是期待该方法由子类填补 .
当行为无法预测 ,非得等到更为具体的子类编写时才知道 ,可用这种方式把类通用化 .
这种“填空 ”的代码结构一般就是OOP软件的框架 .
从delegate 方法的角度来看,这个例子中的超类有时也称作是抽象类 --也就是类的部分行为默认是由其子类所提供的.
如果预期的方法没有在子类定义 ,当继承搜索失败时 ,Python会引发为定义变量名的异常 .
类的编写者偶尔会使用 assert语句 ,使这种子类需求更为明显 ,或者引发内置的异常 NotImplementedError
class Super:
def method(self):
print "in Super.method"
def delegate(self):
self.action()
class Provider(Super):
def action(self):
print "in Provider.method"
类有特殊的属性 __name__类的名字 ,就像模块一样有 __name__属性模块的名字 .
四.运算符重载-类中内置的方法,特殊属性
在Python中有一些内置的方法,这些方法命名都有比较特殊的地方(xx)。类中最常用的就是构造方法和析构方法。
init 构造器方法
构造方法__init__(self,....)在生成对象时调用,可以用来进行一些初始化操作,不需要显示去调用,系统会默认去执行。构造方法支持重载,如果用户自己没有重新定义构造方法,系统就自动执行默认的构造方法。
新实例会传入 __init__的 self参数 ,而在小括号内的任何值会成为第二以及其后的参数 (自动赋值给 self后面的那些变量 ).__init__相当与初始化.
__init__构造方法 ,除了明确传入类的名字的任何参数外 ,还隐性的传入新实例 .
如果希望确保 name这样的变量名一定会在其实例中设置 ,通常都会在构造时填好这个属性 .
class T:
... def init(self,who):
... self.name=who
I1=T('diege')
print I1.name
diege
析构方法__del__(self)
在释放对象时调用,支持重载,可以在里面进行一些释放资源的操作,不需要显示调用。
运算符重载
Python语言提供了运算符重载功能.
Python语言本身提供了很多魔法方法,它的运算符重载就是通过重写这些Python内置魔法方法实现的。这些魔法方法都类似于__X__的形式,python通过这种特殊的命名方式来拦截操作符,以实现重载。当Python的内置操作运用于类对象时,Python会去搜索并调用对象中指定的方法完成操作。
类可以重载加减运算、打印、函数调用、索引等内置运算,如果类实现了__add__方法,当类的对象出现在+运算符中时会调用这个方法。
运算符重载特点
重载是通过提供特殊名称的类方法来实现的 .
所有重载方法的名称前后都有两个下划线字符 ,以便把同类中定义的变量名区别开来 .
特殊方法名称和表达式或运算的映射关系 ,是由 Python语言预先定义好的 .
所有运算符重载的方法都是选用的:如果没有写某个方法 ,那么定义的类就不支持该运算
多数重载方法只用在需要对象行为表现得就像内置函数一样的高级程序中 .然而 ,__init__构造方法常出现在绝大多数类中 .
1.常见的运算符重载方法
方法 重载 调用
init 构造器方法 对象建立: X=Class()
del 析构方法 对象收回
add 运算符+ X+Y,X+=Y
sub 运算符- X-Y,X-=Y
or 运算符|( 位OR) X|Y X|=Y
repr,str 打印 ,转换 print X【 str】、 repr(X)、 str(X)
call 函数调用 X()
getattr 点号运算 X.undefined
setattr 属性赋值语句 X.any=Value
getitem 索引运算 X[key], 没有__iter__ 时的for 循环和其他迭代器
setitem 索引赋值语句 X[key]=value
len 长度 len(X), 真值测试
cmp 比较 XY,X
lt 特定的比较 X<Y(or else cmp)
eq 特定的比较 XY(or else cmp)
radd 左侧加法 + Noninstance + X
iadd 实地(增强的)的加法 X+=Y( or else add)
iter 迭代环境 用于循环, 测试, 列表, 映射及其他
getitem 拦截索引运算 拦截实例的索引运算 .当实例 X出现 X[i]这样的索引运算中时 ,Python会调用这个实例继承的 __getitem__方法 .(如果有) ,把 X作为第一个参数传递 ,并且放括号内的索引值传递给第二个参数
__del__是析构器 每当实例产生时 ,就会调用 __init__构造方法 ,每当实例空间被收回执行 __del__方法
__setattr__会拦截所有赋值语句 ,一般不用 .
repr,str 打印 ,转换 print X、 repr(X)、 str(X)
__call__拦截调用:如果定义了 ,Python就会为实例应用函数调用表达式运行 __call__方法 .当需要为函数的 API编写接口时 ,__call__就变得很用有.
- __iter__和 _getitem__实现迭代 的说明
for循环的作用是从 0到更大的索引值 ,重复对序列进行索引运算 ,直到检测到超出边界的异常 .
getitem__也可以是 Python中一种重载迭代的方式 ,如果定义了这个方法 ,for循环每次循环时都会调用类的 getitem
任何支持for循环的类也会自动支持 Python所有迭代环境 ,包括成员关系测试 in,列表解析 ,内置函数 map,列表和元组赋值运算以及类型构造方法也会自动调用__getitem(如果定义的话) .
如今 ,Python中所有的迭代环境都会先尝试 __iter__方法 ,再尝试 getitem.如果对象不支持迭代协议 ,就会尝试索引运算 .
从技术角度来将 ,迭代环境是通过调用内置函数 iter去尝试寻找 __iter__方法来实现的 ,而这种方法应该返回一个迭代器对象 .
如果已经提供了 ,Python就会重复调用这个迭代器对象的 next方法 ,直到发生 StopIteration异常 .如果没有找到 __iter__方法,Python会改用 __getitem__机制 ,就像之前那样通过偏移量重复索引 ,直到引发 IndexError异常 .
迭代器对象就是实例 self,因为 next方法是这个类的一部分 .在较为复杂的的场景中 ,迭代器对象可定义为个别的类或对象 ,有自己的状态信息 ,对相同数据支持多种迭代 .以 Python的 raise语句发出信号表示迭代结束 .__iter__对象会在调用过程中明确地保留状态信息 .所以比 __getitem__具体更好的通用性 .__iter__迭代器比 __getitem__更复杂和难用 .迭代器是用来迭代 ,不是随机的索引运算 .事实上 ,迭代器根本没有重载索引表达式 .
__iter__机制是在 __getitem__中所见到的其他所有迭代环境的实现方式(成员关系测试 ,类型构造器 ,序列赋值运算) .和 __getitem__不同的是 ,__iter__只循环一次 ,而不是循环多次 ,循环之后就变为空 ,每次新的循环 ,都得创建一个新的迭代器对象 .
五.类属性、实例属性
如果需要在类外修改类属性,必须通过类对象去引用然后进行修改。如果通过实例对象去引用,会产生一个同名的实例属性,这种方式修改的是实例属性,不会影响到类属性,并且之后如果通过实例对象去引用该名称的属性,实例属性会强制屏蔽掉类属性,即引用的是实例属性,除非删除了该实例属性。
class people:
country = 'china'
print people.country -->china
p = people()
print p.country -->china
p.country = 'japan'
print p.country -->japan #实例属性会屏蔽掉同名的类属性
print people.country -->china
del p.country #删除实例属性
print p.country -->china
再未给实例属性重新赋值的情况下,删除实例对象属性后,类属性也被删除了(引用是一致的,内存地址一样):
class people:
name='Jack'
def init(self,age=15):
self.age=age
p=people()
q=people()
print id(people.name),id(p.name),id(q.name) #-->140039664983952 140039664983952 140039664983952
del p.name
print q.name #-->people instance has no attribute 'name'
六.实例方法、类方法和静态方法的区别
实例方法:
实例方法是类中最常定义的成员方法,它至少有一个参数并且必须以实例对象作为其第一个参数,一般以名为'self'的变量作为第一个参数.
class people:
country = 'china'
#实例方法
def getCountry(self):
return self.country
p = people()
print p.getCountry() #正确,可以用过实例对象引用;实例对象可以引用实例方法,实例对象也可以引用类方法;实例方法的这点跟类方法没有区别.
print people.getCountry() #错误,不能通过类对象引用实例方法,这点是实例方法与类方法的区别.
类方法:
是类对象所拥有的方法,需要用装饰器"@classmethod"来标识其为类方法,对于类方法,第一个参数必须是类对象,一般以"cls"作为第一个参数,能够通过实例对象和类对象去访问。
class people:
country = 'china'
#类方法,用classmethod来进行修饰
@classmethod
def getCountry(cls):
return cls.country
p = people()
print p.getCountry() #实例对象引用类方法,可以用过实例对象引用;
print people.getCountry() #类对象引用类方法,可以通过类对象引用;类方法区别于实例方法的一点.
类方法还有一个用途就是可以对类属性进行修改:
class people:
country = 'china'
#类方法,用classmethod来进行修饰
@classmethod
def getCountry(cls):
return cls.country
@classmethod
def setCountry(cls,country):
cls.country = country
p = people()
print p.getCountry() -->china #可以用过实例对象引用
print people.getCountry() -->china #可以通过类对象引用;类方法区别于实例方法的一点.
p.setCountry('japan')
print p.getCountry() -->japan
print people.getCountry() -->japan
结果显示在用类方法对类属性修改之后,通过类对象和实例对象访问都发生了改变。
...
p=people()
print p.getcountry() -->china
q=people()
q.setcountry('Japan')
print p.getcountry(),q.getcountry(),people.getcountry() -->japan japan japan#通过类方法修改了类属性,故类属性和所有相关的对象的这一属性都跟着改变了.
print q.country -->japan
del q.country #这里会报错,说people实例没有country属性;必须通过类方法来修改或删除才行.
静态方法:
装饰器"@staticmethod"来进行修饰,静态方法不需要多定义参数。
class people:
country = 'china'
#静态方法
@staticmethod
def getCountry():
return people.country
print people.getCountry()
实例方法、类方法、静态方法的使用与区别
使用方法:
class A(object):
def foo(self,x): #类实例方法
print "executing foo(%s,%s)"%(self,x)
@classmethod #类方法
def class_foo(cls,x):
print "executing class_foo(%s,%s)"%(cls,x)
@staticmethod
def static_foo(x): #静态方法
print "executing static_foo(%s)"%x
调用方法:
a = A()
a.foo(1) //print : executing foo(<main.A object at 0xb77d67ec>,1)
a.class_foo(1) //executing class_foo(<class 'main.A'>,1)
A.class_foo(1) //executing class_foo(<class 'main.A'>,1)
a.static_foo(1) //executing static_foo(1)
A.static_foo(1) //executing static_foo(1)
实例方法,类方法,静态方法 总结:
对于类属性和实例属性,如果在类方法中引用某个属性,该属性必定是类属性;
而如果在实例方法中引用某个属性(不作更改),并且存在同名的类属性,此时若实例对象有该名称的实例属性,则实例属性会屏蔽类属性,即引用的是实例属性;
若实例对象没有该名称的实例属性,则引用的是类属性;
如果在实例方法更改某个属性,并且存在同名的类属性,此时若实例对象有该名称的实例属性,则修改的是实例属性;
若实例对象没有该名称的实例属性,则会创建一个同名称的实例属性。
想要修改类属性,如果在类外,可以通过类对象修改,如果在类里面,只有在类方法中进行修改。
类方法的第一个参数是类对象cls,那么通过cls引用的必定是类对象的属性和方法;
实例方法的第一个参数是实例对象self,那么通过self引用的可能是类属性、也有可能是实例属性(这个需要具体分析),不过实例属性优先级更高。
静态方法中不需要额外定义参数,因此在静态方法中引用类属性的话,必须通过类对象来引用。
继承和多态
一.继承和多继承
在Python中一个子类可以有多个父类,类的继承定义基本形式如下:
父类
class superClassName:
block
子类
class subClassName(superClassName):
block
在定义一个类的时候,可以在类名后面紧跟一对括号,在括号中指定所继承的父类,如果有多个父类,多个父类名之间用逗号隔开.
class UniversityMember:
def init(self,name,age):
self.name = name
self.age = age
def getName(self):
return self.name
def getAge(self):
return self.age
class Student(UniversityMember):
def init(self,name,age,sno,mark):
UniversityMember.init(self,name,age) #注意要显式调用父类构造方法,并传递参数self.否则会被覆盖吧?
self.sno = sno
self.mark = mark
def getSno(self):
return self.sno
def getMark(self):
return self.mark
class Teacher(UniversityMember):
def init(self,name,age,tno,salary):
UniversityMember.init(self,name,age)
self.tno = tno
self.salary = salary
def getTno(self):
return self.tno
def getSalary(self):
return self.salary
在大学中的每个成员都有姓名和年龄,而学生有学号和分数这2个属性,老师有教工号和工资这2个属性,从上面的代码中可以看到:
1.在Python中,如果父类和子类都重新定义了构造方法__init( )__,在进行子类实例化的时候,子类的构造方法不会自动调用父类的构造方法,必须在子类中显示调用。
2.如果需要在子类中调用父类的方法,需要以 父类名.方法 这种方式调用,以这种方式调用的时候,注意要传递self参数过去。
对于继承关系,子类继承了父类所有的公有属性和方法,可以在子类中通过父类名来调用,而对于私有的属性和方法,子类是不进行继承的,因此在子类中是无法通过父类名来访问的。
对于多重继承,比如 class SubClass(SuperClass1,SuperClass2)
此时有一个问题就是如果SubClass没有重新定义构造方法,它会自动调用哪个父类的构造方法?这里记住一点:以第一个父类为中心。如果SubClass重新定义了构造方法,需要显式去调用父类的构造方法,此时调用哪个父类的构造方法由你自己决定;若SubClass没有重新定义构造方法,则只会执行第一个父类的构造方法。并且若SuperClass1和SuperClass2中有同名的方法,通过子类的实例化对象去调用该方法时调用的是第一个父类中的方法。
二.多态
多态即多种形态,在运行时确定其状态,在编译阶段无法确定其类型,这就是多态。Python中的多态和Java以及C++中的多态有点不同,Python中的变量是弱类型的,在定义时不用指明其类型,它会根据需要在运行时确定变量的类型(个人觉得这也是多态的一种体现),并且Python本身是一种解释性语言,不进行预编译,因此它就只在运行时确定其状态,故也有人说Python是一种多态语言。在Python中很多地方都可以体现多态的特性,比如 内置函数len(object),len函数不仅可以计算字符串的长度,还可以计算列表、元组等对象中的数据个数,这里在运行时通过参数类型确定其具体的计算过程,正是多态的一种体现。
Python以它这种独有的方式体现多态的根本原因我觉得有两点:1)Python是解释性语言;2)Python中变量是弱类型的。
...(代码略)...
在基类的run 方法声明的时候 ,计算机应该是申请了一个内存块 .等子类run方法声明的时候 ,又申请了一个内存块.根据具体情况决定 "指针"(引用 C的概念)具体指向哪个内存块 ,就变成根据"状态 "来决定,就是多态的概念
你会发现,新增一个 Animal的子类,不必对 run_twice()做任何修改 (根据 run指针指向的是哪个 run方法来执行具体的操作 ),实际上,任何依赖 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,这些继承关系看上去就像一颗倒着的树。
小结
继承可以把父类的所有功能都直接拿过来,这样就不必重零做起,子类只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写;
有了继承,才能有多态 。在调用类实例方法的时候,尽量把变量视作父类类型,这样,所有子类类型都可以正常被接收;
任何时候,如果没有合适的类可以继承,就继承自 object类。