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类,objectPython中最根层次的类,所有类都是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中的多态概念比较薄弱,不像一些定义变量时需要指定明确类型的语言(例如JavaC++等)中那么明显。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值