Python面向对象特征:封装、继承、多态
1.面向对象三大特征
面向对象具有三大特征,分别为:
- 封装
- 继承
- 多态
2.封装
2.1 信息隐藏
封装,简单的讲,就是信息隐藏。封装即隐藏具体的实现细节,只提供给外界调用的接口。这样底层细节改变的时候不会对外界造成影响,只要提供给外界的接口不变即可。
2.2 成员的私有化
在程序中可以通过将变量私有化来做到封装。所谓的变量私有化,就是在类中定义的变量仅能在当前类(定义变量的类)中访问,而不能在类的外部访问。
如果一个属性名(或方法名)使用两个下划线__
开头,并且少于两个下划线结尾,则这样的属性(方法)就称为私有属性(方法),私有属性(方法)只能在类的内部访问。
示例:
class person:
#public
def __init__(self,a,b):
self.__name = a
self.__age = b
def set_age(self,x):
self.__age = x
def set_name(self,name):
self.__name =name
a = person("tom",20)
print(a.name,a.age)
报错,因为__name
和__age
是私有的,不能直接在类外访问:
AttributeError Traceback (most recent call last)
<ipython-input-14-fbc02d77b16f> in <module>
9
10 a = person("tom",20)
---> 11 print(a.__name,a.__age)
AttributeError: 'person' object has no attribute '__name'
正确方法是定义一个可以类外使用的方法:
class person(object):
def __init__(self,a,b):
self.__name = a
self.__age = b
def set_age(self,x):
self.__age = x
def set_name(self,name):
self.__name =name
def show_age(self):
print(self.__age)
def show_name(self):
print(self.__name)
a = person("tom",20)
a.show_name()
a.show_age()
输出:
tom
20
说明:如果变量(方法)以两个下划线开头,但同时结尾也是两个(或更多)的下划线,则这样的变量(方法)不是私有变量(方法)。因为Python中很多特殊变量与方法(魔法方法)都是这样命名的,例如__init__方法。如果这样的命名称为私有变量(方法),将会导致无法访问。
名称的私有化会带来一些问题。比如__name
为私有,那么想要获取Person
对象的名字(name
属性)或者设置该属性的值,现在都已经无法做到。为了能够不影响客户端的正常访问,可以提供公有的访问方法,一个用来获取私有属性值,一个用来设置私有属性值。
2.3 封装的优势
封装是一个过程,它分隔构成抽象的结构和行为的元素。封装的作业是分离抽象的概念接口与实现。
不过,在Python
语言中,所谓的私有,不过是一种假象。当我们在类中定义私有成员时,在程序内部会将其处理成_类名 + 原有成员名称
的形式。也就是会将私有成员的名字进行一下伪装而已,如果我们使用处理之后的名字还是能够进行访问的。但是不要这样做,因为这会破坏封装性,从而给自己埋下一颗不定时的炸弹。
对于上面person
类中的私有变量self.__name
,通过以下方式也可访问到,但是不建议这样做:
c=person("tom",19)
print(c._person__name)
print(c._person__age)
输出:
tom
19
2.4 property
在客户端访问时,公有的方法总不如变量访问那样简便,怎样才能既可以直接访问变量,又能够实现很好的封装,做到信息隐藏呢?
可以使用property
的两种方式来实现封装:
- 使用
property
函数 - 使用
@property
装饰器
简单示例:
class Person(object):
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def full_name(self):
return self.first_name+self.last_name
a = Person("zhang","san")
print(a.full_name)
输出:
zhang san
property
详细可以参见:https://www.cnblogs.com/z-x-y/p/10148911.html
3.继承
3.1 继承引入
继承体现的是一种一般与特殊的关系。如果两个类型之间存在一种一般与特殊的关系时(例如苹果与水果),就称特殊的类型继承了一般的类型(苹果继承了水果)。对于一般的类型(水果)称为父类,而对于特殊的类型(苹果)称为子类。
当子类继承了父类,子类就可以继承父类中定义的成员(变量,方法等),就好像在子类中自己定义的一样。
3.2 继承的实现
继承的语法为:
class B(A):
类体
这样,B类就继承了A类,B就成为一种特殊的A,B类就会继承A类的成员。
如果没有显式指定继承的类型,则类隐式继承object
类,object
是Python
中最根层次的类,所有类都是object
的直接或间接子类。
如果我们需要Fruit
,我们可以直接使用Fruit
类啊,为什么还要写一个类去继承这个类呢?答案是,如果现有Fruit
类的功能完全适合我们,我们自然可以使用现有的Fruit
类,但是,我们有时候可能还需要对现有类进行调整,这体现在:
- 现有类的提供的功能不充分,我们需要增加新的功能。
- 现有类的提供的功能不完善(或对我们来说不适合),我们需要对现有类的功能进行改造。
两个内建函数:
instance(对象,类型)
:判断第一个参数指定的对象是否是第二个参数的类型(父类也可以)issubclass(类型,类型)
:第一个参数是否是第二个参数的子类
成员的继承:
子类可以继承父类的成员。父类中声明的类属性、实例属性、类方法、实例方法与静态方法,子类都是可以继承的。
重写:
当子类继承了父类,子类就可以继承父类的成员。然而,父类的成员未必完全适合于子类(例如鸟会飞,但是鸵鸟不会飞),此时,子类就将父类中的成员进行调整,以实现适合子类的特征与功能。我们将父类中的成员在子类中重新定义的现象,称为重写。当通过子类对象访问成员时,如果子类重写了父类的成员,将会访问子类自己的成员。否则(没有重写)访问父类的成员。
重写时访问父类的成员:
子类重写了父类的成员,则在子类中访问的将是自己的成员。如果子类需要访问父类的成员,可以通过一下方法进行访问:
super().父类成员
实例属性的继承:
但是对于实例属性有些特别。因为实例属性是定义__init__
方法中,实现与对象self
的绑定。如果子类没有定义__init__
方法,就会继承父类的__init__
方法,从而在创建对象时,调用父类的__init__
方法,会将子类对象传递到父类的__init__
方法中,从而实现子类对象与父类__init__
方法中实例属性的绑定。但是,如果子类也定义了自己的__init__
方法(重写),则父类__init__
方法就不会得到调用,这样,父类__init__
方法中定义的实例属性就不会绑定到子类对象中。
如果子类与父类的初始化方式完全相同,子类只要继承父类__init__
方法就可以的。但有的时候,子类可能会增加自己的属性,此时,就不能完全套用父类的初始化方式。虽然父类的__init__
不完全适合子类,但是也并非完全不适合子类。因为两个类还是存在相同的属性的,因此,我们应该充分利用现有的功能,不要重复的实现。
实现方式就是,我们在子类的构造器中,去调用父类的构造器,完成公共属性的初始化,然后在子类构造器中,再对子类新增属性进行初始化。我们可以这样来调用父类的构造器:
super().__init__(父类构造器参数)
私有成员的继承:
子类在继承时,会不会继承私有成员?答案是私有成员会被继承,只不过子类不能访问父类的私有成员。
3.3 多重继承
在Python
中,类是支持多重继承的,即一个子类可以继承多个父类。这在现实中也会存在这样的情况。例如,正方形既是一种特殊的矩形(有一组临边相等的矩形),也是一种特殊的菱形(有一个角是直角的菱形),则正方形会继承矩形与菱形两个类,同时,矩形与菱形又都是一种特殊的平行四边形。
当子类继承多个父类时,子类会继承所有父类的成员。当多个父类含有相同名称的成员时,我们可以通过具体的父类名,来指定要调用哪一个父类的成员(如果是实例方法,需要显式传递一个类对象),这样就能够避免混淆。
通过类名调用可以避免混淆,但是,我们以子类的方式来调用从父类继承的成员时,会访问哪一个父类的成员呢?此时,就要求Python
中的方法解析顺序来决定了。
所谓的方法解析顺序(MRO,Method Resolution Order),就是当我们访问某个类的成员时,成员的搜索顺序。该顺序大致如下:
- 如果是单继承(继承一个父类),则比较简单,搜索顺序从子类到父类,一直到
object
为止。 - 如果是多继承(继承多个父类),则子类到每个父类为一条分支,按照继承的顺序,沿着每一条分支,从子类到父类进行搜索,一直到
object
类为止(深度优先)。 - 在搜索的过程中,子类一定会在父类之前进行搜索。
例如,假设我们有如下的继承体系:
在此例中,类B与类C继承类A,类D继承类B,类E继承类C,类F同时继承类C与类D。我们可以划分为两条分支,F -> D与F -> E,因为类F继承的顺序为(D,E),所以先从F -> D这条分支搜索,顺序为F -> D -> B,但是,虽然这条分支的上方还有类A,但这时不会搜索类A,因为搜索时有一个原则,那就是子类一定会在父类之前进行搜索。又因为类E与类C都是类A的子类,而这些子类还尚未搜索,故此时会跳过类A,当然,也会跳过所有类A的父类,例如类object。第一条分支结束后,会进行第二条分支F -> E,因为类F已经搜索过,故此时会搜索类F的父类,顺序为E -> C -> A。注意,类A此时就会搜索到,因为在这个时候,待搜索的所有候选类中,已经不存在类A的子类了。
综上,当通过类F访问某个成员时,成员的搜索顺序为:
F -> D -> B -> E -> C -> A -> object
当通过类F访问其类内的成员,会按照之前介绍的方法解析顺序搜索成员。
成员搜索顺序:
我们之前使用的super
类就是根据方法解析顺序来查找指定类中成员,但是有一点例外,就是super
对象不会在当前类中搜索,即从方法解析顺序的第二个类开始。super
的构造器会返回一个代理对象,该代理对象会将成员访问委派给相关的类型(父类或兄弟类)。
3.4 继承的优势
现在,我们就来处理一下之前提及的案例。可以发现,两个类中存在大量的代码重复,是由于两个类中存在很多公共的功能。我们知道,不管是Python教师,还是Java教师,都是一种特殊的教师。因此,我们可以采用继承的方式来处理。我们可以定义一个父类——教师类(Teacher),然后将所有公共的功能(目前两个类中重复的代码)放入父类中,使用两个子类去继承父类,这样就无需在每个子类中编写重复的内容。
4.多态
所谓多态,就是多种形态。指的是根据运行时对象的真正类型,来表现其应该具有的特征。即根据运行时对象的真正类型来访问该类型所对应的成员。
在Python
语言中,定义变量时没有具体的类型。我们也将Python
语言的这种特性称为“鸭子类型”。因此,Python
中的多态概念比较薄弱,不像一些定义变量时需要指定明确类型的语言(例如Java
,C++
等)中那么明显。