阅读 214

零基础学习 Python 之继承

写在之前

面向对象的程序设计都三个主要的特征:封装,继承,多态,这个也是类里面的重要内容,这三个特征我会从今天开始依次开始写,今天我们先来看第一个:「封装」,这一部分我会分两次来写,接下来进入正题。

概念

对于「继承」的概念,我们先来看在《维基百科》中的定义:

继承(Inheritance)是面向对象软件技术当中的一个概念。如果一个类别 A “继承”自另一个类别 B,就把这个 A 称为 “B 的子类别”,而把 B 称为 “A 的父类别”,也可以称为 “B 是 A 的超类”。

「继承」可以使得子类具有父类的各种属性和方法,而不需要再去重复编写相同的代码。在令子类继承父类的同时,可以重新去定义某些属性,并可以重写某些方法,简单点来说就是覆盖掉父类的属性和方法,使其拥有和父类不同的功能。此外,还可以为子类追加新的属性和方法,这都是些常见的做法。

所以由上面的描述我们可以知道使用继承的好处:一是可以实现代码的重用,但又不仅仅是代码重用;再者就是可以实现属性和方法的继承。当然了这个并不是全部,随着我们之后的学习,相信你对继承会有更深层次的了解。

如果你之前学过 Java 或者其它 OOP 语言的话,可能会对继承有不一样的理解。其实在 Python 里,因为存在**「鸭子类型」(duck typing)** ,使得接口定义的重要性大大的降低,从而继承的作用也被进一步的削弱了。至于什么是鸭子类型,因为现在还有几个没有讲,所以这个我会在之后的文章单独拿出来说。

再补充一点,如果你看过我前面的文章,你可能看到过我过所有的类都是 object 的子类,但是这里并没有显式的把 object 写出来,而是用隐式的方法继承了 object。总而言之,object 就是所有类的父类。

单继承

所谓的单继承,就是只从一个父类那里继承。

>>> class A:
...    pass
... 
>>> class B(A):
...    pass
...
复制代码

上面的例子,类 A 是一个普通的类,类 B 则是定义的一个子类,它用 B(A) 的形式继承了类 A(父类),虽然这个父类里什么也没有。

子类 B 继承父类 A 的方式就是在类名后面的括号里写上父类的类名。既然是继承了父类,那么根据我们上面说的,父类的一切都带到了子类里。上面也说了,在 Python3 里所有的类都是 object 的子类,但是不用写出 object,但是对于继承其它的类就不能隐藏了,必须要显式的写上父类的类名。

还记得我们前面的文章中写过的可以得到类的父类的那个特殊函数吗?我们在这里可以用一下:

>>> A.__base__
<class 'object'>
>>> B.__base__
<class '__main__.A'>
复制代码

为了深入的了解一下「继承」的作用,我们下面让父类做点儿事情:

>>> class A:
...    def __init__(self):
...            print('my name is rocky')
... 
>>> class B(A):
...    pass
... 
>>> b = B()
my name is rocky
复制代码

父类 A 中增加了初始化函数,然后子类 B 继承了它。我们已经知道,当建立实例的时候,首先要执行类中的初始化函数,因为子类 B 继承了父类,就把父类中的初始化函数拿到了子类里面,所以在 b = B() 的时候,执行了父类中定义的初始化函数,这就是继承,而且是从一个父类那里继承来的,所以称为单继承。

下面我们来看一个相对完整一些的例子:

class Person:
   def __init__(self,name):
       self.name = name

   def height(self,m):
       h = dict((['height',m],))
       return h

   def sex(self,se):
       sex1 = se
       return sex1

class Boy(Person):
   def get_name(self):
       return self.name

if __name__ == "__main__":
   lee = Boy('rocky')
   print(lee.get_name())
   print(lee.height(170))
   print(lee.sex('男'))
复制代码

运行上面的代码,所得的结果如下:

rocky
{'height': 170}
男
复制代码

首先我们定义了一个 Person 类,然后定义了一个子类 Boy。

在子类 Boy 中只写了一个方法 get_name(),但是因为继承了 Person,那么 Boy 就拥有了 Person 中的全部方法和属性,在子类 Boy 的方法 get_name() 中,使用了属性 self.name,但是在类 Boy 中并没有创建这个属性,只是因为其继承了 Person,在父类中有初始化函数。所以在使用子类创建实例的时候,必须要传一个参数,然后再调用方法。对于实例方法 lee.height(170) 也是因为继承的缘故。

在上面的程序中,子类 Boy 并没有和父类有重复的属性和方法,但有时候会出现下面的这种情况:

class Boy(Person):
   def __init__(self):
       self.name = 'snow'

   def get_name(self):
       return self.name
复制代码

在子类里面也有一个初始化函数,并且定义了一个实例属性 self.name = 'snow',而在父类中也有初始化函数,在这种情况下运行以后会出现什么结果呢?你先猜一下,猜完了继续往下看:

TypeError: __init__() takes 1 positional argument but 2 were given
复制代码

嚯,竟然报错了。报错的消息是创建实例的时候,传入的参数个数多了。这个的根源在于,子类 Boy 中的初始化函数只有一个 self,但因为跟父类的初始化函数重名,虽然继承了父类,但是将父类中的初始化函数覆盖掉了,所以现在的实例化子类不应该再显式的传参数。

if __name__ == "__main__":
   lee = Boy()
   print(lee.get_name())
   print(lee.height(170))
   print(lee.sex('男'))
复制代码

如此修改以后再运行,显示的结果如下:

snow
{'height': 170}
男
复制代码

从结果中我们不难看出来,如果子类中的属性或者方法与父类同名,那么就不再继承父类的该属性或方法。

调用被覆盖的方法

我们昨天说过,如果子类里有和父类同样名称的方法和属性,那么父类相应的部分不再被继承到子类。那么如果有这么一种情况,如果子类还想继续使用父类的该方法,那么该怎么办?我们可以对子类做如下修改,例子还是昨天的那个例子:

class Boy(Person):
  def __init__(self,name):
      Person.__init__(self,name)
      self.real_name = 'snow'

  def get_name(self):
      return self.name
复制代码

请仔细观察 Boy 的初始化方法,与昨天的例子有所不同,为了能继续在子类中使用父类的初始化方法,以类方法的方式调用 Person.init(self,name),此外在子类的初始化方法的参数中,要增加相应的参数 name。

接下来让我们实例化子类:

if __name__ == "__main__":
  lee = Boy('rocky')
  print(lee.real_name)
  print(lee.get_name())
复制代码

此刻的运行结果估计你也能猜出来:

snow
rocky
复制代码

这样使父类中被覆盖的方法能再次在子类中使用。但是上述的方式有一个问题,那就是如果父类的名称因为某种玄学的方式被改变了,子类所对应的父类的类名也要改,如果你忘记了改,就会出现异常,程序少还可以,程序如果特别多的话,你可能会忘记一两个地方,然后就苦逼的 debug 了。

你想我们号称简洁优雅的 Python 怎么可能会容忍这么费事的东西存在呢,于是 Python 创了一个非常巧妙的方法 -- super

class Boy(Person):
  def __init__(self,name):
      #Person.__init__(self,name)
      super(Boy,self).__init__(name)
      self.real_name = 'snow'

  def get_name(self):
      return self.name
复制代码

在上面的代码中我们只修改了一处,运行程序后显示的结果和以前一样,当然关于 super 还有一些别的用法,在这不做深究,感兴趣的可以自行 Google。

多重继承

昨天的文章中我们学了单继承,所谓单继承就是只从一个父类那继承,推展开来,多重继承就是继承可以来自多个父类,即一个子类的父类不止一个。请看下面的例子:

class Person:
   def hands(self):
       print('two hands')
   def hair(self,color):
       print('The hair color is: ',color)

class Boy:
   age = 23
   def name(self):
       print('The boy name is rocky')

class Children(Person,Boy):
   pass

if __name__ == "__main__":
   lee = Children()
   lee.hands()
   lee.hair('black')
   lee.name()
   print(lee.age)
复制代码

在上面的程序中,前面有两个类分别是 Person 和 Boy,然后第三个类是 Children,它继承了前面的两个类,继承的方法和单继承差不多,也是在类名后面的括号里把要继承的两个类的类名写上。接着我实例化了类 Children,既然它继承了上面的两个类,那么上面两个父类的方法就可以拿来用。

运行一下程序,结果如下所示:

two hands
The hair color is: black
The boy name is rocky
23
复制代码

由上面的几个例子我们已经能很清楚的知道继承的特点,接下来我们还有必要了解一下多重继承的顺序问题。在这里有一个小问题:如果一个子类继承了两个父类,并且两个父类中都有相同的属性或者方法,那么实例化之后的子类所调用的该属性或方法是属于哪个父类的呢?下面就让我们来试一下:

class P1:
   def name(self):
       print('name is P1')

class P2:
   def name(self):
       print('name is P2')
   def age(self):
       print('P2 age is 23')

class Q1(P1,P2):
   pass

class Q2(P1,P2):
   def age(self):
       print('Q2 age is 110')

class A(Q1,Q2):
   pass

if __name__ == "__main__":
   print(A.__mro__)
   cla = A()
   cla.age()
   cla.name()
复制代码

上述程序的运行结果如下:

(<class '__main__.A'>, <class '__main__.Q1'>, <class '__main__.Q2'>, <class '__main__.P1'>, <class '__main__.P2'>, <class 'object'>)
Q2 age is 110
name is P1
复制代码

其中 print(A.mro) 可以打印出类的继承顺序。如果要执行 age() 方法,首先要在 Q1 类中查看有没有这种方法,如果没有,再看 Q2 类,如果还是没有的话再看 Q1 类所继承的 P1 类,找到后就执行该方法,同样 name() 也是按照这样的顺序,在 Q2 类中就找到了该方法。

上述的这种对继承属性和方法的搜索叫做「广度优先搜索」,至于为什么是这样,感兴趣的可以去 Google 一下。

写在之后

更多内容,欢迎关注公众号「Python空间」,期待和你的交流。

在这里插入图片描述

关注下面的标签,发现更多相似文章
评论