0


九 Python 类与对象详解,这是软件工程师的分水岭

对象(object),它是一个很抽象的概念,也是让第一次接触软件开发的小伙伴感觉畏惧的名词。其实这个抽象的概念,我们很小的时候就熟练掌握了。你还记得第一次在众人面前介绍自己的场景吗?你还记得跟别人分享你的理想时的场景吗?

介绍自己的过程,就是一个对象的描述、你的理想也是一个对象、你手上的手机、家里面的宠物、你的同事、你喜欢的人、你的一个情绪它都可以是个对象! 只要你去观察一个事物,这个事物就是一个对象!软件为了模拟我们的世界、解决我们现实中遇到的问题、创造软件的编程语言就不得不去面对我们眼中的对象。无论是Python、Java 都给开发者提供了一套工具和标准,让开发人员通过编程语言来构建对象。

最早把对象这个概念玩成规范的编程语言是C++。在C++的知识体系里,规范系统的介绍了基于对象的软件设计方法OOP(Object Oriented Programming)。开发人员发现基于OOP设计方法设计出来的软件更容易维护、更容易扩展、甚至发现OOP设计出来的软件简直就和地球物种进化 “一模一样”。慢慢的更多的开发语言加入到了OOP阵营,到目前为止OOP设计方和理念思想,依然是软件开发里的主流思想和工具。

OOP包括了很多的软件设计思想和方法,它们大大提高了软件开发设计的效率、降低了软件开发设计的难度,它不是具体的编程语言,但它能告诉你:

  1. 如何用编程语言去解决问题。
  2. 如何设计自己的代码能让程序变得好维护。
  3. 如何组织软件的代码。
  4. 面对实际编程问题是,如何思考找到切入点。

举个很恰当的例子,就比如我们玩英雄联盟,假如编程语言是 QWER 四个技能,那游戏里的意识和走位、就是OOP,游戏玩的好不好手速快还不行,要看的是OOP的能力。

OOP包括了很多有启发性的思想(醍醐灌顶那种启发),也包括了很多设计方式。如果我们单独来讲这些,你肯定会觉得很乏味,所以我们结合实际的问题来逐个介绍。既讲了OOP的思想、也讲了在Python 里如何使用OOP,当你真的深入了解了OOP后,你会发现OOP其实是一种哲学!

现在我们就开始吧。

1 为什么要面向对象编程

在这套课程的一开始,就和大家介绍了软件工程师面对需求时要考虑的两个问题:

  1. 我们要如何组织信息?
  2. 我们要如何操作这些信息?

比如下面这个问题:“用Python语言来模拟你自己”,我们先来看看如何组织这个问题的信息?看下面的代码:

img

我用 6 个普通变量和一个列表变量来记录了自己的信息,当然除了这些基本信息外,我还会有一些行为,这些行为也是我的组成部分:

img

从上面的代码可以看到,我为自己增加了 talk() 函数、还有 sleep()函数。这样组织信息并无大碍,也满足了我们提出问题的需求。但如果我们要整体的 “操作” 这些信息该怎么办 ? 比如我们要把这些信息放到一个叫做 my_self 的变量里? 聪明的小伙伴已经想到了办法:

img

我们可以把这些信息用元祖、列表、集合的方式 “捆绑” 起来,让它变成一个抽象的整体,但这样捆绑起来要访问某一个子信息,变得有点蹩手蹩脚,甚至无法预测(用集合来 “捆绑”)。有没有更好的办法? 有!

img

从上面的代码可以看出,我用字典把我的信息和动作 “捆绑” 在一起,放置到 my_self_3 这个变量里,这样的操作方式明显要比用元祖、列表、集合要清晰很多。再第47~49行代码里,我们通过字典的 key 值来访问 “我” 的信息和动作,这些操作都很清晰很明了,试想一下假如我们使用列表,会是什么样的代码?

其实上面的代码已经很好的组织了 “我” 的信息了,已经满足了 “用Python语言来模拟你自己” 这个需求。但这样的设计方案还不够健壮。假如问题扩展成 “用Python语言来模拟你认识的所有人”,这个时候该怎么办?要解决这个问题,目前你会遇到下面的困难:

  1. 我认识的人很多,我不可能每记录一个人,就用代码写一遍他的结构,这样效率太低!
  2. 我的朋友们虽然有共性,但他们不同的地方也很多,这样的结构该如何处理?
  3. 我未来还会结交新的朋友,也许新朋友会带来新的特性,这时候该怎么办?

在没有OOP前,这些问题要解决起来并不轻松,解决它们甚至会付出很多代价,但有了OOP后,这些问题就变简单了。我们来看看如何在Python中使用OOP的思想来解决这些问题。

2 核心思想

在现实生活中,很多信息结构是可以复用的。比如用PPT模板做PPT、我们亚洲人有着相同的基因、AMG奔驰超跑是根据它的蓝图生产出来的等等,其中PPT模板、亚洲人的基因、AMG奔驰超跑的蓝图,都可以理解成某种可以复用的结构。这种可以复用的结构,在OOP的知识体系里叫做 类(class)。关于类的例子可以有很多,比如:

  1. 现代手机的生产蓝图。
  2. 一个摩托车头盔的模具。
  3. 我的一套学习方法。
  4. 等等。

你看看你的周围,所有物体都可以找到它的类。有的小伙伴会说,那类不就是一个模板嘛!这样说其实说对了一半。类确实有模板的特性,但类有模板望尘莫及的地方。我们先看看类的组成部分:

  1. 属性
  2. 方法
  3. 与其他类的关系

属性:是用来刻画一个类的细节的,比如人是一个类,那它的名字、身高、体重、性别、爱好、母语等等都是这类的属性,这些属性描述了这类是什么。

方法:是这个类可以做什么事情,还是以人为例。人这类可以说话聊天、跳舞、工作、繁衍后代等等,这些动作描述了这类可以做什么。

类之间的关系:这就是让模板望尘莫及的地方,类与类之间可以产生很微妙的关系。比如人这类,它可以衍生出学生这个类,学生这个类又可以衍生出学霸这个类等等。我们后面会举更有趣的例子来说明类的关系。

从上面的知识可以了解到,类是一种可复用的结构,它有属性和方法,它是极度抽象的。它可能是一个汽车蓝图、可能是人的基因。如果我们要把一个类变成实物,把汽车蓝图变成真正的汽车,那就需要工人配合机器在流水线上作业,经过各种折腾,最终形成一辆真正可以开的汽车。这个过程在OOP里就叫做实例化,根据某个类 实例化出来的产物就叫做 对象。有了对象,我们就可以面向对象来编程了。

【做一个图】

3 初次体验

有了上面的知识背景,我们再来看这个问题 “用Python语言来模拟你认识的所有人” 也许我们头脑里就有一些思绪了。我们先基于OOP的知识体系,在Python里,定义一个人的类(定义一个人的信息结构,也可以这么理解)。我们来看代码:

img

  1. 第6行代码,我们使用了Python定义类的关键字 class,在 class后面紧跟这个类的名称 Person ,然后 “:” 冒号进入类定义的代码模块。
  2. 第7~13行代码,我们通过 7 个属性,来描述这个类是什么样子的(也可以用更多的属性来描述),这些属性其实就是Python里面的变量。
  3. 第17~23行代码,我们在Person类里,定义了两个函数 talk,sleep。在这里叫方法更贴切一点。在类里定义方法和在类外面定义函数一模一样,唯一需要注意的就是在类里定义方法,要给它一个默认的参数 self 。这是Python的规定,Python 为啥要这样规定,后面大家就会知道。

这就是在Python里,定义一个类的基本方法。好了,有了类了,我们要通过这类去生产对象了,也就是类的实例化,这个该如何操作呢? 看下面代码:

img

  1. 第27行,我们通过类名(Person)加 “()” 括号,告诉Python,我们需要用Person的类(模板),来创建一个实例,也就是对象。然后把创建好的对象赋值给 my_self 这个变量。
  2. 第28~34行,我们通过 “.” 运算符给 my_self, 这个Person实例化的对象的属性设置具体的值,让这个对象来刻画具体这个人的信息。设置完后,my_self 这个对象就有 “灵魂了”。
  3. 第37~39行,我们面向 my_self 这对象进行了编程,打印了 my_self 这对象的名字,打印了 my_self 这对象的爱好等等,第39行调用了这对象的方法,talk()。

同样我们可以用这个模板Person,创建更多的人类对象,然后让这些对象“活”起来,操作他们完成某项任务,看下面代码:

img

  1. 第42~45行代码,我们基于 Person 实例化了两个对象 my_friend,my_colleague。并设置了不同的属性。
  2. 第49~57行代码,我们面向这两个对象编程,查看了这两个对象的 name 属性,并且调用了它们的 talk()方法。
  3. 第60~62行代码,我们使用Python内置函数 type()查看了my_self,my_friend,my_colleague 这三个对象属于什么类型。

我们运行一下上面的代码,看看效果!

img

上面的例子就是OOP的核心思想,通过抽象的类来组织我们信息的结构,通过类的实例化来创建编程的对象,通过对对象的编程,实现我们的业务代码。这也是整个Python语言的核心思想,我们之前接触过的列表、字符串、整数、等等,它们都是对象,它们都有自己的类(只是创建方式和自定义类有一些不同),所以Python是一门面向对象的编程语言。

现在应该对OOP有点头绪了吧,我们接着学习。

4 初始化方法

我们在通过类实例化对象的时候,类其实做了很多工作。比如在可用内存里开辟出了适合存放这个对象的内存空间,建立内存地址,把对象注册到 Python 的内存垃圾回收检测系统里等等数十个动作。

这些细节大部分开发人员是不用去了解的,我们只需知道 Python 在实例化对象过程中调用了一个方法叫 init() 。这个方法在类的定义里面是隐藏的,也就是说在代码里我们看不见它,但 Python 在处理类的时候,某个时机会调用它,这样的方法统称为 “类的专有方法”。

在一些特殊的场景里,我们需要重写它(overwrite),重写的概念就是用相同的方法名去定义不同的内容,覆盖老的内容,我们来看下面的例子:

img

在Person类定义的基础上,我们重写了 init 这个Person 实例化每次都要调用的 类专有方法。现在我们再来用Person实例化一个对象看看效果:

img

用Person实例化了一个对象,放到了human这对象里,执行这段代码,控制台可以看到如下输出:

img

重写 init 这个初始化类专用函数,其实给我们了个启示! 我们在实例化一个对象的时候,有些设置操作可以放到 init 里,这样在通过类名来创建对象的时候,这些设置就会自动执行,我们看看下面的改进代码:

img

从代码里可以看出,我们再次改进了 init 方法,为它添加了一个参数 name ,希望在实例化Person的时候就把该对象的 name 属性值确定了。并且也为 hight、weight 等其它属性设置了默认值。设置默认值的时候,我们通过 self 这个参数来给之前定义过的 属性设置值,这就是为什么Python规定在类里定义方法时,需要self这默认参数。

我们继续通过这个类创建几个对象看看:

img

第38~39行代码,我们通过Person来创建一个对象,与之前的不同,传入了 “史蒂芬”,“杰克 ”给Person这个类,这类会把 传入的参数原模原样的传给 init 方法。执行上面的代码,控制台会输出下面的信息:

img

这里有个细节,在第38~39行代码里,假如我们不传这个参数会怎样? 不传参数运行代码会报错,Python会提示我们,我们少传了个参数。假如我们就是任性,想传的时候传、不想传的时候不传,这个代码我们该怎么改进?(学过Java或C++的小伙伴可能想到了重载,可惜的是Python并不支持Java那样的重载)看下面例子,相信大家一看就明白:

img

这时候代码就可以这样写:

img

5 类专用的方法

在 Python 的类里 像 ini 这样的类专用函数还有很多,虽然大部分的开发工作中,我们都不会直接接触它们,但了解它们有助于了解Python内部对类处理的细节,难说在某一天遇到一些特殊的场景,这些类专用方法会有妙用。

下面就是类的常用专用方法:
序号类专用函数说明1init类在实例化一个对象的时候,会默认调用这个方法。2del在一个对象彻底从内存中消失的时候,这个方法会被调用。3repr在print里,比如“%r”,或者使用repr函数的时候,对象的 repr 方法会被调用。4setitem每当属性被赋值的时候都会调用该方法,因此不能再该方法内赋值 self.name = value 会死循环。5getitem当访问不存在的属性时会调用该方法。len我们在用Python内置函数 len()来查看某个列表对象长度的时候,这个列表对象调用的就是len 这个类专用函数。7cmp在使用Python内置函数 sorted() 对某个列表进行排序时,列表里的对象之间进行比较就会用到 cmp 这个类专用函数。8callpython中一切皆对象,函数也是对象,同时也是可调用对象(callable)。关于可调用对象,我们平时自定义的函数、内置函数和类都属于可调用对象,但凡是可以把一对括号()应用到某个对象身上都可称之为可调用对象,判断对象是否为可调用对象可以用函数 callable一个类实例要变成一个可调用对象,只需要实现一个特殊方法call()。9add两个对象使用运算符 “+” 进行相加操作时,对象的 add方法被调用。10sub两个对象使用运算符 “-” 进行相减操作时,对象的 sub方法被调用。11mul两个对象使用运算符 “*” 进行乘法操作时,对象的 mul方法被调用。12truediv两个对象使用运算符 “/” 进行除法操作时,对象的 truediv方法被调用。13mod两个对象使用运算符 “%” 进行取模操作时,对象的 mod方法被调用。14pow两个对象使用运算符 “**” 进行次方操作时,对象的 mod方法被调用。

6 继承(有点像生物的基因传递)

我们都很羡慕那些富二代,随随便便喝个酒就是上百万,开着限量版的豪车去吃地摊。有时候还很任性,很霸气,上千万的投资说投就投,买房子就像买菜一样,钱也许在这些人眼里就是数字而已。他们为什么那么牛呢? 因为他们从父辈继承了家族产业。

【富二代 过的奢靡生活】

他们的父辈也许并不像他们一样生活奢靡,但这些老一辈的企业家一定过了不少苦日子,手中的财富是靠自己的双手一点点创造出来的,有的靠创新、靠技术、靠信息差等等。各个都拥有独门绝技、和超出常人的胆识、耐力、情商、与毅力,为社会发展做了很多贡献,在这个过程中积累了雄厚的财富。做为这些成功企业家的下一代,他们继承了家业让自己做事情变得相对简单,同时他们也在扩展家族的事业,这就是继承的力量。

除了财富、知识也是这样,宏观来说我们人类能发展到今天,都是继承了先辈们留下的知识,我们现在拿来就用的知识都是先辈们花费了大量时间精力整理研究出来的,换句话说我们生活在巨人的肩膀上。

在软件开发领域,继承也发挥着重要的作用,OOP里继承的概念让很多软件能持续发展十多年,比如PhotoShop,3dMAX,这些等等,如果没有OOP里继承的概念,软件的发展将是一件高成本困难的事情。

那在Python里,我们要如何实现继承呢? 我们需要通过类来实现继承,看下面的例子:

img

首先我们创建了一个 Vehicle 类,它是一个交通工具类。拥有一个 speed 属性和 run 方法,并且我们重写了它的初始化方法 init。现在我们要再设计一个类 Moto(摩托),它也是一种交通工具,同样需要 speed 属性,和 run 的方法。到目前为止,我们有两种方法去实现这个 Moto 类:

  1. 重新设计一个 Moto 类。
  2. 继承 Vehicle 这个类,在这个基础上做修改。

显然,我们要用第二种方法,我们看代码:

img

目前我们只用了一行代码。

第22行代码定义了 Moto 类,在类的名称后面有个 圆括号,在里面说明了Moto这类要从哪里继承东西。这里虽然属性和方法都没有定义,但它已经从Vechicle这个类里继承了它的所有特性,看下面代码:

img

第27行代码,我们通过Moto类,实例化了一个Moto对象m,这里需要注意,Moto使用的初始化方法,是继承了Vehicle的。

第28行代码,m对象调用了run方法,这个方法也是从Vehicle里继承下来的,这个方法在Moto类里是没有被定义过的。

通过上面的例子,我们可以领悟到,继承能让类拥有父类的一切!Moto不但继承了Vehicle的所有 “资产”,还可以在原有的基础上面做扩展,看下面代码:

img

  1. 第23~29行,我们为 Moto 类新增加了 7 个属性。
  2. 第32行,我们重写了,Moto 类的初始化方法,它会覆盖从 Vehicle 里继承下来的初始化方法。
  3. 第37行,我们创建了一个 start 方法,摩托车启动。
  4. 第41行,我们创建了一个 设置骑行模式的方法 set_cycling_model。

方法的覆盖和重写

我们还可以重写覆盖从 Vehicle 类继承下来的 run 方法,看下面代码:

img

  1. 第37行代码,我们定义了一个 run 方法,它将覆盖从 Vehicle 里继承下来的 run方法。
  2. 第39行代码,我们通过 Python 内置函数 super 方法去调用了 Moto 的父方法,也就是 Vehicle 的方法 run(),这样的操作是让我们在原有的基础上面,改进原来的方法。当然要直接重写也可以。

我们来实例化 Moto,操作一下它看看:

img

运行上面代码的结果:

img

我们已经体会到了在软件开发中,继承带来的好处。我们再来想想这个问题,我们的优秀基因是来自父母双方的,那OOP里的集成能不能也这样操作?及可以从多个类里继承它们的功能 ? 这个当然可以,并且Python也支持这样的操作,来看看下面的例子:

多重继承

我们首先来改进 Vehicle 让它更加抽象:

img

我们重新设计了一个 Vehicle 类,把长宽高,这些通用的属性放到了这个类里,并定义了一个通用方法 run()。然后在分别设计 Aircraft(飞机)、Yacht(快艇)这两个类,它们都继承自 Vehicle。

img

从上面的代码可以看出,Aircraft 和 Yacht 它们都有各自的属性,

  1. 飞行高度:flight_height
  2. 排水量:tonnage

好了,做那么多准备是为了现在这类,Amphibian(水空两用航空器),我们来看看它的代码:

img

第 40 行代码,告诉Python,Amphibian 这类将从 Aircraft 和 Yacht 这两个类里继承它们的 “资产”。

虽然 Amphibian 里面啥也没定义,但从它出生那天,就已经有了 Aircraft 和 Yacht 的能力了,我们来看代码:

img

  1. 第1行,我们通过 Amphibian 这个类模板实例化了一个 对象 amp。
  2. 第2行,我们给 amp 设置了飞行高度,这个属性是从 Aircraft 继承过来的。
  3. 第3行,我们给 amp 设置了排水量,这个属性是从 Yacht 继承过来的。
  4. 第53行,我们调用了 amp 的 run 方法,这个方法也是继承过来的。

上面的例子就是多重继承的玩法。我们人类可以继承父母的基因,但Python的类就不受这样的影响,它们的 “父亲母亲” 可以有无数个。当然Python这样的继承,可以人为设置一些限制,就像我们生活中,父辈的遗产也可以做一些限制一样。

继承的约束

我们先来看看下面的代码:

img

我们对 Vehicle 这个类做了一些改进(红框里)。在属性里,我们添加了一个 remark 的属性,在方法里,我们添加了show_remark 方法。他们两有个共同点,变量名前面都有一个 “__” 。它告诉Python,这个属性,这个方法属于 私人“资产”,在OOP里面叫做私有属性和私有方法,他们有如下特点:

  1. 只能在类的内部使用。
  2. 不能被继承。

我们来试验一下:

img

我们创建了两个对象 v 和 a。他们分别是 Vehicle 、Amphibian 这两个类实例化出来的,并且 Amphibian 这类继承自 Aircraft ,Aircraft继承自 Vehicle,所以我们认为 v 和 a 对象都可以打印**remark,和调用 **show_remark 方法。我们运行上面的代码,会遇到下面的错误,结果和我们想的不一样:

img

这是因为 **remark 和 **show_remark 它们都是 Vehicle 私有的,只有 Vehicle 自己可以使用它们。为啥会有这样的机制? 因为在OOP的理念里,建议类的使用者无需关心类是如何实现的,所以类里的被自己调用的方法,是可以不用 “暴露” 给使用者的,这其实也是一种保护措施,特别是开发第三方插件的时候。

在OOP里,这样的保护手段还有一个名称叫做 “封装” 。像严格遵循OOP开发理念的编程语言,比如JAVA,就经常会封装各种各样的类。Python相对于Java而言就灵活了很多,甚至可以动态的给对象添加属性。

7 灵活的Python,属性自由

我们直接来看代码:

img

我们用 Vehicle 这类实例化了两个对象,v 和 v_2。我们把注意力放在第53行代码!我们给 v 这个对象的 name 属性赋值了 “你好” ,这里需要注意! Vehicle 类里并没有定义过 name 这个属性,可以这样做是因为 Python 允许给对象临时通过 "." 的方式增加属性! 所以第53行代码,我们其实是临时给V对象添加了一个 name 属性,并给这个属性赋值了 “你好” 。

在第54行代码里,我们能通过 print 函数打印了 v.name 这个属性。 在第57行,我们尝试打印 v_2.name的时候,Python会报错:

img

这说明了临时属性的玩法,只能在对象上有效。临时属性是不会添加到类上的。Python之所以会有这样的机制,完全是为了灵活。因为在实际开发中,临时给对象添加属性的场景还是很多的。

8 一切皆是对象

在OOP的世界里,小到一颗尘埃,大到一颗星球它们都是对象,软件工程师们就是创造这些对象,操作这些对象的人。遵循OOP理念的Python自然所有 “东西” 也都是对象。有Python内置的、有工程师设计自定义的、有第三方库的。

我们从一开始接触变量的时候,其实已经在面向对象编程了,哪怕是基础数据类型,它本质上也是对象,符合对象的规则,比如:

img

第7~8行,分别调用了 num_list,num 对象的方法。

另外在Python的世界里,所有对象的类,都继承了 Object 这个原始的类,Object 包括了原始对象的一切属性和方法,我们可以通过 Python 内置函数 dir() 来查看 Object 的方法:

img

可以看到这些方法都是 xxxxx 的方法,在前面我们知道这样的方法叫做 类的专用方法,所有的类都有,这是因为所有的类都继承了 Object 。

9 OOP的设计方法

Python 的面向对象编程相关知识,大伙应该都掌握了,但要实际做一款软件,还需要多加练习。OOP不仅仅提供了面向对象编程的技术,还提供了思想。

OOP给软件工程师提供了一套完整的设计思想、设计工具、设计绘图,它可以帮助工程师们从某个问题出发,基于OOP面向对象的思想找到解决问题的合理办法,从而设计出优秀的软件,编写科学的代码。我们将会在实战部分的内容,和大家分享OOP的设计方法,让大家学到的不仅仅是术,还有意识,智慧。

标签: python list

本文转载自: https://blog.csdn.net/u011511073/article/details/118945050
版权归原作者 深思熟虑的羽毛球 所有, 如有侵权,请联系我们删除。

“九 Python 类与对象详解,这是软件工程师的分水岭”的评论:

还没有评论