Python基础:18类和实例之二
1:绑定和非绑定
当存在一个实例时,方法才被认为是绑定到那个实例了。没有实例时方法就是未绑定的。在很多情况下,调用的都是一个绑定的方法。
调用非绑定方法并不经常用到,其中一个主要的场景是:派生一个子类,而且要覆盖父类的方法,这时需要调用那个父类中被覆盖掉的构造方法:
class EmplAddrBookEntry(AddrBookEntry): 'Employee Address Book Entry class' def __init__(self, nm, ph, em): AddrBookEntry.__init__(self, nm, ph) self.empid = id self.email = em
EmplAddrBookEntry是AddrBookEntry 的子类,重载了__init__()。这是调用非绑定方法的最佳地方了。
当一个EmplAddrBookEntry被实例化,并且调用 __init__() 时,其与AddrBookEntry的实例只有很少的差别,主要是因为我们还没有机会来自定义我们的EmplAddrBookEntry 实例,以使它与AddrBookEntry 不同。所以,可以将EmplAddrBookEntry实例传递给AddrBookEntry的__init__。
子类中 __init__() 的第一行就是对父类__init__()的调用。通过父类名来调用它,并且传递给它 self 和其他所需要的参数。一旦调用返回,我们就能定义那些与父类不同的仅存在我们的(子)类中的(实例)定制。
2:静态方法和类方法
静态方法和类方法在Python2.2中引入。经典类及新式(new-style)类中都可以使用它。
Python中的静态方法和C++或者Java这些语言中的是一样的。它们仅是类中的函数(不需要实例)。
对于类方法而言,需要类而不是实例作为第一个参数,它是由解释器传给方法。类不需要特别地命名,类似self,不过很多人使用cls作为变量名字。
下面是在经典类中创建静态方法和类方法的一些例子(也可以把它们用在新式类中):
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)
内建函数staticmethod和classmethod将它们转换成相应的类型,并且重新赋值给了相同的变量名。如果没有调用这两个函数,二者都会在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
通过使用修饰符,可以避免像上面那样的重新赋值:
class TestStaticMethod: @staticmethod def foo(): print 'calling static method foo()' class TestClassMethod: @classmethod def foo(cls): print 'calling class method foo()' print 'foo() is part of class:', cls.__name__
3:有两种方法可以在代码中利用类。第一种是组合(composition)。就是让不同的类混合并加入到其它类中,来增加功能和代码重用性。另一种方法是通过派生。
组合的例子如下:
class NewAddrBookEntry(object): 'new address book entry class' def __init__(self, nm, ph): self.name = Name(nm) #创建Name实例 self.phone = Phone(ph) #创建Phone实例 print 'Created instance for:', self.name
NewAddrBookEntry类由其它类组合而成。这就在一个类和其它组成类之间定义了一种“有一个”的关系。比如,我们的NewAddrBookEntry 类“有一个” Name 类实例和一个Phone实例。
4:子类和派生
OOP的更强大方面之一是能够使用一个已经定义好的类,扩展它或者对其进行修改,而不会影响系统中使用现存类的其它代码片段。允许类特征在子孙类或子类中进行继承。
新式类创建子类的语法:
class SubClassName (ParentClass1[, ParentClass2,...]): 'optional class documentation string' class_suite
如果你的类没有从任何祖先类派生,可以使用object 作为父类的名字。
经典类的声明唯一不同之处在于其没有从祖先类派生--此时,没有圆括号:
class ClassicClassWithoutSuperclasses: pass
下面还有一个简单的例子:
class Parent(object): def parentMethod(self): print 'calling parent method' class Child(Parent): def childMethod(self): print 'calling child method' >>> p = Parent() >>> p.parentMethod() calling parent method >>> >>> c = Child() >>> c.childMethod() calling child method >>>c.parentMethod() calling parent method
5:继承
一个子类可以继承它的基类的任何属性,不管是数据属性还是方法。举个例子如下。P 是一个没有属性的简单类。C 从P 继承而来,也没有属性:
class P(object): pass class C(P): pass >>> c = C() >>> c.__class__ <class '__main__.C'> >>>C.__bases__ (<class '__main__.P'>,)
下面给 P 添加一些属性:
class P: 'P class' def __init__(self): print 'created an instance of', self.__class__.__name__ class C(P): pass
现在P 有文档字符串__doc__和__init__:
>>> p = P() created an instance of P >>> p.__class__ <class '__main__.P'> >>> P.__bases__ (<type 'object'>,) >>> P.__doc__ 'P class' >>> c = C() created an instance of C >>> c.__class__ <class '__main__.C'> >>> C.__bases__ (<class '__main__.P'>,) >>>C.__doc__ >>>
C没有声明__init__()方法,然而在类C 的实例c 被创建时,还是会有输出信息。原因在于C 继承了P 的__init__()。
需要注意的是,文档字符串对类,函数/方法,还有模块来说都是唯一的,所以特殊属性__doc__不会从基类中继承过来。
对任何(子)类,其__bases__类属性是一个包含其父类(parent)的集合的元组。
那些没有父类的类,它们的__bases__属性为空。下面我们看一下如何使用__bases__的。
>>> class A(object): pass ... >>> class B(A): pass ... >>> class C(B): pass ... >>> class D(A, B): pass ... >>> A.__bases__ (<type 'object'>, ) >>> B.__bases__ (<class '__main__.A'>,) >>> C.__bases__ (<class '__main__.B'>,) >>> D.__bases__ (<class '__main__.B'>, <class '__main__.A'>)
在上面的例子中,尽管C 是A 和B 的子类(通过B 传递继承关系),但C的父类是B,所以,只有B 会在C.__bases__中显示出来。
在父类 P 中再写一个函数,然后在其子类中对它进行覆盖:
class P(object): def foo(self): print 'Hi, I am P-foo()' >>> p = P() >>> p.foo() Hi, I am P-foo() class C(P): def foo(self): print 'Hi, I am C-foo()' >>> c = C() >>> c.foo() Hi, I am C-foo()
尽管C继承了P 的foo()方法,但因为C 定义了它自已的foo()方法,所以 P 中的foo() 方法被覆盖。
尽管父类中的foo被覆盖了,但是还是可以调用那个被覆盖的基类方法。这时就需要去调用一个未绑定的基类方法,需要明确给出子类的实例,例如下边:
>>> P.foo(c)
Hi, I am P-foo()
典型情况下,你不会以这种方式调用父类方法,你会在子类的重写方法里显式地调用基类方法。
class C(P): def foo(self): P.foo(self) print 'Hi, I am C-foo()'
在这个(未绑定)方法调用中我们显式地传递了self. 一个更好的办法是使用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()
类似于上面的覆盖非特殊方法,当从一个带__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__()方法。这是相当普遍(不是强制)的做法,用来设置初始化基类,然后可以执行子类内部的设置。super()内建函数引入到Python中,可以这样写:
class C(P): def __init__(self): super(C, self).__init__() print "calling C's constructor"
使用super()的漂亮之处在于,不需要明确提供父类。这意味着如果改变了类继承关系,只需要改一行代码(class 语句本身)而不必在大量代码中去查找所有被修改的那个类的名字。
如果一个类的构造方法被重写,那么就需要调用超类(你所继承的类)的构造方法,否则对象可能不会被正确地初始化。比如下面的例子:
>>> class Bird: ... def __init__(self): ... self.hungry=True ... def eat(self): ... if self.hungry: ... print 'aaah...' ... self.hungry=False ... else: ... print 'no thanks' >>> b=Bird() >>> b.eat() aaah... >>> b.eat() no thanks
现在考虑子类SongBird,它添加了唱歌的行为:
>>> class SongBird(Bird): ... def __init__(self): ... self.sound='Squawk' ... def sing(self): ... print self.sound ... >>> s=SongBird() >>> s.sing() Squawk
因为SongBird是Bird的一个子类,它继承了eat方法,但如果调用eat方法,就会产生一个问题:
>>> s.eat() Traceback (most recent calllast): File"<input>", line 1, in <module> File"<input>", line 5, in eat AttributeError:SongBird instance has no attribute 'hungry'
原因是:在SongBird中,构造方法被重写,但新的构造方法没有任何关于初始化hungry特性的代码。为了达到预期的效果,SongBird的构造方法必须调用其超类Bird的构造方法来确保进行基本的初始化。
6:继承标准类型
经典类中的一个问题是,不能对标准类型进行子类化。幸运的是,随着类型(types)和类(class)的统一和新式类的引入。这一点已经被修正。下面,介绍两个子类化Python 类型的相关例子。
一个处理浮点数的子类,它带两位小数位:
class RoundFloat(float): def __new__(cls, val): return float.__new__(cls, round(val, 2))
覆盖__new__()特殊方法来定制对象,使之和标准Python 浮点数(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.0
object.__new__(cls, ...),它会创建cls的实例,__new__是静态方法(因其特殊性,不需要显示声明)。而且它的第一个参数是要创建实例的类型。他返回一个cls的实例。
一般的用法中,创建子类的实例,可以调用父类的new:super(currentcls, cls).__new__(cls, ...)。比如:float.__new__(cls, 3.456),创建的就是cls的实例。
7:多重继承
Python允许子类继承多个基类。也就是多重继承。这里的难点在于:如何正确找到没有在当前(子)类定义的属性。也就是所谓的:方法解释顺序(MRO)。
在Python 2.2 以前的版本中,算法非常简单:深度优先,从左至右进行搜索,多重继承则取找到的第一个名字。
由于类,类型和内建类型的子类,都经过全新改造, 有了新的结构,这种算法不再可行。这样一种新的MRO 算法被开发出来,新的查询方法是采用广度优先,而不是深度优先。
下面的示例,展示经典类和新式类中,方法解释顺序有什么不同。
class P1: #(object) def 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): pass class C2(P1, P2): def bar(self): print 'called C2-bar()' class GC(C1, C2): pass
上面这些类的关系如下图所示:
首先来使用经典类,可以验证经典类使用的解释顺序,深度优先,从左至右:
>>> 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()
对于新式类,取消类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。
然而,bar()的结果是不同的。它搜索GC 和C1,紧接着在C2 中找到了。这样,就不会再继续搜索到祖父P1 和P2。
新式类有一个__mro__属性,告诉你查找顺序是怎样的。它是类属性,实例没有该属性。它是新式类的属性,经典类没有:
>>> GC.__mro__ (<class '__main__.GC'>, <class'__main__.C1'>, <class '__main__.C2'>, <class '__main__.P1'>, <class '__main__.P2'>, <type'object'>)
为什么经典类MRO 会失败?在版本2.2 中,类型与类的统一,带来了一个新的“问题”,波及所有从object派生出来的(根)类,一个简单的继承结构变成了一个菱形。
比如,有经典类B 和C,C 覆盖了构造器,B 没有,D 从B 和C 继承而来:
class B: pass class C: def __init__(self): print "the default constructor" class D(B, C): pass
当我们实例化D,得到:
>>> d = D()
the default constructor
上图为B,C 和D 的类继承结构,现在把代码改为采用新式类的方式,问题也就产生了:
class B(object): pass class C(object): def __init__(self): print "the default constructor"
由于在新式类中,需要出现基类,这样就在继承结构中,形成了一个菱形。D 的实例上溯时,不应当错过C, 但不能两次上溯到A(因为B 和C 都从A 派生)。
继承结构已变成了一个菱形,如果使用经典类的MRO,当实例化D 时,不再得到C.__init__()的结果,而是得到object.__init__()!这就是为什么MRO 需要修改的真正原因。
尽管我们看到了,在上面的例子中,类GC的属性查找路径被改变了,但你不需要担心会有大量的代码崩溃。经典类将沿用老式MRO,而新式类将使用它自己的MRO。