面向对象三大特性之继承

Posted yunya-cnblogs

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面向对象三大特性之继承相关的知识,希望对你有一定的参考价值。

面向对象三大特性之继承

继承与__bases__

继承是一种创建新类的方式,极大程度上规避了类与类之间代码重复的问题。
Ps:在Python中,是支持多继承的,即一个子类可以继承多个父类。这是其他大多数语言中所不支持的。

父类也可以叫基类,超类
子类也可以叫派生类

实例名.__class__ 查看当前实例所属的类
类名.__bases__ 查看所继承的父类(元组形式,该元组中每个索引上封装了一个类对象)。Python2.x无法使用
类名.__base__ 查看所继承的父类(返回类对象)。Python2.x无法使用
class Man(object):

    def __init__(self,name,age,gender):
        self.name = name
        self.age = age
        self.gender = gender

class Woman(object):

    def __init__(self, name, age, gender):
        self.name = name
        self.age = age
        self.gender = gender

m1 = Man("小明",18,"male")
w1 = Woman("小红",17,"female")

# 使用 实例名.__class__ 可以看到实例对象属于的类

print(m1.__class__) # <class ‘__main__.Man‘>
print(w1.__class__) # <class ‘__main__.Woman‘>
class Man(object):

    def __init__(self,name,age,gender):
        self.name = name
        self.age = age
        self.gender = gender

class Woman(Man): # 继承 Man, 拿到 Man 的所有方法与属性
    pass

m1 = Man("小明",18,"male")
w1 = Woman("小红",17,"female")

# 使用 实例名.__class__ 可以看到实例对象属于的类

print(m1.__class__) # <class ‘__main__.Man‘>
print(w1.__class__) # <class ‘__main__.Woman‘>

# 使用 类名.__bases__ 可查看继承的父类 Ps: __base__ 返回类对象, __bases__ 返回元组,该元组每个索引上封装了一个类对象

print(Woman.__base__) # <class ‘__main__.Man‘>
print(Woman.__bases__) # (<class ‘__main__.Man‘>,)

经典类与新式类

在Python2.x中具有经典类和新式类之分。
经典类:没有继承object的类及其子类统称为经典类。 新式类:继承object的类及其子类统称为新式类 而在Python3.x中取消了经典类,所有类默认统一继承object类。 优点:新式类对比经典类来说,对于一些操作有了更好的支持。
Ps:个人推荐,即使使用Python3.x,也应当在继承中明确写上继承object类,这样显得更加规范。
# ==== Python2.x中的经典类 ====

class B1:
    pass

class B2(B1):
    pass

# ==== Python2.x中的新式类 ====

class F1(object):
    pass

class F2(F1):
    pass

# Ps:Python3.x中即使没有继承任何类,也都默认继承了object类。因此Python3中的所有类都是新式类。

派生方法覆写

当父类中的某一方法与子类中的方法同名时。子类实例对象的查找顺序是:

实例对象自身的__dict__  ---> 实例对象.__class__.__dict__ ---> 实例对象.__class__.__base__.__dict__ 依次序向上查找(注意:这里是指没有描述符的情况下)

因此,只要父类中的某一方法与子类中的方法同名,那么当前子类的实例对象要想调用该方法时首先拿到的是子类中的方法。

注意!!!不是覆盖!!!父类中的同名方法依旧存在!!!这不是覆盖!!!我称之为覆写
class F1(object):

    def show(self):
        print("F1.show")


class F2(F1):

    def show(self):
        print("F2.show")


f = F2()
f.show() # F2.show

单继承

单继承的查找顺序

在单继承下,不管是经典类还是新式类查找方式都一样。一条路摸到黑...
class A(object):
    
    def show(self):
        print("---A---")

class B(A):
    pass

b1 = B()

b1.show() # ---A---

技术图片

单继承查找注意事项

单继承下每一次找都先从实例对象开始找。
注意事项:普通的未设定为隐藏/私有的方法与设定了隐藏/私有的方法查找顺序本质是一样的。但是!他们的查找结果却完全不同
# 普通方法查找

class
A(object): def show(self): print("---A.show---") self.test() def test(self): print("---A.test---") class B(A): def test(self): print("---B.test---") b1 = B() b1.show() # ==== 执行结果 === """ ---A.show--- ---B.test--- """ # ==== 步骤分析 === """ 1. 查找 b.__dict__ 试图找到show 失败 2. 查找 B.__dict__ 试图找到show 失败 3. 查找 A.__dict__ 试图找到show 成功!!找到实例绑定方法show() 将 实例化对象 b1 自动传入 # === 开始重新接着找 === 4. 查找 b.__dict__ 试图找到test 失败 5. 查找 B.__dict__ 试图找到test 成功!!找到实例绑定方法test() 将 实例化对象 b1 自动传入 """
# 私有方法,隐藏方法查找

class
A(object): def show(self): print("---A.show---") self.__test() def __test(self): print("---A.test---") class B(A): def __test(self): print("---B.test---") b1 = B() b1.show() # ==== 执行结果 === """ ---A.show--- ---A.test--- """ # ==== 步骤分析 === """ 1. 查找 b.__dict__ 试图找到show 失败 2. 查找 B.__dict__ 试图找到show 失败 3. 查找 A.__dict__ 试图找到show 成功!!找到实例绑定方法show() 将 实例化对象 b1 自动传入 # === 开始重新接着找 === 4. 查找 b.__dict__ 试图找到_A__test 失败 5. 查找 B.__dict__ 试图找到_A__test 失败 6. 查找 A.__dict__ 试图找到_A__test 成功!!找到实例绑定方法_A__test() 将 实例化对象 b1 自动传入 """

多继承

普通多继承与钻石多继承

多继承,有两种情况。一种是普通多继承,一种是钻石多继承。

1.普通多继承:
    深度优先:从左至右一条一条的分支开始查找,每条分支找到头。最后找object类。

2.钻石多继承:
    在钻石多继承下经典类与新式类的查找顺序并不一样。
    经典类:深度优先。从左至右一条一条的分支开始查找,每条分支找到头。钻石的顶部类会在第一次就拿到。
    新式类:广度优先。从左至右一条一条的分支开始查找,如果都没有找到才去找钻石的顶部类。最后找object类。

 

技术图片

普通多继承的查找顺序

技术图片

class A(object):
    def show(self):
        print("---A---")

class B(object):
    def show(self):
        print("---B---")

class C(object):
    def show(self):
        print("---C---")

class D(A):
    def show(self):
        print("---D---")

class E(B):
    def show(self):
        print("---E---")

class F(C):
    def show(self):
        print("---F---")

class G(D,E,F):
    def show(self):
        print("---G---")

print(G.mro())  # 继承查找链,Python2.x中无法使用。

# [<class ‘__main__.G‘>, <class ‘__main__.D‘>, <class ‘__main__.A‘>, <class ‘__main__.E‘>, <class ‘__main__.B‘>, <class ‘__main__.F‘>, <class ‘__main__.C‘>, <class ‘object‘>]

钻石多继承下的经典类查找顺序(深度优先)

 技术图片

# -*- coding:utf-8 -*-

#  由于Python2中super方法并不支持经典类。也不支持mro方法,所以我就这样来进行验证了。

class A: def show(self): print("---A---") class B(A): pass class C(A): def show(self): print("---C---") class D(A): def show(self): print("---D---") class E(B): pass class F(C): def show(self): print("---F---") class G(D): def show(self): print("---G---") class H(E,F,G): pass h1 = H() h1.show() # ---A---

钻石多继承下的新式类查找顺序(广度优先)

技术图片

class A(object):
    def show(self):
        print("---A---")

class B(A):
    def show(self):
        print("---B---")

class C(A):
    def show(self):
        print("---C---")

class D(A):
    def show(self):
        print("---D---")

class E(B):
    def show(self):
        print("---E---")

class F(C):
    def show(self):
        print("---F---")

class G(D):
    def show(self):
        print("---G---")


class H(E,F,G):
    def show(self):
        print("---G---")

print(H.mro())  # 继承查找链,Python2.x中无法使用

# [<class ‘__main__.H‘>, <class ‘__main__.E‘>, <class ‘__main__.B‘>, <class ‘__main__.F‘>, <class ‘__main__.C‘>, <class ‘__main__.G‘>, <class ‘__main__.D‘>, <class ‘__main__.A‘>, <class ‘object‘>]

mro方法介绍

类的继承关系是在定义类的时候产生的,底层会根据一个叫c3算法(数学层面的算法)的东西生成一套继承关系。而使用 类名.mro()会调出一个列表,该列表下存放的就是当前类的属性查找顺序关系。

Ps:在Python2.x版本中不支持该方法。 

super方法 - 依赖mro

super的基本使用案例

注意:super方法只能在新式类中使用!不能在经典类中使用!!!

Python2中新式类使用super方法格式如下:
    super(当前的类名,self).需要调用的父类方法(需要传给父类的参数不传self)

Python3中的类使用super方法格式如下:
    super().需要调用的父类方法(需要传给父类的参数不传self)

super方法调用的类中的方法必须是存在于mro关系列表中,也就是说彼此之间必须有继承关系!
class Person(object):
    """人类"""
    def __init__(self,name,age,gender):
        self.name = name
        self.age = age
        self.gender = gender

class Student(Person):
    """学生"""
    def __init__(self,name,age,gender,height,weight):

        self.height = height
        self.weight = weight

        super(Student, self).__init__(name, age, gender)  # 调用父类的方法。根据当前实例对象的类的mro列表的顺序进行查找
# super().__init__(height,weight) 尽管可以这样传参,但是我仍然推荐上面的传参方式。

    def get_msg(self):
        print("姓名是:[{0}],年龄是:[{1}],性别是:[{2}],身高是:[{3}],体重是[{4}]".format(self.name,self.age,self.gender,self.height,self.weight))

s1 = Student("小明",18,"male",178,128)
s1.get_msg()  # 姓名是:[小明],年龄是:[18],性别是:[male],身高是:[178],体重是[128]
print(s1.__dict__)  # {‘height‘: 178, ‘weight‘: 128, ‘name‘: ‘小明‘, ‘age‘: 18, ‘gender‘: ‘male‘}

super方法在多继承下使用的注意事项

super严格依赖实例化对象所属类生成的mro列表进行查找。因此使用super要尽量的在继承关系清晰明了的情况下使用。
注意事项:即使看起来两个毫无关系的类,在mro列表中也是具有了关系。如下:
class A(object):
    def show(self):
        super(A, self).show()  # 1.根据mro列表来找,此时是A,向后。找到B
        print("---A---")  # 3.打印 ---A---

class B(object):
    def show(self):
        print("---B---") # 2.打印 ---B--- 返回

class C(A,B):
    pass

print(C.mro()) # [<class ‘__main__.C‘>, <class ‘__main__.A‘>, <class ‘__main__.B‘>, <class ‘object‘>]

c1 = C()
c1.show()

# ==== 执行结果 ====

"""
[<class ‘__main__.C‘>, <class ‘__main__.A‘>, <class ‘__main__.B‘>, <class ‘object‘>]
---B---
---A---
"""

Python2经典类使用super

由于经典类本身并不能使用super,所以我们在Python2.x经典类中要想使用super方法,需要添加一句代码:

__metaclass__ = type

添加完后这个代码,多继承下的经典类将按照新式类的查找方式进行查找!

这条代码的本质是为你定义的类添加一个元类。
# ==== 不添加 __metaclass__ = type ,经典类使用super()会抛出异常 ====

"""
Traceback (most recent call last):
  File "C:/Users/Administrator/PycharmProjects/learn/logging?????.py", line 44, in <module>
    c1.show()
  File "C:/Users/Administrator/PycharmProjects/learn/logging?????.py", line 31, in show
    super(A, self).show()  # 1.根据mro列表来找,此时是A,向后。找到B
TypeError: super() argument 1 must be type, not classobj
"""
__metaclass__ = type # Python2.x中的经典类查找顺序变为新式类的查找顺序。 如不添加则无法使用super方法

class A:
    def show(self):
        super(A, self).show()  # 1.根据mro列表来找,此时是A,向后。找到B
        print("---A---")  # 3.打印 ---A---

class B(object):
    def show(self):
        print("---B---") # 2.打印 ---B--- 返回

class C(A,B):
    pass

print(C.mro()) # [<class ‘__main__.C‘>, <class ‘__main__.A‘>, <class ‘__main__.B‘>, <class ‘object‘>]

c1 = C()
c1.show()

# ==== 执行结果 ====

"""
[<class ‘__main__.C‘>, <class ‘__main__.A‘>, <class ‘__main__.B‘>, <class ‘object‘>]
---B---
---A---
"""

直接调用某类中的方法 - 不依赖mro

直接调用的基本使用案例

# ==== 可以看到 A 和 B 之间并没有任何关系,我们依旧可以调用其中的方法 ====

class A(object):
    def show(self):
        B.show(self) # 由于是类来调用方法,所以我们需要手动为其传入self参数。即手动传入当前实例化对象a1
        print("---A---")

class B(object):
    def show(self):
        print("---B---")

print(A.mro()) # [<class ‘__main__.A‘>, <class ‘object‘>]

a1 = A()
a1.show()

# ==== 执行结果 ====

"""
[<class ‘__main__.A‘>, <class ‘object‘>]
---B---
---A---
"""

解决继承关系混乱:Mixins机制讲解

 Mixins机制图解:

技术图片

技术图片

  一个子类可以同时继承多个父类,这样的设计常被人诟病,一来它有可能导致可恶的菱形问题,二来在人的世界观里继承应该是个”is-a”关系。 比如轿车类之所以可以继承交通工具类,是因为基于人的世界观,我们可以说:轿车是一个(“is-a”)交通工具,而在人的世界观里,一个物品不可能是多种不同的东西,因此多重继承在人的世界观里是说不通的,它仅仅只是代码层面的逻辑。不过有没有这种情况,一个类的确是需要继承多个类呢?

?   答案是有,我们还是拿交通工具来举例子:

?   民航飞机、直升飞机、轿车都是一个(is-a)交通工具,前两者都有一个功能是飞行fly,但是轿车没有,所以如下所示我们把飞行功能放到交通工具这个父类中是不合理的

class Vehicle:  # 交通工具
    def fly(self):
        ‘‘‘
        飞行功能相应的代码        
        ‘‘‘
        print("I am flying")


class CivilAircraft(Vehicle):  # 民航飞机
    pass


class Helicopter(Vehicle):  # 直升飞机
    pass


class Car(Vehicle):  # 汽车并不会飞,但按照上述继承关系,汽车也能飞了
    pass

  Python提供了Mixins机制,简单来说Mixins机制指的是子类混合(mixin)不同类的功能,而这些类采用统一的命名规范(例如Mixin后缀),以此标识这些类只是用来混合功能的,并不是用来标识子类的从属"is-a"关系的,所以Mixins机制本质仍是多继承,但同样遵守”is-a”关系,如下

class Vehicle:  # 交通工具
    pass


class FlyableMixin:
    def fly(self):
        ‘‘‘
        飞行功能相应的代码        
        ‘‘‘
        print("I am flying")


class CivilAircraft(FlyableMixin, Vehicle):  # 民航飞机
    pass


class Helicopter(FlyableMixin, Vehicle):  # 直升飞机
    pass


class Car(Vehicle):  # 汽车
    pass

# ps: 采用某种规范(如命名规范)来解决具体的问题是python惯用的套路

 

  可以看到,上面的CivilAircraft、Helicopter类实现了多继承,不过它继承的第一个类我们起名为FlyableMixin,而不是Flyable,这个并不影响功能,但是会告诉后来读代码的人,这个类是一个Mixin类,表示混入(mix-in),这种命名方式就是用来明确地告诉别人(python语言惯用的手法),这个类是作为功能添加到子类中,而不是作为父类,它的作用同Java中的接口。所以从含义上理解,CivilAircraft、Helicopter类都只是一个Vehicle,而不是一个飞行器。

  使用Mixin类实现多重继承要非常小心

 

    • 首先它必须表示某一种功能,而不是某个物品,python 对于mixin类的命名方式一般以 Mixin, able, ible 为后缀
    • 其次它必须范围单一,如果有其他交通工具的方法,那就写其他交通工具的Mixin类而不是全部写在飞行器的Mixin类中,但是一个具体的类仍然可以继承多个Mixin类,为了保证遵循继承的“is-a”原则,应当只能继承一个标识其归属含义的父类
    • 然后,它不依赖于子类的实现
    • 最后,子类即便没有继承这个Mixin类,也照样可以工作,就是缺少了某个功能。(比如飞机照样可以载客,就是不能飞了)

 

  ? Mixins是从多个类中重用代码的好方法,但是需要付出相应的代价,我们定义的Minx类越多,子类的代码可读性就会越差,并且更恶心的是,在继承的层级变多时,代码阅读者在定位某一个方法到底在何处调用时会晕头转向,如下

class Displayer:
    def display(self, message):
        print(message)


class LoggerMixin:
    def log(self, message, filename=logfile.txt):
        with open(filename, a) as fh:
            fh.write(message)

    def display(self, message):
        super().display(message) # super的用法请参考下一小节
        self.log(message)


class MySubClass(LoggerMixin, Displayer):
    def log(self, message):
        super().log(message, filename=subclasslog.txt) 


obj = MySubClass()
obj.display("This string will be shown and logged in subclasslog.txt")


# 属性查找的发起者是obj,所以会参照类MySubClass的MRO来检索属性
#[<class ‘__main__.MySubClass‘>, <class ‘__main__.LoggerMixin‘>, <class ‘__main__.Displayer‘>, <class ‘object‘>]

# 1、首先会去对象obj的类MySubClass找方法display,没有则去类LoggerMixin中找,找到开始执行代码
# 2、执行LoggerMixin的第一行代码:执行super().display(message),参照MySubClass.mro(),super会去下一个类即类Displayer中找,找到display,开始执行代码,打印消息"This string will be shown and logged in subclasslog.txt"
# 3、执行LoggerMixin的第二行代码:self.log(message),self是对象obj,即obj.log(message),属性查找的发起者为obj,所以会按照其类MySubClass.mro(),即MySubClass->LoggerMixin->Displayer->object的顺序查找,在MySubClass中找到方法log,开始执行super().log(message, filename=‘subclasslog.txt‘),super会按照MySubClass.mro()查找下一个类,在类LoggerMixin中找到log方法开始执行,最终将日志写入文件subclasslog.txt

扩展:抽象基类的使用

抽象基类:
  当多个具有相同的共性的类需要实现时为了避免代码重复可以定义一个基类,
  但是共性之间又有一些不同的差异如果只用继承的方式解决那么差异就体现不出来了,这个时候抽象基类的用处就来了,抽象基类不需要有具体的方法实现,只是定义一个让所有子类都必须实现的方法。
  下面示例,人的哺乳方式和狗的不一样,但是都具有哺乳的方法,可以定义一个抽象基类来进行约束。

要想在Python中使用抽象基类,必须导入abc模块。

 关于abc模块:

  它实际上是各大序列的一个抽象基类。

  通过查看 有序/无序 类的源码我们可以看见其基本归根结底都是双下方法。

  因此,一个序列的功能取决于双下方法的实现。

 

  而abc模块更像是一种协议,约定了拥有哪些方法才能归入那种序列的一种协议。这也是抽象基类的一种功能:

  1.规范用户操作  

  2.当做一种协议的约束       

  3.与接口类似提供特定功能

import abc
class Mammal(metaclass=abc.ABCMeta):
    """哺乳动物类"""

    @abc.abstractmethod # 定义抽象方法即可,并不需要实现具体功能。只能约束子类
    def lactation(self):
        """哺乳方法"""
        pass

class Dog(Mammal): # 如果继承哺乳动物类,必须实现其哺乳功能。否则抛出异常
    """狗类"""
    def __init__(self,name):
        self.name = name

    def lactation(self):
        print("[{0}]正在哺乳...".format(self.name))

class Wolf(Dog):
    """狼类,作为抽象基类 [Mammal] 的孙子类,并不需要强制定义 [lactation] 哺乳方法,因为其父类 [Dog] 已经定义过了哺乳方法"""
    ...

class Person(Mammal):
    """人类..作为 [Mammal] 的子类,该类并没有定义 [lactation] 哺乳方法,故在实例化时会抛出异常"""
    def __init__(self,name):
        self.name = name

    pass

# ==== 实例化 ====

g1 = Dog("大黄")

g1.lactation()

w1 = Wolf("二哈") 

w1.lactation()

p1 = Person("小花")

# ==== 执行结果 ====

"""
[大黄]正在哺乳...
[二哈]正在哺乳...
Traceback (most recent call last):
  File "C:/Users/Administrator/PycharmProjects/learn/logging模块学习.py", line 39, in <module>
    p1 = Person("小花")
TypeError: Can‘t instantiate abstract class Person with abstract methods lactation
"""

 

 

 

 

 

 

以上是关于面向对象三大特性之继承的主要内容,如果未能解决你的问题,请参考以下文章

19.Python面向对象之:三大特性:继承,封装,多态。

面向对象之:三大特性:继承(已讲),封装,多态

面向对象三大特性之继承

面向对象三大特性之继承

面向对象的三大特性之继承

Python 入门 之 面向对象的三大特性(封装 / 继承 / 多态)