【Python基础】面向对象程序设计
本章目标
- 了解面向过程和面向对象两大编程思想
- 掌握类的定义
- 掌握对象的创建
- 掌握属性和方法的创建
- 掌握面向对象的三大特征
- 掌握动态语言的特点
- 掌握Object类的常用方法
- 了解深拷贝与浅拷贝
一、两大编程思想:与语言无关
面向过程 | 面向对象 |
---|---|
功能上的封装 | 属性和行为上的封装 |
典型代表:C语言 | 典型代表:Python、Java |
面向过程与面向对象的异同点
区别:
- 面向过程:事物比较简单,可以用线性的思维去解决
- 面向对象:事物比较复杂,使用简单的线性思维无法解决
共同点:
- 面向过程和面向对象都是解决实际问题的一种思维方式
- 二者相辅相成,并不是对立的。解决复杂问题,通过面向对象方式便于我们从宏观上把握事物之间复杂的关系,方便我们分析整个系统,具体到微观操作,仍然使用面向过程方式来处理
二、类和对象
类是怎么来的?
类是由N多个对象抽取出的 “像” 的属性和行为从而总结出来的一种类别。在Python中一切皆对象。
比如:str字符串类型、int整数类型,具体到具体的字符、数字就叫做对象了。
可以通过内置函数type() 查看数据类型
自定义数据类型的语法结构为:
class 类名():
pass
创建对象的语法格式为:
对象名=类名()
对象(具体实例)
#编写一个Person类
class Person():
pass
#创建类的对象
#对象名=类名()
#创建一个Person类型的对象
per=Person() #per就是Person类型的对象
print(type(per)) #查看per的数据类型
类的组成
类属性 —> 直接定义在类中,方法外的变量
实例属性—> 定义在__init__方法中,使用self打点的变量
实例方法—> 定义在类中的函数,而且自带参数self
静态方法—> 使用装饰器@staticmethod修饰的方法
类方法 —> 使用装饰器@classmethod修饰的方法
class Student:
#类属性:定义在类中,方法外的变量
school="北京xxx教育"
#初始化方法
def __init__(self,xm,age): #xm,age是方法的参数,是局部变量,xm,age的作用域是整个__init__方法
self.name=xm #=左侧是实例属性,xm是局部变量,将局部变量的值xm赋值给实例属性self.name
self.age=age #实例的名称和局部变量的名称可以相同
#定义在类中的函数,称为方法,自带一个参数self
def show(self):
print(f'我叫:{self.name},今年:{self.age}岁了')
#静态方法
@staticmethod
def sm():
#print(self.name)
#self.show()
print('这是一个静态方法,不能调用调用实例属性,也不能调用实例方法')
#类方法
@classmethod
def cm(cls): #cls-->class的简写
# print(self.name)
# self.show()
print('这是一个类方法,不能调用调用实例属性,也不能调用实例方法')
#创建类的对象
stu=Student('cmm',18) #为什么传了两个参数,因为__init__方法中,有两个形参,self是自带的参数,无需手动传入
#实例属性,使用对象名进行打点调用的
print(stu.name,stu.age)
#类属性,直接使用类名,打点调用
print(Student.school)
#实例方法,使用对象名进行打点调用
stu.show()
#静态方法,@staticmethod进行修饰的方法,直接使用类名打点调用
Student.sm()
#类方法,@classmethod进行修饰的方法,直接使用类名打点调用
Student.cm()
使用类模板创建N多个对象
class Student: #定义一个学生类
#类属性:定义在类中,方法外的变量
school="北京xxx教育"
#初始化方法
def __init__(self,xm,age): #xm,age是方法的参数,是局部变量,xm,age的作用域是整个__init__方法
self.name=xm #=左侧是实例属性,xm是局部变量,将局部变量的值xm赋值给实例属性self.name
self.age=age #实例的名称和局部变量的名称可以相同
#定义在类中的函数,称为方法,自带一个参数self
def show(self):
print(f'我叫:{self.name},今年:{self.age}岁了')
#静态方法
@staticmethod
def sm():
#print(self.name)
#self.show()
print('这是一个静态方法,不能调用调用实例属性,也不能调用实例方法')
#类方法
@classmethod
def cm(cls): #cls-->class的简写
# print(self.name)
# self.show()
print('这是一个类方法,不能调用调用实例属性,也不能调用实例方法')
#根据"图纸"可以创建出N多个对象
stu=Student('张三',18)
stu2=Student('李四',20)
stu3=Student('王五',25)
stu4=Student('Marry',23)
#输出对像类型
print(type(stu))
print(type(stu2))
print(type(stu3))
print(type(stu4))
Student.school='python教育' #给类的类属性赋值
#将学生对象存储到列表中
lst=[stu,stu2,stu3,stu4] #列表中的元素是Student类型的对象
for item in lst: #item是列表中的元素,是Student类型的对象
item.show() #对象名打点调用的实例方法
三、属性和方法的调用
动态绑定属性和方法
每个对象的属性名称相同,但属性值不同
可以为某个对象绑定独有的属性或方法
class Student: #定义一个学生类
#类属性:定义在类中,方法外的变量
school="北京xxx教育"
#初始化方法
def __init__(self,xm,age): #xm,age是方法的参数,是局部变量,xm,age的作用域是整个__init__方法
self.name=xm #=左侧是实例属性,xm是局部变量,将局部变量的值xm赋值给实例属性self.name
self.age=age #实例的名称和局部变量的名称可以相同
#定义在类中的函数,称为方法,自带一个参数self
def show(self):
print(f'我叫:{self.name},今年:{self.age}岁了')
#创建两个Student类型的对象
stu=Student('张三',18)
stu2=Student('王五',20)
print(stu.name,stu.age) #打印输出属性
print(stu2.name,stu2.age)
#为stu2动态绑定一个实例属性
stu2.gender='男'
print(stu2.name,stu2.age,stu2.gender)
#动态绑定方法
def introduce():
print('我是一个普通的函数,我被动态的绑定成了stu2对象的方法')
stu2.fun=introduce #introduce不要加小括号,加小括号是调用,在这里是赋值,把introduce赋值给stu2.fun
#fun就是stu2对象的方法
#调用,实例方法打点调用
stu2.fun()
四、面向对象的三大特征
面向对象的三大特征
封装 | 隐藏内部细节,对外提供操作方式 |
---|---|
继承 | 是在函数调用时,使用 “形参名称=值” 的方式进行传参,传递参数顺序可以与定义时参数的顺序不同 |
多态 | 是在函数定义时,直接对形式参数进行赋值,在调用时如果该参数不传值,则使用传递的值。 |
1.封装
封装 权限控制
权限控制 | 是通过对属性或方法添加单下划线、双下划线以及首尾双下划线来实现 |
---|---|
单下划线开头 | 以单下划线开头的属性或方法表示protected受保护的成员,这类成员被视为仅供内部使用,允许类本身和子类进行访问,但实际上它可以被外部代码访问。 |
双下划线开头 | 表示private私有的成员,这类成员只允许定义该属性或方法的类本身进行访问 |
首尾双下划线 | 一般表示特殊的方法 |
class Student():
#首尾双下划线,表示特殊方法
def __init__(self,name,age,gender):
self._name=name #self._name受保护的,只能本类和子类访问
self.__age=age #self.__age表示私有的,只能类本身去访问
self.gender=gender #普通的实例属性,类的内部,外部,及子类都可以访问
def _fun1(self): #受保护的
print('子类及本身可以访问')
def __fun2(self): #私有的
print('只有定义的类可以访问')
def show(self): #普通的实例方法
self._fun1() #类本身访问受保护的方法
self.__fun2() #类本身访问私有方法
print(self._name) #受保护的实例属性
print(self.__age) #私有的实例属性
#创建一个学生类的对象
stu=Student('cmm',20,'女')
#类的外部
print(stu._name)
#出了类的定义范围就不能调用
#print(stu.__age)
# #AttributeError: 'Student' object has no attribute '__age'. Did you mean: '_name'?
#调用受保护的实例方法
stu._fun1() #子类及本身可以访问
#私有方法
#stu.__fun2()
#AttributeError: 'Student' object has no attribute '__fun2'. Did you mean: '_fun1'?
#私有的实例属性和方法是真的不能访问吗?
print(stu._Student__age) #为什么可以这样访问呢?
stu._Student__fun2()
print(dir(stu)) #使用dir查看stu这个对象所有的属性和方法
属性的设置
使用对象名._类名(stu._student),两个下划线带有属性名(stu.__Student__age)这样访问它的私有属性,实际上这个方法是非常不推荐的,在编写代码过程中,可以使用一个装饰器,装饰器是Python当中的一个名词(概念),它是使用@符号去开头的。
可以使用@property将一个方法去转换成属性去使用,它的定义形式是方法,但是你在使用的时候是不需要加括号的,就把它当属性使用就可以了。
使用@property去修饰方法,就可以将其转换成属性了,在访问的时候,你只能访问属性的值,无法修改的值。
如果想改值,就需要再设置一个setter,就是设置(setter)这样的一个方法,才能够去修改属性值。
class Student:
def __init__(self,name,gender):
self.name=name
self.__gender=gender #self.__gender是私有的实例属性
#使用@property修改方法,将方法转成属性使用
@property
def gender(self):
return self.__gender
#将我们的gender这个属性设置为可写属性
@gender.setter
def gender(self,value):
if value!='男'and value!='女':
print('性别有误,已将性别设置为男')
self.__gender='男'
else:
self.__gender=value
stu=Student('cmm','女')
print(stu.name,'的性别是:',stu.gender) #stu.gender就会去执行stu.gender()
#尝试修改属性值,在没有设置setter时,无法修改其值
#stu.gender='男' #AttributeError: can't set attribute 'gender'
stu.gender='其他'
2.继承
继承的概念是源于生活,子孙辈会从父辈哪里继承一些体貌特征,但是子孙辈又不完全是父辈的翻版,他们还会有一些自己的特征,这就是继承。
被继承的类称为父类或者是基类,新的类称为子类或者是派生类,子类继承父类,就拥有了父类所有共有成员和受保护的成员。
继承的特点:
- 在Python中一个子类可以继承N多个父类
- 一个父类也可以拥有N多个子类
- 如果一个类没有继承任何类,那么这个类默认继承的是object类
继承的语法结构:
class 类名(父类1,父类2,父类3,...父类N):
pass
class Person: #默认继承了object
def __init__(self,name,age):
self.name=name
self.age=age
def show(self):
print(f'大家好,我叫:{self.name},我今年:{self.age}岁')
#Student继承Person类
class Student(Person):
#编写初始化的方法
def __init__(self,name,age,stuno):
super().__init__(name,age) #调用父类的初始化方法
self.stuno=stuno
#Doctor继承Person类
class Doctor(Person):
#编写初始化方法
def __init__(self,name,age,department):
super().__init__(name,age)
self.department=department
#创建第一个子类对象
stu=Student('cmm',20,1001)
stu.show()
doctor=Doctor('张一一',32,'外科')
doctor.show()
多继承
一个子类继承了N多个父类,都需要去调用父类的初始化方法,使用父类的名称去区别,不能使用super,只能使用类名去区分。
class FatherA():
def __init__(self,name):
self.name=name
def show(self):
print('父类A中的方法')
class FatherB():
def __init__(self,age):
self.age=age
def showB(self):
print('父类B中的方法')
#多继承
class Son(FatherA,FatherB):
def __init__(self,name,age,gender):
#需要调用两个父类的初始化方法
FatherA.__init__(self,name)
FatherB.__init__(self,age)
self.gender=gender
son=Son('cmm',20,'女') #调用Son类中的__init__执行
son.show()
son.showB()
方法重写
- 子类继承了父类就拥有了父类中共有成员和受保护的成员
- 父类的方法并不能完全适合子类的需求,这个时候子类就可以重写父类的方法
- 子类在重写父类的方法时,要求方法的名称必须与父类方法的名称相同,在子类重写后的方法中可以通过super().xxx()调用父类中的方法
子类中有show方法,是调用子类自己的show方法,子类没有的,再到父类中去找,子类有,直接调用子类的
class Person: #默认继承了object
def __init__(self,name,age):
self.name=name
self.age=age
def show(self):
print(f'大家好,我叫:{self.name},我今年:{self.age}岁')
#Student继承Person类
class Student(Person):
#编写初始化的方法
def __init__(self,name,age,stuno):
super().__init__(name,age) #调用父类的初始化方法
self.stuno=stuno
def show(self):
#调用父类中的方法
super().show()
print(f'我来自xxx大学,我的学号是:{self.stuno}')
#Doctor继承Person类
class Doctor(Person):
#编写初始化方法
def __init__(self,name,age,department):
super().__init__(name,age)
self.department=department
def show(self):
super().show() #调用父类中show方法
print(f'大家好,我叫:{self.name},我今年{self.age}岁,我的工作科室是:{self.department}')
#创建第一个子类对象
stu=Student('cmm',20,1001)
stu.show() #调用子类自己的show方法
doctor=Doctor('张一一',32,'外科')
doctor.show() #调用子类自己的show()方法
3.多态
- 指的就是"多种形态",即便不知道一个变量所引用的对象到底是什么类型,仍然可以通过这个变量调用对象的方法
- 在程序运行过程中根据变量所引用的对象的数据类型,动态决定调用那个对象中的方法。
- 在Python语言中的多态,更本不关心对象的数据类型,也不关心类之间是否存在继承关系,只关心对象的行为(方法)。只要不同的类中有同名的方法,即可实现多态。
class Person():
def eat(self):
print('人,吃五谷杂粮')
class Cat():
def eat(self):
print('猫,喜欢吃鱼')
class Dog():
def eat(self):
print('狗,喜欢啃骨头')
#这三个类中都有一个同名的方法,eat
#编写函数
def fun(obj): #obj是函数的形式参数,在定义处知道这个形参的数据类型吗?
obj.eat() #通过变量obj(对象)调用eat方法
#创建三个类的对象
per=Person()
cat=Cat()
dog=Dog()
#调用fun函数
fun(per) #Python中的多态,不关心对象的数据类型,只关心对象是否具有同名方法
fun(cat)
fun(dog)
面向对象的三大特征总结:
- 封装的作用是保护程序的安全
- 继承实现程序一个方法的重写,代码的复用
- 多态可以实现程序的可扩展性
五、object类的常用方法
object类:
- 所有类直接或间接的父类
- 所有的父类都拥有object类的属性和方法
object类中特殊的方法 | 功能描述 |
---|---|
__ new__() | 由系统调用,用于创建对象 |
__ init__() | 创建对象时手动调用,用于初始化对象属性值 |
__ str__() | 对象的描述,返回值是str类型,默认输出对象的内存地址 |
执行先后顺序:
先__new__系统调用创建对象,然后再由__init__去给对象的属性赋值。
__str__用于返回对象值得一个描述信息的,输出的是一个内存地址。
class Person(object):
def __init__(self,name,age):
self.name=name
self.age=age
def show(self):
print(f'大家好,我叫:{self.name},我今年:{self.age}岁')
#创建Person类的对象
per=Person('cmm',20) #创建对象的时候会自动调用__init__方法()
print(dir(per))
print(per) #自动调用了__str__方法
重写父类的__str__方法:
重写父类__str__方法之前
实例代码:
class Person(object):
def __init__(self,name,age):
self.name=name
self.age=age
#创建Person类的对象
per=Person('cmm',20)
print(per) #自动调用了__str__方法
执行结果:<__ main__.Person object at 0x000001AE4E3153F0>
重写父类__str__方法之后
实例代码:
class Person(object):
def __init__(self,name,age):
self.name=name
self.age=age
#方法重写
def __str__(self):
return '这是一个人类,具有name和age两个实例属性' #返回值是一个字符串
#创建Person类的对象
per=Person('cmm',20)
print(per) #还是内存地址吗?不是,__str__方法中的内容,直接输出对象名,实际上是调用__str__方法
print(per.__str__()) #手动调用
执行结果:
这是一个人类,具有name和age两个实例属性
这是一个人类,具有name和age两个实例属性
特殊方法:
运算符 | 特殊方法 | 功能描述 |
---|---|---|
+ | __ add__() | 执行加法运算 |
- | __ sub__() | 执行减法运算 |
<, <=, == | __ lt__(), __ le__() , __ eq__() | 执行比较运算 |
>, >=, != | __ gt__(), __ ge__() , __ ne__() | 执行比较运算 |
*, / | __ mul__(), __ truediv__() | 执行乘法运算,非整除运算 |
%, // | __ mod__(), __ floordiv__() | 执行取余运算,整除运算 |
** | __ pow__() | 执行幂运算 |
实例代码:
a=10
b=20
print(dir()) #Python中一切皆对象
print(a+b) #执行加法运算
print(a.__add__(b))
print(a.__sub__(b)) #执行减法运算
print(f'{a}<{b}吗?',a.__lt__(b))
print(f'{a}<={b}吗?',a.__le__(b))
print(f'{a}=={b}吗?',a.__eq__(b))
print('-'*40)
print(f'{a}>{b}吗?',a.__gt__(b))
print(f'{a}>={b}吗?',a.__ge__(b))
print(f'{a}!={b}吗?',a.__ne__(b))
print('-'*40)
print(a.__mul__(b)) #乘法
print(a.__truediv__(b)) #除法
print(a.__mod__(b)) #取余
print(a.__floordiv__(b)) #整除
print(a.__pow__(2)) #幂运算
特殊属性:
特殊属性 | 功能描述 |
---|---|
obj.__ dict__ | 对象的属性字典 |
obj.__ class__ | 对象所属的类 |
class.__ bases__ | 类的父类元组 |
class.__ base__ | 类的父类 |
class.__ mro__ | 类的层次结构 |
class.__ subclasses__ | 类的子类列表 |
class A:
pass
class B:
pass
class C(A,B):
def __init__(self,name,age):
self.name=name
self.age=age
#创建类的对象
a=A()
b=B()
#创建C类的对象
c=C('cmm',20)
print('对象a的属性字典:',a.__dict__) #对象的属性字典
print('对象b的属性字典:',b.__dict__)
print('对象c的属性字典:',c.__dict__)
print('对象a所属的类:',a.__class__)
print('对象a所属的类:',b.__class__)
print('对象a所属的类:',c.__class__)
print('A类的父类元组:',A.__bases__) #A类的父类元组: (<class 'object'>,)
print('B类的父类元组:',B.__bases__) #B类的父类元组: (<class 'object'>,)
print('C类的父类元组:',C.__bases__) #C类的父类元组: (<class '__main__.A'>, <class '__main__.B'>)
print('A类的父类:',A.__base__)
print('B类的父类:',B.__base__)
print('C类的父类:',C.__base__) #A类,如果继承了N多个父类,结果只显示第一个父类
print('A类的层次结构:',A.__mro__)
print('B类的层次结构:',B.__mro__)
print('C类的层次结构:',C.__mro__) #C类继承了A类,B类,间接继承了object类
#子类列表
print('A类的子类列表:',A.__subclasses__()) #A的子类有C类
print('B类的子类列表:',B.__subclasses__())
print('C类的子类列表:',C.__subclasses__()) #[]列表
六、类的深拷贝与浅拷贝
- 变量的赋值:只是形成两个变量,实际上还是指向同一个对象
- 浅拷贝:拷贝时,对象包含的子对象内容不拷贝,因此,源对象与拷贝对象会引用同一个子对象
- 深拷贝:使用copy模块的 deepcopy 函数,递归拷贝对象中包含的子对象,源对象和拷贝对象所有的子对象也不相同
class CPU():
pass
class Disk():
pass
class Computer():
#计算机由CPU和硬盘
def __init__(self,cpu,disk):
self.cpu=cpu
self.disk=disk
cpu=CPU() #创建了一个CPU对象
disk=Disk() #创建了一个硬盘对象
#创建一个计算机对象
com=Computer(cpu,disk)
#变量(对象)的赋值
com1=com
print(com,'子对象的内存地址:',com.cpu,com.disk)
print(com1,'子对象的内存地址:',com1.cpu,com1.disk)
#类对象的浅拷贝
print('-'*50)
import copy
com2=copy.copy(com) #com2是新产生的对象,com2的子对象,cpu,disk不变
print(com,'子对象的内存地址:',com.cpu,com.disk)
print(com2,'子对象的内存地址:',com2.cpu,com2.disk)
print('-'*50)
com3=copy.deepcopy(com) #com3是新产生的对象,com3的子对象,cpu,disk也会重新创建
print(com,'子对象的内存地址:',com.cpu,com.disk)
print(com3,'子对象的内存地址:',com3.cpu,com3.disk)