上一节,我们对面对对象编程的写法规范、封装和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()
从结果可以看出,我们通过实例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)
super()方法调用
正常情况下,由子类调用父类的方法均可这样实现。但是,总会有一些特殊情况需要我们去考虑。举个例子,初始化方法在任何类中定义的名称都必须是__init__。但就是有这么一种情况,我们的子类和父类中都需要使用初始化函数,也都有实际意义,但是由于重载的规则,实例化子类的过程只能够调用子类中的初始化方法。那么有没有什么方法可以让父类的初始化方法也被执行呢?当然有,那就是super()方法:
class Old:
def __init__(self,d):
self.trid=[1]*d
class Child(Old):
def __init__(self,d,e):
e=list(e)
if d>len(e):
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)
这就又给了我们一个思考方向,既然父类中被重载的初始化方法可以在子类中使用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()
print('Child')
print(super().first)
test=Child()
test.seek()
test.use_old()
是不是很神奇?但是需要注意一点,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()
test=Adjust()
test.use_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')
a=child_method(a)
print(a)
self.y=getattr(self,'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)
实例和类命名空间
命名空间是一个抽象名词,它管理着特定范围内定义的所有标识符,将每个名称映射到相应的值。在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)
即使之前定义过该属性,输出的结果也还是x()方法所分配的地址,然而当我们掉用了该方法:
class Test():
x=0
def x(self):
self.x=1
return self
tes=Test()
tes.x()
print(tes.x)
这样才能找到重新赋值后的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()
b=a.ChildProgression()
print(b.thild)
print(b.test().thild)
嵌套类有助于减少潜在的命名冲突,因为它允许类似的命名类存在于另一个上下文中。这种嵌套特性大家会在日后会学习到的链表结构中有更深刻的体会。
名称解析和动态调度
下面我们研究python面向对象框架中检索名称时的过程。当用点运算符语法访问现有的成员(如“zhangsan.student(78,92,59)”)时,python解释器将开始一个名称解析的过程,描述如下:
- 在实例命名空间中搜索,如果找到所需的名称,关联值就可以使用;
- 否则在实例所属的类的命名空间中搜索,如果找到名称,关联值就可以使用;
- 通过继承结构继续向上搜索,检查每一个父类的类命名空间。第一次找到这个名字时,他的关联值可以使用;
- 如果仍未找到该名称,则会引发一个AttributeError异常。
本节主要讲了关于继承的细则,以及类这种特殊的结构,它的命名空间问题。今天的干货还是非常多的,希望大家能够吸收。下节我们继续补充一些类的细枝末节,学到这里,我们就已经了解了很多实用的手段了,相信有了这些知识,日后的数据结构与算法的学习就能够游刃有余了~
|