《零基础入门学习Python》第038讲:类和对象:继承

本文介绍了Python中的类继承机制,包括单继承、方法覆盖和多重继承。通过示例代码解释了如何创建子类并重写父类的方法,以及如何使用super函数来调用父类的方法。文章还讨论了错误处理,如当子类未正确初始化导致的属性缺失问题,并展示了如何通过调用父类的__init__方法来解决。最后,提到了多重继承可能导致的钻石继承问题及其解决方案。
摘要由CSDN通过智能技术生成

0. 请写下这一节课你学习到的内容:格式不限,回忆并复述是加强记忆的好方式!

上节课的课后习题我们试图模拟一个场景,里边有一只乌龟和十条鱼,乌龟通过吃鱼来补充体力,当乌龟体力消耗殆尽或者鱼吃光时游戏结束。

现在我们想扩展游戏,给鱼类进行细分,有金鱼(Goldfish),鲤鱼(Carp),三文鱼(Salmon)和鲨鱼(Shark)。那么我们就在思考一个问题:能不能不要每次都重头到尾重新定义一个新的鱼类呢?因为我们知道大部分鱼的属性和方法是相似的。如果说有一种机制可以让这些相似的东西得以自动传递,我们不用每次都自己动手写,那就方便快捷的多了。这种机制就是我们今天要讲解的继承。

语法:class 类名(父类名):

被继承类被称为基类、父类或超类,继承者被称为子类,一个子类可以继承它的父类的任何属性和方法。举例说明:

 
  1. >>> class Parent:

  2. def hello(self):

  3. print("正在调用父类的方法")

  4. >>> class Child(Parent):

  5. pass

  6. >>> p = Parent()

  7. >>> p.hello()

  8. 正在调用父类的方法

  9. >>> c = Child()

  10. >>> c.hello()

  11. 正在调用父类的方法

需要注意的是:如果子类中定义与父类同名的方法或属性,则会自动覆盖父类对应的方法和属性。

 
  1. >>> class Parent:

  2. def hello(self):

  3. print("正在调用父类的方法")

  4. >>> class Child(Parent):

  5. def hello(self):

  6. print("正在调用子类的方法")

  7. >>> c = Child()

  8. >>> c.hello()

  9. 正在调用子类的方法

  10. >>> p = Parent()

  11. >>> p.hello()

  12. 正在调用父类的方法

但是,这里覆盖的是子类实例化对象里面的方法而已,对父类的方法没有影响。

现在大家一起动手写一下刚才提到的鱼类扩展的例子。

这是一个有错误的程序代码:

 
  1. import random as r

  2. class Fish:

  3. def __init__(self):

  4. self.x = r.randint(0, 10)

  5. self.y = r.randint(0, 10)

  6. def move(self):

  7. self.x -= 1

  8. print("我的位置是:", self.x, self.y)

  9. class Goldfish(Fish):

  10. pass

  11. class Shark(Fish):

  12. def __init__(self):

  13. self.hungry = True

  14. def eat(self):

  15. if self.hungry:

  16. print("吃货的梦想就是天天有吃的^_^")

  17. self.hungry = False

  18. else:

  19. print("太撑了,吃不下了")

但是上面的程序是存在错误的。如下:

 
  1. >>> fish = Fish()

  2. >>> fish.move()

  3. 我的位置是: 2 9

  4. >>> goldfish = Goldfish()

  5. >>> goldfish.move()

  6. 我的位置是: -1 7

  7. >>> shark = Shark()

  8. >>> shark.eat()

  9. 吃货的梦想就是天天有吃的^_^

  10. >>> shark.move()

  11. Traceback (most recent call last):

  12. File "<pyshell#55>", line 1, in <module>

  13. shark.move()

  14. File "C:/Users/XiangyangDai/Desktop/上课代码/38-1.py", line 7, in move

  15. self.x -= 1

  16. AttributeError: 'Shark' object has no attribute 'x'

我们在调用shark.move()方法的时候会报错,这是为什么呢?错误信息告诉我们,Shark 对象没有 x 的属性,Goldfish 和 Shark 都是继承Fish,Fish是有x的属性的,但是Shark重写了 __init__(self)方法,子类重写了父类的方法,就会把父类的方法给覆盖。要解决这个问题的话,我们应该在Shark 的类里面重写 __init__(self)方法的时候先调用父类的__init__(self),实现这样子的继承总共有两种技术。

  • 调用未绑定的父类方法(该方法不重要)
 
  1. import random as r

  2. class Fish:

  3. def __init__(self):

  4. self.x = r.randint(0, 10)

  5. self.y = r.randint(0, 10)

  6. def move(self):

  7. self.x -= 1

  8. print("我的位置是:", self.x, self.y)

  9. class Goldfish(Fish):

  10. pass

  11. class Shark(Fish):

  12. def __init__(self):

  13. #调用未绑定的父类方法

  14. Fish.__init__(self)

  15. self.hungry = True

  16. def eat(self):

  17. if self.hungry:

  18. print("吃货的梦想就是天天有吃的^_^")

  19. self.hungry = False

  20. else:

  21. print("太撑了,吃不下了")

 
  1. >>> shark = Shark()

  2. >>> shark.move()

  3. 我的位置是: 6 0

这样就不会报错了,需要注意的是, Fish.__init__(self) 中的 self 是调用它的父类的方法,但是这个 self 是子类的实例对象。

就相当于:Fish.__init__(shark)。实际上,在上面出错的程序代码运行之后,我们输入下面的语句可是可以的:这里就相当于重新进行了一次初始化。

 
  1. >>> shark = Shark()

  2. >>> Fish.__init__(shark)

  3. >>> shark.move()

  4. 我的位置是: 6 1

  • 使用 super 函数(完美方法)

使用super 函数能够帮我们自动找到父类的方法,而且还会为我们传入 self 参数,super 函数可以完美的替换上述的方法。

 
  1. import random as r

  2. class Fish:

  3. def __init__(self):

  4. self.x = r.randint(0, 10)

  5. self.y = r.randint(0, 10)

  6. def move(self):

  7. self.x -= 1

  8. print("我的位置是:", self.x, self.y)

  9. class Goldfish(Fish):

  10. pass

  11. class Shark(Fish):

  12. def __init__(self):

  13. #使用super函数

  14. super().__init__()

  15. self.hungry = True

  16. def eat(self):

  17. if self.hungry:

  18. print("吃货的梦想就是天天有吃的^_^")

  19. self.hungry = False

  20. else:

  21. print("太撑了,吃不下了")

 
  1. >>> shark = Shark()

  2. >>> shark.move()

  3. 我的位置是: 6 2

super().__init__(),super 函数的超级之处就在于你不用给定任何父类的名字,如果继承有多重继承或者父类的名字太过复杂的时候,也不用给出父类的名字,就可以自动帮你一层一层的找出它所有父类里面对应的方法,由于你不需要给出父类的名字,也就意味着如果你要改变类的继承关系,你只需要修改 class Shark(Fish): 里面的父类的名字即可。

多重继承:就是同时继承多个父类的属性和方法。

语法:class 类名(父类1名,父类2名........):

 
  1. >>> class Base1:

  2. def foo1(self):

  3. print("我是foo1,我为Base1代言...")

  4. >>> class Base2:

  5. def foo2(self):

  6. print("我是foo2,我为Base2代言...")

  7. >>> class C(Base1, Base2):

  8. pass

  9. >>> c = C()

  10. >>> c.foo1()

  11. 我是foo1,我为Base1代言...

  12. >>> c.foo2()

  13. 我是foo2,我为Base2代言...

多重继承可以同时继承多个父类的属性和方法,但是多重继承很容易导致代码混乱,所以当你不确定你真的必须要使用多重继承的时候,请尽量避免使用


测试题

0. 继承机制给程序猿带来最明显的好处是?

答:可以偷懒,据说这是每一个优秀程序猿的梦想!

如果一个类 A 继承自另一个类 B,就把这个 A 称为 B 的子类,把 B 称为 A 的父类、基类或超类。继承可以使得子类具有父类的各种属性和方法,而不需要再次编写相同的代码(偷懒)。

在子类继承父类的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类的原有属性和方法,使其获得与父类不同的功能。另外,为子类追加新的属性和方法也是常见的做法。

1. 如果按以下方式重写魔法方法 __init__,结果会怎样?

 
  1. class MyClass:

  2. def __init__(self):

  3. return "I love FishC.com!"

答:会报错,因为 __init__ 特殊方法不应当返回除了 None 以外的任何对象。

 
  1. >>> myClass = MyClass()

  2. Traceback (most recent call last):

  3. File "<pyshell#13>", line 1, in <module>

  4. myClass = MyClass()

  5. TypeError: __init__() should return None, not 'str'

2. 当子类定义了与相同名字的属性或方法时,Python 是否会自动删除父类的相关属性或方法?

答:不会删除!Python 的做法跟其他大部分面向对象编程语言一样,都是将父类属性或方法覆盖,子类对象调用的时候会调用到覆盖后的新属性或方法,但父类的仍然还在,只是子类对象“看不到”。

3. 假设已经有鸟类的定义,现在我要定义企鹅类继承于鸟类,但我们都知道企鹅是不会飞的,我们应该如何屏蔽父类(鸟类)中飞的方法?

答:覆盖父类方法,例如将函数体内容写 pass,这样调用 fly 方法就没有任何反应了。

 
  1. class Bird:

  2. def fly(self):

  3. print("Fly away!")

  4. class Penguin(Bird):

  5. def fly(self):

  6. pass

  7. >>> bird = Bird()

  8. >>> penguin = Penguin()

  9. >>> bird.fly()

  10. Fly away!

  11. >>> penguin.fly()

4. super 函数有什么“超级”的地方?

答:super 函数超级之处在于你不需要明确给出任何基类的名字,它会自动帮您找出所有基类以及对应的方法。由于你不用给出基类的名字,这就意味着你如果需要改变了类继承关系,你只要改变 class 语句里的父类即可,而不必在大量代码中去修改所有被继承的方法。

5. 多重继承使用不当会导致重复调用(也叫钻石继承、菱形继承)的问题,请分析以下代码在实际编程中有可能导致什么问题?

 
  1. class A():

  2. def __init__(self):

  3. print("进入A…")

  4. print("离开A…")

  5. class B(A):

  6. def __init__(self):

  7. print("进入B…")

  8. A.__init__(self)

  9. print("离开B…")

  10. class C(A):

  11. def __init__(self):

  12. print("进入C…")

  13. A.__init__(self)

  14. print("离开C…")

  15. class D(B, C):

  16. def __init__(self):

  17. print("进入D…")

  18. B.__init__(self)

  19. C.__init__(self)

  20. print("离开D…")

答:多重继承容易导致重复调用问题,下边实例化 D 类后我们发现 A 被前后进入了两次(有童鞋说两次就两次憋,我女朋友还不止呢……)。
这有什么危害?我举个例子,假设 A 的初始化方法里有一个计数器,那这样 D 一实例化,A 的计数器就跑两次(如果遭遇多个钻石结构重叠还要更多),很明显是不符合程序设计的初衷的(程序应该可控,而不能受到继承关系影响)。

 
  1. >>> d = D()

  2. 进入D…

  3. 进入B…

  4. 进入A…

  5. 离开A…

  6. 离开B…

  7. 进入C…

  8. 进入A…

  9. 离开A…

  10. 离开C…

  11. 离开D…

为了让大家都明白,这里只是举例最简单的钻石继承问题,在实际编程中,如果不注意多重继承的使用,会导致比这个复杂N倍的现象,调试起来不是一般的痛苦……所以一定要尽量避免使用多重继承。

想更多的了解,阅读 -> 多重继承的陷阱:钻石继承(菱形继承)问题

6. 如何解决上一题中出现的问题?

答:super 函数再次大显神威。

 
  1. class A():

  2. def __init__(self):

  3. print("进入A…")

  4. print("离开A…")

  5. class B(A):

  6. def __init__(self):

  7. print("进入B…")

  8. super().__init__()

  9. print("离开B…")

  10. class C(A):

  11. def __init__(self):

  12. print("进入C…")

  13. super().__init__()

  14. print("离开C…")

  15. class D(B, C):

  16. def __init__(self):

  17. print("进入D…")

  18. super().__init__()

  19. print("离开D…")

  20. >>> d = D()

  21. 进入D…

  22. 进入B…

  23. 进入C…

  24. 进入A…

  25. 离开A…

  26. 离开C…

  27. 离开B…

  28. 离开D…


动动手

0. 定义一个点(Point)类和直线(Line)类,使用 getLen 方法可以获得直线的长度。

提示:

  • 设点 A(X1,Y1)、点 B(X2,Y2),则两点构成的直线长度 |AB| = √((x1-x2)2+(y1-y2)2)
  • Python 中计算开根号可使用 math 模块中的 sqrt 函数
  • 直线需有两点构成,因此初始化时需有两个点(Point)对象作为参数
 
  1. import math

  2. class Point():

  3. def __init__(self, x=0, y=0):

  4. self.x = x

  5. self.y = y

  6. def getX(self):

  7. return self.x

  8. def getY(self):

  9. return self.y

  10. class Line():

  11. def __init__(self, p1, p2):

  12. self.x = p1.getX() - p2.getX()

  13. self.y = p1.getY() - p2.getY()

  14. self.len = math.sqrt(self.x*self.x + self.y*self.y)

  15. def getLen(self):

  16. return self.len

  17. >>> p1 = Point(1, 1)

  18. >>> p2 = Point(4, 5)

  19. >>> line = Line(p1, p2)

  20. >>> line.getLen()

  21. 5.0

1. 展示一个你的作品:你已经掌握了 Python 大部分的基础知识,要开始学会自食其力了!

请花一个星期做一个你能做出来的最好的作品(可以是游戏、应用软件、脚本),使用上你学过的任何东西(类,函数,字典,列表……)来改进你的程序。"

答:这一次,我不打算提醒你要具体做点什么和怎么做,你需要自己来想创意。试着下手吧,编程就是解决问题的过程,这就意味着你要尝试各种可能性,进行实验,经历失败,然后丢掉你做出来的东西重头开始!

当你被某个问题卡住的时候,你可以到论坛寻求帮助,把你的代码贴出来给其他鱼油看,争取得到别人的建议并持续修改你的代码,直到它让你满意为止!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值