首发于Python学习
一文掌握Python中的面向对象

一文掌握Python中的面向对象

面向对象是相对于面向过程而言的,它是一种对现实世界理解和抽象的方法,是计算机编程技术发展到一定阶段的产物。本文重点介绍Python中面向对象的一些语法规范和实现细节,包括成员变量和方法的定义、类的多继承、方法的重写、对象的拷贝等。

面向对象是相对于面向过程而言的,它是一种对现实世界理解和抽象的方法,是计算机编程技术发展到一定阶段的产物。

  • 向过程编程主要是分析出实现需求所需要的步骤,通过函数一步一步实现这些步骤,接着依次调用函数即可。
  • 面向对象编程则是分析出需求中涉及到哪些对象,这些对象各自有哪些特征、有什么功能,对象之间存在何种关系等,将存在共性的事物或关系抽象成类。最后通过对象的组合和调用完成需求。

面向过程编程性能更高,适合于简单系统,容易理解。面向对象编程易维护、易扩展、易复用,适合于复杂系统,灵活方便。

1、类和对象

对象是指实实在在存在的各种事物,例如某张桌子、某辆汽车、某个学生等。对象通常包含两部分信息:属性和行为。一般使用变量表示对象的属性,用函数或方法表示对象的行为。类是用来描述一组具有相同属性和行为的对象的模板,是对这组对象的概括、归纳和抽象表达。现实世界中,先有对象后有类,物以类聚;而在计算机的世界里,先有类后有对象。在面向对象程序设计中,先在类中定义共同的属性和行为,然后通过类创建具有特定属性值和行为的实例,这便是对象。类不是一种具体存在,实例才是具体存在。

(1)Python中类的定义

类的定义举例

(2)对象的创建

类是一种抽象概念,要使用类的功能,就必须进行类的实例化,即创建类的对象。例如矩形是一种抽象概念,长为 5 宽为 3的矩形就是具体的矩形对象。

创建类的对象的方式类似函数调用方式:

对象名 = 类名(参数列表)

注意:程序通过类的__init__()方法接受(参数列表)中的参数,参数列表中的参数要与__init__()方法中除了 self 以外的参数匹配。

2、类中的变量

成员变量用于存储或描述类和对象属性信息,根据位置不同可分为:类变量和实例变量。成员变量可以被该类中定义的方法访问,也可以在外部通过对象进行访问,而在方法体中定义的局部变量,则只能在方法内进行访问。

(1)实例变量

实例变量在方法内部通过“self.变量名”定义的变量,注意和局部变量的区别,实例变量在类的内部通过“self.变量名”访问,在外部通过“对象名.变量名”来访问。实例变量一般是在__init__()方法中进行初始化,self.变量名 = __init__ ()方法传递过来的实参。也可以在其他方法中定义或创建对象后添加,但不建议。

  • 在Python中可通过dir(对象)查看该对象所有的成员名称,包括成员变量和成员方法。
  • 直接访问一个不存在的实例变量时,将会抛出属性异常:AttributeError。
  • 直接对一个不存在的实例变量赋值,将会为该对象添加一个实例变量。有点类似于字典中键的操作。

注意:如果实例变量名以两个下划线开始,即“__变量名”,表示该变量属于私有变量,只允许在这个类的内部调用,在外部无法直接调用。

(2)类变量

注意:对类变量的赋值只能通过“类名.类变量名”来执行, 对“对象名.类变量名”进行赋值相当于创建了一个同名的实例变量。

  • 当类变量名和实例变量名相同时,通过“对象名.变量名”访问时,访问的是实例变量。如果确实想要访问类变量,只能通过“类名.类变量名”。
  • 如果类变量名以两个下划线开始,即“__类变量名”,表示该变量属于私有变量,只允许在这个类的内部调用,在外部无法直接调用。

(3)将方法装饰成属性

3、类中的方法

方法是与类相关的函数,类中方法的定义与普通的函数大致相同。类中定义的方法大致分为三类:实例方法、类方法和静态方法。

(1)实例方法

实例方法和函数定义类似,但第一个参数必须为实例对象,参数名通常为self,表示当前调用这个方法的对象。实例方法定义语法如下。

  • 在类的内部通过self.方法名(参数)来调用实例方法,在类的外部需通过对象名.方法名(参数)调用实例方法。调用时无需传递self参数,系统会自动进行传值。
  • 如果方法名以两个下划线开始,即“__方法名”,表示该方法属于私有方法,只允许在这个类的内部调用,在外部无法直接调用。

(2)类方法

类方法 定义前一般添加“@classmethod”装饰器,第一个参数必须为类对象,参数通常为 cls,表示当前类,由系统自动传值。

  • 类方法只能访问类变量,不能访问实例变量。
  • 使用类方法时,既可以通过“对象名.类方法名”来访问,也可以通过“类名.类方法名”来访问。
  • 类方法通常用于定义与该类相关而与具体对象无关的操作。

(3)静态方法

静态方法定义前一般添加“@staticmethod”装饰器。形式上与普通函数无区别。

  • 静态方法只能访问属于类的成员,不能访问属于对象的成员。
  • 使用静态方法时,既可以通过“对象名.静态方法名”来访问,也可以通过“类名.静态方法名”来访问。
  • 静态方法主要作为一些工具方法,通常与类和对象无关。

(4)构造方法和初始化方法

Python的类中有两个特殊的方法:__new__()和 __init__()。这两个方法用于创建并初始化一个对象。当实例化一个类对象时,最先被调用的是 __new__() 方法。__new__()方法创建完对象后,将该对象传递给 __init__() 方法中的 self 参数。而__init__()方法是在对象创建完成之后初始化对象状态。这两个方法都是在实例化对象时被自动调用的,不需要程序显式调用。__new__() 方法在 object 类中定义,该方法至少需要一个参数 cls 表示需要实例化的类。__new__()方法必须要有返回值,返回实例化对象。python中的类都直接或间接继承自object类。如果类定义中没有提供 __new__() 方法,将使用从父类继承而来的__new__() 方法。__init__() 方法有一个参数 self,该参数就是 __new__() 方法返回的实例对象。若__new__() 方法没有正确返回当前类 cls 的实例对象,那么__init__()方法将无法被调用。__init__() 方法在 __new__() 方法的基础上完成一些初始化工作,不需要返回值。如果用户在类中未提供 __init__() 方法,系统将默认调用父类的初始化方法。

4、类的继承

面向对象编程的一个显著优势就是代码复用,继承就是实现代码复用的一种方式。所谓的继承是指创建一个类时,并不是从零开始构建,而是在一个已有类的基础上进行扩展,可以大大降低工作量。例如:已经定义了一个动物类,对动物共同的属性和行为进行了抽象,现在需要定义一个狗类,而我们知道狗是一种动物,动物拥有的属性和行为狗都拥有,此外狗还有一些特殊的属性和行为,此时,我们可以让狗类继承动物类,然后只需要添加一些狗所特有的属性和方法即可。

  • 在Python中,新建的类可以继承一个或多个父类,父类又可称为基类或超类,新建的类称为派生类或子类。
  • 定义类时,如果没有指定父类,则默认的父类为object,object类是所有类的直接父类或间接祖先类。
  • 子类可以继承父类的公有成员,但不能继承其私有成员。如果需要在子类中调用父类的方法,可以使用“super().方法名()” 或者通过“基类名.方法名()”的方式来实现。

Python中类的继承语法如下:class子类名(父类1, 父类2, ...., 父类n):类体注意圆括号中父类的顺序,使用类的实例对象调用一个方法时,若在子类中未找到,则会从左到右查找父类中是否包含该方法。当没有指定父类时,可以省略一对圆括号。通过类提供的__bases__ 属性可以查看该类的所有直接父类。

(1)万类之源:object类

object类是Python中所有类的基类。object类中定义了所有类都拥有的一些属性和方法,object类中大部分方法名都是以两个下划线开始、以两个下划线结束,其中比较重要的方法有__new__()、__init_()、__str__()、 __eq__() 和__dir__()等。

  • __str__() 方法返回一个描述该对象的字符串。当我们通过print()方法打印一个对象时,默认会调用该方法。默认情况下,它返回一个由该对象所属的类名以及该对象十六进制形式的内存地址组成的字符串,这对用户来说并不友好,一般来说,会重写该方法打印对象的具体信息;
  • __eq__() 方法用于比较两个对象是否相等,默认是没有实现,可根据业务需求重写该方法。使用“==”判断两个对象是否相等时将会调用该方法,如果没有重写该方法,则判断两个对象是否指向同一引用。
  • __dir__() 方法用于显示对象内部所有的属性名和方法名,包括从父类继承而来的属性和方法。
  • __dict__ 属性以字典的形式显示对象内部存储的所有属性名和属性值。

(2)方法的重写

当父类的方法不能满足子类的要求时,可以在子类中重写父类的方法,也就是在子类中写一个和父类的方法名相同的方法。

(3)多继承时调用顺序

创建子类对象时,将会默认按顺序创建所有父类的对象,即调用父类的__new__()方法。但并不会调用所有父类的__init__() 方法。 如果子类没有提供自己的 __init__() 方法,将会默认调用第一个父类的 __init__() 方法;如果子类提供了 __init__()方法,则默认不会调用父类的 __init__() ,此时,父类的成员变量将无法初始化,因此,通常会在子类的 __init__()方法中,显示调用父类的 __init__() 方法。

对于支持继承的编程语言来说,其方法(属性)可能定义在当前类,也可能来自于基类,所以在方法调用时就需要对当前类和基类进行搜索以确定方法所在的位置。而搜索的顺序就是所谓的「方法解析顺序」(Method Resolution Order,MRO)。对于只支持单继承的语言来说,MRO 一般比较简单,就是从当前类开始,逐个搜索它的父类;而对于支持多继承的语言来说,MRO 就复杂很多。在Python中提供了__mro__ 属性可查看类的搜索顺序。

5、对象拷贝

赋值符号“=”使得两个对象引用相同,此时一个对象的变化也会影响另一对象。如果想将原对象的内容复制一份,可用copy模块的copy函数,如 b = copy.copy(a),此时 a 和 b 对象内容相同,但是引用不同。copy函数是浅拷贝,只拷贝当前对象,不会拷贝对象内部的其他对象。如果要递归拷贝对象中的其他对象,可用copy模块的 deepcopy 进行深拷贝,如 b = copy.deepcopy(a),此时,b 完全拷贝了 a 对象及其子对象,a 和 b是完全独立的。


小小练习题

发布于 2021-07-07 08:44