Python 学习笔记(08)面向对象基础
文章目录
8.1 编程思想
面向过程与面向对象体现了编程者的两种不同的思维方式,以下将讲解两者的概念及区别。
8.1.1 面向过程
面向过程是一种以过程为中心的编程思想,它首先分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,在使用时依次调用,是一种基础的顺序的思维方式。
特点:功能模块化,代码流程化
以下棋为例:
bool chess
{
while(judge() != true)
{
black();
judge();
white();
judge();
}
print_Result();
}
下棋过程可以被分为黑棋落子、判断输赢、白棋落子、判断输赢、重复过程、输出结果。其中每一个小过程都可以使用一个函数表示。面向过程的编程思想始终关注如何一步步推进下棋过程判断胜负,按照设定的顺序依次执行函数。
8.1.2 面向对象
面向对象是按人们认识客观世界的系统思维方式,采用基于对象(实体)的概念建立模型,模拟客观世界分析、设计、实现软件的编程思想,通过面向对象的理念使计算机软件系统能与现实世界中的系统一一对应。
面向对象方法直接把所有事物都当作独立的对象,处理问题过程中所思考的不再是怎样用数据结构来描述问题,而是直接考虑重现问题中各个对象之间的关系。【对象】作为程序的基本单元,一个对象中包含了【数据】以及操作数据的【函数】。
面向对象的特点为程序是一组对象的集合,每个对象都可以接受/处理其他对象发来的消息。
特点 :抽象、继承、封装、多态
- 抽象:就是忽略一个主题中与当前目标无关的方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节。抽象包括两个方面,一是过程抽象,二是数据抽象。
过程抽象是指任何一个明确定义功能的操作都可被使用者看作单个的实体看待,尽管这个操作实际上可能由一系列更低级的操作来完成。数据抽象定义了数据类型和施加于该类型对象上的操作,并限定了对象的值,只能通过使用这些操作修改和观察。
- 继承:这是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。
派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。这也体现了大自然中一般与特殊的关系。继承性很好地解决了软件的可重用性问题。
- 封装:就是把过程和数据包围起来,对数据的访问只能通过已定义的接口。面向对象的计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。一旦定义了一个对象的特性,则有必要决定这些特性的可见性,即哪些特性对外部世界是可见的,哪些特性用于表示内部状态。
在这个阶段定义对象的接口。通常,应禁止直接访问一个对象的实际表示,而应通过操作接口访问对象,这称为信息隐藏。封装保证了模块具有较好的独立性,使得程序维护修改较为容易。对应用程序的修改仅限于类的内部,因而可以将应用程序修改带来的影响减少到最低限度。
- 多态:是指允许不同类的对象对同一消息做出响应。比如同样的复制-粘贴操作,在字处理程序和绘图程序中有不同的效果。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好地解决了应用程序函数同名问题。
同样以下棋为例,可以将整体分为三个对象:
(1)黑白棋子 两方行为一致
(2)棋盘系统 负责绘制图像
(3)规则系统 判断输赢和犯规等
赋予以上三个对象属性以及行为:
(1)黑白棋子对象负责接收用户输入,并传递位置信息给棋盘系统对象
(2)棋盘系统对象接收到了棋子的变化就要负责在屏幕上面显示出这种变化
(3)规则系统对象对当前棋局进行判定
面向对象的编程思维以功能划分问题而不是用步骤进行解决,使用符合常规思维的方式来处理客观世界的问题,强调把解决问题领域的“动作”直接映射到对象之间的接口上。
如要加入悔棋功能,若是面向过程设计,则从输入到判断到显示的若干步骤都要改动,甚至步骤之间的先后顺序都可能需要调整。而若是面向对象设计,则只需改动第 ② 类对象(棋盘对象)即可,棋盘对象保存了黑白双方的棋谱和落子先后顺序,简单回溯操作即可实现悔棋功能,并不涉及显示和规则部分,改动是局部可控的。
8.1.3 两者优缺点对比
面向过程
优点:
流程化编程方式使得编程任务更加明确,在开发前已经基本确定实现方式及最终结果,具体步骤更加清晰便于分析每一部分;
效率高,善于结合数据结构开发程序。
缺点:
需要先思考完成代码逻辑,耗费精力;
代码重用性差,拓展能力差,不利于后期维护。
面向对象
优点:
易于拓展,代码重用率高,可继承,可覆盖,可以设计出代码之间低耦合的系统;
易于维护,减少后期程序维护工作量。
缺点:
开销大,当要修改某一对象内部时对象的属性不允许外部设置直接存取,需要增加许多额外步骤,增加运行开销;
性能低,由于面向更高的逻辑抽象层,使得在面向对象实现的时候对计算时间和空间存储的开销很大。
8.2 面向对象相关术语
在系统学习面向对象编程之前,需要了解有关面向对象的术语,对后续学习以及查找问题会有所帮助。
1) 类
类可以被理解为模板/图纸/定义,通过类可以相应的创造出无数具体的实例。例如给定一个大致的物种定义,可以寻找出代表不同特征的该生物,这一过程又被称为类的实例化。
2) 对象
类并不能直接进行使用,通过类创建出的实例(对象)才可被使用。
3) 属性
类中的所有变量称之为属性。
4) 方法
类中的所有函数统称为方法,但与类外函数不同的是,类方法至少要包括一个self
参数。类方法无法单独使用,只能与类的实例一起使用。
8.3 类的定义
类仅仅作为定义的作用,无法直接进行使用。而只有根据定义创造出的实例对象才能进行一系列的操作。因此在python中需要先创建(定义)类,之后创建类的实例对象,通过实例对象创建出相应的功能。
class ClassName(bases):
""" 这是一个自定义的类 """
# 定义类属性
多个(>= 0)类属性
# 定义类方法
多个(>= 0)类方法
"""
bases 为一个或多个用于继承的【父类】的名称。
和函数相同,类中也可以定义说明文档,需要放在类头之后类体之前的位置。
"""
无论是类属性还是类方法都不是必须的,此外python类中属性和方法所在的位置随意,并没有固定的前后位置关系。
和变量名相同,类名本质上为标识符,当作为类名时,单词首字母需大写;模块内部的类名,可以采用**“下划线+首字母大写”**的形式。
根据定义属性位置的不同,在各个类方法之外定义的属性称之为类属性或类变量,而在类方法中定义的属性被称为实例属性或实例变量。
如果一个类中没有任何的类属性和类方法,那么可以直接用pass
关键字作为类体即可。
8.4 构造函数
在创建类时可以手动添加一个__init__()
方法,该方法为一个特殊的类实例方法,称之为构造函数。
构造函数主要在创建对象时使用,每当创建一个类的实例对象时,python解释器都会自动调用构造函数。
class ClassName(bases):
def __init__(self, ...):
CodeBlock
"""
__init__()方法中可以包含多个参数,但必须包含一个名为self的参数且必须作为第一个参数。
"""
另外,如果不手动为类添加任何构造函数,python也会自动为类添加一个仅仅包含self参数的构造函数。仅包含self参数的__init__()构造方法也被称为类的默认构造方法。
class FirstDemo():
def __init__(self, string):
print("调用构造函数")
print(string)
def say(self, content):
print(content)
if __name__ == "__main__":
demo = FirstDemo("已调用构造函数")
Out:
调用构造函数
已调用构造函数
由于创建对象时会调用类的构造函数,当构造函数中有多个参数时,需要按顺序在定义类的实例对象时传递参数。可以看到,虽然以上构造函数中有 self 、string 两个参数,但实际传参时仅有 string ,也就是说 self 参数不需要手动传递参数。
8.5 类的实例对象的使用
定义的类只有进行实例化,也就是使用该类创建对象之后才能进行利用。总的来说,实例化后的类对象可以实现以下操作:
1) 访问或修改类对象具有的实例变量,也可以添加或删除已有的实例变量;
2) 调用类对象的方法,包括调用现有的方法以及给类对象动态添加新的方法。
class SecondDemo():
string = "调用构造函数"
def __init__(self, string):
self.string = string
print("构造函数中:" + self.string)
def say(self, content):
print(content)
if __name__ == "__main__":
demo = SecondDemo("已调用构造函数")
print(demo.string) # 输出string实例变量的值
# 修改实例变量的值
demo.string = "再次调用构造函数"
print(demo.string)
# 调用SecondDemo的say()方法
demo.say("调用SecondDemo的say()方法")
# 为demo对象增加一个plus实例变量
demo.plus = "为demo对象增加一个plus实例变量"
print(demo.plus)
# 删除新添加的实例变量
del demo.plus
# 为demo对象添加其他方法
def func(self):
print("新添加的方法", self)
demo.func = func
demo.func(demo) # 由于python不会自动将调用者绑定至self参数,需要手动将调用者与参数进行绑定
# 使用lambda为demo对象的func_2方法赋值
demo.func_2 = lambda self: print("使用lambda为demo对象的func_2方法赋值", self)
demo.func_2(demo)
Out:
构造函数中:已调用构造函数
已调用构造函数
再次调用构造函数
调用SecondDemo的say()方法
为demo对象增加一个plus实例变量
新添加的方法 <__main__.SecondDemo object at 0x000001FEB6CF0AF0>
使用lambda为demo对象的func_2方法赋值 <__main__.SecondDemo object at 0x000001FEB6CF0AF0>
8.6 self参数详解
在定义类的过程中,无论是显式创建构造函数还是向类中添加实例方法都要求将self作为第一个参数。
实际上,python只是规定无论时构造函数还是实例方法,最少要包含一个参数且并没有明确规定该参数的名称,命名为self只是程序猿之间约定俗成的习惯。
如果将【类】比作建筑图纸,那么【类实例化的对象】就是可以实际使用的建筑。根据一张图纸可以设计出不同的建筑,每个建筑的功能都是类似的(具有相同的类变量和类方法)。self 的作用类似身份验证机制,用以区分不同的建筑(每个类对象只能调用自己类变量和类方法)。
也就是说,同一个类可以实例化不同的对象,当某个对象调用类方法时,该对象会把自身的引用作为第一个参数自动传给该方法。换句话说,python会自动绑定类方法的第一个参数指向调用该方法的对象,这样python解释器就能知道需要操作哪个对象的方法。
因此,程序在调用实例对象和构造函数时,不需要手动为第一个参数传值。
class ThirdDemo():
name = "xxx"
def __init__(self, name):
self.name = name
print(self, "已调用构造函数", self.name)
if __name__ == "__main__":
demo_1 = ThirdDemo("demo_1")
demo_2 = ThirdDemo("demo_2")
Out:
<__main__.ThirdDemo object at 0x000001FEB6CF0A30> 已调用构造函数 demo_1
<__main__.ThirdDemo object at 0x000001FEB6CF03D0> 已调用构造函数 demo_2
对于构造函数的 self
参数来说,其代表的是当前正在初始化的类对象。可以看到demo_1在初始化时调用的构造函数中的 self
代表的是demo_1;而demo_2在初始化时调用的构造函数中的 self
代表的是demo_2。
总之,无论类中的构造函数还是类方法,实际调用他们的是谁则第一个参数 self
就代表谁。
8.7 类属性 & 实例属性
无论是类属性还是类方法,都无法在类的外部直接进行使用。通常将类看作一个独立的空间,类属性则是在类体中、类方法之外定义的变量。
根据变量定义的位置的不同以及定义的方式的不同,可以划分为以下三种类型:
1)类体中、所有函数之外:类属性(类变量)
2)类体中、所有函数内部、以【self. name】形式定义的变量:实例属性(实例变量)
3)类体中、所有函数内部、以【name = value】形式定义的变量:局部变量
8.7.1 类属性
类属性指的是在类中但在各个类方法外定义的变量:
class FourthDemo():
string = "已调用构造函数"
def say(self, content):
print(content)
if __name__ == "__main__":
print(FourthDemo.string)
# 修改类变量的值
FourthDemo.string = "再次调用构造函数"
print(FourthDemo.string)
# 添加类变量
FourthDemo.plus = "添加类变量"
print(FourthDemo.plus)
Out:
已调用构造函数
再次调用构造函数
添加类变量
在该段代码中,string即为类属性。类属性的特点是所有类的实例化对象都同时共享,类属性的调用方式建议使用类名直接进行调用。
要注意的是,由于类属性为所有实例化对象共有,通过类名修改类属性的值会影响所有实例化对象。同时,通过类对象是无法修改类变量的,通过类对象对类属性进行赋值,其本质为定义新的实例变量而不是修改类变量。
8.7.2 实例属性
实例属性是指在任意类方法内部,以【self. name】形式定义的变量,其特点是仅仅作用于调用该类方法的对象。另外实例属性只能通过对象名进行访问,无法通过类名进行访问。
class FourthDemo():
increase = "类属性"
def __init__(self):
self.string = "调用构造函数"
def say(self):
self.plus = "调用say()类方法"
if __name__ == "__main__":
demo = FourthDemo()
demo.say()
print(demo.plus)
# 添加实例属性
demo.increase = "实例属性"
print(demo.increase)
print(FourthDemo.increase)
Out:
调用say()类方法
实例属性
类属性
在此类中,string
/plus
都是实例属性。由于构造函数在创建类实例时都会被使用到,而 say()
方法需要类实例手动进行调用,因此所有该类的实例对象都会包含 string
实例属性,但只有调用了say()
方法的类实例才包含 plus
实例属性。
在类中,实例属性和类属性是可以同名的,但此时使用类实例调用该属性名时会直接调用实例属性,相当于将类属性覆盖屏蔽了,因此建议直接使用类名访问类属性。同时实例属性和类属性的不同在于,通过某个类实例修改实例属性的值,不会影响类的其他实例的实例属性,更不会对它同名的类属性产生影响。
8.7.3 局部变量
除了实例属性外,类方法中还可以定义局部变量。和实例属性不同,局部变量直接以【name = value】的形式进行定义。
class FourthDemo():
def say(self, str):
string = str * 2
print(string)
if __name__ == "__main__":
demo = FourthDemo()
demo.say("局部变量")
Out:
局部变量局部变量
通常情况下定义局部变量的意义在于辅助其所在的类方法的功能的实现,在方法执行完成后局部变量就会被销毁。
8.8 实例方法 & 类方法 & 静态方法
和类属性相似,类方法也被分为三种:实例方法、类方法以及静态方法。
1)直接定义不使用任何装饰器的方法:实例方法
2)使用装饰器 @classmethod
修饰的方法:类方法
3)使用装饰器 @staticmethod
修饰的方法:静态方法
8.8.1 实例方法
通常情况下,在类中定义的方法默认都是实例方法,实际上类的构造函数在理论上也属于实例方法。
实例方法的最大特点在于最少需要包含一个self
参数,用于绑定调用此方法的实例对象。实例方法可以使用类实例直接进行调用,同时也支持使用类名进行调用,但需要指定self
参数的绑定实例。
class FifthDemo():
def say(self, string):
print(string)
if __name__ == "__main__":
demo = FifthDemo()
string = "调用实例方法"
demo.say(string)
FifthDemo.say(demo, string)
Out:
调用实例方法
调用实例方法
8.8.2 类方法
python类方法与实例方法类似,最少需要包含一个参数。但在类方法中通常将其命名为 cls
,python会自动绑定【类本身】给该参数而并非【类实例】。因此在调用类方法时无需为其传参。
class FifthDemo():
def __init__(self):
self.string = "调用构造函数"
@classmethod
def info(cls):
print("调用类方法", cls)
if __name__ == "__main__":
FifthDemo.info()
Out:
调用类方法 <class '__main__.FifthDemo'>
类方法和类属性一样,建议使用类名进行直接调用,而不是使用类实例的形式进行调用。
8.8.3 静态方法
静态方法实际上就是之前所说的函数,其与普通函数的区别在于,静态方法定义于类这个独立命名空间中,而函数则定义在程序所在的全局命名空间中。
静态方法没有特殊参数,因此解释器不会对它包含的参数做任何类或实例的绑定,同时类的静态方法中也无法调用任何类属性和类方法。
class FifthDemo():
@staticmethod
def info(string):
print(string)
if __name__ == "__main__":
FifthDemo.info("调用静态方法")
demo = FifthDemo()
demo.info("调用静态方法")
Out:
调用静态方法
调用静态方法
在实际编程中,几乎不会用到类方法以及静态方法,因为完全可以被普通函数替代实现它们的功能,但在一些特殊情况下(例如工厂模式中),使用类方法和静态方法是不错的选择。
8.9 调用实例方法
实例的调用方法有两种:通过类名进行调用、类对象调用。
class SixthDemo():
def info(self):
print("调用实例方法")
if __name__ == "__main__":
demo = SixthDemo()
SixthDemo.info(demo)
demo.info()
Out:
调用实例方法
调用实例方法
实际上通过类名调用实例方法时 self
参数只是需要传入一个参数,并不一定需要是该类的实例,然而胡乱给 self
参数传参极有可能会导致程序运行崩溃。
总的来说,python中允许使用类名直接调用实例方法,这种调用方法的方式被称为【非绑定方法】;而用类的实例对象访问类成员的方式被称为【绑定方法】。
8.10 类命名空间
所有位于 class
语句中的代码其实都位于特殊的命名空间中,通常将之称为【类命名空间】。在python中,编写的整个程序默认处于全局命名空间中,而类体则处于类命名空间中。
对于定义在全局命名空间中的函数,程序使用正常调用函数的方式即可,只需要为各个形参绑定实参;而对于定义在类命名空间中的函数,这个函数就变成了实例方法,因此程序必须使用调用方法的方式来调用。
8.11 python描述符
本质上,python描述符就是一个类,其中定义了另一个类中属性的访问方式。换句话说,一个类可以将属性管理委托给描述符类。
描述符类基于三个特殊方法,这三个方法组成了描述符协议:
__set__(self, obj, type=None) # 在设置属性时调用
__get__(self, obj, value) # 在读取属性时调用
__del__(self, obj) # 对属性调用del时调用
实现了__set__
和 __get__
方法的描述符类被称为数据描述符;如果只实现 __get__
方法的描述符类被称为非数据描述符。
实际上,每次查找属性时,描述符协议中的方法都会由类的特殊方法 __getattribute__()
调用。例如使用【类实例.属性】的调用方式时,都会隐式调用该特殊方法 __getattribute__()
,它会按照以下顺序查找属性:
1)验证该属性是否为类实例对象的数据描述符;
2)查看类实例对象的 __dict__
中是否包含该属性;
3)查看该属性是否为类实例对象的非数据描述符。
class SeventhDemo_1():
"""创建一个描述符类"""
def __init__(self, num, string):
self.string = string
self.num = num
def __get__(self, obj, value):
print("正在获取", self.string)
return self.num
def __set__(self, obj, num):
print("正在更新", self.string)
self.num = num
class SeventhDemo_2():
initial = SeventhDemo_1(10, "测试数据")
if __name__ == "__main__":
demo = SeventhDemo_2()
print(demo.initial)
demo.initial = 20
print(demo.initial)
Out:
正在获取 测试数据
10
正在更新 测试数据
正在获取 测试数据
20
如果一个类的某个属性有数据描述符,那么每次使用【类实例.属性】的方式查找这个属性时,都会调用描述符的 __get__
方法并返回它的值;同样的,每次在对该属性进行赋值时,也会调用 __set__()
方法。
8.12 property()
实际上,使用【类实例.属性】的方式去访问属性是不推荐的,因为在原则上违背了类的封装原则。正常情况下,类包含的属性应该是隐藏的,只允许通过类提供的方法来间接实现对类属性的访问和操作。
因此,为了不破坏类的封装原则且能够高效操作类中的属性,类中应该包含多个用于读写类属性的方法,这样就可以通过【类实例.实例方法】的形式操作属性,在此基础上,python中提供了 property()
函数,可以实现在不破坏类封装原则的前提下,使开发者可以使用【类实例.属性】的方式操作类中的属性。
class EighthDemo():
def __init__(self, string):
self.__string = string
def getString(self):
return self.__string
def setString(self, string):
self.__string = string
def delString(self):
self.__string = "None"
string = property(getString, setString, delString, "用于访问string属性")
if __name__ == "__main__":
help(EighthDemo.string) # 调取说明文档
demo = EighthDemo("测试数据")
print(demo.string)
demo.string = "二次测试数据"
print(demo.string)
del demo.string
print(demo.string)
Out:
Help on property:
用于访问string属性
测试数据
二次测试数据
None
要注意的是在该程序中,由于 getString()
方法中需要返回 string
属性,如果写作 self.string
其本身又会调用 getString()
方法,这将会造成死循环。为避免这种情况,需要将 string
属性设置为私有属性(__string
)。
同时,property()
方法可以按顺序减少传入参数的数量:
string = property(getString, setString, "用于访问string属性")
这意味这 string
属性是一个可读写但不能删除的属性,因为property()
方法中并没有为其配置用于函数该属性的方法,即使类中定义了 delString()
方法也不能用来删除该属性。
class EighthDemo():
def __init__(self, string):
self.__string = string
@property
def word(self):
return self.__string
@word.setter
def word(self, string):
self.__string = string
@word.deleter
def word(self):
self.__string = "None"
if __name__ == "__main__":
demo = EighthDemo("测试数据")
print(demo.word)
demo.word = "二次测试数据"
print(demo.word)
del demo.word
print(demo.word)
Out:
测试数据
二次测试数据
None
删除的属性,因为property()
方法中并没有为其配置用于函数该属性的方法,即使类中定义了 delString()
方法也不能用来删除该属性。
class EighthDemo():
def __init__(self, string):
self.__string = string
@property
def word(self):
return self.__string
@word.setter
def word(self, string):
self.__string = string
@word.deleter
def word(self):
self.__string = "None"
if __name__ == "__main__":
demo = EighthDemo("测试数据")
print(demo.word)
demo.word = "二次测试数据"
print(demo.word)
del demo.word
print(demo.word)
Out:
测试数据
二次测试数据
None
除了使用 property() 函数,python还提供了 @property 装饰器。通过 @property 装饰器,可以直接通过【类实例.方法名】来访问方法,不需要在方法名后添加一对“()”小括号。