面对对象编程细则(二)


上一节,我们对面对对象编程的写法规范、封装和self的更多用法和细则,这节我们继续探索面对对象编程:

继承细则

成员重载

上节中,我们提到过,继承是一个分层过程,下层的组件更具体,上层组件更通用。在之前python基础补充中,我们讲过引用来的函数如果和自己编写的内容重名,那么之后定义的内容会覆盖掉之前定义的内容,这个过程也会以相似的形式出现在类继承过程中。如果父类和子类中有名称相同的方法或属性,并将子类实例化:

class Old:
    first=1
    def find_where(self):
        print('Old')
class Child(Old):
    first=10
    def find_where(self):
        print('Child')

a=Child()
print(a.first)
a.find_where()
# 输出为:10
#         Child

从结果可以看出,我们通过实例a调用的find_where方法是子类中定义的,这说明子类定义的方法会对父类相同的方法进行重载,因此在实例中无法调用父类中的find_where方法。

子类调用父类成员

self调用

在实例中,我们可以直接使用子类和父类中的所有属性和方法,子类的方法中想要使用父类定义的方法其实非常简单:

class Old:
    def __init__(self,d):
        self.trid=[1]*d
    def fids(self):
        return len(self.trid)
class Child(Old):
    def generate(self):
        d=self.fids() # 调用父类方法
        for i in range(d):
            self.trid[i]=(i+1)**2
        return self
test=Child(10) # 为父类的初始化方法提供参数
print(test.generate().trid)
# 输出为:[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

super()方法调用

正常情况下,由子类调用父类的方法均可这样实现。但是,总会有一些特殊情况需要我们去考虑。举个例子,初始化方法在任何类中定义的名称都必须是__init__。但就是有这么一种情况,我们的子类和父类中都需要使用初始化函数,也都有实际意义,但是由于重载的规则,实例化子类的过程只能够调用子类中的初始化方法。那么有没有什么方法可以让父类的初始化方法也被执行呢?当然有,那就是super()方法:

class Old:
    def __init__(self,d):
        self.trid=[1]*d # 生成长度为d的一维列表
class Child(Old):
    def __init__(self,d,e): # 修订属性trid的内容
        e=list(e)
        if d>len(e): # 处理e列表长度比d短的情况
            e=e+[e[len(e)-1]]*(d-len(e))
        super().__init__(d) # 调用父类中的初始化函数
        for i in range(d):
            self.trid[i]+=e[i]
number=Child(10,range(6))
print(number.trid)
# 输出为:[1, 2, 3, 4, 5, 6, 6, 6, 6, 6]

这就又给了我们一个思考方向,既然父类中被重载的初始化方法可以在子类中使用super()调用,那么父类中其他被重载的成员是不是也可以这样调用呢:

class Old:
    first=1
    def seek(self):
        print('old')
class Child(Old):
    first=10
    def seek(self):
        print('not old')
    def use_old(self):
        super().seek() # 调用父类的seek方法
        print('Child')
        print(super().first) # 调用父类的first属性
test=Child()
test.seek() # 调用的是子类的seek()方法
test.use_old()
# 输出为:not old
#        old
#        Child
#        1

是不是很神奇?但是需要注意一点,super()方法在实例中是不能够使用的,我们想要调用父类中被重载掉的方法,最好还是考虑在类内的方法中实现。
然后,问题又来了,如果我们不方便修改子类的代码,又想使用父类被重载掉的成员,该怎么办呢?还记得多层继承吗,我们可以再设立一个类继承之前的类,定义一个新的方法调用被重载的父类成员:

class Old:
    first=1
    def seek(self):
        print('old')
class Child(Old):
    first=10
    def seek(self):
        print('not old')
class Adjust(Child):
    def use_old(self):
        super(Child,self).seek() # 调用父类的seek方法

test=Adjust()
test.use_old()
# 输出为:old

super()方法有两个可以填写的参数,在这个例子中,我们想要使用Old类的seek()方法就需要指定第一个参数是Child(继承了Old类的子类)。事实上,第一个参数需要填写想要调用成员所在类的子类,而第二个参数通常写成self即可。前例中调用super没有填如参数实际上和写法super(Child,self).seek()是一样的。
此外,我们用鼠标点击一下super方法后面跟着的成员名,调用到的成员也会跟着变成深色:
在这里插入图片描述
super方法当然也可以用来调用没有被重载的成员,写法相同。不过必要性不大。

父类调用子类成员

有些时候,父类的成员不够具体,不能够获得或处理足够的数据,但是却有处理具这些数据的具体步骤;但子类成员可以生成或处理较为具体的数据,刚好可供父类的方法使用。这类似种情况下,我们就有必要知道如何在父类的成员函数之中调用子类的方法。
之前我们提到的子类想要调用父类成员时可以直接使用self.的形式直接找到是因为在继承关系下,子类空间已经可以找到了父类的空间。但是父类的作用空间并不能返向找到子类空间(有点像单链表结构),因此也就无法直接在父类的方法中使用self.找到想要调用的成员。
简单地说,实例化有继承关系的子类,只是让子类空间可以找到父类空间,而并非真正意义上的合并子类空间与父类空间。因此,想要在父类中使用子类的方法会有些复杂,我们需要先找到子类的方法再行调用。想要在父类的成员函数中找到子类的成员,需要用到getattr()方法,具体方法如下:

class Old():
    def call_child(self,a):
        child_method = getattr(self, 'sort_list') # 获取子类的create_list()方法
        a=child_method(a)  # 执行子类的sort_list()方法,为列表排序
        print(a)
        self.y=getattr(self,'first') # 获取子类的first属性
class Child(Old):
    first=10
    def sort_list(self,x):
        x.sort() # 排序
        return x
test = Child()
test.call_child([1,10,2,5,7,9,8,6])
print(test.y)
# 输出为:[1, 2, 5, 6, 7, 8, 9, 10]
#         10

实例和类命名空间

命名空间是一个抽象名词,它管理着特定范围内定义的所有标识符,将每个名称映射到相应的值。在Python中,函数、类和模块也是第一类对象,命名空间内与标识符相关的值可能实际上是一个函数、类或模块。所谓第一类对象,其特点为:

  • 可被存入变量
  • 可被作为参数传递给其他参数
  • 可作为函数返回值

实例和类命名空间

我们先来看这样一个例子:

class Message():
    Class = 1
    Grade = 4
    def __init__(self,name,number,gender): # 初始化学生基础信息
        self.name=name
        self.number=number
        self.gender=gender
class Performance(Message):
    def student(self,Chinese,English,Math): # 录入该学生的成绩信息
        self.Chinese=Chinese
        self.English=English
        self.Math=Math
        return self
    def grade_evaluate(self,result): # 判断成绩等级的方法
        if 90<=result<=100:
            return '优秀'
        elif 60<=result<90:
            return '良好'
        else:
            return '不合格'
    def grade_Chinese(self): # 判断语文等级
        self.grade_Chinese=self.grade_evaluate(self.Chinese)
    def grade_English(self): # 判断英语等级
        self.grade_English = self.grade_evaluate(self.English)
    def grade_Math(self): # 判断数学等级
        self.grade_Math = self.grade_evaluate(self.Math)
zhangsan=Performance('张三','01','男')

gra=zhangsan.student(78,92,59)
gra.grade_Chinese()
gra.grade_English()
gra.grade_Math()

print(gra.grade_Chinese)
print(gra.grade_English)
print(gra.grade_Math)

下面我们分析在这个例子里的命名空间情况:
在这里插入图片描述

这是Performance()类的属性命名空间,由于python是动态语言,实例化或使用Performance()类必然会调用父类的__init__,所以尽管Performance()类中没有初始化函数,初始化函数也会出现在子类的命名空间里。如果Performance()类里调用了父类的成员,那就需要开辟存储这个方法的空间了。
在这里插入图片描述
这是Message()类属性的实例命名空间,由于我们可以只是用父类而不必非要使用有继承关系的子类,所以实例的命名空间之中存储的是所有属于Message()类的属性。如果研究Performance()类,那么实例命名空间就会包含所有出现在父类和子类中的所有属性。需要注意的是,在普通的成员函数中追加的属性,会在调用该成员函数之后才能将其加入到命名空间:

class Test():
    def create_attribute_x(self):
        self.x=1
        return self
tes=Test()
print(tes.x)

结果为:
在这里插入图片描述
如果我们的属性名称和成员函数名称相同时:

class Test():
    x=0
    def x(self):
        self.x=1
        return self
tes=Test()
print(tes.x)
# 结果为:<bound method Test.x of <__main__.Test object at 0x00000295062EE910>>

即使之前定义过该属性,输出的结果也还是x()方法所分配的地址,然而当我们掉用了该方法:

class Test():
    x=0
    def x(self):
        self.x=1
        return self
tes=Test()
tes.x()
print(tes.x)
# 输出为:1

这样才能找到重新赋值后的x属性。由此可见,属性和方法名相同时,也可能会引发结果的异常,导致异常的原因很类似于前文所说的重载,所以大家在使用时尽量不要让属性名和方法名相同。
此外,还需要注意一点,如果我们只是声明了属性名,却并没有赋值,解释器也会认为该属性并没有在命名空间之中:

class A:
    x
a=A()
print(a.x)

结果为:
在这里插入图片描述

属性创建

属性可以是直接赋值的,即创建一个在整个类的空间均有效的常量,但有些时候属性需要我们通过参数传递进行赋值,如出现在方法之中的self.name=name,就是在方法中创建属性的通常用法。self在此作为限定符使用,使name标识直接添加到实例的命名空间之中。

类嵌套

类空间有一定的独立性,这种独立性体现在类的内部允许嵌套另一个类。类的嵌套并服从继承关系,它是一种在一个类内开辟空间存入另一个类的做法。因此,嵌套类的存在需要存在外部支持。嵌套类的语法结构为:

class A:
	class B:

想要找到并实例化B类,做法和找到并调用A类中的属性或方法成员一样:

class Progression:
    _first=10
    __second=20
    class ChildProgression():
        thild=30
        def test(self):
            return self
a=Progression()
# 进入Progression()的实例a空间之中,找到并对ChildProgression()类进行实例化
b=a.ChildProgression()
print(b.thild)
print(b.test().thild)
# 输出为:30
#         30

嵌套类有助于减少潜在的命名冲突,因为它允许类似的命名类存在于另一个上下文中。这种嵌套特性大家会在日后会学习到的链表结构中有更深刻的体会。

名称解析和动态调度

下面我们研究python面向对象框架中检索名称时的过程。当用点运算符语法访问现有的成员(如“zhangsan.student(78,92,59)”)时,python解释器将开始一个名称解析的过程,描述如下:

  1. 在实例命名空间中搜索,如果找到所需的名称,关联值就可以使用;
  2. 否则在实例所属的类的命名空间中搜索,如果找到名称,关联值就可以使用;
  3. 通过继承结构继续向上搜索,检查每一个父类的类命名空间。第一次找到这个名字时,他的关联值可以使用;
  4. 如果仍未找到该名称,则会引发一个AttributeError异常。

本节主要讲了关于继承的细则,以及类这种特殊的结构,它的命名空间问题。今天的干货还是非常多的,希望大家能够吸收。下节我们继续补充一些类的细枝末节,学到这里,我们就已经了解了很多实用的手段了,相信有了这些知识,日后的数据结构与算法的学习就能够游刃有余了~

猜你喜欢

转载自blog.csdn.net/weixin_54929649/article/details/124541487