小白python入门 - 17. Python面向对象编程入门
面向对象编程(Object-Oriented Programming, OOP)是一种被广泛采用的编程范式。所谓编程范式,本质上就是程序设计的方法论,它定义了程序员如何理解程序结构以及编写代码的核心方式。
在之前的课程中,我们提到“程序是指令的集合”:程序运行时,语句被转化为指令由CPU执行。为了简化设计,我们引入了函数的概念——将相对独立且重复使用的代码封装成函数,在需要时调用即可。当单个函数变得过于复杂臃肿时,我们又能将其进一步拆分为多个子函数,以此降低系统复杂度。
细心的你可能已经发现,传统编程方式要求开发者遵循计算机的工作逻辑来编写指令控制机器。然而,计算机的底层运作模式与人类的自然思维方式存在显著差异。若编程必须完全摒弃人类的思维习惯去迁就机器,其过程难免会丧失许多乐趣。需要澄清的是,我们并非否定这种方式本身;但当面对复杂系统开发时,严格遵循计算机思维编码会导致代码结构异常复杂,使得开发与维护工作都变得异常困难。
随着软件复杂性的激增,编写正确可靠的代码已成为一项艰巨挑战,这也印证了许多人的观点——“软件开发是人类活动中最为复杂的领域之一”。如何用程序有效地描述复杂系统、解决复杂问题,成为每位开发者必须直面的核心命题。诞生于上世纪70年代的Smalltalk语言为开发者带来了曙光,因为它引入了一种革命性的新范式:面向对象编程。在OOP的世界里,程序中的数据及其操作函数被组织成一个逻辑整体,称为对象。对象能够接收消息,解决问题的方式转变为创建对象并向它们发送各种消息。通过这种消息传递机制,程序中的多个对象得以协同工作,从而构建出能够应对现实世界复杂性的系统。当然,OOP的雏形可以追溯至更早的Simula语言,但这并非我们讨论的重点。
说明:今天我们使用的很多高级程序设计语言都支持面向对象编程,但是面向对象编程也不是解决软件开发中所有问题的“银弹”,或者说在软件开发这个行业目前还没有所谓的“银弹”。关于这个问题,大家可以参考 IBM360 系统之父弗雷德里克·布鲁克斯所发表的论文《没有银弹:软件工程的本质性与附属性工作》或软件工程的经典著作《人月神话》一书。
面向对象介绍
面向对象编程(OOP)是一种基于“对象”(Objects)概念的强大编程范式,它将软件系统视为一系列相互作用对象的集合。每个对象都是一个独立的实体,封装了特定的数据(称为属性或状态)以及操作这些数据的方法(称为行为)。
OOP的核心思想在于通过以下几个关键特性来提升软件的结构性、可维护性和可重用性:
封装(Encapsulation): 将数据和对数据的操作捆绑在一个单元(即对象)内部,并对外隐藏具体的实现细节。对象通过定义清晰的接口(方法)与外界交互,外部代码无需了解内部运作机制。这有效保护了数据的完整性,降低了模块间的耦合度。
继承(Inheritance): 允许创建新的类(称为子类或派生类)基于现有类(称为父类、基类或超类)进行定义。子类自动继承父类的属性和方法,并可以添加自己特有的属性或方法,或重写继承来的方法以实现特定行为。这促进了代码的重用,建立了类之间的层次关系(is-a关系),并支持多态的实现。
多态(Polymorphism): 指同一个接口(通常是方法名)可以作用于不同的对象,并呈现出不同的行为。这意味着程序可以处理不同类型的对象,而无需关心其具体类型,只需知道它们支持某个共同接口即可。多态通常通过继承(子类重写父类方法)或接口来实现,极大地增强了代码的灵活性和可扩展性。
在OOP中,类(Class)扮演着“蓝图”或“模板”的角色,它定义了某一类对象共有的属性和方法。而对象(Object)则是类的一个具体实例(Instance),是程序运行时在内存中实际创建并操作的实体。通过抽象出现实世界或问题域中的关键概念并将其建模为类和对象,OOP能。够更自然、更有效地构建复杂、模块化和易于演进的软件系统。
在面向对象编程的世界中,一切皆为对象,对象都有属性和行为,每个对象都是独一无二的,而且对象一定属于某个类。对象的属性是对象的静态特征,对象的行为是对象的动态特征。按照上面的说法,如果我们把拥有共同特征的对象的属性和行为都抽取出来,就可以定义出一个类。
定义类
在 Python 语言中,我们可以使用class关键字加上类名来定义类,通过缩进我们可以确定类的代码块,就如同定义函数那样。在类的代码块中,我们需要写一些函数,我们说过类是一个抽象概念,那么这些函数就是我们对一类对象共同的动态特征的提取。写在类里面的函数我们通常称之为方法,方法就是对象的行为,也就是对象可以接收的消息。方法的第一个参数通常都是self,它代表了接收这个消息的对象本身。
classStudent:defstudy(self,course_name):print(f'学生正在学习{course_name}.')defplay(self):print(f'学生正在玩游戏.')创建和使用对象
在我们定义好一个类之后,可以使用构造器语法来创建对象,代码如下所示。
stu1=Student()stu2=Student()print(stu1)# <__main__.Student object at 0x10ad5ac50>print(stu2)# <__main__.Student object at 0x10ad5acd0>print(hex(id(stu1)),hex(id(stu2)))# 0x10ad5ac50 0x10ad5acd0在类的名字后跟上圆括号就是所谓的构造器语法,上面的代码创建了两个学生对象,一个赋值给变量stu1,一个赋值给变量stu2。当我们用print函数打印stu1和stu2两个变量时,我们会看到输出了对象在内存中的地址(十六进制形式),跟我们用id函数查看对象标识获得的值是相同的。现在我们可以告诉大家,我们定义的变量其实保存的是一个对象在内存中的逻辑地址(位置),通过这个逻辑地址,我们就可以在内存中找到这个对象。所以stu3 = stu2这样的赋值语句并没有创建新的对象,只是用一个新的变量保存了已有对象的地址。
接下来,我们尝试给对象发消息,即调用对象的方法。刚才的Student类中我们定义了study和play两个方法,两个方法的第一个参数self代表了接收消息的学生对象,study方法的第二个参数是学习的课程名称。Python中,给对象发消息有两种方式,请看下面的代码。
# 通过“类.方法”调用方法# 第一个参数是接收消息的对象# 第二个参数是学习的课程名称Student.study(stu1,'Python程序设计')# 学生正在学习Python程序设计.# 通过“对象.方法”调用方法# 点前面的对象就是接收消息的对象# 只需要传入第二个参数课程名称stu1.study('Python程序设计')# 学生正在学习Python程序设计.Student.play(stu2)# 学生正在玩游戏.stu2.play()# 学生正在玩游戏.初始化方法
大家可能已经注意到了,刚才我们创建的学生对象只有行为没有属性,如果要给学生对象定义属性,我们可以修改Student类,为其添加一个名为__init__的方法。在我们调用Student类的构造器创建对象时,首先会在内存中获得保存学生对象所需的内存空间,然后通过自动执行__init__方法,完成对内存的初始化操作,也就是把数据放到内存空间中。所以我们可以通过给Student类添加__init__方法的方式为学生对象指定属性,同时完成对属性赋初始值的操作,正因如此,__init__方法通常也被称为初始化方法。
我们对上面的Student类稍作修改,给学生对象添加name(姓名)和age(年龄)两个属性。
classStudent:"""学生"""def__init__(self,name,age):"""初始化方法"""self.name=name self.age=agedefstudy(self,course_name):"""学习"""print(f'{self.name}正在学习{course_name}.')defplay(self):"""玩耍"""print(f'{self.name}正在玩游戏.')修改刚才创建对象和给对象发消息的代码,重新执行一次,看看程序的执行结果有什么变化。
# 调用Student类的构造器创建对象并传入初始化参数stu1=Student('骆昊',44)stu2=Student('王大锤',25)stu1.study('Python程序设计')# 骆昊正在学习Python程序设计.stu2.play()# 王大锤正在玩游戏.总结
面向对象编程有三大支柱,就是我们之前给大家划重点的时候圈出的三个词:封装、继承和多态。后面两个概念在下一节课中会详细说明,这里我们先说一下什么是封装。我自己对封装的理解是:隐藏一切可以隐藏的实现细节,只向外界暴露简单的调用接口。我们在类中定义的对象方法其实就是一种封装,这种封装可以让我们在创建对象之后,只需要给对象发送一个消息就可以执行方法中的代码,也就是说我们在只知道方法的名字和参数(方法的外部视图),不知道方法内部实现细节(方法的内部视图)的情况下就完成了对方法的使用。
举一个例子,假如要控制一个机器人帮我倒杯水,如果不使用面向对象编程,不做任何的封装,那么就需要向这个机器人发出一系列的指令,如站起来、向左转、向前走5步、拿起面前的水杯、向后转、向前走10步、弯腰、放下水杯、按下出水按钮、等待10秒、松开出水按钮、拿起水杯、向右转、向前走5步、放下水杯等,才能完成这个简单的操作,想想都觉得麻烦。按照面向对象编程的思想,我们可以将倒水的操作封装到机器人的一个方法中,当需要机器人帮我们倒水的时候,只需要向机器人对象发出倒水的消息就可以了,这样做不是更好吗?
在很多场景下,面向对象编程其实就是一个三步走的问题。第一步定义类,第二步创建对象,第三步给对象发消息。当然,有的时候我们是不需要第一步的,因为我们想用的类可能已经存在了。之前我们说过,Python内置的list、set、dict其实都是类,如果要创建列表、集合、字典对象,我们就不用自定义类了。当然,有的类并不是 Python 标准库中直接提供的,它可能来自于第三方的代码,如何安装和使用三方代码在后续课程中会进行讨论。在某些特殊的场景中,我们会用到名为“内置对象”的对象,所谓“内置对象”就是说上面三步走的第一步和第二步都不需要了,因为类已经存在而且对象已然创建过了,直接向对象发消息就可以了,这也就是我们常说的“开箱即用”。
后记
对于初学者来说,可能理解这三个概念比较困难,大家一定要多动手写代码实践,从实战中理解面向对象的三个支柱。
我在给新员工上课的时候常常以马云老师为例给大家讲解面向对象。
封装:我们看到马云老师知道其身上的特征:有钱,创建了阿里巴巴等等。类定义我们也要追求简单,见名知意。
继承:大家为什么要叫马爸爸?就是为了能花马老师的钱嘛
多态:马老师的所有特征我们都需要继承吗?要继承马老师的颜值吗?要继承马老师对钱不感兴趣的特征吗?我们可以选择想要的特征进行继承,不需要的特征可以通过多态修改。
