12.面向对象(继承/super/接口/抽象类)

Posted 简明现代魔法

tags:

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

面向对象

继承与派生

继承

  1. 什么是继承?继承是一种创建新的类的方式
class A:
    pass
class B(A):
    pass

在python中,新建的类可以继承自一个或者多个父类,原始类称为基类或者超类,新建的类称为派生类或者子类

python中类的继承分为,单继承和多继承。
查看继承的方法B.__bases__

如果没有指定基类,python的类会默认集成object类,object是所有python类的基类。

  1. 如何继承-》如何寻找继承关系

继承是一种‘是’的关系:

人类、猪类、狗类都继承动物类,因而他们都是动物

抽象:抽取类似或者说比较相似的部分

老师是这样定义这个抽象的,并且还介绍了一个抽象类的概念abc,但是这个抽象的意思翻译过来就是寻找对象的相似点,将对象根据特征进行归纳总结为类的过程。
在找到相似点(也就是“抽象”)之后,其实对象往往还有细分部分的一些特征,并且会有层级地可以划分类别
例如:
动物>>>
人/猪/狗>>>
奥巴马/梅西 // 麦兜/猪八戒 // 史努比/丁丁

由此将对象原本模糊的关注点隔离开,降低复杂度

继承:基于抽象的结果,通过编程语言去实现,经历抽象的过程,通过继承去表达出抽象的结构

  1. 为什么要用继承?
    解决代码重用问题:
    在了解了“继承就是‘是’的关系”之后,我们会发现,我们定义划分开的父子类之间,其实是有些包含的、并且共有的功能。
    例如“人”这个类,都是可以“走”、“说”的;动物这个个类,都可以有“吃”的动作。
    由此,我们就可以在父类中定义共同有的方法、属性,并且让所有子类都享用。
class hero:
    def __init__(self, name,aggressivity, life_value):
        self.name = name
        self.aggressivity = aggressivity
        self.life_value = life_value
    def attack(self,enemy):
        print(\'hero attack\')

class garen(hero):
    def __init__(self,name,aggressivity,life_value,script):
        hero.__init__(self, name,aggressivity, life_value)
        self.script = script

这段代码中,我们就定义了一个英雄类,他的子类盖伦可以继承一些他定义的属性
hero.__init__(self, name,aggressivity, life_value)
这就节约了盖伦类定义的代码,并且给以后定义新的子类英雄提供了便利。

但实际上,这种调用方法跟继承并没有关系,这种类名.函数的调用方法其实可以调用所有其他类的方法。而且当子类修改父类之后,代码还要随之更改,非常麻烦,并不推荐这种方式。

这就是代码的重用

当然子类也可以有自己的属性,例如上面代码中的self.script = script
如果说子类自己定义的属性或者方法和父类中有重名,那么就以子类自己定义的为准。

上面使用hero.__init__(self, name,aggressivity, life_value)就是在调用父类函数的方法作为子类定义函数的参数,在使用父类函数的时候,要记得不能再使用self,而是跟上要调用的类的名称。

派生:
子类继承了父类的属性,然后衍生出自己新的属性,
如果子类衍生出的新的属性与父类的某个属性名字相同,
那么再调用子类的这个属性,就以子类自己这里的为准了

继承顺序

class A(object):
    def test(self):
        print(\'from A\')

class B(A):
    def test(self):
        print(\'from B\')

class C(A):
    def test(self):
        print(\'from C\')

class D(B):
    def test(self):
        print(\'from D\')

class E(C):
    def test(self):
        print(\'from E\')

class F(D,E):
    # def test(self):
    #     print(\'from F\')
    pass
f1=F()
f1.test()
print(F.__mro__) #只有新式才有这个属性可以查看线性列表,经典类没有这个属性

#新式类继承顺序:F->D->B->E->C->A
#经典类继承顺序:F->D->B->A->E->C
#python3中统一都是新式类
#pyhon2中才分新式类与经典类

作业分析:

  • 新式类的继承顺序总结:
    C3算法非广度优先,利用MRO顺序进行继承

    “不彻底”,“从左往右”
    向父类遍历,到达根类的前一个类就往另外的支线遍历
  • 经典类中的集成顺序总结:
    深度优先
    “彻底”,“从左往右”
    向父类遍历,到达根类才向另外的支线遍历

继承原理

每定义一个泪,python会计算出一个方法解析顺序(MRO)列表,这个MRO列表就是一个简单的所有基类的线性顺序列表:

PyObject* tp_mro
Tuple containing the expanded set of base types, starting with the type itself and
ending with object, in Method Resolution Order.
以元组的形式返回所有基类的扩展的集合,从他本身的类开始,到object类结束
This field is not inherited; it is calculated fresh by PyType_Ready().
这个字段并不是通过继承得到的,而是通过by PyType_Ready()方法计算更新的

>>> class tsr:
…     pass
…
>>> str.__mro__
(<class \'str\'>, <class \'object\'>)
>>> str.mro()
[<class \'str\'>, <class \'object\'>]

为了实现继承,python会在MRO列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。
而这个MRO列表的构造是通过一个C3线性化算法来实现的。我们不去深究这个算法的数学原理,它实际上就是合并所有父类的MRO列表并遵循如下三条准则:

  1. 子类会先于父类被检查
  2. 多个父类会根据它们在列表中的顺序被检查
  3. 如果对下一个类存在两个合法的选择,选择第一个父类

子类调用父类的方法(super)

方法一:
父类名.父类方法()

不多说,参考上面的引用

方法二:
super()
当你使用super()函数时,Python会在MRO列表上继续搜索下一个类。只要每个重定义的方法统一使用super()并只调用它一次,那么控制流最终会遍历完整个MRO列表,每个方法也只会被调用一次(注意注意注意:使用super调用的所有属性,都是从MRO列表当前的位置往后找,千万不要通过看代码去找继承关系,一定要看MRO列表
思考题:

上图的代码以及运行结果已经放在图中,下面的代码使用的是纠正使用super()的代码:

class A(object):
    def __init__(self):
        print("enter A")
        super(A, self).__init__()  # new
        print("leave A")

class B(object):
    def __init__(self):
        print("enter B")
        super(B, self).__init__()  # new
        print("leave B")

class C(A):
    def __init__(self):
        print("enter C")
        super(C, self).__init__()
        print("leave C")

class D(A):
    def __init__(self):
        print("enter D")
        super(D, self).__init__()
        print("leave D")
class E(B, C):
    def __init__(self):
        print("enter E")
        super(E, self).__init__()  # change
        print("leave E")

class F(E, D):
    def __init__(self):
        print("enter F")
        super(F, self).__init__()  # change
        print ("leave F")

f = F()

小结:

  1. super并不是一个函数,是一个类名,形如super(B, self)事实上调用了super类的初始化函数,产生了一个super对象;
  2. super类的初始化函数并没有做什么特殊的操作,只是简单记录了类类型和具体实例;
  3. super(B, self).func的调用并不是用于调用当前类的父类的func函数;
  4. Python的多继承类是通过mro的方式来保证各个父类的函数被逐一调用,而且保证每个父类函数只调用一次(如果每个类都使用super);
  5. 混用super类和非绑定的函数是一个危险行为,这可能导致应该调用的父类函数没有调用或者一个父类函数被调用多次。

组合

组合对比继承来说,也是用来重用代码,但是组合描述的是一种“有”的关系

老师有课程
学生有成绩
学生有课程
学校有老师
学校有学生

#定义课程类
class Course:
    def __init__(self,name,price,period):
        self.name=name
        self.price=price
        self.period=period

#定义老师类
class Teacher:
    def __init__(name,course):
        self.name=name
        self.course=course
#定义学生类
class Student:
    def __init__(name,course):
        self.name=name
        self.course=course
#学生类和老师类中,都有关联到课程这个属性
#所以实例化一项课程
python=Course(\'python\',15800,\'7m\')
#在实例化学生和老师的时候, 将实例化后的课程作为参数传入
t1=Teacher(\'egon\',python)
s1=Student(\'alex\',python)

#这样学生和老师就获得了课程实例的参数,并且可以引用
print(s1.course.name)
print(s1.course.period)

上述代码就表示了一个“组合”的过程。类之间通过实例化传参的过程,来互相获取一些需要的数据,并且建立关系。

接口

继承有两种用途:
一:继承基类的方法,并且做出自己的改变或者扩展(代码重用)
二:声明某个子类兼容于某基类,定义一个接口类Interface,接口类中定义了一些接口名(就是函数名)且并未实现接口的功能,子类继承接口类,并且实现接口中的功能

接口的概念:

=================第一部分:Java 语言中的接口很好的展现了接口的含义: IAnimal.java
/*
* Java的Interface很好的体现了我们前面分析的接口的特征:
* 1)是一组功能的集合,而不是一个功能
* 2)接口的功能用于交互,所有的功能都是public,即别的对象可操作
* 3)接口只定义函数,但不涉及函数实现
* 4)这些功能是相关的,都是动物相关的功能,但光合作用就不适宜放到IAnimal里面了 */

package com.oo.demo;
public interface IAnimal {
    public void eat();
    public void run(); 
    public void sleep(); 
    public void speak();
}

=================第二部分:Pig.java:猪”的类设计,实现了IAnnimal接口 
package com.oo.demo;
public class Pig implements IAnimal{ //如下每个函数都需要详细实现
    public void eat(){
        System.out.println("Pig like to eat grass");
    }

    public void run(){
        System.out.println("Pig run: front legs, back legs");
    }

    public void sleep(){
        System.out.println("Pig sleep 16 hours every day");
    }

    public void speak(){
        System.out.println("Pig can not speak"); }
}

=================第三部分:Person2.java
/*
*实现了IAnimal的“人”,有几点说明一下: 
* 1)同样都实现了IAnimal的接口,但“人”和“猪”的实现不一样,为了避免太多代码导致影响阅读,这里的代码简化成一行,但输出的内容不一样,实际项目中同一接口的同一功能点,不同的类实现完全不一样
* 2)这里同样是“人”这个类,但和前面介绍类时给的类“Person”完全不一样,这是因为同样的逻辑概念,在不同的应用场景下,具备的属性和功能是完全不一样的 */

package com.oo.demo;
public class Person2 implements IAnimal { 
    public void eat(){
        System.out.println("Person like to eat meat");
    }

    public void run(){
        System.out.println("Person run: left leg, right leg");
    }

    public void sleep(){
        System.out.println("Person sleep 8 hours every dat"); 
    }

    public void speak(){
        System.out.println("Hellow world, I am a person");
    }
}

=================第四部分:Tester03.java
package com.oo.demo;

public class Tester03 {
    public static void main(String[] args) {
        System.out.println("===This is a person==="); 
        IAnimal person = new Person2();
        person.eat();
        person.run();
        person.sleep();
        person.speak();

        System.out.println("\\n===This is a pig===");
        IAnimal pig = new Pig();
        pig.eat();
        pig.run();
        pig.sleep();
        pig.speak();
    }
}

java中的interface

接口的概念解释和使用:

这里有一篇解释接口的文章,其中关于什么时候使用继承,什么时候用(组合)接口的方面,我看的模模糊糊。但是说不定以后能看懂:http://blog.csdn.net/xiaoxiongli/article/details/2791853
其中有一段解释说的很好,用来描述接口的概念真的妙到毫颤,特意摘下来整理,留作以后装逼用:

一马平川 19:58:54

接口是对类的抽象,我如果直接跟你说接口编程,你一定不理解,或者说很难理解,因为接口本身是很抽象的东西,现在我举例跟你说:

“电源插座就是接口”:

比方说,插座有两孔的,有三孔的,不同的插头需要不同的插座。
接口就描述了能适应的插头范围现在有一种插座是三孔的,但既可以插三孔的,也可插两孔的,知道么?
那么,我们可以说,这个插座设计的好,因为他能适用更广的范围。
当然,这个范围不能超出电源插座这个概念。
如果是用来插笔,做笔筒用,那也不适合。
如果电源插座不但能适用两孔和三孔的插头,还能适用笔的话,那么我们可以肯定的说,这个接口设计的太差了。因为接口(插座)的设计应该是对某一类事物的抽象。而且,接口(插座)实现以后,实现该接口的类(插头)必须符合接口的定义(插座和插口匹配),而且需要完全符合,一点不符合都不行。

所以实现某个接口的类,必须重写接口中定义的所有方法。如果你觉得该方法不需要实现,你可以留空,但必须重写。

看我这句话:“接口只定义了方法的原型,即参数和方法名以及返回值,集成接口的类需要实现它。”

而且,接口(插座)实现以后,实现该接口的类(插头)必须符合接口的定义(插座和插口匹配)。其实,你会发现插座生产出来后,如果某电器的插头和插座不匹配,那么就无法使用该电器了。实际上,你在设计一个接口的时候,很难想到要怎么去设计,尽管你知道集成这个接口的类是怎么样的。就像如果你开一个工厂生产插线板,你在不知道电器,或不完全知道电器的插头如何设计的时候,你是很难生产出能用的插线板的。

那么,如何设计插线板呢?或者说如何设计接口呢?

先看看插线板厂商是如何生产的吧。

某天,有人生产一个电器是4个孔的,那就用不了了。这时候,插线板厂商为了生产出一种插线板,能适用于目前的大部分电器,也能适用于将来的电器,他找到了一个机构。机构是专门指定规则的,专门制定协议的。机构叫来了大部分的重要电器厂商的头头,和插线板老板一起开了个会。大家为了共同的利益,决定了一份协议。
协议是这样的:电器厂商以后生产的电器的插头,只能生产三孔的,但为了兼容目前市场上已有的电器,也能生产两孔的,但是尽量生产三孔的。而且孔的大小和之间的距离有明确的规定。插线板厂商的插线板也只能有两孔的和三孔的,而且孔的大小和之间的距离也必须按照协议来生产。
于是问题解决了,而且插线板厂商老板很聪明,他发现可以生产出既可以插两孔,又可以插三孔的插口,于是他的插线板大卖,他发财了。优秀的接口设计,给他带来了大大的好处,但他很聪明,他没忘记如果没有规范协议的机构,一切都是空白。

再补充几句吧,不然你还是难以理解。

当你想设计一个接口的时候,你最好先写几个将要继承这个几口的类,写几个只有框架而无实际内容的类,看看他们之间的共性,找到写接口的点,这就正如找电器老板来开会。写接口的时候,你需要在之前对接口进行说明,说明接口的适用范围,以及继承该接口的注意事项,这就好比请机构来制定协议规范。有了这些以后,你的接口在被使用的时候就不会错用,在写继承该接口的类的时候,也会按照规范完全的匹配接口。

最后一句话,即使你理解了我今晚所讲的每一句话,你还是不会写接口,因为你需要实践,实践才会出真知。最后这句话才是至理名言,我说的基本都是空话(在你学会了写接口后)。


第一次写接口时,第一个感觉就是,写接口跟没写一样。定义一个接口,马上去写实现类!其实此时就是用着面向过程的思路写程序,然后挂了个羊头,说起来怎么也有个接口了!

今天看了一位老兄写的对于接口的心得体会,真是太有同感了!

不要为了接口而接口,当你把自己不当做是个程序员来思考时,就能把用人的思想来思考了,你不会写程序,就不会考虑细节的实现了!此时你所关注的问题就是比较抽象的了,你看这不正符合面向对象的原则吗?当年张三丰教张无忌打太极就是要把招式全忘了,你要定义接口前就先忘了自己是个程序员吧!

当然不可能有100%的抽象,最终你还是要回到实现细节上来的,可此时你已是学会了太极的张无忌了!

python中的接口:

单纯为了代码重用的继承,在实际的使用中很容易造成“高耦合”的问题。
而接口继承,根据接口的概念“给使用接口的对象一个很好的抽象”,将对象的一些特征进行归一化处理,使外部调用者无需关心细节,可以一视同仁地处理特定接口的所有对象。

但是在python中没有interface的概念。在python中定义接口,仅仅是看起来像接口:

第一次写接口时,第一个感觉就是,写接口跟没写一样。定义一个接口,马上去写实现类!其实此时就是用着面向过程的思路写程序,然后挂了个羊头,说起来怎么也有个接口了!

接口提取了一群类共同的函数,可以把接口当做一个函数的集合。

然后让子类去实现接口中的函数。

这么做的意义在于归一化,什么叫归一化,就是只要是基于同一个接口实现的类,那么所有的这些类产生的对象在使用时,从用法上来说都一样。

归一化,让使用者无需关心对象的类是什么,只需要的知道这些对象都具备某些功能就可以了,这极大地降低了使用者的使用难度。

抽象类

  • 定义:
    抽象类是一个特殊的类,他的特殊之处在于:只能被继承,不能被实例化

  • 为什么要有抽象类
    类是对相似的对象进行一个总结
    抽象类就是对有共同点的类的一种抽取总结(不完全说是总结,是因为类和类之间一定存在不同,不能完全统一总结成一类,真的是总结关系的话,应该就是子类和父类的关系了)。

从实现的角度来看,抽象类与普通类的不同在于:
抽象类只能有抽象方法(没有实现功能),这类不能被实例化,只能被继承,而且子类必须实现方法
从以上描述来看,抽象类非常类似接口,但是仍然是有差别的。

#_*_coding:utf-8_*_
__author__ = \'Linhaifeng\'
#一切皆文件
import abc #利用abc模块实现抽象类

class All_file(metaclass=abc.ABCMeta):
    all_type=\'file\'
    @abc.abstractmethod #定义抽象方法,无需实现功能
    def read(self):
        \'子类必须定义读功能\'    #如果子类不实现,就会报出此错误,以此来实现类似接口的功能
        pass

    @abc.abstractmethod #定义抽象方法,无需实现功能
    def write(self):
        \'子类必须定义写功能\'
        pass

# class Txt(All_file):
#     pass
#
# t1=Txt() #报错,子类没有定义抽象方法

class Txt(All_file): #子类继承抽象类,但是必须定义read和write方法
    def read(self):
        print(\'文本数据的读取方法\')

    def write(self):
        print(\'文本数据的读取方法\')

class Sata(All_file): #子类继承抽象类,但是必须定义read和write方法
    def read(self):
        print(\'硬盘数据的读取方法\')

    def write(self):
        print(\'硬盘数据的读取方法\')

class Process(All_file): #子类继承抽象类,但是必须定义read和write方法
    def read(self):
        print(\'进程数据的读取方法\')

    def write(self):
        print(\'进程数据的读取方法\')

wenbenwenjian=Txt()

yingpanwenjian=Sata()

jinchengwenjian=Process()

#这样大家都是被归一化了,也就是一切皆文件的思想
wenbenwenjian.read()
yingpanwenjian.write()
jinchengwenjian.read()

print(wenbenwenjian.all_type)
print(yingpanwenjian.all_type)
print(jinchengwenjian.all_type)

由上面的代码可以看出,抽象类基本上实现了接口的功能,但是抽象类的本质还是一个类。指的是一组泪的相似性,包括数据属性和函数属性,而接口只强调函数属性的相似性。
所以,抽象类是一个介于类和接口之ijede一个概念,同时具备类和接口的部分特性,可以用来实现归一化设计。

<wiz_tmp_tag id="wiz-table-range-border" contenteditable="false" style="display: none;">

以上是关于12.面向对象(继承/super/接口/抽象类)的主要内容,如果未能解决你的问题,请参考以下文章

面向对象编程

Java 面向对象

Day21 抽象接口多太鸭子类型

oop面向对象继承superthis抽象类

Java面向对象(OOP)--面向对象三大特性之一: 继承(继承Object类Super关键字方法重写)

Java面向对象OOP