Python封装和继承

面向对象的三大特性

  • 封装
  • 继承
  • 多态

封装

封装就是对对象的成员进行访问限制

封装的三个级别:

  • 公开:public
  • 受保护的:protected
  • 私有的:private
  • 这三个不是关键字

判别对象的位置

  • 对象内部
  • 对象外部
  • 子类中

私有成员

  • 私有成员是最高级的封装,只能在当前类或对象中访问

  • 在成员前面添加两个下划线即“__a”

      ```
      class Person ():
          # 公有成员
          name = "Tom"
    
          # 私有成员,
           __age = 29
    
      p = Person()
    
      # 访问公有成员
      print(p.name)
    
      # 不能访问私有成员
      # print(p.__age)
      ```
    
  • 在Python中,私有成员只是一个障眼法,可以通过“_dict_”属性访问到私有成员。

通过“_dict_”访问私有成员:

    print(Person.__dict__)
    print(p.__dict__)

结果:

    {'__module__': '__main__', 'name': 'Tom', '_Person__age': 25, '__dict__': , '__weakref__': , '__doc__': None}
    {}

可以看到“Person__age:25”,说明已经访问到私有成员了。我们知道,实例化对象调用“_dict”得到的是空对象,只有实例对象才会返回所有的属性和方法。

如果我们添加以下代码,还是可以访问到私有成员的:

    p._Person__age = 20
    print(p._Person__age)

结果:

    20

受保护的封装

  • 将对象或成员进行一定级别的封装,然后,在类中或者子类中可以访问,而在外部不能访问
  • 封装方法:在成员前面添加一个 下划线 即可。

公共 public

  • 对成员不进行任何操作,任何地方均可访问

继承

  • 格式

  • 子类与父类的交互方式

  • 多重继承

  • 包含

  • 概念:一个类获得另一个类的所有成员属性和方法叫做继承

  • 作用:减少代码冗余,增加代码的复用功能,同时可以设置类与类之间的关系

  • 被继承者与继承之间的关系:

    • 被继承者叫做父类,也叫基类,或叫超类
    • 继承者叫做子类,或叫派生类
    • 继承者与被继承者之间存在“is——a”的关系,也就是继承者是被继承者的子集、子类

继承的一般格式:

    # 这是一个父类
    class Parent (object):
        name = "Tom"

        def func1 (self):
            print(self.name)
            return

    # 这是一个子类,它继承了父类的属性和方法
    class Chind (Parent):
        pass

    # 类的实例化
    dad = Parent()
    son = Chind()

    # 调用属性
    print(dad.name)
    print(son.name)
    print(Child.name)

    # 调用方法
    dad.func1()
    son.func1()
    Child.func1()

结果如下:

    Tom
    Tom
    Tom
    Tom
    Tom
    Tom

其中,“Child(Parent)”表示子类Child继承了父类Parent所有的属性和方法。“Parent(Object)”的意思是所有类都是继承了Object对象的属性和方法,在Python3.0以后可以省略掉Object,程序自动默认继承。

当子类继承了父类的属性和方法后,不仅子类可以调用父类的所有属性和方法,子类的实例化对象也可以调用父类的属性和方法。

从上例可以得知,如果类Child不使用继承功能的话,那么它就必须重新定义和书写成员属性和方法,如果它的成员属性和方法与类Parent的相同,那么就会使代码冗余,代码重复。继承就省去了这样的麻烦,还充分发挥了重复使用代码的功能。

特点注意:

我们知道,Python中的所有数据均可以看作是对象,类也是同理。Parent和Child仅仅是两个名称,是指向类的 引用。Child继承Parent并不是将Parent中的成员赋值给了Child,而是通过Child这个引用访问Paren中的成员而已。

如,Parent中的name属性和func1函数存放在一个内存空间,可以通过Parent这个引用访问、调用,当Child继承后,也可以通过Child这个引用访问它们。

子类与父类的交互方式

  • 子类隐性继承父类
  • 子类重写父类方法
  • 重写方法后,使用super调用父类同名的方法

子类继承父类,是为了减少代码冗余度。子类不仅可以拥有父类的属性和方法,还可以拥有自己独有的属性和方法。

也就是说,继承是让通用的功能放在父类中,而自己独有的属性和方法就需要子类自己定义,这也叫子类与父类的交互方式。

子类与父类的交互方式有三种:

子类隐性继承父类

这一种就是最常见的普通继承,子类继承父类的所有属性和方法,它本身没有独自的属性和方法。

    # 定义一个Teacher父类
    class Teacher ():
        name = "Jon"
        age = 25
        duty = "育人"

    def doWork (self):
        print("你好,我叫{},我今年{}岁了,我的职责是:{}。".format(self.name, self.age, self.duty))
        return

    # 定义一个子类
    class Teacher1 (Teacher):
        pass


    # 实例化对象
    teacher1 = Teacher1()

    # 调用继承的属性和方法
    print(teacher1.name)

    teacher1.doWork()

结果:

    Jon
    你好,我叫Jon,我今年25岁了,我的职责是:育人。

这种情况是子类没有父类中的属性和方法,只有继承父类。

子类中重写方法

  • 所谓重写,一是改变继承父类后的属性和方法的功能,即方法名相同,但功能却不相同。二是在继承的基础上再添加新的功能方法。
  • 调用重写后的方法,它的功能是子类中定义的功能,不是父类中的功能。
  • 重写方法后,子类重写的方法实现自己的功能,父类的方法还是实现原来的功能,父类方法不受影响。

在上面的例子的基础上,我们为子类Teacher1新添加属性和重写doWork方法,并再新添加doLover方法:

    # 重写属性和方法
    # 定义一个Teacher父类
    class Teacher ():
        name = "Jon"
        age = 25
        duty = "教师"

    def doWork (self):
        print("你好,我叫{},我今年{}岁了,我的职责是:{}".format(self.name, self.age, self.duty))
        return

    # 定义一个子类
    class Teacher1 (Teacher):
        #重写部分属性
        name = "God"
        lover = "打乒乓球"

        # 重写doWork方法
        def doWork (self):
            print("大家都叫我{},我今年已有{}岁了,现在我的职业是一名{}。".format(self.name, self.age, self.duty))
            return

        # 添加新的功能方法
        def doLover (self):
            print("我的爱好是{}".format(self.lover))
            return


    # 实例化对象
    teacher = Teacher()
    teacher1 = Teacher1()

    # 调用属性
    print(teacher1.name)
    print(teacher.name)

    teacher.doWork()
    # 调用重写后的方法
    teacher1.doWork()

    # 调用新添加的方法
    teacher1.doLover()

结果:

    Jon
    God
    你好,我叫Jon,我今年25岁了,我的职责是:教师
    大家都叫我God,我今年已有25岁了,现在我的职业是一名教师。
    我的爱好是打乒乓球

我们可以看出,当子类的实例化对象调用doWork方法时,它实现的功能不是父类Teacher中定义的功能,而下子类Teacher1中重写的功能。

然后,我们又将父类中的属性name重写了一遍,将它赋值God,实例化对象调用后的结果也是God。更加有意思的是,我们为子类Teacher1新添加的方法也实现了它的功能,这个功能是父类没有的。

使用super()方法重新调用父类方法

  • 使用这个方法的前提:子类已经重写父类的方法。
  • 格式:super(子类名, self).父类方法
  • 在Python3中,super()中的参数可以省略,程序自动识别继承。
  • super不是关键字,它是一个可以调用到父类的类

还是以上面的例子为例,使用super()方法让子类可以重新调用父类中的方法:我们在子类Teacher1中修改doWork方法的代码:

    # 重写doWork方法
    # 使用super()方法重新调用父类的方法
    def doWork (self):
        # 与super()方法作对比
        print("大家都叫我{},我今年已有{}岁了,现在我的职业是一名{}。".format(self.name, self.age, self.duty))

        #子类重新调用父类的方法
        print("子类重新调用父类的方法:")
        super(Teacher1, self).doWork()
        return

结果:

        Jon
        God
        你好,我叫Jon,我今年25岁了,我的职责是:教师
        大家都叫我God,我今年已有25岁了,现在我的职业是一名教师。
        子类重新调用父类的方法:
        你好,我叫God,我今年25岁了,我的职责是:教师
        我的爱好是打乒乓球      

可以看到,子类又可以重新调用父类的方法了。

super是一个可以调用父类的类,它不是关键字:

    print(type(super))
    help(super)

结果:

    
    class super(Object)

关于私有成员和受保护成员的访问权限

  • 私有成员:在属性前面加两个下划线“__”
  • 受保护成员:在属性前面加一个下划线“_”
  • 子类中可以访问私有成员和受保护成员,外部不能访问这两种成员

现在,我们在父类中新添加一个私有成员和一个受保护成员,在子类中可以访问到这两种成员,但在外部即直接打印这两种成员是不允许的。

    class Person ():
        name = "刘三"

        # 定义私有成员
        __age = 20

        # 定义受保护的数据
        _corde = 90
        loverN = "网上冲浪"

        def doG (self):
            print("大家好,我叫{},我今年{}岁了,我的爱好是{},我的成绩是{}分。". format(self.name, self.__age, self.loverN, self._corde))
            return

    class Per (Person):
        pass


    person = Per()

    print(person.name)

    # 外部不能访问私有成员
    #print(person.__age)

    # 外部不能访问受保护的数据,打印为空
    print(person._corde)

    # 子类可以访问父类中私有的成员和受保护的成员
    person.doG()

结果:

    刘三
    90
    大家好,我叫刘三,我今年20岁了,我的爱好是网上冲浪,我的成绩是90分。

可以看到,通过外部访问受保护的成员即“print(person._corde)”打印出来的是空白。

构造函数(_init_)

  • 构造函数是特殊的函数,在类实例化之前调用。
  • 如果定义了构造函数,在实例化对象时会自动调用。
  • 构造函数的查找顺序:优先级别
    • 先从子类中查找,如果子类中定义了构造函数,就调用子类的构造函数
    • 如果子类中没有定义构造函数,父类中定义了构造函数,那么就会自动向上查找调用父类的构造函数
    • 如果父类也没有定义构造函数,那么就会继续向上查找父类的父类中是否有构造函数,如果有就调用,如果没有就不调用构造函数
  • 构造函数中除了了self这个默认的参数外,还可以定义其它的参数:
    • 如果构造函数有其它参数,在实例化对象时,需要传入实参
    • 实例化对象时传入的实参数量需要与构造函数中的形参数量相同

现在,我们定义三个类并依次继承,先在子类中定义构造函数

    # 构造函数的使用

    # 父类中没有构造函数
    class Animal ():
        pass

    # 父类没有构造函数
    class PaxingAni (Animal):
        pass

    # 子类中有构造函数
    class Dog (PaxingAni):
        # 定义构造函数
        def __init__ (self):
            print("这是子类上的构造函数__init__")

    # 实例化对象
    dog = Dog()

结果:

    这是子类上的构造函数__init__

可以看到,我们只是实例化了对象,并没有使用如“dog._init_()”的格式调用构造函数,程序就自动为实例化对象调用了构造函数。

然后,我们取消子类中的构造函数,在PaxingAni这个父类中定义构造函数:

    # 父类中没有构造函数
    class Animal ():
        pass

    # 父类中定义构造函数
    class PaxingAni (Animal):
        def __init__ (self):
            print("这是PaxingAni中的构造函数__init__")

    # 子类中有构造函数
    class Dog (PaxingAni):
        # 没有定义构造函数
        pass

# 实例化对象
dog = Dog()

结果:

    这是PaxingAni中的构造函数__init__

子类中没有定义构造函数,在PaxingAni这个父类中定义了构造函数,程序会自动向上查找,自动调用父类的构造函数。

同理,如果只在Animal这个父类中定义构造函数,程序也会自动查找到它并调用。

当然,如果子类和父类同时定义了构造函数,实例化对象会按构造函数查找顺序调用子类本身的构造函数:

    # 父类中没有构造函数
    class Animal ():
        pass

    # 父类中定义构造函数
    class PaxingAni (Animal):
        def __init__ (self):
            print("这是PaxingAni中的构造函数__init__")

    # 子类中有构造函数
    class Dog (PaxingAni):
        # 没有定义构造函数
        pass

    # 子类中定义了构造函数
    class Cat (PaxingAni):
        def __init__ (self):
        print("子类中的构造函数__init__")

    # 实例化对象
    dog = Dog()

    cat = Cat()

结果:

    这是PaxingAni中的构造函数__init__
    子类中的构造函数__init__

即使PaxingAni父类中有构造函数,实例化对象cat也会调用子类Cat中的构造函数。

当构造函数中有形参时,实例化对象时也需要传入实参:

    # 父类中没有构造函数
    class Animal ():
        pass

    # 父类中定义构造函数
    class PaxingAni (Animal):
        def __init__ (self, name):
            self.name = name
            print("这是PaxingAni中的构造函数__init__,名字:{}".format(self.name))

    # 子类中有构造函数
    class Dog (PaxingAni):
        # 没有定义构造函数
        pass

    # 子类中定义了构造函数
    class Cat (PaxingAni):
        def __init__ (self, name):
            self.name = name
            print("子类中的构造函数__init__,名字:{}".format(self.name))

    # 实例化对象
    dog = Dog("Tom")

    cat = Cat("Jon")

结果:

    这是PaxingAni中的构造函数__init__,名字:Tom
    子类中的构造函数__init__,名字:Jon

如果在实例化对象时不传入实参,或者定义构造函数时不定义形参,使实参数量与形参数量不相等,程序出错。

单继承和多继承

  • 单继承:每个类只能继承一个父类。
  • 多继承:每个类可以继承多个父类,不推荐。
  • 两者的优缺点
    • 单继承:
      • 优点:逻辑清晰,语法简单明了。
      • 缺点:功能无法无限扩展性,只能在当前继承链中扩展。
    • 多继承:
      • 优点:功能可以无限扩展。
      • 缺点:继承关系混乱。如菱形继承/钻石继承问题:关系错综复杂
  • mro:表示继承的顺序列表,返回父类的列表。它的顺序是继承时括号内书写父类的顺序。
    • 查找顺序:从子类本身出发,依次查找父类。

举例:我们实现单个继承

    # 依次单个继承
    class A ():
        name = "Tom"

        def doA (self):
            print("我的名字:{}".format(self.name))
            return

    class B (A):
        age = 21

        def doB (self):
            print("年龄:{}".format(self.age))
            return

    class C (B):
        lover = "网上音浪"

        def doC (self):
            print("爱好:{}".format(self.lover))
            return

    c = C()

    print(c.name)

    print(c.age)

    print(c.lover)

    # 返回父类链表
    print(C.__mro__)

结果:

    Tom
    21
    网上音浪
    (, , , )

可以看出,B继承了A,然后C继承了B。C的实例化对象可以访问它的父类中的所有非私有成员,查找顺序为子类本身,然后向上查找父类。

其中,“C.mro”表示C类的所有父类链表,它表示继承的顺序列表,也就是它继承的所有父类的列表。A是默认继承Object对象的。

然后我们使用多继承:

    # 依次单个继承
    class A ():
        name = "Tom"

        def doA (self):
            print("我的名字:{}".format(self.name))
            return

    class B ():
        age = 21

        def doB (self):
            print("年龄:{}".format(self.age))
            return

    class C (A,B):
        lover = "网上音浪"

        def doC (self):
            print("爱好:{}".format(self.lover))
            return

    c = C()

    print(c.name)

    print(c.age)

    print(c.lover)

    print(C.__mro__)

结果:

    Tom
    21
    网上音浪
(, , , )

结果还是和依次单个继承一样。我们将C类的继承方式修改成了“class C(A, B)”表示同时继承A和B。最后返回的结果一样。

我们可以注意到,“mro”返回的是继承的所有父类的列表,它表示继承的顺序列表。

如果我们写成这样“class C (B, A)”,那么父类B就在A的前面。它的顺序就是继承括号内父类的书写顺序。

多态

  • 同一对象在不同情况下以不同状态出现
  • 多态不是语法,是一种设计思想
  • 多态性:一种调用方式,多种执行结果

Mixin设计模式

  • 采用多继承对类进行功能扩展
  • 使用多继承语法实现Mixin
  • 条件:
    • 首先表示某一单一功能,如果有多个功能,需要写多个Mixin
    • 子类如果没有继承这个Mixin,它照样工作,只是少了一个功能

Mixin举例:我们知道超人可以在天上飞,也可以在海里自由自在地游泳且不用任何装备。这里超人拥有两个功能:飞和游泳。这里就需要写两个Mixin,分别代表飞和游泳。如果超人没有继承飞这个Mixin,他照样可以游泳,只是不能飞而已。

在这里,超人有三个父类,分别是人、鸟、鱼。

Mixin例子:

    # A的功能,单一功能
    class A ():
        name = "Tom"

        def doA (self):
            print("我的名字:{}".format(self.name))
            return

    # B的功能,单一功能
    class B ():
        age = 21

        def doB (self):
            print("年龄:{}".format(self.age))
            return

    class C (A,B):
        lover = "网上音浪"

        def doC (self):
            print("爱好:{}".format(self.lover))
            return

    c = C()

    print(c.name)

    print(c.age)

    print(c.lover)

    print(C.__mro__)

以上,A和B分别表示一种功能,这种功能均是单一的功能,然后通过多继承全部继承给C,让C同时拥有两种功能。

A和B都是Mixin,各自表示各自的功能。

继承相关的函数

  • issubclass(B, A):判断B是否是A的子类,如果是返回True。
  • isinstance(a, A):判断a是否为A的实例化对象,如果是返回True。
  • hasattr(a, attr):检查类中是否有某成员,如果有返回True。
  • getattr
  • setattr
  • dir:获取对象的成员列表
  • delatrr:删除成员

举例:

    class A ():
        pass

    class B (A):
        name = "Tom"

    a = A()
    b = B()
    print(issubclass(B, A))
    print(isinstance(a, A))
    print(hasattr(b, "name"))
    print(hasattr(b, "age"))

结果:

    True
    True
    True
    False

继承的特点:

  • 在Python中,所有类都继承Object对象,也就是说,Object是所有类的父类。Python3.0以后,所有类默认继承,可以不书写Object。

  • 子类继承父类后,可以调用除了私有成员外的所有成员(属性和方法)。

  • 子类继承父类,不是将父类的所有成员赋值给子类,而是通过引用方式访问父类成员。

    • 父类名也只是指向类的引用而已,成员在一个内存空间,通过父类名访问它们,子类继承后也是通过子类名这个引用访问这些成员的。
  • 子类除了继承父类的属性和方法外,还可以拥有自己独有的属性和方法,这也是子类的作用之一。

  • 子类中定义的属性和方法与父类的属性和方法相同,优先使用子类中定义的属性和方法,这称为 重写

  • 子类可以扩展父类的属性和方法。即在子类中定义方法的同时,可以访问父类的成员,一般使用“super.父类成员”的格式访问。

  • 继承后,属性和方法的查找顺序:先从子类自身查找,如果没有,再从其继承的父类开始查找。

  • super

    • super不是关键字,而是一个类。
    • super是一个获取父类的类。
    • super与父类没有直接性的关系,但可以调用到父类。

你可能感兴趣的:(Python封装和继承)