专栏首页极客起源Python编程思想(27):类的继承

Python编程思想(27):类的继承

继承是面向对象的3大特征之一(另两个特性是封装和组合),也是实现软件复用的重要手段。Python的继承是多继承机制,也就是一个子类可以同时有多个直接父类。

1. 继承的语法

Python子类继承父类的语法是在定义子类时,将多个父类放在子类之后的圆括号中。语法格

式如下:

class SubClass(SuperClassl, SuperClass2,..)
     # 类定义部分

从上面的语法格式来看,定义子类的语法非常简单,只需在原来的类定义后增加圆括号,并在圆括号中添加多个父类,即可表明该子类继承了这些父类。

如果在定义一个 Python类时并未显式指定这个类的直接父类,则这个类默认继承 object类。因此,object类是所有类的父类,要么是其直接父类,要么是其间接父类。

实现继承的类被称为子类,被继承的类被称为父类,也被称为基类、超类。父类和子类的关系是一般和特殊的关系。例如水果和香蕉的关系,香蕉继承了水果,香蕉是水果的子类,则香蕉是种特殊的水果。

由于子类是一种特殊的父类,因此父类包含的范围总比子类包含的范围要大,所以可以认为父类是大类,而子类是小类。

从实际意义上看,子类是对父类的扩展,子类是一种特殊的父类。从这个意义上看,使用继承来描述子类和父类的关系是不准确的,用扩展更恰当。因此,这样的说法更加准确:Banana类扩展了Fruit类。

从子类的角度来看,子类扩展( extend)了父类,但从父类的角度来看,父类派生(derive)出子类。也就是说,扩展和派生所描述的是同一个动作,只是观察角度不同而已。

下面程序示范了子类继承父类的特点。下面是 Fruit类的代码。

示例代码:inherit.py

class Fruit:
    def info(self):
        print(f"我是水果!重{self.weight}克" )

class Food:
    def taste(self):
        print("不同食物的口感不同")

# 定义Banana类,继承了Fruit和Food类
class Banana(Fruit, Food):
    pass

# 创建Banana对象
b = Banana()
b.weight = 16
# 调用Banana对象的info()方法
b.info()
# 调用Banana对象的taste()方法
b.taste()

这段代码开始定义了两个父类:Fruit类和Food类,接下来程序定义了一个 Banana类,该 Banana类是一个空类。

在主程序部分,主程序创建了 Banana对象之后,可以访问Banana对象的info()和 taste()方法,这表明 Banana对象也具有了info和 taste方法,这就是继承的作用:子类扩展(继承)了父类。在子类中将可以继承得到父类定义的方法,这样子类就可复用父类的方法了。

2. 关于多继承

大部分面向对象的编程语言(除了C++)都只支持单继承,而不支持多继承,这是由于多继承不仅增加了编程的复杂度,而且很容易导致一些莫名的错误。

Python虽然在语法上明确支持多继承,但建议如果不是很有必要,则尽量不要使用多继承,而是使用单继承,这样可以保证编程思路更清晰,而且可以避免很多麻烦。

当一个子类有多个直接父类时,该子类会继承得到所有父类的方法,这一点在前面代码中已经演示了。现在的问题是,如果多个父类中包含了同名的方法,此时会发生什么呢?此时排在前面的父类中的方法会“遮蔽”排在后面的父类中的同名方法。

示例代码:multiple_inherit.py

class Item:
    def info (self):
        print("Item中方法:", '这是一个商品') 
class Product:
    def info (self):
        print("Product中方法:", '这是一个移动产品')
class Mouse1(Item, Product):
    pass
class Mouse2(Product, Item):
    pass
m1 = Mouse1()
m1.info()

m2 = Mouse2()
m2.info()

在这段代码中让 Mouse1继承了Item类和 Product类,Mouse2继承了Product类和Item类,这两个类的父类继承顺序是相反的。由于Mouse1类的Item排在前面,因此Item中定义的方法优先级更高, Python会优先到Item父类中搜寻方法,一旦在Item父类中搜寻到目标方法,Python就不会继续向下搜寻了。

上面程序中Item和 Product两个父类中都包含了info()方法,当 Mouse1子类对象调用info方法时,子类中没有定义info方法,因此 Python会从父类中寻找info方法,此时优先使用第1个父类Item中的info方法。而Mouse2子类对象调用的info()方法属于Product类。

运行上面程序,将看到如下输出结果:

Item中方法: 这是一个商品 Product中方法: 这是一个移动产品

3. 重写父类的方法

子类扩展了父类,子类是一种特殊的父类。大部分时候,子类总是以父类为基础,额外增加新的方法。但有一种情况例外:子类需要重写父类的方法。例如鸟类都包含了飞翔方法,其中鸵鸟是种特殊的鸟类,因此鸵鸟应该是鸟的子类,它也将从鸟类获得飞翔方法,但这个飞翔方法明显不适合鸵鸟,因此,鸵鸟需要重写鸟类的方法。

示例代码:override.py

class Bird: 
    # Bird类的fly()方法
    def fly(self):
        print("我在天空里自由自在地飞翔...")
class Ostrich(Bird):
    # 重写Bird类的fly()方法
    def fly(self):
        print("我只能在地上奔跑...")
  
# 创建Ostrich对象
os = Ostrich()
# 执行Ostrich对象的fly()方法,将输出"我只能在地上奔跑..."
os.fly()

运行上面程序,将看到运行os.fly()时执行的不再是Bird类的fly()方法,而是 Ostrich类的fly()方法。

这种子类包含与父类同名的方法的现象被称为方法重写(Override),也被称为方法覆盖。可以说子类重写了父类的方法,也可以说子类覆盖了父类的方法。

4. 使用未绑定方法调用被重写的方法

如果在子类中调用重写之后的方法,Python总是会执行子类重写的方法,不会执行父类中被重写的方法。如果需要在子类中调用父类中被重写的实例方法,那该怎么办呢?

读者别忘了,Python类相当于类空间,因此 Python类中的方法本质上相当于类空间内的函数。所以,即使是实例方法,Python也允许通过类名调用。区别在于,在通过类名调用实例方法时,Python不会为实例方法的第1个参数self自动绑定参数值,而是需要程序显式绑定第一个参数self。

示例代码:invoke_parent. py

class BaseClass:
    def name (self):
        print('父类中定义的name方法')
class SubClass(BaseClass):
    # 重写父类的name方法
    def name (self):
        print('子类重写父类中的name方法')
    def process (self):
        print('执行process方法')
        # 直接执行name方法,将会调用子类重写之后的name()方法
        self.name()
        # 使用类名调用实例方法调用父类被重写的方法
        BaseClass.name(self)
sc = SubClass()
sc.process()

上面程序中 SubClass继承了 BaseClass类,并重写了父类的name()方法。接下来程序在 SubClass类中定义了process()方法,该方法直接通过self调用name方法, Python将会执行子类重写之后的name方法。后面的代码通过显式调用 Base_Class 中的name方法,并显式为第1个参数self绑定参数值,这就实现了调用父类中被重写的方法。

5. 使用 super函数调用父类的构造方法

Python的子类也会继承得到父类的构造方法,如果子类有多个直接父类,那么排在前面的父类的构造方法会被优先使用。例如如下代码:

示例代码: super:py

class Employee :
    def __init__ (self, salary):
        self.salary = salary
    def work (self):
        print('普通员工正在写代码,工资是:', self.salary)
class Customer:
    def __init__ (self, favorite, address):
        self.favorite = favorite
        self.address = address
    def info (self):
        print(f'我是一个顾客,我的爱好是: {self.favorite},地址是{self.address}' )
class Manager1 (Employee,Customer):
    pass
class Manager2 (Customer, Employee):
    pass
m1 = Manager1(1235)
m1.work()
m2 = Manager2('服务器', '北京')
m2.info()

上面程序中定义了 Manager1类,该类继承了 Employee和 Customer两个父类。接下来程序中的Manager类将会优先使用 Employee类的构造方法(因为它排在前面),所以程序使用Manager(1235)来创建 Manager1对象。该构造方法只会初始化 salary实例变量,因此执行上面程序是没有任何问题的。但如果为Manager2传递一个数值就会引发错误,因为Manager2使用了Customer的构造方法,因此应该使用Manager2('服务器', '北京')创建Manager2对象。

为了让 Manager能同时初始化两个父类中的实例变量,Manager应该定义自己的构造方法就是重写父类的构造方法。Python要求:如果子类重写了父类的构造方法,那么子类的构造方法必须调用父类的构造方法。子类的构造方法调用父类的构造方法有两种方式。

  • 使用未绑定方法,这种方式很容易理解。因为构造方法也是实例方法。当然可以通过这种方式来调用;
  • 使用supe()函数调用父类的构造方法;

在交互式解释器中输入help(super)查看 super()函数的帮助信息,可以看到如下输出信息。

class super(object)
   super() -> same as super(__class__, <first argument>)
   super(type) -> unbound super object
   super(type, obj) -> bound super object; requires isinstance(obj, type)
   super(type, type2) -> bound super object; requires issubclass(type2, type)
   Typical use to call a cooperative superclass method:
   class C(B):
       def meth(self, arg):
           super().meth(arg)
   This works for class methods too:
   class C(B):
       @classmethod
       def cmeth(cls, arg):
           super().cmeth(arg)
   
   Methods defined here:
   
   __get__(self, instance, owner, /)
       Return an attribute of instance, which is of type owner.
   
   __getattribute__(self, name, /)

从输出的内容可以看出,super其实是一个类,因此调用 super()的本质就是调用 super类的构造方法来创建 super对象。

从上面的帮助信息可以看到,使用 super()构造方法最常用的做法就是不传入任何参数(这种做法与 super(type,obj)的效果相同),然后通过 super对象的方法既可调用父类的实例方法,也可调用父类的类方法。在调用父类的实例方法时,程序会完成第1个参数self的自动绑定。

现在将上面的代码改成下面的形式:

class Employee :
    def __init__ (self, salary):
        self.salary = salary
    def work (self):
        print('普通员工正在写代码,工资是:', self.salary)
class Customer:
    def __init__ (self, favorite, address):
        self.favorite = favorite
        self.address = address
    def info (self):
        print(f'我是一个顾客,我的爱好是: {self.favorite},地址是{self.address}')
# Manager继承了Employee、Customer
class Manager(Employee, Customer):
    # 重写父类的构造方法
    def __init__(self, salary, favorite, address):
        print('--Manager的构造方法--')
        # 通过super()函数调用父类的构造方法
        super().__init__(salary)
        # 使用未绑定方法调用父类的构造方法
        Customer.__init__(self, favorite, address)
# 创建Manager对象
m = Manager(25000, 'IT产品', '广州')
m.work()
m.info()

在这段代码中两行粗体字代码分别示范了两种方式调用父类的构造方法。通过这种方式,Manager类重写了父类的构造方法,并在构造方法中显式调用了父类的两个构造方法执行初始化,这样两个父类中的实例变量都能被初始化。

运行上面程序,可以看到如下运行结果:

--Manager的构造方法--
普通员工正在写代码,工资是: 25000
我是一个顾客,我的爱好是: IT产品,地址是广州

本文分享自微信公众号 - 极客起源(geekculture),作者:geekori

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-06-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 微信小程序开发实战(10):单选、多选和开关组件

    radio是选项按钮组件,该组件不能单独使用,必须作为radio-group的子组件使用,否则多个radio只有一个被选中。

    蒙娜丽宁
  • 冠状病毒传播仿真器(Python版本)【附源代码】

    本文首先会解释一下到底什么是"冠状病毒",以及杀死"冠状病毒"的方法。然后会利用Python实现一个"冠状病毒"传播仿真器,来演示一下为何“不出门“ +“疯狂建...

    蒙娜丽宁
  • Python的内存管理机制

    任何编程语言都会有一个内存模型,以便管理为变量分配的内存空间。不同的编程语言,如C、C++、Java、C#,Python,它们的内存模型都是不相同的,本文将以现...

    蒙娜丽宁
  • Python面向对象——继承

    py3study
  • Python 面向对象的总结_

    版权声明:Copyright © ...

    zhengzongwei
  • Python面向对象_

    版权声明:Copyright © ...

    zhengzongwei
  • Python_子类调用父类的方法

    1.方式一 子类调用父类的方法,包含2中形式的调用。一种形式是在类内部通过继承的方式调用父类的方法,另外一种形式是子类实例化后之后通过继承的方式来调用父类的方法...

    py3study
  • 送你们几个字!对!就是MACCMS注入!

    文末答题赢福利 0x00 相关信息 源码信息:maccms8_mfb(苹果CMS视频分享程序 8.0 | 2017.09.27 发布) 源码下载:http:...

    漏斗社区
  • Core官方DI解析(5)-ServiceProviderEngine

    最后来看看前面一直说的Engine(工作引擎),工作引擎接口是IServiceProviderEngine在ServiceProvider的构造函数中看到了根据...

    莫问今朝
  • Core官方DI解析(5)-ServiceProviderEngine

    最后来看看前面一直说的Engine(工作引擎),工作引擎接口是IServiceProviderEngine在ServiceProvider的构造函数中看到了根据...

    莫问今朝

扫码关注云+社区

领取腾讯云代金券