11. 进阶面向对象 - 3 11.1 super 方法 引入 上一节课程我们知道,如果父类中的方法在派生的子类中不能满足其需求的话,可以在子类中通过重写解决这个问题
但是很多情况下,父类中的方法并不是全部一点都不能用,即子类的需求往往是在父类方法实现的功能基础上提出了更多的需求而已,此时如果我们在子类中重写此方法时就会发现出现了很多冗余的代码,这个问题该怎么解决呢?
答:在子类重写的方法中通过调用父类中被重写的方法
代码示例 示例一:
1 2 3 4 5 6 7 8 9 10 11 12 13 class Father (object ): def play_game (self ): print ("父类中的play_game" ) class Son (Father ): def play_game (self ): super ().play_game() print ("子类中的play_game" ) son = Son() son.play_game()
运行效果:
1 2 父类中的play_game 子类中的play_game
示例二:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Father (object ): def __init__ (self, name, age ): self .name = name self .age = age def __str__ (self ): return "%s的年龄是: %d" % (self .name, self .age) class Son (Father ): def __init__ (self, name, age, collage ): super ().__init__(name, age) self .collage = collage def __str__ (self ): return "%s的年龄是: %d,他的学历是: %s" % (self .name, self .age, self .collage) father = Father("父亲" , 50 ) print (father)son = Son("儿子" , 18 , "大学" ) print (son)
运行结果:
1 2 父亲的年龄是: 50 儿子的年龄是: 18,他的学历是: 大学
示例三:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Father (object ): def __init__ (self, name, age ): self .name = name self .age = age def __str__ (self ): return "%s的年龄是:%d" % (self .name, self .age) class Son (Father ): def __init__ (self, name, age, collage ): super ().__init__(name, age) self .collage = collage def __str__ (self ): return "%s的年龄是:%d,他的学历是:%s" % (self .name, self .age, self .collage) class GrandChild (Son ): def __init__ (self, *args, **kwargs ): super ().__init__(*args, **kwargs) print ("----这里模拟其要做的额外事情....----" ) father = Father("父亲" , 50 ) print (father)son = Son("儿子" , 18 , "大学" ) print (son)grandchild = GrandChild("孙子" , 1 , "未上学" ) print (grandchild)
运行结果:
1 2 3 4 父亲的年龄是:50 儿子的年龄是:18,他的学历是:大学 ----这里模拟其要做的额外事情....---- 孙子的年龄是:1,他的学历是:未上学
简单总结
如果想要在子类方法中调用被重写的父类方法就可以使用super().父类方法名()
11.2 多态 面向对象的特征说明 面向对象编程有3个特征:
封装与继承之前我们已经研究过了,本节课研究多态
多态的概念 多态从字面意思来看,就是多种形态的意思。
在python中它的实际功能是:
如果一个变量存储了某一个实例对象的引用,且通过这个变量调用指向的对象中的某个方法,此时如果变量指向的对象是子类创建的那么就调用子类中的方法,如果是父类创建的对象那么就调用父类的方法
代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Dog (object ): def bark (self ): print ("狗汪汪叫..." ) class LangDog (Dog ): def bark (self ): print ("狼狗震耳欲聋的叫..." ) class ZangAo (Dog ): pass class Person (object ): def pk_dog (self, dog ): print ("人用力的向狗进行了攻击..." ) dog.bark() xiaoming = Person() dog1 = Dog() dog2 = LangDog() dog3 = ZangAo() xiaoming.pk_dog(dog1) xiaoming.pk_dog(dog2) xiaoming.pk_dog(dog3)
运行效果:
1 2 3 4 5 6 人用力的向狗进行了攻击... 狗汪汪叫... 人用力的向狗进行了攻击... 狼狗震耳欲聋的叫... 人用力的向狗进行了攻击... 狗汪汪叫...
简单总结 想要实现多态,需要的条件如下:
有继承
有重写
11.3 静态方法 引入 默认情况下,python类中定义的方法是实例方法,即这个方法有一个默认的形参self,这个self会在方法被调用的时候指向对象
但是有些时候,我们并不需要对象的引用,即self没用,那该怎么处理呢?能不写self吗?
答:可以
静态方法的概念 如果一个方法不写self即不需要实例对象的引用,此时在定义方法的时候可以用@staticmethod对函数进行修饰,被修饰的函数就可以不写self
一句话:被@staticmethod修饰的方法,就是静态方法
代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 class Calculator (object ): """计算器类""" def __init__ (self ): self .num1 = 0 self .num2 = 0 @staticmethod def show_menu (): """因为打印菜单功能方法并不需要self指向的对象,所以就考虑使用静态方法""" print (" 计算机" ) print ("1. 加法" ) print ("2. 减法" ) print ("3. 乘法" ) print ("4. 除法" ) print ("5. 退出" ) def get_nums (self ): self .num1 = int (input ("请输入第1个数:" )) self .num2 = int (input ("请输入第2个数:" )) def add (self ): print (self .num1 + self .num2) def min (self ): print (self .num1 - self .num2) def mul (self ): print (self .num1 * self .num2) def div (self ): print (self .num1 / self .num2) def run (self ): while True : self .show_menu() op = input ("请输入要进行的操作:" ) if op == "1" : self .get_nums() self .add() elif op == "2" : self .get_nums() self .min () elif op == "3" : self .get_nums() self .mul() elif op == "4" : self .get_nums() self .div() elif op == "5" : break cal = Calculator() cal.run()
运行效果:
1 2 3 4 5 6 7 计算机 1. 加法 2. 减法 3. 乘法 4. 除法 5. 退出 请输入要进行的操作:
简单总结 如果不需要用到对象,那么就可以将方法用@staticmethod进行修饰,如此一来此方法就变成了静态方法。
11.4 类属性 引入 默认情况下 ,当通过同一个类创建了多个实例对象之后,每个实例对象之间是相互隔离的
但是有时候有些数据需要在多个对象之间共享,此时该怎么办呢?
答:类属性
类属性的概念 想要在多个对象之间共享数据,即一些属性需要在多个对象之间共享,这样的属性就是类属性
那怎样定义类属性呢?格式如下:
即在class内且在def之外定义的变量,就叫做类属性
代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Tool (object ): tools_num = 0 def __init__ (self, name ): self .name = name Tool.tools_num += 1 def print_info (self ): print ("工具的总数为:" , Tool.tools_num) def print_info2 (): print ("工具的总数为:" , Tool.tools_num) tieqiao = Tool("铁锹" ) chutou = Tool("锄头" ) dianciluo = Tool("电磁炉" ) print ("工具的总数为:" , Tool.tools_num) tieqiao.print_info() tieqiao.print_info2() Tool.print_info2() Tool.print_info()
11.5 类方法 引入 为了更好的对类属性进行操作,Python中提供了另外一种方法类方法
类方法的概念 之前在学习静态方法的时候我们知道可以在方法的名字前面添加@staticmethod此时这个方法就是静态方法,
与这种添加@的方式很类似,如果想要让一个方法成为类方法我们只需要在这个方法的前面添加@classmethod即可,与此同时需要在方法的第1个形参位置添加cls
1 2 3 4 class 类名 : @classmethod def 类方法名 (cls ): pass
示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Tool (object ): tools_num = 0 def __init__ (self, name ): self .name = name Tool.tools_num += 1 def print_info (self ): print ("工具的总数为:" , Tool.tools_num) @classmethod def print_info2 (cls ): print ("工具的总数为:" , cls.tools_num) tieqiao = Tool("铁锹" ) chutou = Tool("锄头" ) dianciluo = Tool("电磁炉" ) tieqiao.print_info() tieqiao.print_info2() Tool.print_info() Tool.print_info2()
运行效果:
1 2 3 工具的总数为: 3 工具的总数为: 3 工具的总数为: 3
简单总结
定义类方法时,前面添加@classmethod
类方法的第1个形参,一般都叫做cls(当然了叫什么名字可以任意,但一般都用cls)
调用类方法的时候,可以用实例对象、类对象调用,但无论用哪种方式调用,类方法中的cls指向类对象
11.6 类对象 引入 之前在学习类属性的时候,我们提到过:类属性是可以在多个实例对象之间共享的属性
那么问题来了,类属性到底存在哪里呢?
答:类对象
类对象的概念 之前我们说到通过class定义的就是类(就是一个要创建的商品的模板),通过类名()创建出来的叫做实例对象
其实,定义的类(即用class定义的类)实际上也是一个对象(试想即使我们把类称之为模板,模板也不是空的啊,也是需要占用内存的对吗)
定义的类其实就是一个对象,为了能够将这个对象与其创建出来的实例对象进行区分,将这个class定义的类叫做类对象
类对象的作用 我们知道实例对象是类 (即类对象)创建出来的,所以类对象对于实例对象而言是共享的,既然是共享的那么就干脆将实例对象都有的而且不变化的内容存储到 类对象 即可,这样会减少内容的占用
那,哪些东西在类对象中存储呢?
对你没有看错,除了熟知的类属性之外,类对象中存储了class定义的所有的方法(无论是魔法方法、实例方法、静态方法 、类方法都在类对象中存储),因为方法(即函数)的代码是不变的,变化的仅仅是数据而已。
实例对象怎么用类对象 每个实例对象中都会有1个额外默认的属性__class__,这个属性指向了创建当前对象的模板即类对象,所以当调用实例对象.xxx()时,实际上实例对象.__class__.xxx()
dir() 方法既然我们知道了实例对象中有默认的__class__,那除了它之外还有哪些呢?怎么查看呢?
例如:
1 ['__class__' , '__delattr__' , '__dict__' , '__dir__' , '__doc__' , '__eq__' , '__format__' , '__ge__' , '__getattribute__' , '__gt__' , '__hash__' , '__init__' , '__init_subclass__' , '__le__' , '__lt__' , '__module__' , '__ne__' , '__new__' , '__reduce__' , '__reduce_ex__' , '__repr__' , '__setattr__' , '__sizeof__' , '__str__' , '__subclasshook__' , '__weakref__' ]
这么多的__开始的属性,用到什么我们就研究 什么,不用现在立刻研究。
11.7 多继承以及MRO顺序 多继承中调用父类方式不同结果不同
单独调用父类的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 print ("******多继承使用类名.__init__发生的状态******" )class Parent (object ): def __init__ (self, name ): print ('parent的init开始被调用' ) self .name = name print ('parent的init结束被调用' ) class Son1 (Parent ): def __init__ (self, name, age ): print ('Son1的init开始被调用' ) self .age = age Parent.__init__(self , name) print ('Son1的init结束被调用' ) class Son2 (Parent ): def __init__ (self, name, gender ): print ('Son2的init开始被调用' ) self .gender = gender Parent.__init__(self , name) print ('Son2的init结束被调用' ) class Grandson (Son1, Son2): def __init__ (self, name, age, gender ): print ('Grandson的init开始被调用' ) Son1.__init__(self , name, age) Son2.__init__(self , name, gender) print ('Grandson的init结束被调用' ) gs = Grandson('grandson' , 12 , '男' ) print ('姓名:' , gs.name)print ('年龄:' , gs.age)print ('性别:' , gs.gender)print ("******多继承使用类名.__init__发生的状态******\n\n" )
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ******多继承使用类名.__init__发生的状态****** Grandson的init开始被调用 Son1的init开始被调用 parent的init开始被调用 parent的init结束被调用 Son1的init结束被调用 Son2的init开始被调用 parent的init开始被调用 parent的init结束被调用 Son2的init结束被调用 Grandson的init结束被调用 姓名: grandson 年龄: 12 性别: 男 ******多继承使用类名.__init__发生的状态******
多继承中super调用被重写的父类方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 print ("******多继承使用super().__init__发生的状态******" )class Parent (object ): def __init__ (self, name, *args, **kwargs ): print ('parent的init开始被调用' ) self .name = name print ('parent的init结束被调用' ) class Son1 (Parent ): def __init__ (self, name, age, *args, **kwargs ): print ('Son1的init开始被调用' ) self .age = age super ().__init__(name, *args, **kwargs) print ('Son1的init结束被调用' ) class Son2 (Parent ): def __init__ (self, name, gender, *args, **kwargs ): print ('Son2的init开始被调用' ) self .gender = gender super ().__init__(name, *args, **kwargs) print ('Son2的init结束被调用' ) class Grandson (Son1, Son2): def __init__ (self, name, age, gender ): print ('Grandson的init开始被调用' ) super ().__init__(name, age, gender) print ('Grandson的init结束被调用' ) print (Grandson.__mro__)gs = Grandson('grandson' , 12 , '男' ) print ('姓名:' , gs.name)print ('年龄:' , gs.age)print ('性别:' , gs.gender)print ("******多继承使用super().__init__发生的状态******\n\n" )
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ******多继承使用super().__init__发生的状态****** (<class '__main__.Grandson'>, <class '__main__.Son1'>, <class '__main__.Son2'>, <class '__main__.Parent'>, <class 'object'>) Grandson的init开始被调用 Son1的init开始被调用 Son2的init开始被调用 parent的init开始被调用 parent的init结束被调用 Son2的init结束被调用 Son1的init结束被调用 Grandson的init结束被调用 姓名: grandson 年龄: 12 性别: 男 ******多继承使用super().__init__发生的状态******
上述两种调用父类的方法是有区别的
如果2个子类中都继承了父类,当在子类中通过父类名调用时,parent被执行了2次
如果2个子类中都继承了父类,当在子类中通过super调用时,parent被执行了1次
单继承中的super 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 print ("******单继承使用super().__init__发生的状态******" )class Parent (object ): def __init__ (self, name ): print ('parent的init开始被调用' ) self .name = name print ('parent的init结束被调用' ) class Son (Parent ): def __init__ (self, name, age ): print ('Son1的init开始被调用' ) self .age = age super ().__init__(name) print ('Son1的init结束被调用' ) class Grandson (Son ): def __init__ (self, name, age, gender ): print ('Grandson的init开始被调用' ) self .gender = gender super ().__init__(name, age) print ('Grandson的init结束被调用' ) gs = Grandson('grandson' , 12 , '男' ) print ('姓名:' , gs.name)print ('年龄:' , gs.age)print ('性别:' , gs.gender)print ("******单继承使用super().__init__发生的状态******\n\n" )
运行结果:
1 2 3 4 5 6 7 8 9 10 11 ******单继承使用super().__init__发生的状态****** Grandson的init开始被调用 Son1的init开始被调用 parent的init开始被调用 parent的init结束被调用 Son1的init结束被调用 Grandson的init结束被调用 姓名: grandson 年龄: 12 性别: 男 ******单继承使用super().__init__发生的状态******
简单总结
super().__init__相对于类名.__init__,在单继承上用法基本没有区别
但在多继承上有区别,super方法能保证每个父类的方法只会执行一次,而使用类名的方法会导致方法被执行多次,具体看前面的输出结果
多继承时,使用super方法,对父类的传参,由于super的算法导致的原因,必须把参数全部传递,否则会报错
单继承时,使用super方法,则不能全部传递,只能传父类方法所需的参数,否则会报错
多继承时,相对于使用类名.__init__方法,要把每个父类全部写一遍,而使用super方法,只需写一句话便执行了全部父类的方法,这也是为何多继承需要全部传参的一个原因
面试题 以下代码将会输出什么?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Parent (object ): x = 1 class Child1 (Parent ): pass class Child2 (Parent ): pass print (Parent.x, Child1.x, Child2.x)Child1.x = 2 print (Parent.x, Child1.x, Child2.x)Parent.x = 3 print (Parent.x, Child1.x, Child2.x)
输出结果:
使你困惑或是惊奇的是关于最后一行的输出是 3 2 3 而不是 3 2 1。为什么改变了 Parent.x 的值还会改变 Child2.x 的值,但是同时 Child1.x 值却没有改变?
答案的关键是,在 Python 中,类变量在内部是作为字典处理的。如果一个变量的名字没有在当前类的字典中发现,将搜索祖先类(比如父类)直到被引用的变量名被找到(如果这个被引用的变量名既没有在自己所在的类又没有在祖先类中找到,会引发一个 AttributeError 异常 )。
因此,在父类中设置 x = 1 会使得类变量 x 在引用该类和其任何子类中的值为 1。这就是因为第一个 print 语句的输出是 1 1 1。
随后,如果任何它的子类重写了该值(例如,我们执行语句 Child1.x = 2),然后,该值仅仅在子类中被改变。这就是为什么第二个 print 语句的输出是 1 2 1。
最后,如果该值在父类中被改变(例如,我们执行语句 Parent.x = 3),这个改变会影响到任何未重写该值的子类当中的值(在这个示例中被影响的子类是 Child2)。这就是为什么第三个 print 输出是 3 2 3。
扩展题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Parent (object ): x = 1 class Child1 (Parent ): Parent.x += 1 class Child2 (Parent ): Parent.x += 2 print (Parent.x, Child1.x, Child2.x)Child1.x = 2 print (Parent.x, Child1.x, Child2.x)Parent.x = 3 print (Parent.x, Child1.x, Child2.x)
输出结果为何?