[Python学习日记-63] 继承与派生

news2024/11/24 8:36:16

[Python学习日记-63] 继承与派生

简介

继承

派生

简介

        上一篇文章我们学习了类如何使用,以及相关特性,也做了相关的练习,在练习当中发现类与类之间有时也会存在重复代码,其实在类中我们还有一个继承和派生的概念没有说,下面我们一起来看看到底是怎么一回事吧。

继承

一、初识继承

        继承指的是类与类之间的关系,是一种什么“是”什么的关系,继承的功能之一就是用来解决代码重用问题继承是一种创建新类的方式,在 Python 中,新建的类可以继承一个或多个父类,父类又可以称为基类或超类,新建的类称为派生类或子类。Python 中类的继承分为单继承和多继承,如下

class ParentClass1:    # 定义父类
    pass

class ParentClass2:    # 定义父类
    pass

class SubClass1(ParentClass1):    # 单继承,基类是 ParentClass1,派生类是 SubClass
    pass

class SubClass2(ParentClass1,ParentClass2):    # Python 支持多继承,用逗号分隔开多个继承的类
    pass

# 查看当前类的父类
print(SubClass1.__bases__)    # __base__ 只查看从左到右继承的第一个子类,__bases__ 则是查看所有继承的父类
print(SubClass2.__bases__)

代码输出如下:

        在 Python 中存在两种类,分别是经典类与新式类,具体的区别将会在后面的原理解析中进行介绍,我们先看看这两个类的一个特征:

  1. 只有在 Python2 中才分新式类和经典类,Python3 中统一都是新式类
  2. 在 Python2 中,没有显式的继承 object 类的类,以及该类的子类,都是经典类
  3. 在 Python2 中,显式的声明继承 object 类的类,以及该类的子类,都是新式类
  4. 在 Python3 中,无论是否继承 object,都默认继承 object,即 Python3 中所有类均为新式类

注意:如果没有指定基类,Python 的类会默认继承 object 类,object 是所有 Python 类的基类,它提供了一些常见方法(如 __str__)的实现。

print(ParentClass1.__bases__)
print(ParentClass2.__bases__)

代码输出如下:

二、继承与抽象(先抽象再继承)

        抽象即抽取类似或者说比较像的部分。其中抽象分成两个层次:

  1. 将奥巴马和梅西这两个对象比较像的部分抽取成类
  2. 将人、猪、狗这三个类比较像的部分抽取成父类

        抽象:最主要的作用是划分类别(可以隔离关注点,降低复杂度),如下图所示

        继承:是基于抽象的结果,通过编程语言去实现它,肯定是先经历抽象这个过程,才能通过继承的方式去表达出抽象的结构。

        其实抽象只是分析和设计的过程中的一个动作或者说一种技巧,开发时我们可以通过抽象可以得到类。继承的关系如下图所示

三、继承与重用性

        在上一篇文章中,我们有一个练习是模仿 LOL 开发了一个英雄相互攻击的程序,分析一下练习答案的代码发现类 Garen 和类 Riven 都是英雄,而且属性也有一部分是相同的,如果我们建立了一个类 Hero,类 Hero的大部分内容与类 Garen 和类 Riven 的相同时,我们写的类 Garen 和类 Riven 的大多数代码都显得非常的多余,所以这就要用到了类的继承的概念了。通过继承的方式新建类 Garen 和类 Riven,让类 Garen 和类 Riven 继承类 Hero,类 Garen 和类 Riven 会“遗传”类 Hero 的所有属性(数据属性和函数属性),实现代码重用,如下

class Hero:
    def __init__(self, nickname, life_value, aggresivity):
        self.nickname = nickname
        self.life_value = life_value
        self.aggresivity = aggresivity

    def attack(self, enemy):
        enemy.life_value -= self.aggresivity


class Garen(Hero):
    pass


class Riven(Hero):
    pass


g1 = Garen('草丛轮', 100, 30)
r1 = Riven('可爱的瑞文', 80, 50)

print(g1.life_value)
r1.attack(g1)
print(g1.life_value)

代码输出如下:

注意:用已经有的类建立一个新的类,这样就重用了已经有的软件中的一大部分设置,大大节省了编程的工作量,这就是常说的软件重用,不仅可以重用自己的类,也可以继承别人的,例如标准库,来定制新的数据类型,这样就是大大缩短了软件开发周期,对大型软件开发来说,意义重大

四、继承时的属性查找

        在之前我们介绍类与对象的时候已经介绍过类的属性查找了,当时是说当通过对象来访问属性时,会先在对象自己的命名空间里查找,然后才到类里面查找,而现在再加入继承的情况下,那查找顺序又是怎么样的呢?我们来看看下面的代码

class Foo:    # 3:之后再找父类的命名空间
    def f1(self):
        print('from Foo.f1')

    def f2(self):
        print('from Foo.f2')
        self.f1()  # b.f1()

class Bar(Foo):    # 2:再找类的命名空间
    def f1(self):
        print('from Bar.f1')


b = Bar()
b.f2()    # 1:先找 b 自己的命名空间

代码输出如下:

        在有继承的情况下, 会先查找对象自己的命名空间,然后再去查找自己的类的命名空间,最后才会去查找父类的命名空间。上述的都是继承单个父类的情况,我们知道 Python 是支持继承多个父类的,而这种情况会比较复杂,详细的我们将在继承的实现原理里面介绍。

五、继承的实现原理

        在之前使用类的继承的时候我们都只是使用了单个父类的继承,并没有使用过多个父类的继承,在涉及到多个父类的继承时,如果多个父类中有重名的属性那我们应该按照什么顺序来进行查找呢?那我们要先了解一下 Python 是如何实现继承的。

        Python 到底是如何实现继承的,对于你定义的每一个类,Python 会计算出一个方法解析顺序(MRO)列表,这个 MRO 列表就是一个简单的所有基类的线性顺序列表,代码如下

print(A.mro())

代码输出如下:

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

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

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

         在其他语言中,例如 Java 和 C# 中子类只能继承一个父类,而 Python 中子类可以同时继承多个父类,如果继承了多个父类,那么属性的查找方式有两种,分别是深度优先和广度优先,如下:

        当类是经典类时,多继承情况下,在要查找属性不存在时,会按照深度优先的方式查找下去

        当类是新式类时,多继承情况下,在要查找属性不存在时,会按照广度优先的方式查找下去

示例代码:

# 在 Python3 中也可以写成 class G(object):
# 在 Python2 中
#     经典类:class G:(没有继承 object 类)
#     新式类:class G(object):

class G:
    def test(self):
        print('from G')

class F(G):
    def test(self):
        print('from F')

class E(G):
    def test(self):
        print('from E')

class D(G):
    def test(self):
        print('from D')

class C(F):
    def test(self):
        print('from C')

class B(E):
    def test(self):
        print('from B')

class A(B,C,D):
    def test(self):
        print('from A')

a1 = A()
a1.test()
print(A.mro(),'\n',A.__mro__)    # 只有新式才有这个属性可以查看线性列表,经典类没有这个属性
  • 新式类继承顺序:A -> B -> E -> C -> F -> D -> G
  • 经典类继承顺序:A -> B -> E -> G -> C -> F -> D

注意:可以尝试一下注释掉不同类当中的 test 函数来观察以下输出结果

派生

        在继承了父类的同时,子类也可以添加自己新的属性或者在自己这里重新定义这些属性(不会影响到父类),需要注意的是,一旦重新定义了自己的属性且与父类重名,那么调用新增的属性时,就以自己为准了,如下

class Hero:
    def __init__(self, nickname, life_value, aggresivity):
        self.nickname = nickname
        self.life_value = life_value
        self.aggresivity = aggresivity

    def attack(self, enemy):
        enemy.life_value -= self.aggresivity


class Garen(Hero):
    camp = 'Demacia'    # 派生的变量,子类独有
    def attack(self, enemy):    # 派生的方法会重写从父类当中继承的方法
        enemy.life_value -= self.aggresivity
        print('attack for Garen...')

class Riven(Hero):
    camp = 'Noxus'
    def attack(self, enemy):    # 在自己这里定义新的 attack,不再使用父类的 attack,且不会影响父类
        print('attack for Riven... ')
        
    def fly(self):    # 在自己这里定义新的
        print('%s is flying' % self.nickname)

g = Garen('草丛轮',100,30)
r = Riven('瑞问问',80,50)
print(g.camp)
g.attack(r)
print(r.life_value)
r.fly()

代码输出如下:

        在子类中有时候可能你并不是想完全重写父类中的方法,可能只是想在父类的基础上加上一些自己独有的属性或者参数,而 Python 提供了两种方法来解决这个问题:

1、调用普通函数的方式(指名道姓,不依赖继承)

class Garen(Hero):
    camp = 'Demacia'

    def __init__(self,nickname,life_value,aggresivity,weapon):
        Hero.__init__(self,nickname,life_value,aggresivity)
        self.weapon = weapon

    def attack(self, enemy):
        Hero.attack(self, enemy)    # 和普通函数没什么两样
        enemy.life_value -= self.aggresivity
        print('attack for Garen...')

class Riven(Hero):
    camp = 'Noxus'
    def attack(self, enemy):
        Hero.attack(self, enemy)
        print('attack for Riven... ')
        
    def fly(self):
        print('%s is flying' % self.nickname)

g = Garen('草丛轮',100,30,'金箍棒')
r = Riven('瑞问问',80,50)

print(g.__dict__)
print(g.camp)
g.attack(r)
print(r.life_value)
r.fly()

代码输出如下:

        这种方式并不依赖继承,所以它会像普通函数一样来进行调用,即类名.func(),此时就与调用普通函数无异了,因此即便是 self 参数也要为其传值。

2、super()(依赖继承)

class Garen(Hero):
    camp = 'Demacia'

    def __init__(self,nickname,life_value,aggresivity,weapon):
        super(Garen, self).__init__(nickname,life_value,aggresivity)    # Python2 当中一定要这样使用
        self.weapon = weapon

    def attack(self, enemy):
        super().attack(enemy)    # Python3 可以简写成 super()
        enemy.life_value -= self.aggresivity
        print('attack for Garen...')

class Riven(Hero):
    camp = 'Noxus'
    def attack(self, enemy):
        Hero.attack(self, enemy)    # 指名道姓的形式,不依赖继承
        print('attack for Riven... ')
        
    def fly(self):
        print('%s is flying' % self.nickname)

g = Garen('草丛轮',100,30,'金箍棒')
r = Riven('瑞问问',80,50)

print(g.__dict__)
print(g.camp)
g.attack(r)
print(r.life_value)
r.fly()

代码输出如下:

        这种方式调用父类的属性是依赖于继承的,为什么说是依赖于继承呢?这是因为当有多个父类的时候他的查找顺序是会按照介绍原理时提到的 MRO 列表的顺序来进行查找的,为了方便理解请看下面的代码

class A:
    def f1(self):    # c.f1() 执行后 MRO 列表查找到 A 的位置 super() 则接着往下找
        print('from A')
        super().f1()    # 会根据 方法解析顺序(MRO)列表 来进行查询,所以就算没有定义父类也会按照 MRO 列表的顺序找到 B 中的 f1


class B:
    def f1(self):
        print('from B')


class C(A,B):
    pass

print(C.__mro__)    # (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)
c = C()
c.f1()    # 基于 C 的 MRO 列表来查找

代码输出如下:

        根据输出可以看到即使类 A 没有把类 B 设置为父类,super() 也会根据 MRO 列表的排列顺序进行查找。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2237063.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于 Encoder-only 架构的大语言模型

基于 Encoder-only 架构的大语言模型 Encoder-only 架构 Encoder-only 架构凭借着其独特的双向编码模型在自然语言处理任务中表现出色&#xff0c;尤其是在各类需要深入理解输入文本的任务中。 核心特点&#xff1a;双向编码模型&#xff0c;能够捕捉全面的上下文信息。 En…

Python学习------第四天

Python的判断语句 一、布尔类型和比较运算符 二、 if语句的基本格式 if语句注意空格缩进&#xff01;&#xff01;&#xff01; if else python判断语句的嵌套用法&#xff1a;

uniapp实现H5和微信小程序获取当前位置(腾讯地图)

之前的一个老项目&#xff0c;使用 uniapp 的 uni.getLocation 发现H5端定位不准确&#xff0c;比如余杭区会定位到临平区&#xff0c;根据官方文档初步判断是项目的uniapp的版本太低。 我选择的方式不是区更新uniapp的版本&#xff0c;是直接使用高德地图的api获取定位。 1.首…

测试网空投进行中 — 全面了解 DePIN 赛道潜力项目 ICN Protocol 及其不可错过的早期红利

随着云计算技术的飞速发展&#xff0c;越来越多的企业和个人对云服务的需求变得多样化且复杂化。然而&#xff0c;传统的中心化云服务平台&#xff08;如AWS、微软Azure等&#xff09;往往存在着高成本、数据隐私保护不足以及灵活性差等问题。 为了解决这些挑战&#xff0c;Imp…

IntelliJ IDEA 使用心得与常用快捷键

刚开始学习写Java的时候&#xff0c;用的eclipse&#xff0c;正式工作后&#xff0c;主要用的myeclipse&#xff0c;去年初在前辈的推荐下&#xff0c;在2折的时候买了正版的 IntelliJ IDEA 和 Pycharm&#xff0c;12.0版终生使用&#xff0c;一年更新。 使用前早就久闻其名&am…

【rust】rust基础代码案例

文章目录 代码篇HelloWorld斐波那契数列计算表达式&#xff08;加减乘除&#xff09;web接口 优化篇target/目录占用一个g&#xff0c;仅仅一个actix的helloWorld demo升级rust版本&#xff0c; 通过rustupcargo换源windows下放弃吧&#xff0c;需要额外安装1g的toolchain并且要…

施工企业为什么要用工程项目管理软件?工程项目管理软件的用处是什么?

施工企业一定会遇到哪些问题&#xff1f;工人怠工、材料浪费、数据造假、工期拖延、质量问题、安全隐患等。这些问题正在悄然侵蚀建施工业的经济效益。每一个环节的失控都可能导致巨大的经济损失&#xff0c;还可能损害企业的声誉。面对日益复杂的工程管理环境&#xff0c;如何…

【C++】详解RAII思想与智能指针

&#x1f308; 个人主页&#xff1a;谁在夜里看海. &#x1f525; 个人专栏&#xff1a;《C系列》《Linux系列》 ⛰️ 丢掉幻想&#xff0c;准备斗争 目录 引言 内存泄漏 内存泄漏的危害 内存泄漏的处理 一、RAII思想 二、智能指针 1.auto_ptr 实现原理 模拟实现 弊端…

所谓的情商高,其实就是会说话!

所谓的情商高&#xff0c;其实就是会说话&#xff01; 1.当遇到不知道的事情时&#xff0c;不要直截了当地说“不知道”。而应委婉地表达为“我想听听你的看法”。 如此既能避免尴尬&#xff0c;又能展现出对对方见解的尊重和期待。 2.不要简单地说“我迟到了”&#xff0c;…

ALB搭建

ALB: 多级分发、消除单点故障提升应用系统的可用性&#xff08;健康检查&#xff09;。 海量微服务间的高效API通信。 自带DDoS防护&#xff0c;集成Web应用防火墙 配置&#xff1a; 1.创建ECS实例 2.搭建应用 此处安装的LNMP 3.创建应用型负载均衡ALB实例 需要创建服务关联角…

【spark面试】spark的shuffle过程

概述 所有的shuffle的过程本质上就是一个task将内存中的数据写入磁盘&#xff0c;然后另一个task将磁盘中的数据读入内存的过程。 对于mapreduce来说&#xff0c;我们将内存中的数据写入磁盘成为maptask&#xff0c;将磁盘中的数据读入内存称为reducetask。 而对于spark来说&…

Android 实现一个系统级的悬浮秒表

前言 由于项目需要将手机录屏和时间日志对应起来&#xff0c;一般的手机录屏只能看到分钟&#xff0c;但是APP的日志输出通常都是秒级别的&#xff0c;于是决定自己手撸一个悬浮秒表&#xff08;有拖拽效果&#xff09;。 效果如下 具体实现 大致的实现思路&#xff1a; 创…

【科普小白】LLM大语言模型的基本原理

一、要了解LLM大模型的基本原理就要先来了解一下自然语言处理&#xff08;NLP&#xff09;。 NLP 是 AI 的一个子领域&#xff0c;专注于使计算机能够处理、解释和生成人类语言&#xff0c;主要任务包括&#xff1a;文本分类、自动翻译、问题回答、生成文本等。到底是NLP促生了…

Go语言开发商城管理后台-GoFly框架商城插件已发布 需要Go开发商城的朋友可以来看看哦!

温馨提示&#xff1a;我们分享的文章是给需要的人&#xff0c;不需要的人请绕过&#xff0c;文明浏览&#xff0c;误恶语伤人&#xff01; 前言 虽然现在做商城的需求不多&#xff0c;但有很多项目中带有商城功能&#xff0c;如社区医院系统有上服务套餐、理疗产品需求、宠物…

ts 如何配置引入 json 文件

ts 如何配置引入 json 文件 参考文档&#xff1a; https://maxgadget.dev/article/how-to-import-a-json-file-in-typescript-a-comprehensive-guide 项目中有一个 .json 的文件是配置文件&#xff0c;如何引入到 ts 项目中 配置 tsconfig.json 文件&#xff0c;添加这两个 {…

如何让ffmpeg运行时从当前目录加载库,而不是从/lib64

程序在linux下运行时&#xff0c;一般从 /lib64 目录下加载依赖的库文件&#xff0c;如xxx.so. 有时候&#xff0c;系统里没有这些库&#xff0c;也不想从系统目录下加载&#xff0c;怎么办呢&#xff1f; 看下面的调整过程。 使用的源代码是 ffmpeg-6.1.tar.xz 解压后&…

【基于轻量型架构的WEB开发】课程 12.4 页面跳转 Java EE企业级应用开发教程 Spring+SpringMVC+MyBatis

12.4 页面跳转 12.4.1 返回值为void类型的页面跳转 返回值为void类型的页面跳转到默认页面 当Spring MVC方法的返回值为void类型&#xff0c;方法执行后会跳转到默认的页面。默认页面的路径由方法映射路径和视图解析器中的前缀、后缀拼接成&#xff0c;拼接格式为“前缀方法…

订单日记助力“实峰科技”提升业务效率

感谢北京实峰科技有限公司选择使用订单日记&#xff01; 北京实峰科技有限公司&#xff0c;成立于2022年&#xff0c;位于北京市石景区&#xff0c;是一家以从事生产、销售微特电机、输配电及控制设备等业务为主的企业。 在业务不断壮大的过程中&#xff0c;想使用一种既能提…

【Ubuntu24.04】部署服务(基础)

目录 0 背景1 设置静态IP2 连接服务器3 部署服务3.1 安装JDK3.2 下载并安装MySQL8.43.2.1 从官网下载 APT Repository 配置文件3.2.2 安装 MySQL8.43.2.3 配置远程连接 3.3 下载并配置Redis3.4 上传jar包并部署应用3.5 开放端口 4 总结 0 背景 在成功安装了Ubuntu24.04操作系统…

Docker镜像分成

1. 镜像分层原理 1.1 镜像分层的定义与结构 Docker 镜像的分层存储机制是其核心特性之一&#xff0c;它允许 Docker 镜像由多个只读层组成&#xff0c;这些层叠加在一起形成一个完整的文件系统。每个层代表 Dockerfile 中的一个指令&#xff0c;并且每一层都是不可变的&#…