• #章节十四:类与对象2


    章节十四:类与对象2

    上一章节,我们知道了面向对象编程的重要性,也学习了类的基础知识(1.面向对象编程,2.类的创建和调用,3.创建类的两个关键点)。可以参考一下总结图:

    image.png-280.6kB

    在理清知识的框架后,我们来回顾一下代码:

    #看下参考注释:
    
    class Chinese:  # 类的创建
        eye = 'black'  # 类属性的创建
    
        def __init__(self,hometown):  # 类的初始化方法
            self.hometown = hometown  # 实例属性的创建
            print('程序持续更新中……')  # 初始化中的语句
        
        def born(self):  # 实例方法的创建
            print('我生在%s。'%(self.hometown))  # 方法的具体语句
    
    wufeng = Chinese('广东')  # 类的实例化
    print(wufeng.eye)  # 打印实例的属性(从类传递的)
    wufeng.born()  # 实例方法的调用
    

    好,相信你已经掌握了类的基础知识。

    这一关,我们会拓展类的知识疆界,探索两个类的拓展玩法:类的定制和类的继承

    说是拓展,是因为这两个知识点属于类中较高阶的操作,让用类写成的代码更容易复用、拓展和维护(这些优势在了解继承和定制后,就能理解了)。

    可以这么说:类能成为面向对象编程的主要工具,帮助 Python 在编程世界打下一片疆土,很大程度上是基于它的继承和定制。

    1. 类的继承和定制是什么?

    1.1 继承,从广东人说起

    假设你有个外国朋友,刚来中国。某天,他看到“广东人”这个词,就来问你,你会怎么跟他解释?

    可能你会回答:广东是中国的一个地方。广东人就是中国人。他可能会复述:哦!原来就是中国人。

    深究的话,“广东人就是中国人”中的“就是”的确切含义是“属于”,即广东人属于中国人。

    因此,中国人有的属性(如黑头发)和方法(如用筷子),广东人也都有。这么一来,用一句话“广东人是中国人”,便能让他接受多个信息:广东人有黑头发,会用筷子……

    这个过程相当于:把他脑子里对“中国人”这个类的所有信息都复制了一份,然后放到了“广东人”这个类下面。

    我们通过事物的归属关系,使信息的传递更为高效。听到“Python是一种计算机语言”,我们就知道Python可以编程;看到“云浮市在广东省”,我们就明白云浮市在中国南方……

    同样的,编程世界也是如此。我们也可以用一句话,让计算机知道:A类属于B类,自然也拥有了B类的所有属性和方法。这句话在编程里就是:A类继承了B类。

    image.png-303.9kB

    在Python中,我们的习惯表述是:A类是B类的子类,而B类是A类的父类(或超类)。

    所以,类的继承,让子类拥有了父类拥有的所有属性和方法。如此,不用白手起家(从头写代码),直接一夜暴富(代码的复用)。

    不过,只有继承的话,子类只是父类的复制而已。那样,为什么不直接用父类,还要增加一个子类?

    要回答这个问题,就需要了解另一个重要的概念:类的定制。

    1.2 定制,广东人又来了

    还是说回广东人,广东人除了继承中国人的属性方法外,还可以创造【属于自己】的属性或方法,如籍贯开头是广东省(属性)、会说广东话(方法)。

    甚至,广东人还可以调整继承到的属性或方法,如中国人有个属性“居住的陆地面积(单位:万平方公里)”的值为960,广东人继承后需要将这个属性的值改为17.98。

    上面的操作,都可以说是广东人在继承的基础上又做了定制。

    同样,子类也可以在继承的基础上进行个性化的定制,包括:(1)创建新属性、新方法;(2)修改继承到的属性或方法。

    简而言之:类的定制,不仅可以让子类拥有新的功能,还能让它有权修改继承到的代码——在写这句话时,我仿佛看到子类化成了一个人,抬头瞟了一眼在他上方的父类,淡淡地说了一句话:以我为主,为我所用。

    所以,当我们谈定制时,已经包含了继承。毕竟,类的定制的前提是继承,而定制的加入让类的继承不仅仅只是单纯的复制而已。这也是我们创建子类的意义,也可以回答上面提到的那个问题——为什么我们不直接用父类而创建子类?因为……可以定制啊!

    2. 类的继承,要怎么写?

    2.1 继承的基础语法

    用代码表示继承,语句是:

    image.png-77.1kB

    而子类继承的属性和方法,也会传递给子类创建的实例。跑个代码感受一下吧(注:广东人的英文单词是 Cantonese)。

    image.png-330.7kB

    发现了吗?实例yewen(叶问)是Cantonese(广东人)这个类创建的实例,却拥有Chinese才有的属性和方法。原因你也知道,继承的呗~

    再看下代码的注释,加深对这一过程的了解吧。

    class Chinese:
        eye = 'black'
    
        def eat(self):
            print('吃饭,选择用筷子。')
    
    class Cantonese(Chinese):  
    # 通过继承,Chinese类有的,Cantonese类也有
        pass
    
    # 验证子类可以继承父类的属性和方法,进而传递给子类创建的实例
    yewen = Cantonese()  
    # 子类创建的实例,从子类那间接得到了父类的所有属性和方法
    print(yewen.eye)  
    # 子类创建的实例,可调用父类的属性
    yewen.eat()  
    # 子类创建的实例,可调用父类的方法
    

    可见:通过一个小括号,子类就能轻轻松松地拥有父类所拥有的一切。不用复制大段大段的代码,只要一个括号,就能复用整块代码。

    你也来试一试,为下面的父类Cat创建一个子类Ragdoll(布偶猫),并用这个子类的实例来调用父类的属性和方法。

    image.png-172.3kB

     class Cat:
        tail = True
        
        def say(self):
            print('喵喵喵喵喵~')
            
    class Ragdoll(Cat):
        pass
    
    root = Ragdoll()
    print(root.tail)
    root.say()
    

    恭喜,你已经学会了最基本的继承语法了。

    不过,很多类在创建时也不带括号,如class Chinese:。这意味着它们没有父类吗?

    并不。实际上,class Chinese:在运行时相当于class Chinese(object):。而object,是所有类的父类,我们将其称为根类(可理解为类的始祖)。

    我们可以用一个函数来验证这一点:函数isinstance(),可以用来判断某个实例是否属于某个类。

    具体用法是输入两个参数(第一个是实例,第二个是类或类组成的元组),输出是布尔值(True 或 False)。跑下代码你就完全懂了:

    image.png-163.6kB

    好。可以正式验证了:

    image.png-393.5kB

    总结一下:

    image.png-246.6kB

    所以,在类的继承中,不仅子类属于父类,子类所创建的实例实际上也同时属于父类。

    理论上,父类可以被无限个子类所继承(这一点好比类的属性方法可以传递给无限个实例)。这个点有什么现实意义吗?举个简单的例子:

    如果要为每个省级行政区的人各创建一个类,并添加各种属性和方法。那么,只要创建一个父类Chinese,在父类中将共同的属性和方法写好,然后34个类都可以通过类的继承得到Chinese的属性和方法,代码量可以减少十几甚至几十倍。

    除此之外,继承还有两个更有趣的玩法:多层继承和多重继承。让我们见识一下吧。

    2.2 类的继承之多层继承

    继承不仅可以发生在两个层级之间(即父类-子类),还可以有父类的父类、父类的父类的父类……

    image.png-22.2kB

    这样一来,层级就出来了。只要你愿意,你可以继续拓展上面的例子,或往上(地球人),或往下(深圳人)。跑个代码体验一下:

    image.png-243.3kB

    # 先阅读代码和注释,然后直接运行代码。
    class Earthman:
        eye_number = 2
    
    # 中国人继承了地球人
    class Chinese(Earthman):
        eye_color = 'black'
    
    # 广东人继承了中国人,同时也继承了地球人。
    class Cantonese(Chinese):
        pass
    
    yewen = Cantonese()
    print(yewen.eye_number)
    print(yewen.eye_color)
    

    在代码最后两行,我们看到:实例yewen可以调用父类Chinese和父类的父类Earthman中的属性。可得结论:子类创建的实例可调用所有层级父类的属性和方法

    相信你能感知到:多层继承,属于继承的深度拓展。而下面要讲的多重继承,则是继承的宽度拓展。

    2.3 类的继承之多重继承

    一个类,可以同时继承多个类,语法为class A(B,C,D):。假设我们将“出生在江苏,定居在广东的人”设为一个类Yuesu,那么,它的创建语句则为class Yuesu(Yue,Su)

    class Yuesu(Yue,Su)括号里Yue和Su的顺序是有讲究的。和子类更相关的父类会放在更左侧。我认为“出生在江苏,定居在广东的人”在穿着和饮食等方面会更接近广东人,所以将 Yue 放在 Su 的左侧。

    所以,广东人创建的实例在调用属性和方法时,会先在左侧的父类中找,找不到才会去右侧的父类找。(可理解为“就近原则”)

    请你根据就近原则,推测下面代码会打印出什么?

    class Su:
        born_city = 'Jiangsu'
        wearing = 'thick'  # 穿得较厚
    
        def diet(self):
            print('我们爱吃甜。')
    
    class Yue:
        settle_city = 'Guangdong'
        wearing = 'thin'  # 穿得较薄
    
        def diet(self):
            print('我们吃得清淡。')
    
    class Yuesu(Yue,Su):
        pass
    
    xiaoming = Yuesu()
    print(xiaoming.wearing)
    print(xiaoming.born_city)
    xiaoming.diet()
    

    你的心里已经有答案了吧?跑下代码验证一下吧。

    image.png-255.6kB

    小结一下代码中体现的就近原则:越靠近子类(即越靠左)的父类,越亲近,越优先考虑。子类调用属性和方法时,会先在靠左的父类里找,找不到才往右找

    对比一下这两种有趣的继承方法:

    image.png-573.8kB

    多层继承和多重继承的结合,让继承的类拥有更多的属性和方法,且能更灵活地调用。进而,继承的力量也得以放大了很多倍。

    现在,请你尝试用代码完成下面的继承关系,按照下图类名和属性创建5个类,并打印出C4类的实例的属性namenum

    image.png-201kB

    image.png-188.1kB

    对比一下参考代码:

    class C0:
        name = 'C0'
    
    class C2(C0):
        num = 2
    
    class C1:
        num = 1
    
    class C3:
        name = 'C3'
    
    class C4(C1,C2,C3):
        pass
    
    ins = C4()
    print(ins.name) # 打印出C0
    print(ins.num) # 打印出1
    

    可以发现就近原则中的一个细节:多重继承中,若某父类还有父类的话,会先继续往上找到顶。例如代码中的ins.name调用的是C2的父类C0的值而非 C3。

    至此,我们已经将类的继承的主要玩法探索得七七八八了。终于,我们可以一起进入类的定制了!

    3. 类的定制,要怎么写?

    3.1 定制,可以新增代码

    请阅读一下代码和注释:

    class Chinese:
        eye = 'black'
    
        def eat(self):
            print('吃饭,选择用筷子。')
    
    class Cantonese(Chinese):  # 类的继承
        native_place = 'guangdong'  # 类的定制
    
        def dialect(self):  # 类的定制
            print('我们会讲广东话。')
    
    yewen = Cantonese()
    print(yewen.eye)
    # 父类的属性能用
    print(yewen.native_place)
    # 子类的定制属性也能用
    yewen.eat()
    # 父类的方法能用
    yewen.dialect()
    # 子类的定制方法也能用
    

    可见:我们可以在子类下新建属性或方法,让子类可以用上父类所没有的属性或方法。这种操作,属于定制中的一种:新增代码

    我们直接运行一下:

    image.png-296.3kB

    除了新增代码外,定制还有另一种操作:重写代码。

    3.2 定制,也可重写代码

    重写代码,是在子类中,对父类代码的修改

    我们举个例子:已知中国的陆地面积,也知道广东的陆地面积占比为1.88%。那么,两个类的方法可以写成这样:

    class Chinese:
        def land_area(self,area):
            print('我们居住的地方,陆地面积是%d万平方公里左右。'% area)
    
    class Cantonese(Chinese):
        # 直接对方法进行重写
        def land_area(self,area):
            print('我们居住的地方,陆地面积是%d万平方公里左右。'% int(area * 0.0188))
    

    这样的话,两个类都有个各自的land_area()方法,我们分别创建实例,运行一下看下结果:

    image.png-273.7kB

    不过,这个其实是不好的示范。虽然目的达成了,但直接重写并不优雅(有点类似洗去了旧方法,然后补上新方法)。

    想一想:假设有34个子类需定制这个方法,都是直接重写。那么,假设父类的方法改变,如说法改为“我们脚下的大地的面积有960万平方公里”。那么,就需要将所有子类的代码中的说法也改变。

    显然,这样对代码的维护很不友好。所以,下面介绍更优雅的重写方式:

    class Chinese:
    
        def land_area(self,area):
            print('我们居住的地方,陆地面积是%d万平方公里左右。'% area)
    
    class Cantonese(Chinese):
        # 间接对方法进行重写
        def land_area(self, area, rate = 0.0188):
            Chinese.land_area(self, area * rate)
            # 直接继承父类方法,再调整参数。
    
    gonger = Chinese()
    yewen = Cantonese()
    gonger.land_area(960)
    yewen.land_area(960)
    

    子类继承父类方法的操作是在def语句后接父类.方法(参数),如上述代码的第八、九行。

    这样一来,父类方法land_area中的说法改变,子类也不用去动,因为子类直接继承了父类的方法。只不过,在继承的基础上,通过参数的调整完成了定制。

    而参数的调整,可以增加参数(如 rate),也可以改变参数的默认值,如下:

    image.png-270.7kB

    # 阅读代码后运行
    class Chinese:
    
        def land_area(self,area):
            print('我们居住的地方,陆地面积是%d万平方公里左右。' % area)
    
    class Cantonese(Chinese):
        # 为参数 area 设置默认值。
        def land_area(self, area = 960, rate = 0.0188):
            Chinese.land_area(self, area * rate)
    
    yewen = Cantonese()
    yewen.land_area()
    # 两个参数都有默认值,所以可以这么调用。
    

    下面,请你通过参数默认值的改变,完成子类的定制,让程序的运行结果为“雷猴!欢迎来到广东。”

    image.png-232.8kB

    看下参考代码:

    class Chinese:
    
        def __init__(self, greeting = '你好', place = '中国'):
            self.greeting = greeting
            self.place = place
    
        def greet(self):
            print('%s!欢迎来到%s。' % (self.greeting, self.place))
    
    class Cantonese(Chinese):
    
        def __init__(self, greeting = '雷猴', place = '广东'):
            Chinese.__init__(self, greeting, place)
        
    yewen = Cantonese()
    yewen.greet()
    

    这便是定制:在复用代码的基础上,又能满足个性化的需求。

    类的继承和定制,从某个角度来看,和人类的科技史很像:每一代人,都“继承”了上一代的科技,同时“定制”属于这一代的科技。于是,科技的发展越来越先进。

    4. 习题练习

    4.1 习题一

    1.练习介绍:
    在这个作业,我们会通过对类属性这个切入点,温习类的继承和定制。

    2.练习要求:
    每个人都有好几个不同的身份,且不同身份都附带一些特定的特征(属性)和行为(方法)。
    例如,有这样一群人:在学校时被归在老师,脸是严肃的;亲子关系中(parenthood)则被归到父亲,脸是甜蜜的。
    下面,我们就以这群人为例,探索类属性在类的继承和定制中的传递和改变。

    3.创建两个类:老师和父亲
    首先,我们需要创建两个类,并为它们添加属性

    image.png-218.2kB

    class Teacher:
        face = 'serious'
        job = 'teacher'
    
    
    class Father:
        face = 'sweet'
        parenthood = 'dad'
    
    
    class TeacherMore(Teacher, Father):
        pass
    
    class FatherMore(Father, Teacher):
        face = 'gentle'
    
    time3 = TeacherMore()
    time4 = FatherMore()
    print(time3.face)
    print(time4.face)
    

    4.子类的继承和定制

    请你创建两个子类,同时继承已有的两个类(注:多重继承);
    然后,在其中选个子类进行定制:将 face 属性的值改变为'gentle';
    再者,创建实例 time3、time4,以调用子类的 face 属性。

    image.png-215.8kB

    class Teacher:
        face = 'serious'
        job = 'teacher'
    
    class Father:
        face = 'sweet'
        parenthood = 'dad'
    
    class TeacherMore(Teacher, Father):
        pass
    
    class FatherMore(Father, Teacher):
        face = 'gentle'
    
    time3 = TeacherMore()
    time4 = FatherMore()
    print(time3.face)
    print(time4.face)
    

    4.2 习题二

    1.练习介绍:
    这个练习,主要是训练你对“子类的继承”的理解和运用。

    2.练习要求:
    练习会先提供一个类,用以记录学生学习 Python 的投入时间和有效时间。
    需要你创建一个子类,为某一类学生提供定制化的记录方案。

    3.用代码记录时间

    image.png-271.1kB

    # 请先读懂代码,再运行。
    class Student:
        # 初始化函数,为每个实例创建4个参数(其中后3个参数有默认值)
        def __init__(self, name, job=None, time=0.00, time_effective=0.00): 
            self.name = name
            self.job = job
            self.time = time
            self.time_effective = time_effective
    
        def count_time(self, hour, rate):
            self.time += hour
            self.time_effective = hour * rate  # 有效时间=投入时间×学习效率
    
    student1 = Student('韩梅梅')
    print(student1.job)
    student1.count_time(10, 0.8)  # 学习效率为0.8
    print(student1.time_effective)
    

    4.通过类的定制升级代码
    假设:编程开发人员学 Python 的话,学习效率很高,默认为1。
    而且,job 的属性为 programmer。

    image.png-342.6kB

    class Student:
        def __init__(self, name, job=None, time=0.00, time_effective=0.00): 
            self.name = name
            self.job = job
            self.time = time
            self.time_effective = time_effective
    
        def count_time(self, hour, rate):
            self.time += hour
            self.time_effective = hour * rate
    
    class Programmer(Student):
        def __init__(self, name):
            Student.__init__(self, name, job='programmer', time=0.00, time_effective=0.00)
    
        def count_time(self, hour, rate=1):
            Student.count_time(self, hour, rate)
    
    student1 = Student('韩梅梅')
    student2 = Programmer('李雷')
    print(student1.job)
    print(student2.job)
    student1.count_time(10, 0.8)
    student2.count_time(10)
    print(student1.time_effective)
    print(student2.time_effective)
    
  • 相关阅读:
    22.抽象类
    21.多态
    20.修饰符
    19.继承
    day46---MySQL数据库进阶(一)
    day45---mysql数据库基本操作初阶(二)
    day45---计算机安全项目(癞蛤蟆)
    day44---数据库初阶(一)
    day44---windows下安装MySQL-5.6.37
    day42---几种network IO模型
  • 原文地址:https://www.cnblogs.com/ywb123/p/16375528.html
Copyright © 2020-2023  润新知