SECTION 24 面向对象编程(三)
面向对象编程
- 24.1 再谈绑定和方法调用
- 24.1.1 核心笔记:self 是什么?
- 24.1.2 调用绑定方法
- 24.1.2 调用未绑定方法
- 24.2 静态方法和类方法
- 24.2.1 静态方法
- 24.2.2 类方法
- 24.2.3 staticmethod()和 classmethod()内建函数
- 24.2.4 使用函数修饰符
- 24.3 组合
- 24.4 子类和派生
- 24.4.1 创建子类
- 24.4.2 继承
- 24.4.3 `__bases__`类属性
- 24.4.4 通过继承覆盖(Overriding)方法
- 24.4.5 核心笔记:重写`__init__`不会自动调用基类的`__init__`
- 24.4.6 从标准类型派生
- 24.4.6.1 不可变类型的例子
- 24.4.6.2 可变类型的例子
- 24.4.7 多重继承
- 24.4.7.1 方法解释顺序(MRO)
- 24.4.7.2 经典类
- 24.4.7.3 新式类
24.1 再谈绑定和方法调用
现在我们需要再次阐述 Python 中绑定(binding)的概念,它主要与方法调用相关连。
我们先来回顾一下与方法相关的知识:
首先,方法仅仅是类内部定义的函数。(这意味着方法是类属性而不是实例属性)。
其次,方法只有在其所属的类拥有实例时,才能被调用。当存在一个实例时,方法才被认为是绑定到那个实例了。没有实例时方法就是未绑定的。
最后,任何一个方法定义中的第一个参数都是变量 self,它表示调用此方法的实例对象。
24.1.1 核心笔记:self 是什么?
self 变量用于在类实例方法中引用方法所绑定的实例。因为方法的实例在任何方法调用中总是作为第一个参数传递的,self 被选中用来代表实例。你必须在方法声明中放上 self(你可能已经注意到了这点),但可以在方法中不使用实例(self)。
如果你的方法中没有用到 self , 那么请考虑创建一个常规函数,除非你有特别的原因。毕竟,你的方法代码没有使用实例,没有与类关联其功能,这使得它看起来更像一个常规函数。在其它面向对象语言中,self 可能被称为 this。
24.1.2 调用绑定方法
方法,不管绑定与否,都是由相同的代码组成的。唯一的不同在于是否存在一个实例可以调用此方法。在很多情况下,程序员调用的都是一个绑定的方法。假定现在有一个 MyClass 类和此类的一个实例 mc,而你想调用 MyClass.foo()方法。因为已经有一个实例,你只需要调用 mc.foo()就可以。记得 self 在每一个方法声明中都是作为第一个参数传递的。当你在实例中调用一个绑定的方法时,self 不需要明确地传入了。这算是"必须声明 self 作为第一个参数"对你的报酬。当你还没有一个实例并且需要调用一个非绑定方法的时候你必须传递 self 参数。
24.1.2 调用未绑定方法
调用非绑定方法并不经常用到。需要调用一个还没有任何实例的类中的方法的一个主要的场景是:你在派生一个子类,而且你要覆盖父类的方法,这时你需要调用那个父类中想要覆盖掉的构造方法。这里是一个本章前面介绍过的例子:
class EmplAddrBookEntry(AddrBookEntry):'Employee Address Book Entry class' # 员工地址记录条目def __init__(self, nm, ph, em):AddrBookEntry.__init__(self, nm, ph)self.empid = idself.email = em
EmplAddrBookEntry 是 AddrBookEntry 的子类,我们重载了构造器__init__()
。我们想尽可能多地重用代码, 而不是去从父类构造器中剪切,粘贴代码。这样做还可以避免 BUG 传播,因为任何修复都可以传递给子类。这正是我们想要的 — 没有必要一行一行地复制代码。只需要能够调用父类的构造器即可,但该怎么做呢?
我们在运行时没有 AddrBookEntry 的实例。那么我们有什么呢?我们有一个 EmplAddrBookEntry的实例,它与 AddrBookEntry 是那样地相似,我们难道不能用它代替呢?当然可以!
当一个 EmplAddrBookEntry 被实例化,并且调用 __init__()
时,其与 AddrBookEntry 的实例只有很少的差别,主要是因为我们还没有机会来自定义我们的 EmplAddrBookEntry 实例,以使它与AddrBookEntry 不同。
调用非绑定方法的最佳地方
,我们将在子类构造器中调用父类的构造器并且明确地传递(父类)构造器所需要的 self 参数(因为我们没有一个父类的实例)。子类中__init__()
的第一行就是对父类__init__()
的调用。我们通过父类名来调用它,并且传递给它 self 和其他所需要的参数。一旦调用返回,我们就能定义那些与父类不同的仅存在我们的(子)类中的(实例)定制。
24.2 静态方法和类方法
24.2.1 静态方法
静态方法是类中的函数,不需要实例。静态方法主要是用来存放逻辑性的代码,主要是一些逻辑属于类,但是和类本身没有交互,即在静态方法中,不会涉及到类中的方法和属性的操作。可以理解为将静态方法存在此类的名称空间中。事实上,在python引入静态方法之前,通常是在全局名称空间中创建函数。
示例,我想定义一个关于时间操作的类,其中有一个获得当前时间的函数
import timeclass TimeTest(object):def __init__(self, hour, minute, second):self.hour = hourself.minute = minuteself.second = second@staticmethoddef showTime():return time.strftime("%H:%M:%S", time.localtime())print(TimeTest.showTime())
t = TimeTest(2, 10, 10)
nowTime = t.showTime()
print(nowTime)
如上,使用静态函数,既可以将获得时间的函数功能与实例解绑,我想获得当前时间的字符串时,并不一定需要实例化对象,此时更像是一种名称空间。我们可以在类外面写一个简单的方法来做这些,但是这样做就扩散了类代码的关系到类定义的外面,这样写就会导致以后代码维护的困难。
静态函数可以通过类名以及实例两种方法调用!
24.2.2 类方法
类方法是将类本身作为对象进行操作的方法。通常的方法需要一个实例(self)作为第一个参数,并且对于(绑定的)方法调用来说,self 是自动传递给这个方法的。而对于类方法而言,需要类而不是实例作为第一个参数,它是由解释器传给方法。类不需要特别地命名, 类似 self,不过很多人使用 cls 作为变量名字。
他和静态方法的区别在于:不管这个方式是从实例调用还是从类调用,它都用第一个参数把类传递过来。
示例:做一个颜色的动态匹配
class ColorTest(object):color = "color"@classmethoddef value(cls):return cls.colorclass Red(ColorTest):color = "red"class Green(ColorTest):color = "green"g = Green()print g.value()
print Green.value()
24.2.3 staticmethod()和 classmethod()内建函数
现在让我们看一下在经典类中创建静态方法和类方法的一些例子(你也可以把它们用在新式类中):
class TestStaticMethod:def foo():print 'calling static method foo()'
foo = staticmethod(foo)class TestClassMethod:def foo(cls):print 'calling class method foo()'print 'foo() is part of class:', cls.__name__
foo = classmethod(foo)
对应的内建函数被转换成它们相应的类型,并且重新赋值给了相同的变量名。如果没有调用这两个函数,二者都会在 Python 编译器中产生错误,显示需要带 self 的常规方法声明。现在, 我们可以通过类或者实例调用这些函数…这没什么不同:
>>> tsm = TestStaticMethod()
>>> TestStaticMethod.foo()
calling static method foo()
>>> tsm.foo()
calling static method foo()
>>>
>>> tcm = TestClassMethod()
>>> TestClassMethod.foo()
calling class method foo()
foo() is part of class: TestClassMethod
>>> tcm.foo()
calling class method foo()
foo() is part of class: TestClassMethod
24.2.4 使用函数修饰符
现在,看到像 `foo=staticmethod(foo)这样的代码会刺激一些程序员。很多人对这样一个没意义的语法感到心烦,即使 van Rossum 曾指出过,它只是临时的,有待社区对些语义进行处理。你可以用修饰符@把一个函数应用到另个函数对象上, 而且新函数对象依然绑定在原来的变量。我们正是需要它来整理语法。通过使用 decorators,我们可以避免像上面那样的重新赋值:
class TestStaticMethod:@staticmethoddef foo():print 'calling static method foo()'
class TestClassMethod:@classmethoddef foo(cls):print 'calling class method foo()'print 'foo() is part of class:', cls.__name__
24.3 组合
一个类被定义后,目标就是要把它当成一个模块来使用,并把这些对象嵌入到你的代码中去,同其它数据类型及逻辑执行流混合使用。有两种方法可以在你的代码中利用类。
第一种是组合(composition)。就是让不同的类混合并加入到其它类中,来增加功能和代码重用性。你可以在一个大点的类中创建你自已的类的实例,实现一些其它属性和方法来增强对原来的类对象。另一种方法是通过派生,我们将在下一节中讨论它。
举例来说,让我们想象一个对本章一开始创建的地址本类的加强性设计。如果在设计的过程中,为 names,addresses 等等创建了单独的类。那么最后我们可能想把这些工作集成到 AddrBookEntry类中去,而不是重新设计每一个需要的类。这样就节省了时间和精力,而且最后的结果是容易维护的代码 — 一块代码中的 bugs 被修正,将反映到整个应用中。这样的类可能包含一个 Name 实例,以及其它的像 StreetAddress, Phone ( home, work,telefacsimile, pager, mobile, 等等),Email (home, work, 等等。),还可能需要一些 Date 实(birthday,wedding,anniversary,等等)。下面是一个简单的例子:
class NewAddrBookEntry(object): # class definition 类定义'new address book entry class'def __init__(self, nm, ph): # define constructor 定义构造器self.name = Name(nm) # create Name instance 创建 Name 实例self.phone = Phone(ph) # create Phone instance 创建 Phone 实例print 'Created instance for:', self.name
NewAddrBookEntry 类由它自身和其它类组合而成。这就在一个类和其它组成类之间定义了一种“has-a / 有一个”的关系。比如,我们的 NewAddrBookEntry 类“有一个” Name 类实例和一个 Phone实例。创建复合对象就可以实现这些附加的功能,并且很有意义,因为这些类都不相同。每一个类管理它们自己的名字空间和行为。
不过当对象之间有更接近的关系时,派生的概念可能对你的应用程序来说更有意义,特别是当你需要一些相似的对象,但却有少许不同功能的时候。
24.4 子类和派生
当类之间有显著的不同,并且(较小的类)是较大的类所需要的组件时,组合表现得很好,但当你设计“相同的类但有一些不同的功能”时,派生就是一个更加合理的选择了
OOP 的更强大方面之一是能够使用一个已经定义好的类,扩展它或者对其进行修改,而不会影响系统中使用现存类的其它代码片段。
OOD允许类特征在子孙类或子类中进行继承。这些子类从基类(或称祖先类,超类)继承它们的核心属性。而且,这些派生可能会扩展到多代。在一个层次的派生关系中的相关类(或者是在类树图中垂直相邻)是父类和子类关系。从同一个父类派生出来的这些类(或者是在类树图中水平相邻)是同胞关系。父类和所有高层类都被认为是祖先。
使用前一节中的例子,如果我们必须创建不同类型的地址本。即,不仅仅是创建地址本的多个实例,在这种情况下,所有对象几乎是相同的。如果我们希望 EmplAddrBookEntry 类中包含更多与工作有关的属性,如员工 ID 和 e-mail 地址?这跟 PersonalAddrBookEntry 类不同,它包含更多基于家庭的信息,比如家庭地址,关系,生日等等。
两种情况下,我们都不想到从头开始设计这些类,因为这样做会重复创建通用的 AddressBook类时的操作。包含 AddressBook 类所有的特征和特性并加入需要的定制特性不是很好吗?这就是类派生的动机和要求。
24.4.1 创建子类
创建子类的语法看起来与普通(新式)类没有区别,一个类名,后跟一个或多个需要从其中派生的父类:
class SubClassName (ParentClass1[, ParentClass2, ...]):'optional class documentation string'class_suite
如果你的类没有从任何祖先类派生,可以使用 object 作为父类的名字。经典类的声明唯一区别在于没有从祖先类派生–此时,没有圆括号:
class ClassicClassWithoutSuperclasses:pass
至此,我们已经看到了一些类和子类的例子,下面还有一个简单的例子:
class Parent(object): # define parent class 定义父类def parentMethod(self):print 'calling parent method'
class Child(Parent): # define child class 定义子类def childMethod(self):print 'calling child method'
>>> p = Parent() # instance of parent 父类的实例
>>> p.parentMethod()
calling parent method
>>>
>>> c = Child() # instance of child 子类的实例
>>> c.childMethod()# child calls its method 子类调用它的方法
calling child method
>>> c.parentMethod() # calls parent's method 调用父类的方法
calling parent method
24.4.2 继承
继承描述了基类的属性如何“遗传”给派生类。一个子类可以继承它的基类的任何属性,不管是数据属性还是方法。举个例子如下。P 是一个没有属性的简单类。C 从 P 继承而来(因此是它的子类),也没有属性:
class P(object): # parent class 父类pass
class C(P): # child class 子类pass
>>> c = C() # instantiate child 实例化子类
>>> c.__class__ # child "is a" parent 子类“是一个”父类
<class '__main__.C'>
>>> C.__bases__ # child's parent class(es) 子类的父类
(<class '__main__.P'>,)
因为 P 没有属性,C 没有继承到什么。下面我们给 P 添加一些属性:
class P: # parent class 父类'P class'def __init__(self):print 'created an instance of', self.__class__.__name__
class C(P): # child class 子类pass
现在所创建的 P 有文档字符串(__doc__
)和构造器,当我们实例化 P 时它被执行,如下面的交互会话所示:
>>> p = P() # parent instance 父类实例
created an instance of P
>>> p.__class__ # class that created us 显示 p 所属的类名
<class '__main__.P'>
>>> P.__bases__ # parent's parent class(es) 父类的父类
(<type 'object'>,)
>>> P.__doc__ # parent's doc string 父类的文档字符串
'P class'
“created an instance”是由__init__()
直接输出的。我们也可显示更多关于父类的信息。我们现在来实例化 C,展示 __init__()
(构造)方法在执行过程中是如何继承的:
>>> c = C() # child instance 子类实例
created an instance of C
>>> c.__class__ # class that created us 显示 c 所属的类名
<class '__main__.C'>
>>> C.__bases__ # child's parent class(es) 子类的父类
(<class '__main__.P'>,)
>>> C.__doc__ # child's doc string 子类的文档字符串
C 没有声明__init__()
方法,然而在类 C 的实例 c 被创建时,还是会有输出信息。原因在于 C 继承了 P 的__init__()
。__bases__
元组列出了其父类 P。需要注意的是文档字符串对类,函数/方法,还有模块来说都是唯一的,所以特殊属性__doc__
不会从基类中继承过来。
24.4.3 __bases__
类属性
__bases__
类属性对任何(子)类,它是一个包含其所有父类(parent)的集合的元组,那些没有父类的类,它们的__bases__
属性为空。下面我们看一下如何使用__bases__
的。
>>> class A(object): pass # define class A 定义类 A
...
>>> class B(A): pass # subclass of A A 的子类
...
>>> class C(B): pass # subclass of B (and indirectly, A) B 的子类(A 的间接子类)
...
>>> class D(A, B): pass # subclass of A and B A,B 的子类
...
>>> A.__bases__
(<type 'object'>,)
>>> C.__bases__
(<class __main__.B at 8120c90>,)
>>> D.__bases__
(<class __main__.A at 811fc90>, <class __main__.B at 8120c90>)
在上面的例子中,尽管 C 是 A 和 B 的子类(通过 B 传递继承关系),但 C 的父类是 B,这从它的声明中可以看出,所以,只有 B 会在 C.__bases__
中显示出来。另一方面,D 是从两个类 A 和 B 中继承而来的。
24.4.4 通过继承覆盖(Overriding)方法
我们在 P 中再写一个函数,然后在其子类中对它进行覆盖。
class P(object):def foo(self):print 'Hi, I am P-foo()'
>>> p = P()
>>> p.foo()
Hi, I am P-foo()
现在来创建子类 C,从父类 P 派生:
class C(P):def foo(self):print 'Hi, I am C-foo()'
>>> c = C()
>>> c.foo()
Hi, I am C-foo()
>>> p.foo()
Hi, I am P-foo()
尽管 C 继承了 P 的 foo()方法,但因为 C 定义了它自已的 foo()方法,所以 P 中的 foo() 方法被覆盖。覆盖方法的原因之一是,你的子类可能需要这个方法具有特定或不同的功能。
所以,你接下来的问题肯定是:“我还能否调用那个被我覆盖的基类方法呢?”
答案是肯定的,但是这时就需要你去调用一个未绑定的基类方法,明确给出子类的实例,例如下边:
>>> P.foo(c)
Hi, I am P-foo()
注意,我们上面已经有了一个 P 的实例 p,但上面的这个例子并没有用它。我们不需要 P 的实例调用 P 的方法,因为已经有一个 P 的子类的实例 c 可用(但是仍可以传实例p)。典型情况下,你不会以这种方式调用父类方法,你会在子类的重写方法里显式地调用基类方法。
class C(P):def foo(self):P.foo(self)print 'Hi, I am C-foo()'
注意,在这个(未绑定)方法调用中我们显式地传递了 self. 一个更好的办法是使用 super()(super下文介绍)内建方法:
class C(P):def foo(self):super(C, self).foo()print 'Hi, I am C-foo()'
super()不但能找到基类方法,而且还为我们传进 self,这样我们就不需要做这些事了。现在我们只要调用子类的方法,它会帮你完成一切:
>>> c = C()
>>> c.foo()
Hi, I am P-foo()
Hi, I am C-foo()
24.4.5 核心笔记:重写__init__
不会自动调用基类的__init__
类似于上面的覆盖非特殊方法,当从一个带构造器 __init()__
的类派生,如果你不去覆盖__init__()
,它将会被继承并自动调用。但如果你在子类中覆盖了__init__()
,子类被实例化时,基类的__init__()
就不会被自动调用。
class P(object):def __init__(self):print "calling P's constructor"
class C(P):def __init__(self):print "calling C's constructor"
>>> c = C()
calling C's constructor
如果你还想调用基类的 __init__()
,你需要像上边我们刚说的那样,明确指出,使用一个子类的实例去调用基类(未绑定)方法。相应地更新类 C,会出现下面预期的执行结果:
class C(P):def __init__(self):P.__init__(self)print "calling C's constructor"
>>> c = C()
calling P's constructor
calling C's constructor
上边的例子中,子类的__init__()
方法首先调用了基类的的__init__()
方法。这是相当普遍(不是强制)的做法,用来设置初始化基类,然后可以执行子类内部的设置。这个规则之所以有意义的,原因是,你希望被继承的类的对象在子类构造器运行前能够很好地被初始化或作好准备工作,因为它(子类)可能需要或设置继承属性。
Python 使用基类名来调用类方法,对应在 JAVA 中,是用关键字 super 来实现的,这就是 super()内建函数引入到 Python 中的原因,这样你就可以“依葫芦画瓢”了:
class C(P):def __init__(self):super(C, self).__init__()print "calling C's constructor"
使用 **super()**的漂亮之处在于,你不需要明确给出任何基类名字…“跑腿事儿”,它帮你干了!使用 super()的重点,是你不需要明确提供父类。这意味着如果你改变了类继承关系,你只需要改一行代码(class 语句本身)而不必在大量代码中去查找所有被修改的那个类的名字。
super语法格式:
super([type[, object-or-type]])
函数描述:
返回一个代理对象,它会将方法调用委托给 type 的父类或兄弟类。参数说明:
type —— 类,可选参数。
object-or-type —— 对象或类,一般是 self,可选参数。返回值:
super object —— 代理对象。super 是一个继承自 object 的类,调用 super() 函数其实就是 super 类的实例化。
根据官方文档的解释 super() 函数返回的对象 —— super object,就是一个代理对象(笔者也不太理解代理对象的含义)。
当从多个基类继承时,super()会按照“从左至右”顺序继承,后文会详细说明
class P(object):def foo(self):print 'Hi, I am P-foo()'class Q(object):def foo(self):print 'Hi, I am Q foo()'class C(Q,P):def foo(self):#P.foo(self)super(C,self).foo()print 'Hi, I am C-foo()'c=C()
c.foo()
>>>
Hi, I am Q foo()
Hi, I am C-foo()
super详解
24.4.6 从标准类型派生
经典类中,一个最大的问题是,不能对标准类型进行子类化。幸运的是,在 2.2 以后的版本中,随着类型(types)和类(class)的统一和新式类的引入, 这一点已经被修正。下面,介绍两个子类化 Python 类型的相关例子,其中一个是可变类型,另一个是不可变类型。
24.4.6.1 不可变类型的例子
假定你想在金融应用中,应用一个处理浮点数的子类。每次你得到一个贷币值(浮点数给出的),你都需要通过四舍五入,变为带两位小数位的数值。(当然,Decimal 类比起标准浮点类型来说是个用来精确保存浮点值的更佳方案,但你还是需要[有时候]对其进行舍入操作!)你的类开始可以
这样写:
class RoundFloat(float):def __new__(cls, val):return float.__new__(cls, round(val, 2))
我们覆盖了__new__()
特殊方法来定制我们的对象,使之和标准 Python 浮点数(float)有一些区别:
我们使用 round()内建函数对原浮点数进行舍入操作,然后实例化我们的 float,RoundFloat。
我们是通过调用父类的构造器来创建真实的对象的,float.__new__()
。注意,所有的__new()__
方法都是类方法,我们要显式传入类传为第一个参数,这类似于常见的方法如__init__()
中需要的 self。现在的例子还非常简单,比如,我们知道有一个 float,我们仅仅是从一种类型中派生而来等.通常情况下,最好是使用 super()内建函数去捕获对应的父类以调用它的__new()__
方法,下面,对它进行这方面的修改:
class RoundFloat(float):def __new__(cls, val):return super(RoundFloat, cls).__new__(cls, round(val, 2))
这个例子还远不够完整,所以,请留意本章我们将使它有更好的表现。下面是一些样例输出:
>>> RoundFloat(1.5955)
1.6
>>> RoundFloat(1.5945)
1.59
>>> RoundFloat(-1.9955)
-2
24.4.6.2 可变类型的例子
子类化一个可变类型,你可能不需要使用__new__()
(或甚至__init__()
),因为通常设置不多。一般情况下,你所继承到的类型的默认行为就是你想要的。下例中,我们简单地创建一个新的字典类型,它的 keys()方法会自动排序结果:
class SortedKeyDict(dict):def keys(self):return sorted(super( SortedKeyDict, self).keys())
回忆一下,字典(dictionary)可以由 dict(),dict(mapping),dict(sequence_of_2_tuples),或者 dict(**kwargs)来创建,看看下面使用新类的例子:
d = SortedKeyDict((('zheng-cai', 67), ('hui-jun', 68),('xin-yi', 2)))
print 'By iterator:'.ljust(12), [key for key in d]
print 'By keys():'.ljust(12), d.keys()
把上面的代码全部加到一个脚本中,然后运行,可以得到下面的输出:
By iterator: ['zheng-cai', 'xin-yi', 'hui-jun']
By keys(): ['xin-yi', 'hui-jun', 'zheng-cai']
在上例中,通过 keys 迭代过程是以散列顺序的形式,而使用我们(重写的)keys()方法则将keys 变为字母排序方式了。一定要谨慎,而且要意识到你正在干什么。如果你说,“你的方法调用 super()过于复杂”,取而代之的是,你更喜欢 keys()简简单单(也容易理解)…,像这样:
def keys(self):return sorted(self.keys())
24.4.7 多重继承
同 C++一样,Python 允许子类继承多个基类。这种特性就是通常所说的多重继承。概念容易,但最难的工作是,如何正确找到没有在当前(子)类定义的属性。当使用多重继承时,有两个不同的方面要记住。首先,还是要找到合适的属性。另一个就是当你重写方法时,如何调用对应父类方
法以“发挥他们的作用”,同时,在子类中处理好自己的义务。我们将讨论两个方面,但侧重后者,讨论方法解析顺序。
24.4.7.1 方法解释顺序(MRO)
精确顺序解释很复杂,超出了本文的范畴,但你可以去阅读本节后面的参考书目提到的有关内容。
这里提一下,新的查询方法是采用广度优先,而不是深度优先。
下面的示例,展示经典类和新式类中,方法解释顺序有什么不同。这个例子将对两种类的方案不同处做一展示。脚本由一组父类,一组子类,还有一个子孙类组成。
class P1: #(object): # parent class 1 父类 1def foo(self):print 'called P1-foo()'
class P2: #(object):def foo(self):print 'called P2-foo()'def bar(self):print 'called P2-bar()'
class C1(P1, P2): # child 1 der. from P1, P2 #子类 1,从 P1,P2 派生pass
class C2(P1, P2): # child 2 der. from P1, P2 #子类 2,从 P1,P2 派生def bar(self):print 'called C2-bar()'
class GC(C1, C2): # define grandchild class #定义子孙类 #从 C1,C2 派生pass # derived from C1 and C2
我们看到父类,子类及子孙类的关系。P1 中定义了 foo(),P2 定义了 foo()和 bar(),C2 定义了 bar()。下面举例说明一下经典类和新式类的行为。
24.4.7.2 经典类
首先来使用经典类。通过在交互式解释器中执行上面的声明,我们可以验证经典类使用的解释顺序,深度优先,从左至右:
>>> gc = GC()
>>> gc.foo() # GC ==> C1 ==> P1
called P1-foo()
>>> gc.bar() # GC ==> C1 ==> P1 ==> P2
called P2-bar()
当调用 foo()时,它首先在当前类(GC)中查找。如果没找到,就向上查找最亲的父类,C1。查找未遂,就继续沿树上访到父类 P1,foo()被找到。同样,对 bar()来说,它通过搜索 GC,C1,P1 然后在 P2 中找到。因为使用这种解释顺序的缘故,C2.bar()根本就不会被搜索了。现在,你可能在想,
"我更愿意调用 C2 的 bar()方法,因为它在继承树上和我更亲近些,这样才会更合适。”在这种情况下,你当然还可以使用它,但你必须调用它的合法的全名,采用典型的非绑定方式去调用,并且提供一个合法的实例:
>>> C2.bar(gc)
called C2-bar()
24.4.7.3 新式类
取消类 P1 和类 P2 声明中的对(object)的注释,重新执行一下。新式方法的查询有一些不同:
>>> gc = GC()
>>> gc.foo() # GC ==> C1 ==> C2 ==> P1
called P1-foo()
>>> gc.bar() # GC ==> C1 ==> C2
called C2-bar()
与沿着继承树一步一步上溯不同,它首先查找同胞兄弟,采用一种广度优先的方式。当查找foo(),它检查 GC,然后是 C1 和 C2,然后在 P1 中找到。如果 P1 中没有,查找将会到达 P2。foo()的底线是,包括经典类和新式类都会在 P1 中找到它,然而它们虽然是同归,但殊途!然而,bar()的结果是不同的。它搜索 GC 和 C1,紧接着在 C2 中找到了。这样,就不会再继续搜索到祖父 P1 和 P2。这种情况下,新的解释方式更适合那种要求查找 GC 更亲近的 bar()的方案。当然,如果你还需要调用上一级,只要按前述方法,使用非绑定的方式去做,即可。
>>> P2.bar(gc)
called P2-bar()
新式类也有一个__mro__
属性,告诉你查找顺序是怎样的:
>>> GC.__mro__
(<class '__main__.GC'>, <class '__main__.C1'>, <class'__main__.C2'>, <class '__main__.P1'>,<class'__main__.P2'>, <type 'object'>)
SECTION 24 面向对象编程(三)相关推荐
- 面向对象编程三种特性
文章目录 面向过程 面向过程特点 面向对象 面向对象特点 对象和类 定义类 类创建对象 封装特性 封装的特性 继承特性 多继承 新式类与经典类 私有属性与私有方法 私有属性和私有方法: 多态 多态特点 ...
- Java语言基础-面向对象编程三步走之打开冰箱门
开头: 何谓"面向对象" 面向对象是一种编程思想. 思想是一个很虚无缥缈的东西,但是它可以从一个人的具体行动中体现出来,如果说你坚持每天跑步,每天读书,做一些有益于身心健康的事情, ...
- 面向对象编程(三) --- 抽象类和接口
抽象类和接口 1抽象类 1.1抽象类的使用 2.接口 2.1接口的使用规则 2.2实现多个接口 2.3接口使用实例 2.4接口间的继承 2.5 Clonable 接口和深拷贝 1抽象类 1.1抽象类的 ...
- SECTION 22 面向对象编程(一)
面向对象编程(类) 22.1 介绍 22.1.1类与实例 22.1.2 方法 22.1.2.1 方法定义 22.1.2.2 创建一个类(类定义) 22.1.2.3 创建实例(实例化) 22.1.2.4 ...
- 面向对象编程三⼤特性 --封装、继承、多态
封装 把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏.封装是面向对象的特征之一,是对象和类概念的主要特性. 通俗的说,一个类就是一个封装了数据以 ...
- python面向对象编程
小白如何成为python数据分析师 第 十五 天---->面向对象编程 你有对象吗?下次遇到有人这样问,你就可以直接回答他,谁都有对象,因为一切皆为对象. 我们之前的编程都是指令式编程,即输入指 ...
- python面向对象编程 -- 封装、继承
面向对象编程 -- 封装.继承 面向对象编程三要素:封装.继承和多态.本文主要看和封装.继承相关的概念:在python中多态的概念比较模糊,本文不做讨论. 1 封装 封装:将数据和操作组装到一起,对外 ...
- Python基础学习——面向对象编程(第一讲:面向对象概述、面向对象三个基本特征(封装性、继承性、多态性)、类和对象(定义类、创建和使用对象、实例变量、类变量、构造方法、实例方法、类方法、静态方法))
面向对象是Python最重要的特性,在Python中一切数据类型都是面向对象的. 1.面向对象概述 面向对象的编程思想是,按照真实世界客观事物的自然规律进行分析,客观世界中存在什么样的实体,构建软件系 ...
- Python编程基础:第三十九节 面向对象编程Object Oriented Programming
第三十九节 面向对象编程Object Oriented Programming 前言 实践 前言 到目前为止我们都是函数式编程,也即将每一个功能块写为一个函数.其实还有一种更常用的编程方式被称为面向对 ...
最新文章
- UA MATH571A 一元线性回归II 统计推断1
- python3.9出了吗_Python 3.9 正式版要来了,会有哪些新特性?
- sqlbulkcopy mysql_SqlBulkCopy 的 Timeout 和 BatchSize
- Spring里的容器和Bean对象
- 图像色调,饱和度,对比度等相关定义
- BZOJ3019 : [Balkan2012]handsome
- P4449 于神之怒加强版
- P4126-[AHOI2009]最小割【网络流,tarjan】
- excel分段排序_学会这个神操作,报表填报不再五花八门,效率远超Excel
- 人人网,微博,QQ空间,朋友圈,常用API调用实现方法
- 2019 年对 VR 设备的五个期待
- restfulframework详解
- BZOJ1176[Balkan2007] Mokia
- 因结婚彩礼起积怨 山西一男子行凶致1死5伤
- .NET : 一定不要忘记关闭DataReader对象
- Imagination异构计算平台力助AI芯片厂商加速冲击110亿美元市场 | AIIA 2020
- 股神巴菲特名言及股神巴菲特选股技巧有哪些
- Multi-hop QA based KG
- 已知两点坐标求直线的一般表达式
- pythonurllib库获取yahoo财经数据_Yahoo! Finance财经数据PYTHON临时读取方法