第25天面向对象编程详解之继承
面向对象补充知识
面向对象概念
面向对象核心二字在与对象,对象就是特征和技能的结合体,基于该思想编程就好比在创建一个世界,世界上的任何事物都是对象,你就好比是这个世界的上帝,这是一种基于上帝式的思维方式。 优点:扩展性强 缺点:编程的复杂度要远远高于面向过程
问题一:既然面向对象这么好,我们之后的编程是不是都要用面向对象呢?
不是的,衡量一个软件的标准除了扩展性之外,其实还有很多的方面,如性能,可维护性,可移植性等等,但是面向对象编程设计之初就是为了解决扩展性的,所以在其他的一些软件质量的考核上面并没有想象中的那么好,因此,如果我们的软件从一写出来
很长时间都不会再去改动的话,用面向对象编程就不太合适了。
类的概念
对象是特征和技能的结合体,类就是一系列对象相同特征和技能的结合体。
现实生活中
先有对象,随着人类文明的发展总结出了类。
代码世界中
我们需要先定义类,然后才能通过类去创建对象。
类的创建过程: 创建一个老男孩选课系统
现实世界的分析:
步骤一:要从需求中分析出对象的特征和技能
分析对象的特征与技能是要根据特定的环境下进行分析的,因为我们分析的特征与技能是希望之后可以使用的,而不是说只是用来看看而已的。 例如:下面分析的一个对象特征也没错,但是就是没有结合特定的环境下分析的特征与技能,在选课系统中,对于特征,我们需要的是一个人的信息,便于之后查询,如名字,年龄,性别等等,但是对于外貌的特征我们是不需要的。对与技能,我们需要的是对象是如何选课的,而不是它是怎么吃饭和怎么喝水的。因此在分析需求的时候一定要根据特定的环境来分析对象的特征和技能。 老男孩选课系统分析 对象1: 特征 两个耳朵 一个眼睛 技能 吃 喝
老男孩选课系统分析 学生类 对象1: 特征 学校 school = 'oldboy' 姓名 name = '张铁蛋' 性别 gender = 'male' 年龄 age = 12 技能 选课 对象2: 特征 学校 school = 'oldboy' 姓名 name = '王铁锤' 性别 gender = 'female' 年龄 age = 10 技能 选课 教师类 对象3: 特征 学校 school = 'oldboy' 姓名 name = 'egon' 性别 gender = 'male' 年龄 age = 18 级别 level = 10 薪资 salary = 100000 技能 修改分数
步骤二:寻找相似的特征和技能
学生类: 相似的特征 学校 school = 'oldboy' 相似的技能 选课
代码世界的分析:
步骤三:根据相似的特征和技能定义类
当我们寻找出相似的特征与技能之后我们就可以根据现实世界中的类别来通过class关键字创建自己的类
# 根据我们现实世界中分析出来的伪代码来定义我们的类 #学生类: class oldboystudent: # 相似的特征 # 学校 school = 'oldboy' school = 'oldboy' # 相似的技能 # 选课 def choose_course(self): print('choose_course')
步骤四:根据类创建对象
# 创建一个学生对象 stu1 = oldboystudent()
虽然还有很多的细节没有实现,当对象创建完成之后就代表着我们已经成功的把现实中的内容迁移到了我们代码世界中。
类的用途
用途一: 类本质上就是一个命名空间,我们可以对该名称空间进行增删改查 用途二: 调用类产生对象,执行了两个步骤 1. 产生一个空对象obj 2. 触发类中__init__方法,oldboystudent.__init__(obj)
用途一:
名称空间就是名字和空间地址的一一映射关系,在python中我们可以使用自动触发函数__dict__去查看当前对象的命名空间中都有哪些名字,说到这里,可能你就会意识到,既然名称空间的存储是一个字典,那么对于这个字典的增删改查是不是就是对名称空间的增删改查呢,没错,就是这样的。
例如对于上面的例子,我们要查看类中的名称空间 #学生类: class oldboystudent: # 相似的特征 # 学校 school = 'oldboy' school = 'oldboy' # 相似的技能 # 选课 def choose_course(self): print('choose_course') # 创建一个学生对象 # 查看的两种方法 print(oldboystudent.__dict__['school']) print(oldboystudent.school) # 增加就是在__dict__中添加一对键值对 oldboystudent.name = 'egon' # oldboystudent.__dict__['name'] = 'egon'
用途二
对象的创建
在我们上面的例子中发现,类中只有一个属性school和一个方法choose_course,那么对于一个对象而言它的名字,年龄,和性别就需要我们每次创建完成之后重新去定义它的属性。
# 创建学生对象,并且添加属性
stu1 = oldboystudent()
stu1.name = 'egon'
stu1.age = 18
stu1.gender = 'male'
当我需要再创建一个对象的时候,还需要重新去定义这个属性,这太麻烦了,因此,python帮我们封装了一个函数__init__函数,我们可以在创建的时候直接通过传递属性参数进行赋值就可以了
class oldboystudent:
school = 'oldboy'
def __init__(self, name, age, gender):
self.name=name
self.age=age
self.gender=gender
def choose_course(self):
print('choose_course')
# 创建学生对象,并且添加属性
stu1 = oldboystudent('egon', 11, 'male')
理解:面向对象是更高程度的一种封装
问题一:当我们有很多此调用这个函数的时候,我们都需要去传递这样的一组数据,很麻烦
def exec1(address, port, db, charset, sql): print(address, port, db, charset, sql) # 当我们有很多此调用这个函数的时候,我们都需要去传递这样的一组数据,很麻烦 exec1('127.0.0.1', 3306, 'db1', 'utf-8', 'select * from db1') exec1('127.0.0.1', 3306, 'db1', 'utf-8', 'select * from db1') exec1('127.0.0.1', 3306, 'db1', 'utf-8', 'select * from db1') exec1('127.0.0.1', 3306, 'db1', 'utf-8', 'select * from db1')
解决方法一:将函数exec1的参数设置成默认参数,这样的话虽然解决了上面存在的问题,但是如果一旦出现另一组数据的话一样是这样的情况,并没有太大的进步。
def exec1(address='127.0.0.1', port=3306, db='db1', charset='utf-8', sql='select * from db1'): print(address, port, db, charset, sql) # 虽然说对于第一种的调用我们简化了很多,但是一旦出现另一组数据和之前一样比较麻烦 exec1() exec1() exec1() exec1() exec1('192.168.0.1', 3307, 'db2', 'utf-8', 'select * from db1') exec1('192.168.0.1', 3307, 'db2', 'utf-8', 'select * from db1') exec1('192.168.0.1', 3307, 'db2', 'utf-8', 'select * from db1') exec1('192.168.0.1', 3307, 'db2', 'utf-8', 'select * from db1')
解决方法二:将数据定义成变量,通过变量进行传递参数,虽然说稍微简单了一点点,但是当出现两组或者几组数据的时候将会变的非常混乱,不仅如此,数据的耦合性非常强。
def exec1(address, port, db, charset, sql): print(address, port, db, charset, sql) host='127.0.0.1' port=3306 db='db1' charset='utf-8' sql='select * from db1' # 这样比之前直接传入数据会稍微简单一点,但是对于定义的一些变量,我们并不需要它可以被其他的程序所使用 # 因此,我们需要把变量和函数绑定起来 exec1(host, port, db, charset, sql) exec1(host, port, db, charset, sql) exec1(host, port, db, charset, sql) exec1(host, port, db, charset, sql)
解决方法三:通过函数将数据和方法进行绑定,这也是一种面向对象编程的一种思想,但是我们一般并不会这样去写。
# 通过函数的方式把变量和函数绑定到一块,其他的函数自然就使用不到此函数内的变量和方法 # 并且通过字典的形式将之前的变量包括到一块,可以简化我们函数传递的参数 def func(): obj_dict = { 'host': '127.0.0.1', 'port': 3306, 'db': 'db1', 'charset': 'utf-8', 'sql': 'select * from db1', } def exec1(obj_dict): print(obj_dict['host'], obj_dict['port'], obj_dict['db'], obj_dict['charset'], obj_dict['sql']) exec1(obj_dict) func()
解决方法四:通过类的形式来将变量和方法进行绑定
# 并且通过字典的形式将之前的变量包括到一块,可以简化我们函数传递的参数 class mysql: def __init__(self, host, port, db, charset, sql): self.host=host self.port=port self.db=db self.charset=charset self.sql=sql def exec1(self): print(self.host, self.port, self.db, self.charset, self.sql) mysql_obj = mysql('127.0.0.1', 3306, 'db1', 'utf-8', 'mysql') mysql_obj.exec1() mysql_obj.exec1() mysql_obj.exec1()
# python3中统一了类与类型的概念 l = list([1, 2, 3]) l.append(4) list.append(l, 5) print(l) # 结果 # [1, 2, 3, 4, 5]
总结:
面向对象是一种更高程度的封装 在之前没有面向对象的时候,我们会发现如果需要传递参数的时候,我们无非有两种方式,一种是传递数据,一种就是传递功能,但是没有说我可以通过传递一个变量,这个变量既有数据又有功能的。也就是说一旦我们需要的参数较多,而且调用的次数较为频繁的时候,我们难免就会产生大量的重复操作,因为我们没有一个变量可以将其进行封装。 而对象呢就是高度封装了一系列的方法和属性的变量,我们可以通过传递一个对象,就可以获得它所有的方法和属性,简化了我们传递参数时的操作。 面向对象的精髓虽在: # 掌握了一种方法,能够把专门的数据和专门的方法整合到一块, # 当我们拿到一个对象的时候不仅仅能够拿到对应的数据,也能拿到相应的配套方法。
继承
人生三问
什么是继承
继承是一种遗传关系,子类可以重用父类中的属性。
在程序中继承是一种新建子类的方式,新创建的类称为子类或者派生类,被继承的类称为父类\基类\超类。
为什么要用继承 减少类与类之间的代码冗余的问题
怎么使用继承
先抽象再继承
只有在python2中才会分新式类和经典类,python3都是新式类
新式类:但凡继承了object类的子类,以及该子类的子子类,...都称为新式类。
经典类:但凡没有继承object类的子类,以及该子类的子子类,....都称为经典类。
继承概览:
# 在python3中默认是继承object类的 # 在python2中默认是没有继承的,如果想要继承要把object传进去 class parent1: pass class parent2: pass class sub1(parent1): pass class sub2(parent1, parent2): pass print(sub1.__bases__) # __bases__显示的是当前子类继承的父类 print(sub2.__bases__) print(parent2.__bases__) # 在python3中默认是继承object类的 print(parent1.__bases__) # 结果: # (<class '__main__.parent1'>,) # (<class '__main__.parent1'>, <class '__main__.parent2'>) # (<class 'object'>,) # (<class 'object'>,)
属性的查找顺序
情况一:单继承问题查找顺序
情况二:非菱形多继承问题
情况三:菱形多继承问题
python2中是深度优先查找(经典类)
python3中是广度优先查找(新式类)
总结:只有在python2中的菱形问题才会出现深度查找。
案例:继承是如何解决代码冗余问题的
首先创建了一个学生类和一个教师类
# 根据之前写的oldboy选课系统来说 class oldboystudent: school = 'oldboy' def __init__(self, name, age, gender): self.name=name self.age=age self.gender=gender def choose_course(self): print('choose_course') class oldboyteacher: school = 'oldboy' def __init__(self, name, age, gender, level, salary): self.name=name self.age=age self.gender=gender self.level=level self.salary=salary def change_score(self, stu, score): print('change score')
问题一:我们发现这两个类中有些重复的代码,如他们的共有属性school = 'oldboy',为了简化代码,我们需要抽象一个父类,将共有属性放进去,然后通过继承让两个类可以获得相应的属性值。
# 创建一个类然后让学生类和教师类继承
class oldboyperson:
school = 'oldboy'
# 根据之前写的oldboy选课系统来说
class oldboystudent(oldboyperson):
# school = 'oldboy' 因为父类中有属性,所以这里就不需要了
def __init__(self, name, age, gender):
self.name=name
self.age=age
self.gender=gender
def choose_course(self):
print('choose_course')
class oldboyteacher(oldboyperson):
# school = 'oldboy'
def __init__(self, name, age, gender, level, salary):
self.name=name
self.age=age
self.gender=gender
self.level=level
self.salary=salary
def change_score(self, stu, score):
print('change score')
问题二:这样确实是减少了子类中的公共属性,但是我们发现在__init__方法中也有一部分是重复的,对于这样的重复的选项我们应该怎么去减少呢
步骤一:先将学生类和教师类中相同的参数的name, age, gender提取出来放在父类中的__init方法中 步骤二: 通过不同的方法让子类中的init方法中接受到传递过来的额外的参数
方法一:指名道姓的通过类去找到相应的方法实现
class oldboyperson: school = 'oldboy' def __init__(self, name, age, gender): self.name=name self.age=age self.gender=gender # 根据之前写的oldboy选课系统来说 class oldboystudent(oldboyperson): def choose_course(self): print('choose_course') class oldboyteacher(oldboyperson): def __init__(self, name, age, gender, level, salary): # 此处就是指名道姓的要调用父类中的__init__方法 # 此时父类中的__init__方法就是一个普通的方法,我们需要把四个参数全部传进去 # 这样就达到了继承的效果了 oldboyperson.__init__(self, name, age, gender) self.level=level self.salary=salary def change_score(self, stu, score): print('change score')
方法二:严格依照继承的方法去继承用到函数super
class oldboyperson: school = 'oldboy' def __init__(self, name, age, gender): self.name=name self.age=age self.gender=gender # 根据之前写的oldboy选课系统来说 class oldboystudent(oldboyperson): def choose_course(self): print('choose_course') class oldboyteacher(oldboyperson): def __init__(self, name, age, gender, level, salary): # 参数一是当前类,参数二当前对象 super(oldboyteacher, self).__init__(name, age, gender) self.level=level self.salary=salary def change_score(self, stu, score): print('change score')
难点:
# super(oldboyteacher, self)在python3中不需要传递参数,它会创建一个特殊的对象 # 该对象是强调: super()函数会严格按照类的mro列表的顺序依次查找属性
例题:
#a没有继承b, class a: def test(self): print('a.test') # 首先打印 # 当执行到super函数的时候,会安好mro列表的顺序去查找 # 当前mro列表已经执行到a,所以下一个查找地方是b因此会执行b类的test方法 super().test() class b: def test(self): print('from b') # 所以打印了 class c(a,b): pass c=c() # 首先创建对象 # 1. 对象中没有此方法 # 2. 去c类中查找,没有找到 # 3. 去父类a中查找,有test,开始执行test函数 c.test() print(c.mro())
上一篇: Django之form总结
下一篇: 机器学习mlxtend_01