继承、派生、组合

本文深入探讨了面向对象编程中的继承与组合概念,解释了如何利用这两种机制减少代码冗余并提高代码复用性。文中还介绍了接口与归一化设计的重要性,以及Python中的抽象类和对象序列化的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

继承

类之间会有一些相同的属性,提取这些相同的属性做成基类(父类)
继承是创建类的一种方式,通过代码重用,减少代码冗余。把父类的属性遗传给子类。
在创建类时,新建的类可以继承一个或多个父类,方式如下:

class ClassName(BaseName1, BaseName2,...):  # 括号内是继承的父类
    '类注释文档'
    pass

继承是一种类与类之间的关系: 什么 是 什么
查看所有继承:ClassName.__bases__ ,返回元组。
在python3中,所有类默认继承object。
在python2中,继承了object的子类都称为新式类,而没有继承object及其子类的,称为经典类。
继承的子类会获得父类的所有属性。对象在调用属性时,寻找顺序如下:
对象名称空间 >>> 所属类名称空间 >>> 父类名称空间
单纯的继承没有意义,子类需要派生自己新的属性:

class People():  # 默认继承object
    def __init__(self, name, age, sex):
        print('initializing...')
        self.name = name
        self.age = age
        self.sex= sex
    def walk(self):
        print('%s can walk well' % self.name)

class Teacher(People):
    def __init__(self, name, age, sex, salary, level):
        People.__init__(self, name, age, sex)   # 继承父类的属性
        # 派生子类自己的数据属性
        self.salary = salary
        self.level = level
    # 派生子类自己的函数属性,父类中有的就不用重复写了。
    def teach(self):
        print('%s is good at teaching' % self.name)

组合

组合也是一种类与类之间的关系: 什么 有 什么 。也是通过代码重用,减少代码冗余。

class Date():
    def __init__(self,year,mon,day):
        self.year = year
        self.mon = mon
        self.day = day
    def tell_info(self):
        print('birth is %s-%s-%s' % (self.year, self.mon, self.day))

class Teacher():
    def __init__(self, name, age, *args): # 这里用*args接收year,mon,day
        self.name = name
        self.age = age
        self.birth = Date(*args)   # birth属性的是Date类的对象。

t = Teacher('egon',18,1990,2,30)    # 1990,2,30 这三个参数传给Date中的__init__函数
t.birth.tell_info()
'''
birth is 1990-2-30
'''
class People:
    def __init__(self,name,age,sex):    # 函数的默认参数不要写成可变的,所以couser=[]不要写在形参位置
        self.name = name
        self.age = age
        self.sex = sex
        self.course = []

    def tell_info(self):
        print('''
        ----- %s info -----
        NAME:   %s
        AGE:    %s
        SEX:    %s
        '''%(self.name,self.name, self.age,self.sex))

    def tell_course(self):
        if self.course:
            for i in self.course:
                print(i.tell_info())
        else:
            print('no course added!')
    # def tell_couser(self):
    #     if 'couser' in self.__dict__: # 如果不设couser默认参数,那么就没有couser属性,
            # 如果用 if self.couser: 就会报错。判断字符串在名称空间的字典就没问题
    #         for i in self.couser:
    #             print(i.tell_info)
    #     else:
    #         print('no couser added')


class Teacher(People):
    def __init__(self,name,age,sex,salary,level):    # 函数的默认参数不要写成可变的,所以couser=[]不要写在形参位置
        People.__init__(self,name,age,sex)
        self.salary = salary
        self.level = level


class Student(People):
    def __init__(self,name,age,sex,group):
        People.__init__(self,name,age,sex)
        self.group=group


class Date:
    def __init__(self,year,mon,day):
        self.year = year
        self.mon = mon
        self.day = day

    def tell_info(self,obj):
        print('%s 出生于:%s-%s-%s' % (obj.name,self.year, self.mon, self.day))


class Course:
    def __init__(self,name,price,period):
        self.name = name
        self.price = price
        self.period = period

    def tell_info(self):
        print('''
        ----- %s info -----
        name:   %s
        price:  %s
        period: %s
        '''%(self.name,self.name,self.price,self.period))

Birth = Date(1990,2,31) # 创建时间对象
python = Course('python',15800,'6monts')    # 创建课程对象python
go = Course('go', 10000, '6monts')  # 创建课程对象 go

alex = Teacher('alex',84,'female',300,1)    # 创建老师对象
alex.birth = Birth  # 将日期组合进老师对象
alex.course.append(python)  # 将课程对象组合进老师对象
alex.course.append(go)
alex.birth.tell_info(alex)
alex.tell_course()
alex.tell_info()

ayhan = Student('ayhan',18,'male','group7')
ayhan.tell_course()

接口与归一化设计

接口,隐藏具体的实现细节,将使用简单化。比如,在linux中,一切皆文件,比如文本文件、磁盘文件、进程文件,都有读写操作,使用者不需要关心每种文件的读和写是如何具体实现的,只需要知道只要是文件,就应该有读方法和写方法,调用这两个方法,就可以完成想要的效果。我们可以通过继承,来模拟这种情况:

class File():   # 定义文件类来模仿接口的概念
    def read(self):  # 定义接口函数read
        pass

    def write(self):    # 定义接口函数write
        pass


class Txt(File):    # 具体实现文本文件的read和write
    def read(self):
        print('文本文件读方法')    # 这里用print来模拟,具体的实现可能是很复杂的。

    def write(self):
        print('文本文件读方法')

class Sata(File):   # 具体实现磁盘文件的read和write
    def read(self):
        print('磁盘文件读方法')

    def write(self):
        print('磁盘文件写方法')

class Process(File):    # 具体实现进程文件的read和write
    def read(self):
        print('进程文件读方法')

    def write(self):
        print('进程文件写方法')


t = Txt() 
s = Sata()
p = Process()
# 具体到三个不同的对象,使用者只需要知道它们是文件,是文件就有read和write方法:
# t.read() t.read() s.read() s.write() p.read() p.write()

上面这个栗子中,定义的File这个父类,只放方法名(read, write),相当于一个模板,具体的功能由子类来实现。
但是,子类在定义时,并不一定要遵循父类中的方法,因此,父类有必要对此加以限制:
1. 子类必须要有父类的方法
2. 子类实现的方法必须跟父类的方法的名字一样

# 知识准备:抽象类就是基于类抽象而来的。
# 抽象类中只能有抽象方法(没有实现功能),该类不能被实例化,只能被继承,且子类必须实现抽象方法
import abc  # 导入abc模块实现抽象类

class File(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def read(self):
        pass

    @abc.abstractmethod
    def write(self):
        pass

通过上面这种方式,我们再定义子类时,就必须要有父类的方法,并且方法名一样,否则就无法实例化对象:

class Txt(File):    # 父类是File
    def du(self):
        print('文本文件读方法')

    def xie(self):
        print('文本文件写方法')

t = Txt()
'''虽然上面的Txt类在定义阶段没问题,但是在实例化创建对象时,会提示无法实例化抽象类,
    t = Txt()
TypeError: Can't instantiate abstract class Txt with abstract methods read, write
'''

这种把所有方法都统一起来的方式,就是归一化设计,方便使用。

对象的序列化

pickle可以序列化任何python的数据类型

import pickle

class Sample:   # 定义类
    pass

obj = Sample()  # 创建对象

with open(file,'wb')as f:   # 序列化
    pickle.dump(obj,f)

with open(file, 'rb')as f:  # 反序列化
    obj = pickle.load(f)

注意,对象是依赖于类的,如果反序列化出的对象没有在内存中找到所属的类,就会报错。解决这个问题,可以通过导入模块的方式,将类导入。因为导入会执行模块的内容,加载到内存。

继承的实现原理MRO

情况一:

这里写图片描述
属性的查找,从左到右,一条条分支的找。这种情况下,经典和新式类寻找都一样:
DAE > B > C

情况二

这里写图片描述
新式类:HEB>FC>GDA 最后一个分支时才找到头。广度优先。
经典类:一条分支找到头,再找下一个分支。深度优先。
HEBA > FC > GD

class A:
    pass
    def foo(self):
        print('from A')

class B(A):
    pass
    # def foo(self):
    #     print('from B')

class C(A):
    pass
    def foo(self):
        print('from C')

class D(A):
    pass
    def foo(self):
        print('from D')

class E(B):
    pass
    # def foo(self):
    #     print('from E')

class F(C):
    pass
    def foo(self):
        print('from F')

class G(D):
    pass
    def foo(self):
        print('from G')

class H(E,F,G):
    pass
    # def foo(self):
    #     print('from H')

obj = H()
obj.foo()   
# 经典类打印结果是 from A         
# 经典类查找:HEBA > FC > GD  深度优先,一个分支找到头,再找下一个分支
# 如果让class A(object) 继承object,成为新式类,那么,打印结果是 from F
# 新式类查找:HEB > FC > GDA > (object)  广度优先,在F时找到

再说self

self就是调用方法的对象本身!无论何时,找方法时先回自己类开始找!

class A(object):
    def f3(self):
        print('A.f3')


class B(object):
    def f1(self):
        print('B.f1')
        self.f2()


class C(B):
    def f2(self):
        print('C.f2')
        self.f3()

    def f3(self):
        print('C.f3')

class D(C):
    def f3(self):
        print('D.f3')

obj = D() 
obj.f1()

''' 打印结果
B.f1
C.f2
D.f3
'''

mro()继承属性的查找顺序

print(H.mro()) 查看继承,只在新式类有这个属性:
[<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'>]

子类调用父类的方法

class People:
    def __init__(self,name,age,sex):
        self.name=name
        self.age=age
        self.sex=sex

class Teacher(People):
    def __init__(self,name,age,sex,level):
        # 指明道姓的调用父类的属性
        People.__init__(self,name,age,sex,)  # 明确写出父类的名称,如果父类改了,那么这里也要改。
        self.level=level

class Student(People):
    def __init__(self,name,age,sex,group):
        # 通过super()调用父类的属性
        super().__init__(name,age,sex)  # super()自动传入'Student' 'self'两个参数,产生一个对象
        # 对象调用函数是绑定方法,因此__init__()的第一个参数self也不用手动传入了。
        self.group=group

在python2中使用super() 子类名和self这个两个参数还是要手动传,即super(子类名,self)
另外,只有新式类才可以使用super() 函数,因为该函数在寻找继承属性时,是根据mro列表。并且是只要找到一个父类的属性,就会停止。因此,如果子类中要调用多个父类中的同名属性时,还是要用指名道姓的方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值