Python面向对象特征

版权声明:原创文章转载请注明出处~ https://blog.csdn.net/PecoHe/article/details/90044610

10.1 面向对象三大特征

面向对象具有三大特征,分别为:

  • 封装
  • 继承
  • 多态

10.2 封装

10.2.1 信息隐藏

封装,简单的讲,就是信息隐藏。封装即隐藏具体的实现细节,只提供给外界调用的接口。这样,底层细节改变的时候,不会对外界造成影响,只要提供给外界的接口不变即可。

10.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()

output:

tom
20
  • 说明:如果变量(方法)以两个下划线开头,但同时结尾也是两个(或更多)的下划线,则这样的变量(方法)不是私有变量(方法)。因为Python中很多特殊变量与方法(魔法方法)都是这样命名的,例如__init__方法。如果这样的命名称为私有变量(方法),将会导致无法访问。

名称的私有化会带来一些问题。比如__name为私有,那么想要获取Person对象的名字(name属性),或者设置该属性的值,现在都已经无法做到。为了能够不影响客户端的正常访问,我们可以提供公有的访问方法,一个用来获取私有属性值,一个用来设置私有属性值。

10.2.3 封装的优势

封装是一个过程,它分隔构成抽象的结构和行为的元素。封装的作业是分离抽象的概念接口与实现。
不过,在Python语言中,所谓的私有,不过是一种假象。当我们在类中定义私有成员时,在程序内部会将其处理成_类名 + 原有成员名称的形式。也就是会将私有成员的名字进行一下伪装而已,如果我们使用处理之后的名字,还是能够进行访问的。但是,我们不要这样做,因为这会破坏封装性,从而给自己埋下一颗不定时的炸弹。

10.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)

output:

zhang san

property详细可以参见:https://www.cnblogs.com/z-x-y/p/10148911.html

10.3 继承

10.3.1 继承引入

继承体现的是一种一般与特殊的关系如果两个类型之间,存在一种一般与特殊的关系时(例如苹果与水果),我们就称特殊的类型继承了一般的类型(苹果继承了水果)。对于一般的类型(水果),我们称为父类,而对于特殊的类型(苹果),我们称为子类。
当子类继承了父类,子类就可以继承父类中定义的成员(变量,方法等),就好像在子类中自己定义的一样。

10.3.2 继承的实现

继承的语法为:

class B(A):
    类体

这样,B类就继承了A类,B就成为一种特殊的A,B类就会继承A类的成员。
如果没有显式指定继承的类型,则类隐式继承object类,object是Python中最根层次的类,所有类都是object的直接或间接子类。
如果我们需要Fruit,我们可以直接使用Fruit类啊,为什么还要写一个类去继承这个类呢?
答案是,如果现有Fruit类的功能完全适合我们,我们自然可以使用现有的Fruit类,但是,我们有时候可能还需要对现有类进行调整,这体现在:

  • 现有类的提供的功能不充分,我们需要增加新的功能。
  • 现有类的提供的功能不完善(或对我们来说不适合),我们需要对现有类的功能进行改造。
  • 两个内建函数:isinstance与issubclass

  • 成员的继承
    子类可以继承父类的成员,父类中声明的类属性、实例属性、类方法、实例方法与静态方法,子类都是可以继承的。

  • 重写
    当子类继承了父类,子类就可以继承父类的成员。然而,父类的成员未必完全适合于子类(例如鸟会飞,但是鸵鸟不会飞),此时,子类就将父类中的成员进行调整,以实现适合子类的特征与功能。我们将父类中的成员在子类中重新定义的现象,称为重写。
    当通过子类对象访问成员时,如果子类重写了父类的成员,将会访问子类自己的成员。否则(没有重写)访问父类的成员。

  • 重写时访问父类的成员
    子类重写了父类的成员,则在子类中,访问的将是自己的成员。如果子类需要访问父类的成员,可以通过一下方法进行访问:

super().父类成员
  • 实例属性的继承
    但是,对于实例属性有些特别。因为实例属性是定义__init__方法中,实现与对象(self)的绑定。如果子类没有定义init方法,就会继承父类的init方法,从而在创建对象时,调用父类的init方法,会将子类对象传递到父类的init方法中,从而实现子类对象与父类init方法中实例属性的绑定。但是,如果子类也定义了自己的init方法(重写),则父类init方法就不会得到调用,这样,父类init方法中定义的实例属性就不会绑定到子类对象中。
    如果子类与父类的初始化方式完全相同,子类只要继承父类__inir__方法就可以的。但有的时候,子类可能会增加自己的属性,此时,就不能完全套用父类的初始化方式。
    人与学生
    虽然父类的__init__不完全适合子类,但是也并非完全不适合子类。因为两个类还是存在相同的属性的,因此,我们应该充分利用现有的功能,不要重复的实现。
    实现方式就是,我们在子类的构造器中,去调用父类的构造器,完成公共属性的初始化,然后在子类构造器中,再对子类新增属性进行初始化。我们可以这样来调用父类的构造器:
    super().init(父类构造器参数)
  • 私有成员的继承
    子类在继承时,会不会继承私有成员?
    私有成员被继承,只不过子类不能访问父类的私有成员

10.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的构造器会返回一个代理对象,该代理对象会将成员访问委派给相关的类型(父类或兄弟类)。

10.3.4. 继承的优势

现在,我们就来处理一下之前提及的案例。可以发现,两个类中存在大量的代码重复,是由于两个类中存在很多公共的功能。我们知道,不管是Python教师,还是Java教师,都是一种特殊的教师。因此,我们可以采用继承的方式来处理。我们可以定义一个父类——教师类(Teacher),然后将所有公共的功能(目前两个类中重复的代码)放入父类中,使用两个子类去继承父类,这样就无需在每个子类中编写重复的内容。

10.4. 多态

所谓多态,就是多种形态。指的是根据运行时对象的真正类型,来表现其应该具有的特征。即根据运行时对象的真正类型来访问该类型所对应的成员。
在Python语言中,定义变量时,没有具体的类型。我们也将Python语言的这种特性成为“鸭子类型”。因此,Python中的多态概念比较薄弱,不像一些定义变量时需要指定明确类型的语言(例如Java,C++等)中那么明显。

猜你喜欢

转载自blog.csdn.net/PecoHe/article/details/90044610
今日推荐