第九章 面向对象程序设计
面向过程和面向对象编程思想
编程思想与语言无关,无论是学习Java,Python,C语言都会涉及编程思想这个问题。
面向过程是功能上的封装,比如之前学习过的函数,编写函数就是实现功能的封装。所以实际上编写的函数采用的就是面向过程的编程思想,其典型代表就是C语言。
面向对象是属性和行为上的封装,也就是类和对象。Python语言是面向对象的编程语言,Java语言也是面向对象的编程语言。
面向过程的编程思想和面向对象的编程思想并不是相互独立的,而是相辅相成的。
面向过程与面向对象的异同点:
面向过程 | 面向对象 | |
区别 | 事物比较简单,可以用线性的思维去解决 | 事物比较复杂,使用简单的线性思维无法解决 |
共同点 | 面向过程和面向对象都是解决实际问题的一种思维方式 | |
二者相辅相成,并不是对立的 解决复杂问题,通过面向对象方式便于我们从宏观上把握事物之间复杂的关系,方便我们分析整个系统,具体到微观操作,仍然使用面向过程方式来处理 |
自定义类和创建自定义类的对象
类是怎么来的?
类是由N多个对象抽取出“像”的属性和行为从而归纳总结出来的一种类别。
在Python中一切皆对象。
a = 10
b = 9.8
c = 'hello'
print(type(a))
print(type(b))
print(type(c))
自定义数据类型的语法结构为:【注意类名的首字母要大写,小括号()可写可省略】
class 类名():
pass
创建对象的语法格式为:
对象名 = 类名()
# 编写一个Person类
class Person():
pass
# 编写一个Cat类:
class Cat():
pass
# 编写一个Dog类(这里省略小括号)
class Dog:
pass
# 编写一个Student类(这里省略小括号)
class Student:
pass
# 创建类的对象
# 对象名=类名()
per = Person() # per就是Person类型的对象
c = Cat() # c就是Cat类型的对象
d = Dog() # d就是Dog类型的对象
stu = Student() # stu就是Student类型的对象
print(type(per))
print(type(c))
print(type(d))
print(type(stu))
类的组成
类的组成:
- 类属性:直接定义在类中,方法外的变量
- 实例属性:定义在__init__方法中,使用self打点的变量
- 实例方法:定义在类中的函数,而且自带参数self
- 静态方法:使用装饰器@staticmethod修饰的方法
- 类方法:使用装饰器@classmethod修饰的方法
注意:
- 类方法、类属性、静态方法都是使用类名打点调用。
- 跟实例有关的:实例属性、实例方法都是使用对象名打点调用。
- 静态方法不能调用实例属性,也不能调用实例方法。
- 类方法不能调用实例属性,也不能调用实例方法。
class Student:
# 类属性:直接定义在类中,方法外的变量
school = '北京大学'
# 实例属性:定义在__init__方法中,使用self打点的变量
# 初始化方法
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修饰的方法
# 注意:静态方法不能调用实例属性,也不能调用实例方法
@staticmethod
def sm():
# self.name,不能调用实例属性
# self.show(),不能调用实例方法
print('这是一个静态方法,不能调用实例属性,也不能调用实例方法')
# 类方法:使用装饰器@classmethod修饰的方法
# 注意:类方法不能调用实例属性,也不能调用实例方法
@classmethod
def cm(cls): # cls-->class的简写
# self.name,不能调用实例属性
# self.show(),不能调用实例方法
print('这是一个类方法,不能调用实例属性,也不能调用实例方法')
# 创建类的对象
stu = Student('lxl', 24) # 为什么传了两个参数,因为__init__方法中,有两个形参,self是自带的参数,无需手动传入
# 实例属性,使用对象名进行打点调用
print(stu.name, stu.age)
# 类属性,直接使用类名打点调用
print(Student.school)
# 实例方法,使用对象名进行打点调用
stu.show()
# 静态方法,@staticmethod进行修饰的方法,直接使用类名打点调用
Student.sm()
# 类方法,@classmethod进行修饰的方法,直接使用类名打点调用
Student.cm()
'''
类方法、类属性、静态方法都是使用类名打点调用
跟实例有关的:实例属性、实例方法都是使用对象名打点调用
'''
使用类模版创建多个对象
class Student:
# 类属性:直接定义在类中,方法外的变量
school = '北京大学'
# 实例属性:定义在__init__方法中,使用self打点的变量
# 初始化方法
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}岁')
# 根据“图纸”可以创建出N多个对象
stu = Student('lxl', 24)
stu2 = Student('mdm', 25)
stu3 = Student('xl', 25)
stu4 = Student('hmm', 25)
print(type(stu))
Student.school = '清华大学' # 给类的类属性赋值
# 将学生对象存储到列表中
lst = [stu, stu2, stu3, stu4] # 列表中的元素是Student类型的对象
for item in lst: # item是列表中的元素,是Student类型的对象
item.show() # 对象名打点调用实例方法
动态绑定属性和方法
类是模版,可以创建N多个同类型的对象,由于类型是相同的,所以每个对象的属性名称都是相同的,但是属性值却可以不相同。
Python是一门动态语言,可以在创建对象后再去绑定它独有的属性。
可以为某个对象绑定独有的属性或方法。
class Student:
# 类属性:直接定义在类中,方法外的变量
school = '北京大学'
# 实例属性:定义在__init__方法中,使用self打点的变量
# 初始化方法
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('lxl', 24)
stu2 = Student('mdm', 25)
print(stu.name, stu.age)
print(stu2.name, stu2.age)
# 为stu2动态绑定一个实例属性
stu2.gender = '男'
print(stu2.name, stu2.age, stu2.gender)
# print(stu.gender) # AttributeError: 'Student' object has no attribute 'gender'
# 动态绑定方法
def introduce():
print('我是一个普通的函数,我被动态绑定成了stu2对象的方法')
stu2.fun = introduce # 函数的赋值,不能加()。若加了()则为调用
# fun就是stu2对象的方法
stu2.fun()
面向对象三大基本特征
面向对象程序设计的三大基本特征:封装、继承、多态。
- 封装:隐藏内部细节,提高程序的安全性(健壮性)。
- 继承:实现代码的复用(重复使用),通过继承可以理顺类与类之间的关系。
- 多态:不关心对象的数据类型,不关心是否具有继承关系,只关心对象的行为(方法),提高程序的可扩展性和可维护性。
Python中的权限控制
封装:隐藏内部细节,对外提供访问属性和方法的访问方式,其目的是保证数据的安全性。
在Python中没有明确的权限访问修饰符,对于权限的访问控制是通过对属性或方法添加下划线实现的。
权限控制:是通过对属性或方法添加单下划线、双下划线以及首尾双下划线来实现。
- 单下划线开头:以单下划线开头的属性或方法表示protected受保护的成员,这类成员被视为仅供内部使用,允许类本身和子类进行访问,但实际上它可以被外部代码访问。
- 双下划线开头:表示private私有的成员,这类成员只允许定义该属性或方法的类本身进行访问。
- 首尾双下划线:一般表示特殊的方法。
class Student():
# 首尾双下划线
def __init__(self, name, age, gender):
self._name = name # self._name,单下划线开头表示受保护的,只能本类和子类访问
self.__age = age # self.__age,双下划线开头表示私有的,只能类本身去访问
self.gender = gender # self.gender普通的实例属性,类的内部、外部及子类都可以访问
def _fun1(self): # 单下划线开头,受保护的,
print('本身及子类可以访问')
def __fun2(self): # 双下划线开头,私有的,
print('只有类本身可以访问')
def show(self): # 普通的实例方法
self._fun1() # 类本身访问受保护的方法
self.__fun2() # 类本身访问私有方法
print(self._name) # 受保护的实例属性
print(self.__age) # 私有的实例属性
# 创建一个学生类的对象
stu = Student('lxl', 20, '女')
# 类的外部
print(stu._name)
# print(stu.__age) # AttributeError: 'Student' object has no attribute '__age'
# 调用受保护的实例方法
stu._fun1() # 子类及本身可以访问
# 私有方法
# stu.__fun2() # AttributeError: 'Student' object has no attribute '__fun2'
# 私有的实例属性和方法是真的不能访问吗?-->用下面的方法就可以访问了
print(stu._Student__age)
stu._Student__fun2()
# dir()可以输出对象中所有的属性和方法
print(dir(stu))
'''
会发现在这里面私有属性age会被定义为_Student__age;
私有方法fun2会被定义为_Student__fun2;
因此可以用上面的方式访问私有的实例属性和方法
'''
属性的设置
在上述例子中使用对象名._类名.__属性名(stu._Student__age)
访问私有属性的方式是非常不推荐的。
在编写程序的过程中可以使用一个装饰器,装饰器是Python中的一个名词(概念),是使用 @ 符号开头,可以使用 @property 将一个方法去转换成属性去使用,就是说它的定义形式是方法,但是在使用的时候是不需要加括号的,将其当做属性使用。使用 @property 去修饰方法就可以将其转换成属性了,在访问时只能访问属性的值,却无法修改属性的值;如果想修改还要再设置一个 @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('lxl', '男')
print(stu.name, '的性别是:', stu.gender) # stu.gender就会去执行stu.gender()
# 修改属性值
stu.gender = '其他'
print(stu.name, '的性别是:', stu.gender)
继承的概念
继承的概念源于人们的生活,子孙辈会从父辈那里继承一些体貌特征,但是子孙辈又不完全是父辈的翻版,他们还会有一些自己的特征,这就是继承。
在程序设计当中,被继承的类称为父类或基类,新的类称为子类或派生类。子类继承了父类就拥有了父类所有共有成员和受保护的成员。
继承:
- 在Python中一个子类可以继承N多个父类
- 一个父类也可以拥有N多个子类
- 如果一个类没有继承任何类,那么这个类默认继承的是object类
继承的语法结构:
class 类名(父类1,父类2,…,父类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):
# name,age是父类中有的,可以直接调用
super().__init__(name, age) # 调用父类的初始化方法给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('lxl', 20, '1001')
stu.show()
doc = Doctor('wz', 20, '外科')
doc.show()
Python中的多继承
在Python中一个父类可以拥有N多个子类,它与其他编程语言不一样的地方就是在Python中一个子类也可以继承多个父类。继承多个父类后就拥有了多个父类的共有的成员和受保护的成员。
class FatherA():
def __init__(self, name):
self.name = name
def showA(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):
# 需要调用两个父类的初始化方法
# 当一个类继承多个父类时不能再使用super(),而应该用类名进行区分
FatherA.__init__(self, name)
FatherB.__init__(self, age)
self.gender = gender
son = Son('mdm', 20, '女') # 调用Son类中__inti__执行
son.showA()
son.showB()
方法重写
- 子类继承了父类就拥有了父类中共有成员和受保护的成员
- 父类的方法并不能完全适合子类的需求,这个时候子类就可以重写父类的方法
- 子类在重写父类的方法时,要求方法的名称必须与父类的方法名称相同,在子类重写后的方法中可以通过super().xxx()调用父类中的方法
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):
# name,age是父类中有的,可以直接调用
super().__init__(name, age) # 调用父类的初始化方法给name,age赋值
self.stuno = stuno
# 重写父类中的方法,方法名要与父类中的方法名相同
def show(self):
# 调用父类中的方法
super().show()
print(f'我来自北京大学,我的学号是:{self.stuno}')
# Doctor继承Person类
class Doctor(Person):
# 编写初始化方法
def __init__(self, name, age, department):
super().__init__(name, age)
self.department = department
# 重写父类中的方法,方法名要与父类中的方法名相同
def show(self):
print(f'大家好,我叫{self.name},今年{self.age},我的工作科室是{self.department}')
# 创建子类对象
stu = Student('lxl', 20, '1001')
stu.show() # 调用子类自己的show()方法。子类中没有的才再去父类中找
doc = Doctor('wz', 20, '外科')
doc.show() # 调用子类自己的show()方法
多态
多态:
- 指的就是“多种形态”,即便不知道一个变量所引用的对象到底是什么类型,仍然可以通过这个变量调用对象的方法
- 在程序运行过程中根据变量所引用对象的数据类型,动态决定调用哪个对象中的方法
- Python语言中的多态,根本不关心对象的数据类型,也不关心类之间是否存在继承关系,只关心对象的行为(方法)。只要不同的类中有同名方法,即可实现多态
在程序运行过程中根据变量所引用对象的数据类型,动态决定调用哪个对象中的方法。也就是说在编写方法的时候,参数类型不知道,只有在调用的时候传入具体的参数后才能知道是谁,但是这个参数可以很确定它是一个对象,可以调用这个对象的方法,只有这个对象有这个方法就可以了。
所以说Python中的多态不关心对象的数据类型,也不关心是否存在继承关系,它只关心是否具有同名的方法,只要具有同名的方法就可以实现多态。这点也是Python语言和其他语言当中不一样的地方,在Java语言中封装、继承、多态是必不可少的,要想实现多态必须得有继承去做前提;但是在Python中则不一样,Python中只关心方法,不关心是否具有继承关系。再次强调Python中的多态只关心方法。
class Person():
# 这里没有写初始化方法__init__()。
# 当有实例属性时在初始化方法中赋值,没有实例属性时可以不写初始化方法
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类
在Python中如果一个类没有继承任何类的话,那么这个类默认继承的就是object类。所以object类是所有类的直接或间接父类,因此所有类都拥有object类的属性和方法。
object类:
- 所有类直接或间接的父类
- 所有类都拥有object类的属性和方法
object类中特殊的方法 | 功能描述 |
---|---|
__new__() | 由系统调用,用于创建对象 |
__init__() | 创建对象时手动调用,用于初始化对象属性值 |
__str__() | 对象的描述,返回值是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('zhangsan', 21) # 创建对象的时候会自动调用__init__()方法
print(dir(per)) # dir(对象) 会返回该对象所具有的所有属性和方法
print(per) # 自动调用了__str__(),输出对象的描述信息,默认输出对象的内存地址
是__new__()先执行还是__init__()先执行呢?————会先执行父类中的__new__()去创建对象开辟内存空间,创建完成之后再由__init__()去给实例属性进行赋值。
重写__str__():
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
# 方法重写,方法名要与父类方法名相同
def __str__(self): # 返回str类型
return '这是一个人类,具有name和age两个实例属性' # 返回值是一个字符串
# 创建Person类的对象
per = Person('zhangsan', 21) # 创建对象的时候会自动调用__init__()方法
# 直接输出对象名,实际上是调用__str__()方法(即默认调用)
print(per) # 重写__str__()后这里便不再是内存地址了,而是重写后__str__()里的内容
# 亦可手动调用
print(per.__str__())
对象的特殊方法
在Python中首尾双下划线的方法表示的是特殊方法,比如__init__()。实际上Python中运算符也是通过调用特殊方法来实现的。
运算符 | 特殊方法 | 功能描述 |
---|---|---|
+ | __add__() | 执行加法运算 |
- | __sub__() | 执行减法运算 |
<、<=、== | __lt__()、__le__()、__eq__() | 执行比较运算 |
>、>=、!= | __gt__()、__ge__()、__ne__() | 执行比较运算 |
*、/ | __mul__()、__truediv__() | 执行乘法运算、非整除运算 |
%、// | __mod__()、__floordiv__() | 执行取余运算、整除运算 |
** | __pow__() | 执行幂运算 |
a = 10
b = 20
print(dir(a)) # 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('-' * 30)
print(f'{a}>{b}吗?', a.__gt__(b))
print(f'{a}>={b}吗?', a.__ge__(b))
print(f'{a}!={b}吗?', a.__ne__(b))
print('-' * 30)
print(a.__mul__(b)) # 乘法
print(a.__truediv__(b)) # 除法
print(a.__floordiv__(b)) # 整除
print(a.__mod__(b)) # 取余
print(a.__pow__(2)) # 幂运算
Python中的特殊属性
在Python中首尾双下划线的方法表示的是特殊方法,首尾双下划线的属性表示的是特殊属性。
特殊属性 | 功能描述 |
---|---|
obj.__dict__ | 对象的属性字典 |
obj.__class__ | 对象所属的类 |
obj.__bases__ | 类的父类元组 |
obj.__base__ | 类的父类 |
obj.__mro__ | 类的层次结构 |
obj.__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('lxl', 20)
print('对象a的属性字典:', a.__dict__) # 对象的属性字典(实例属性)
print('对象b的属性字典:', b.__dict__)
print('对象c的属性字典:', c.__dict__)
print('对象a所属的类:', a.__class__) # 对象所属的类
print('对象b所属的类:', b.__class__)
print('对象c所属的类:', c.__class__)
print('A类的父类元组:', A.__bases__) # 类的父类元组(一个类可以继承N多个父类)
print('B类的父类元组:', B.__bases__)
print('C类的父类元组:', C.__bases__)
print('A类的父类:', A.__base__) # 类的父类
print('B类的父类:', B.__base__)
print('C类的父类:', C.__base__) # 如果继承了N多个父类,则类的父类结果只显示第一个父类
# 如果继承了N多个父类,其中有同名方法,那么默认调用的是第一个父类的方法
# 类的层次结构--->都继承了谁
print('A类的层次结构:', A.__mro__)
print('B类的层次结构:', B.__mro__)
print('C类的层次结构:', C.__mro__) # C类继承了A类、B类,间接继承了object类
# 类的子类列表
print('A类的子类列表:', A.__subclasses__())
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) # 两者内存地址相同,说明是同一个对象
print(com1)
print(com, '子对象的内存地址:', com.cpu, com.disk) # 同一个对象,两者子对象内存地址也相同,在内存中实际上只有一个对象
print(com1, '子对象的内存地址:', com1.cpu, com1.disk)
画内存分析图:
类对象的浅拷贝
浅拷贝会产生一个新的对象,但是子对象不会产生新的。
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) # 两者内存地址相同,说明是同一个对象
print(com1)
print(com, '子对象的内存地址:', com.cpu, com.disk) # 同一个对象,两者子对象内存地址也相同,在内存中实际上只有一个对象
print(com1, '子对象的内存地址:', com1.cpu, com1.disk)
print('-' * 30)
# 类对象的浅拷贝
import copy
com2 = copy.copy(com) # com2是新产生的对象,com2的子对象cpu,disk不变
print(com, '子对象的内存地址:', com.cpu, com.disk)
print(com2, '子对象的内存地址:', com2.cpu, com2.disk)
画内存分析图:
类对象的深拷贝
深拷贝是连子对象也会重新创建。
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) # 两者内存地址相同,说明是同一个对象
print(com1)
print(com, '子对象的内存地址:', com.cpu, com.disk) # 同一个对象,两者子对象内存地址也相同,在内存中实际上只有一个对象
print(com1, '子对象的内存地址:', com1.cpu, com1.disk)
print('-' * 30)
# 类对象的浅拷贝
import copy
com2 = copy.copy(com) # com2是新产生的对象,com2的子对象cpu,disk不变
print(com, '子对象的内存地址:', com.cpu, com.disk)
print(com2, '子对象的内存地址:', com2.cpu, com2.disk)
print('-' * 30)
# 类对象的深拷贝
com3 = copy.deepcopy(com) # com3是新产生的对象,com3的子对象cpu,disk也会重新创建
print(com, '子对象的内存地址:', com.cpu, com.disk)
print(com3, '子对象的内存地址:', com3.cpu, com3.disk)
画内存分析图:
章节习题
object类中特殊的方法 | 功能描述 |
---|---|
__new__() | 由系统调用,用于创建对象 |
__init__() | 创建对象时手动调用,用于初始化对象属性值 |
__str__() | 对象的描述,返回值是str类型,默认输出对象的内存地址 |
练习题
练习一
定义一个圆的类计算面积和周长。
需求:定义一个圆类——Circle,提供一个属性 r (半径),提供两个方法:计算圆的面积 get_area(self) 和计算圆的周长 get_perimeter(self),通过两个方法计算圆的周长和面积并且对计算结果进行输出,最后从键盘录入半径,创建圆类的对象,并调用计算面积和周长的方法输出面积和周长。
from math import pi
class Circle():
def __init__(self, r):
self.radius = r
# 计算面积的方法
def get_area(self):
# return pi * self.radius * self.radius
return pi * pow(self.radius, 2) # 幂运算
# 计算周长的方法
def get_perimeter(self):
return 2 * pi * self.radius
# 创建对象
r = eval(input('请输入圆的半径:'))
cir = Circle(r)
# 调用方法
area = cir.get_area() # 调用计算面积的方法
perimeter = cir.get_perimeter() # 调用计算周长的方法
print('圆的面积为:', area)
print('圆的周长为:', perimeter)
print('圆的面积为:{0:.2f};圆的周长为:{1:.2f}'.format(area, perimeter))
练习二
定义学生类录入5个学生信息存储到列表中。
需求:定义学生类Student,包含姓名,年龄,性别,分数四个属性,提供一个用于学员信息输出的方法 info(self)。编写测试代码,使用循环录入5位学生的信息,由于录入的学生信息中间使用 “#” 进行分隔,所以需要使用字符串的 split( ) 方法进行劈分(该方法会将劈分后的结果构成一个列表),使用劈分的信息创建学生对象,使用列表存储学生信息,最后使用循环遍历列表,调用对象的 info( ) 方法输出学员信息。
class Student(object):
def __init__(self, name, age, gender, score):
self.name = name
self.age = age
self.gender = gender
self.score = score
# 实例方法
def info(self):
print(self.name, self.age, self.gender, self.score)
print('请输入5位学生信息:(姓名#年龄#性别#成绩)')
lst = [] # 用于存储5个学生对象
for i in range(1, 6):
s = input(f'请输入第{i}位学生信息及成绩:')
s_lst = s.split('#') # 劈分后的结果返回一个列表。索引为0的是姓名,索引为1的是年龄,索引为2的是性别,索引为3的是成绩
# 创建学生对象
stu = Student(s_lst[0], s_lst[1], s_lst[2], s_lst[3])
# 将学生对象添加到列表中
lst.append(stu)
# 遍历列表,调用学生对象的info()方法
for item in lst: # item的数据类型是Student类型
item.info() # 对象名.方法名()
本题考察到了面向对象,字符串的分割方法。
练习三
使用面向对象思想(封装、继承、多态)实现乐器演奏。
需求:乐手可以弹奏不同的乐器从而发出不同的声音。可以弹奏的乐器包括二胡、钢琴和小提琴。定义乐器类 Instrument ,包括方法 make_sound( ) 。定义乐器类的子类:二胡 Erhu、钢琴 Piano 和小提琴 Violin ,定义一个函数可以弹奏各种乐器 play(instrument),测试给乐手不同的乐器让他弹奏。
class Instrument(): # 父类
def make_sound(self):
pass
class Erhu(Instrument):
def make_sound(self):
print('二胡在弹奏')
class Piano(Instrument):
def make_sound(self):
print('钢琴在弹奏')
class Violin(Instrument):
def make_sound(self):
print('小提琴在弹奏')
# 事实上在Python中多态并不关心类之间是否有继承关系,只需要有同名方法即可。
# 题目要求有继承关系,因此这里进行了继承
# 编写一个函数
def play(obj):
obj.make_sound()
# 创建对象
erhu = Erhu()
piano = Piano()
violin = Violin()
# 调用方法
play(erhu)
play(piano)
play(violin)
本题考察了方法重写和多态。
练习四
使用面向对象的思想(封装、继承、多态),设计自定义类,描述出租车和家用轿车的信息。
- 出租车类:
- 属性:车型、车牌、所属出租公司
- 方法:启动、停止
- 家用轿车类:
- 属性:车型、车牌、车主姓名
- 方法:启动、停止
需求:
(1)分析出租车和家用轿车的公共成员,提取出父类——汽车类
(2)利用继承机制,实现出租车类和家用轿车类
(3)最后,分别测试出租车类和家用轿车类对象的相关方法
class Car():
def __init__(self, type, no):
self.type = type
self.no = no
def start(self):
print('我是车,我能启动')
def stop(self):
print('我是车,我可以停止')
# 出租车
class Taxi(Car):
def __init__(self, type, no, company):
super().__init__(type, no)
self.company = company
# 重写父类的启动和停止的方法
def start(self):
print('乘客您好!')
print(f'我是{self.company}出租车公司的,我的车牌是:{self.no},您要去哪里?')
def stop(self):
print('目的地到了,请您扫码付款,欢迎下次乘坐')
# 家用轿车
class FamilyCar(Car):
def __init__(self, type, no, name):
super().__init__(type, no)
self.name = name
# 重写父类的启动和停止的方法
def start(self):
print(f'我是{self.name},我的汽车我做主')
def stop(self):
print('目的地到了,我们去玩吧!')
# 测试
taxi = Taxi('北汽', '京A888888', '长城')
taxi.start()
taxi.stop()
print('-' * 30)
family_car = FamilyCar('问界', '京A666666', 'lxl')
family_car.start()
family_car.stop()