面向对象编程踏上了进化的阶梯,增强了架构化编程,实现了数据与动作的融合:数据层和逻辑层现在由一个可以创建这些对象的简单抽象层来描述。现实世界中的问题和实体完全暴露了本质,从中提供的一种抽象,可以用来进行相似编码,或者编入能与系统中对象进行交互的对象中。类提供了这样一些对象的定义,实例即是这些定义的实现。二者对面向对象设计(object-oriented design, OOD)来说都是重要的,OOD仅意味来创建你采用面向对象方式架构来创建系统。
文章目录
1. OOD 与 OOP
面向对象设计(OOD)不会特别要求面向对象编程语言。OOD可以由纯结构化语言来实现,比如C。当一门语言内建OO特性,OO编程开会更加方便高效。
Python从一开始设计就是面向对象的,但并非日常编程所必须。Python面向对象编程(OOP)具有强大能力,在Python中使用OOP可以提高许多效率。
2. 常用术语
- 抽象/实现
- 封装/接口
- 合成
- 派生/继承/继承结构
- 泛华/特化
- 多态
- 自省/反射
3. 类
3.1 创建类
Python 类使用class关键字来创建。
class ClassNmae(bases):
'class documentation string' # '类文档字符串'
class_suite # 类体
bases 是基类。
3.2 声明与定义
对于Python,声明与定义没什么区别。
4. 类属性
属性就是属于另一个对象的数据或者函数元素。
3.1 类的数据属性
类数据属性仅仅是所定义类的变量。
>>> class C(object):
... foo = 100
...
>>> print(C.foo)
100
>>> C.foo = C.foo + 1
>>> print(C.foo)
101
上面的代码中,看不到任何类实例的引用。
3.2 Methods
方法,比如下面,类MyClass中的myNoActionMethod方法,仅仅是一个作为类定义一部分定义的函数(这使得方法成为类属性)。这表示myNoActionMethod仅应用在MyClass类型的对象(实例)上。这里,myNoActionMethod是通过句点属性标识法与它的实例绑定的。
- 方法
>>> class MyClass(object):
... def myNoActionMethod(self):
... pass
...
>>> mc = MyClass()
>>> mc.myNoActionMethod()
任何像函数一样对myNoActionMethod自身调用都将失败:
>>> myNoAction()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'myNoAction' is not defined
引发了NameError异常,因为全局名字空间中,没有这样的函数存在。
由类对象调用此方法也失败了:
>>> MyClass.myNoActionMethod()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: myNoActionMethod() missing 1 required positional argument: 'self'
TypeError异常看起来让人困惑,因为这种方法是类的一个属性。
下面进行解释。
- 绑定(绑定及非绑定方法)
Python严格要求,没有实例,方法不能被调用。这种限制即Python所描述的绑定概念(binding),在此,方法必须绑定(到一个实例)才能直接被调用。
3.3 决定类的属性
>>> class MyClass(object):
... 'MyClass class definition' # 类定义
... myVersion = '1.1' # 静态数据
... def showMyVersion(self): # 方法
... print(MyClass.myVersion)
...
查看类的属性,有两种方法。
最简单的是使用dir()内建函数。
>>> dir(MyClass)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'myVersion', 'showMyVersion']
另外通过访问类的字典属性__dict__,这是所有类都具备的特殊属性之一。
>>> MyClass.__dict__
mappingproxy({'__module__': '__main__', '__doc__': 'MyClass class definition', 'myVersion': '1.1', 'showMyVersion': <function MyClass.showMyVersion at 0x7fa5e37c5048>, '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>})
>>> print(MyClass.__dict__)
{'__module__': '__main__', '__doc__': 'MyClass class definition', 'myVersion': '1.1', 'showMyVersion': <function MyClass.showMyVersion at 0x7fa5e37c5048>, '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>}
可以看出,dir()返回的仅是对象的属性的一个名字列表,而__dict__返回的是一个字典,它的键(key)是属性名,键值(value)是相应的属性对象的数据值。
3.4 特殊的类属性
C.__name__ | 类C的名字(字符串) |
C.__doc__ | 类C的文档字符串 |
C.__bases__ | 类C的所有父类构成的元祖 |
C.__dict__ | 类C的属性 |
C.__module__ | 类C定义所在的模块 |
C.__class__ | 实例C对应的类(仅新式类) |
>>> MyClass.__name__
'MyClass'
>>> MyClass.__doc__
'MyClass class definition'
>>> MyClass.__bases__
(<class 'object'>,)
>>> print(MyClass.__dict__)
{'__module__': '__main__', '__doc__': 'MyClass class definition', 'myVersion': '1.1', 'showMyVersion': <function MyClass.showMyVersion at 0x7fa5e37c5048>, '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>}
>>> MyClass.__module__
'__main__'
>>> MyClass.__class__
<class 'type'>
__name__是给定类的字符名字。
>>> stype = type('What is your quest?')
>>> stype
<class 'str'>
>>> stype.__name__
'str'
>>>
>>> type(3.14159265)
<class 'float'>
>>> type(3.14159265).__name__ # 得到类型名(字符串表示)
'float'
__doc__是类的文档字符串,与函数及模块的文档字符串相似,必须紧随头行(header line)后的字符串。文档字符串不能被派生类继承。派生类必须含有它们自己的文档字符串。
__bases__用来处理继承,它包含了一个所有父类组成的元祖。
__dict__包含一个字典,由类的数据属性组成。
Python支持模块间的类继承。
>>> class C(object):
... pass
...
>>> C
<class '__main__.C'>
>>> C.__module__
'__main__'
类C的全名是“__mian__.C
”。如果类C位于一个导入的模块中,如mymod,像下面的。
>>> from mymod import C
>>> C
<class 'mymod.C'>
SyntaxError: invalid syntax
>>> C.__module__
'mymod'
5. 实例
如果说类是一种数据结构定义类型,那么实例则声明了一个这样类型的量。
5.1 初始化:通过调用类对象创建实例
其它的OOP提供new关键字,用过new来创建类的实例。Python的方式更加简单,在定义类后,调用类就创建了一个实例。
>>> class MyClass(object):
... pass
...
>>> mc = MyClass()
当使用函数记法调用一个类时,解释器会实例化该对象,将这个实例返回。
5.2 __init__()
“构造器”方法
当类被调用,实例化的第一步是创建实例对象。一旦对象创建了,Python检查是否实现了__init__()
方法。默认情况下,如果没有定义(或覆盖)特殊方法__init__()
,对实例不会施加任何特别的操作。任何所需的特定操作,都需要手动实现__init__()
,覆盖它的默认行为。如果__init__()
没有实现,则返回它的对象,实例化过程完毕。
然而,如果__init__()
已经被实现,那么它将被调用,实例对象作为第一个参数(self
)被传递进去,像标准方法调用一样。调用类时,传进的任何参数都交给了__init__()
。实际中,可以想象成这样,把创建实例的调用当成是对构造器的调用。
5.3 __new__()
“构造器”方法
5.4 __del__()
“解构器”方法
>>> class P():
... def __del__(self):
... pass
...
>>> class C(P): # 类声明
... def __init__(self): # 构造器
... print('initialized')
... def __del__(self): # 解构器
... P.__del__(self) # 调用父类解构器打印
... print('deleted')
...
>>> c1 = C() # 实例初始化
initialized
>>> c2 = c1 # 创建另外一个别名
>>> c3 = c1 # 创建第三个别名
>>> id(c1), id(c2), id(c3) # 同一对象所有引用
(140445373906056, 140445373906056, 140445373906056)
>>> del c1 # 清除一个引用
>>> del c2 # 清除另外一个引用
>>> del c3 # 清除最终引用
deleted # 解构器最后调用
6. 实例属性
实例仅拥有数据属性(方法严格来说是类属性)
6.1 “实例化”实例属性(或创建一个更好的构造器)
- 在构造器中首先设置实例属性
构造器是最早可以设置实例属性的地方,因为__init__()是实例创建后第一个被调用的方法。再没有比这更早的可以设置实例属性的机会了。一旦__init__()执行完毕,返回实例对象,即完成了实例化过程。
- 默认参数提供默认的实例安装
在实际当中,带默认参数的__init__()提供一个有效的方式来初始化实例。
hotel.py
class HotelRoomCalc(object):
'Hotel room rate calculator'
def __init__(self, rt, sales=0.085, rm=0.1):
'''
HotelRoomCalc default arguments:
sales tax == 8.5%
room tax == 10%
'''
self.salesTax = sales
self.roomTax = rm
self.roomRate = rt
def calcTotal(self, days=1):
'Calculate total; default to daily rate'
daily = round((self.roomRate * (1 + self.roomTax + self.salesTax)), 2)
return float(days) * daily
>>> sfo = HotelRoomCalc(299)
>>> sfo.calcTotal()
354.31
>>> sfo.calcTotal(2)
708.62
>>> sea = HotelRoomCalc(189, 0.086, 0.058)
>>> sea.calcTotal()
216.22
>>> sea.calcTotal(4)
864.88
>>> wasWkDay = HotelRoomCalc(169, 0.045, 0.02)
>>> wasWkEnd = HotelRoomCalc(119, 0.045, 0.02)
>>> wasWkDay.calcTotal(5) + wasWkEnd.calcTotal()
1026.6299999999999
- __init__() 应当返回 None
>>> class MyClass(object):
... pass
...
>>> mc = MyClass()
>>> mc
<__main__.MyClass object at 0x000000CA65386E80>
>>> class MyClass:
... def __init__(self):
... print('initialized')
... return 1
...
>>> mc = Myclass()
Traceback (most recent call last):
File "<input>", line 1, in <module>
NameError: name 'Myclass' is not defined
6.2 查看实例属性
>>> class C(object):
... pass
...
>>> c = C()
>>> c.foo = 'roger'
>>> c.bar = 'shrubber'
>>> dir(c)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bar', 'foo']
>>> c.__dict__
{'foo': 'roger', 'bar': 'shrubber'}
6.3 特殊的实例属性
实例仅有两个特殊属性(见下表)。对于任意对象 I:
I.__class__ | 实例化 I 的类 |
I.__dict__ | I 的属性 |
使用类 C 及其实例 c 来看看这些特殊实例属性:
>>> class C(object): # 定义类
... pass
...
>>> c = C() # 创建实例
>>> dir(c) # 返回基类属性,实例还没有属性
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> c.__dict__ # 实例还没有属性
{}
>>> c.__class__ # 实例化 c 的类
<class '__main__.C'>
c 目前还没有数据属性,添加一些再检查 __dict__ 属性。
>>> c.foo = 1
>>> c.bar = 'SPAM'
>>> '%d can of %s please' % (c.foo, c.bar)
'1 can of SPAM please'
>>> c.__dict__
{'foo': 1, 'bar': 'SPAM'}
__dict__ 属性由一个字典组成,包含一个实例的所有属性。键是属性名,值是属性相应的数据值。字典中仅有实例属性,没有类属性或特殊属性。
6.4 内建类型属性
内建类型也是类。用 dir() 考察它们有没有像类或实例一样的属性。
>>> x = 3 + 0.14j
>>> x.__class__
<class 'complex'>
>>> dir(x)
['__abs__', '__add__', '__bool__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__int__', '__le__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__pos__', '__pow__', '__radd__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rmod__', '__rmul__', '__rpow__', '__rsub__', '__rtruediv__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', 'conjugate', 'imag', 'real']
>>>
>>> [type(getattr(x, i)) for i in ('conjugate', 'imag', 'real')]
[<class 'builtin_function_or_method'>, <class 'float'>, <class 'float'>]
已知一个复数的属性,可以访问它的数据属性,调用它的方法:
>>> x.imag
0.14
>>> x.real
3.0
>>> x.conjugate()
(3-0.14j)
访问 __dict__ 会失败,因为在内建类型中,不存在这个属性:
>>> x.__dict__
Traceback (most recent call last):
File "<input>", line 1, in <module>
AttributeError: 'complex' object has no attribute '__dict__'
6.5 实例属性 vs 类属性
- 访问类属性
类属性可通过类或实例来访问。
>>> class C(object): # 定义类
... version = 1.2 # 静态成员
...
>>> c = C() # 实例化
>>> C.version # 通过类来访问
1.2
>>> c.version # 通过实例来访问
1.2
>>> C.version += 0.1 # 通过类(只能这样)来更新
>>> C.version # 类访问
1.3
>>> c.version # 实例访问
1.3
- 从实例中访问类属性须谨慎
与通常Python变量一样,任何对实例属性的赋值都会创建一个实例属性(如果不存在的话)并且对其赋值。如果类属性中存在同名的属性,又去的副作用即产生(经典类和新式类都存在)。
>>> class Foo(object):
... x = 1.5
...
>>> foo = Foo()
>>> foo.x
1.5
>>> foo.x = 1.7 # 更新类属性
>>> foo.x # 似乎已更新
1.7
>>> Foo.x # 类属性没有变,只是创建了一个新的实例属性
1.5
>>> foo.__dict__
{'x': 1.7}
>>> foo.__dict__['x']
1.7
>>> Foo.__dict__
mappingproxy({'__module__': '__main__', 'x': 1.5, '__dict__': <attribute '__dict__' of 'Foo' objects>, '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None})
>>> Foo.__dict__['x']
1.5
上述代码创建了名为 x 的新实例属性,它覆盖了对类属性的引用。
>>> del foo.x
>>> foo.x
1.5
>>> foo.x += .2
>>> foo.x
1.7
>>> Foo.x
1.5
同样创建了一个新的实例属性,类属性原封不动(深入理解 Python 相关知识:属性已存在于类字典 [__dict__] 中。通过赋值,其被加入到实例的 __dict__ 中了)。
在类属性可变的情况下,一切都不同了:
>>> class Foo(object):
... x = {2003: 'poe2'}
...
>>> foo Foo()
File "<input>", line 1
foo Foo()
^
SyntaxError: invalid syntax
>>> foo = Foo()
>>> foo.x
{2003: 'poe2'}
>>> foo.x[2004] = 'valid path'
>>> foo.x
{2003: 'poe2', 2004: 'valid path'}
>>> Foo.x # 生效了!
{2003: 'poe2', 2004: 'valid path'}
>>> del foo.x # 没有遮蔽,所以不能删除掉
Traceback (most recent call last):
File "<input>", line 1, in <module>
AttributeError: x
- 类属性持久性
静态成员,顾名思义,任凭整个实例(及其属性)的如何进展,它都不理不睬(因此独立于实例)。同时,当一个实例在类属性被修改后才创建,那么更新的值就将生效。类属性的修改会影响到所有的实例:
>>> class C(object):
... spam = 100
...
>>> c1 = C()
>>> c1.spam
100
>>> C.spam += 100
>>> C.spam
200
>>> c1.spam
200
>>> c2 = C()
>>> c2.spam
200
>>> del c1
>>> c2.spam
200
>>> C.spam
200
>>> C.spam += 200
>>> c2.spam
400
【提示】: 使用类属性来修改自身(不是实例属性)
7. 绑定和方法调用
Pythong 中绑定(binding)主要与方法调用相关连。
首先,方法仅仅是类内部定义的函数(这意味着方法是类属性而不是实例属性)。
其次,方法只有在其所属的类拥有实例时,才能被调用。当存在一个实例时,方法才被认为是绑定到那个实例了。没有实例时方法就是未绑定的。
最后,任何一个方法定义中的第一个参数都是变量 self
,它表示调用此方法的实例对象。
笔记:
self
是什么?
self
变量用于在实例方法中引用方法所绑定的实例。因为方法的实例在任何方法调用中总是作为第一个参数传递的,self
被选中用来代表实例。你必须在方法声明中放上self
(你可能已经注意到这点),但可以在方法中不适用实例(self
)。如果你的方法中没有用到self
,那么请考虑创建一个常规函数,除非你有特别的原因。毕竟,你的方法代码没有使用实例,没有与类关联其功能,这使得它看起来更像一个常规函数。在其他面向对象语言中,self
可能被称为this
。
7.1 调用绑定方法
通过实例调用类的方法,即调用绑定的方法。 所属 MyClass 类的方法 foo() 和 实例 mc,调用绑定的方法:mc.foo()。InstanceName.Method() (实例名.方法())即调用绑定的方法。调用绑定的方法 self
不需要明确地传入。
7.2 调用非绑定方法
通过类调用类的方法,即调用非绑定的方法。所属 MyClass 类的方法 foo() 和实例 mc,调用非绑定的方法:MyClass.foo(self, …)。ClassName.Method(self, …) (类名.方法(self, …))即调用非绑定的方法。调用非绑定的方法必须传递 self
参数。
调用非绑定方法并不经常用到。需要调用一个还没有任何实例的类中的方法的一个主要场景是:你在派生一个子类,而且你需要覆盖父类的方法,这是你需要调用那个父类中想要覆盖掉的构造方法。这里给出一个例子:
class AddrBookEntry(object):
"""address book entry class"""
def __init__(self, nm, ph):
self.name = nm
self.phone = ph
print('Created instance for:', self.name)
def updatePhone(self, newph):
self.phone = newph
print('Updated phone# for:', self.name)
class EmpAddrBookEntry(AddrBookEntry):
"""Employee Address Book Entry class"""
def __init__(self, nm, ph, em):
AddrBookEntry.__init__(self, nm, ph)
self.empid = id
self.email = em
8. 静态方法和类方法
经典类和新式类(new-style)都可以使用静态方法和类方法。一对内建函数被引入,用于将作为类定义的一部分的某一方法声明“标记”(tag),“强制类型转换”(cast)或者“转换”(convert)为这两种类型的方法之一。
通常的方法需要一个实例(self
)作为第一个参数,并且对于(绑定的)方法调用来说,self
是自动传递给这个方法的。而对于类方法而言,需要类而不是实例作为第一个参数,它是由解释器传给方法。类不需要特别的命名,类似 self
,不过很多人使用 cls
作为变量名字。
8.1 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
赋值给不同的变量名,Python 解释器会报错:
class TestStaticMethod:
def foo():
print('calling static method foo()')
faa = staticmethod(foo)
class TestClassMethod:
def foo(cls):
print('calling class method foo()')
print('foo() is part of class:', cls.__name__)
faa = classmethod(foo)
结果如下:
>>> tsm = TestStaticMethod()
>>> TestStaticMethod.foo()
calling static method foo()
>>> TestStaticMethod.faa()
calling static method foo()
>>> tsm.foo()
Traceback (most recent call last):
File "<input>", line 1, in <module>
TypeError: foo() takes 0 positional arguments but 1 was given
>>> tsm.faa()
calling static method foo()
>>> tsm.faa
<function TestStaticMethod.foo at 0x000000170945D048>
>>> tcm = TestClassMethod()
>>> TestClassMethod.foo()
Traceback (most recent call last):
File "<input>", line 1, in <module>
TypeError: foo() missing 1 required positional argument: 'cls'
>>> tcm.foo()
Traceback (most recent call last):
calling class method foo()
File "<input>", line 1, in <module>
File "F:/A1_SLAM/03__projects/learn_python/staticmethod.py", line 11, in foo
print('foo() is part of class:', cls.__name__)
AttributeError: 'TestClassMethod' object has no attribute '__name__'
>>> TestClassMethod.faa()
calling class method foo()
foo() is part of class: TestClassMethod
>>> TestClassMethod.faa
<bound method TestClassMethod.foo of <class '__main__.TestClassMethod'>>
>>> tcm.faa()
calling class method foo()
foo() is part of class: TestClassMethod
>>> tcm.faa
<bound method TestClassMethod.foo of <class '__main__.TestClassMethod'>>
8.2 使用函数修饰符
通过使用装饰器,我们可以避免像上面那样的重新赋值:
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__)
9. 组合
一个类被定义后,目标就是要把他当成一个模块使用,并把这些对象嵌入到代码中去,同其他类型及逻辑执行流混合使用。有两种方法可以在代码中利用类。第一种是组合(Composition)。就是让不同的类混合并加入到其他类中,类增加功能和代码的重用性。我可以在一个大点的类中创建我自己的类的实例,实现一些其他属性和方法来增强原来的类对象。另一种方法是通过派生,我们将在下一节中讨论。
举例来说,如果在设计的过程中,为Name,Phone创建了单独的类,那么最后我们可能想把这些工作继承到AddrBookEntry类中去,而不是重新设计每一个需要的类。这样就节省了时间和精力,而且最后的结果是容易维护的代码——块代码中的bug被修正,将反映到整个应用中。
这样的类可能包含以Name实例,再加一个Phone实例,以及其它如StreetAddress、Email(home、work等),还可能需要一些Date实例(birthday、wedding、anniversary等)。
下面给出一个例子
class Name(object):
def __init__(self, name):
self.first_name = name.split(' ')[0]
self.last_name = name.split(' ')[1]
self.full_name = name
class Phone(object):
def __init__(self, phone):
self.phone = phone
class NewAddBookEntry(object):
def __init__(self, nm, ph):
self.name = Name(nm) # 创建 Name 实例
self.full_name = self.name.full_name
self.first_name = self.name.first_name
self.last_name = self.name.last_name
self.phone = Phone(ph) # 创建 Phone 实例
print('Created instance for: Mr.', self.last_name, '--', self.full_name)
if __name__ == '__main__':
new_entry = NewAddBookEntry('James Harden', '18012345678')
输出结果
Created instance for: Mr. Harden -- James Harden
NewAddrBookEntry类由它自身和其它类组合而成。这就在一个类和其它组成类之间定义了一种“有一个”(has-a)的关系。比如,我们的NewAddrBookEntry类“有一个”Name类实例和一个Phone实例。
创建复合对象就可以实现这些附加的功能,并且很有意义,因为这些类都不相同。每一个类管理它们自己的名字空间和行为。不过当对象之间有更接近的关系时,派生的概念可能对我的程序来说更有意义,特别是当我需要一些相似的对象,但却有少许不同功能的时候。
10. 子类和派生
当类之间有显著的不同,并且(较小的类)是较大的类所需要的组件时,组合表现得很好,但当我们设计“相同的类担忧一些不同的功能”时,派生就是一个更加合理的选择了。
OOP的更强大功能之一是能够使用一个已经定义好的类,扩展它或者对其进行修改,而不会影响系统中使用现存类的其他代码片段。OOD允许类特征在子孙类或子类中进行继承。这些子类从基类(或称祖先类、超类)继承它们的核心属性。而且,这些派生可能会扩展到多代。从同一个父类派生出来的这些类(或者是在类树图中水平相邻)是同胞关系。父类和所有高层类都被认为是祖先。
创建子类
创建子类的语法看起来与普通(新式)类没有区别,一个类名,后跟一个或多个需要从其中派生的父类:
class SubClassName(ParentClass1[, ParentClass2, ...]):
"""optional class documentation string"""
class_suite
如果你的类没有从任何祖先类派生,可以使用object作为父类的名字。经典类的声明唯一不同之处在于其没有从祖先类派生——此时,没有圆括号:
class ClassicClassWithouSuperclasses:
pass
至此,我们已经看到了一些类和子类的例子,下面还有一个简单的例子:
class Parent(object): # 调用父类
def parentMethod(self):
print('calling parent method')
class Child(Parent): # 调用子类
def childMethod(self):
print('calling parent method')
p = Parent() # 父类的实例
p.parentMethod()
calling parent method
c = Child()
c.childMethod()
calling parent method
c.parentMethod()
calling parent method
>>> p = Parent() # 父类的实例
>>> p.parentMethod()
calling parent method
>>> c = Child() # 子类的实例
>>> c.childMethod() # 子类调用它的方法
calling parent method
>>> c.parentMethod() # 子类调用父类的方法
calling parent method
11. 继承
继承描述了基类的属性如何“遗传”给派生类。一个子类可以继承它的基类的任何属性,不管是数据属性还是方法。
举个例子如下。P是一个没有属性的简单类。C从P继承而来(因此是它的子类),也没有属性:
class P(object):
pass
class C(P):
pass
>>> c = C()
>>> c.__class__
<class '__main__.C'>
>>> C.__bases__
(<class '__main__.P'>,)
因为P没有属性,C没有继承到什么。下面我们给P添加一些属性:
class P:
"""P class"""
def __init__(self):
print('Created an instance of', self.__class__.__name__)
class C(P):
pass
现在所创建的P有文档字符串(__doc__)和构造器,当我们实例化P时它被执行,如下面的交互会话所示:
>>> p = P()
Created an instance of P
>>> p.__class__
<class '__main__.P'>
>>> P.__bases__
(<class 'object'>,)
“created an instance”是由__init__()直接输出的。我们也可以显示更多关于父类的信息。我们现在来实例化C,展示__init__()(构造)方法在执行过程中是如何继承的:
>>> c = C()
Created an instance of C
>>> c.__class__
<class '__main__.C'>
>>> C.__bases__
(<class '__main__.P'>,)
>>> C.__doc__
>>> c.__doc__
C没有声明__init__()方法,然而在类C的实例c被创建时,还是会有输出信息。原因在于C继承了P的__init__()。__bases__元祖列出了其父类P。需要注意的是文档字符串对类,函数/方法,还有模块来说都是唯一的,所以特殊属性__doc__不会从基类中继承过来。
11.1 __bases__类属性
对任何(子)类,它是一个包含其父类(patent)的集合的元祖。注意,我们明确指出“父类”是相对所有基类(它包括了所有祖先类)而言的。
在Python2.x中,那些没有父类的类,它们的__bases__属性为空。
在Python3.x中,取消了经典类,仅保留新式类。
python2.x
# -*- coding:utf-8 -*-
# !/usr/bin/env python2
class A(object): # 声明新式类
pass
class B(): # 声明经典类
pass
class C: # 声明经典类
pass
>>> A.__bases__
(<type 'object'>,)
>>> B.__bases__
()
>>> C.__bases__
()
python3.x
# -*- coding:utf-8 -*-
# !/usr/bin/env python3
class A(object):
pass
class B():
pass
class C:
pass
>>> A.__bases__
(<class 'object'>,)
>>> B.__bases__
(<class 'object'>,)
>>> C.__bases__
(<class 'object'>,)
class A(object):
pass
class B(A):
pass
class C(B):
pass
class D(B, A):
pass
交互结果
>>> A.__bases__
(<class 'object'>,)
>>> C.__bases__
(<class '__main__.B'>,)
>>> D.__bases__
(<class '__main__.B'>, <class '__main__.A'>)
在上面的例子中,尽管C是A和B的子类(通过B传递继承关系),但C的父类是B,这从它的声明中可以看出,所以,只有B会在C.__bases__中显示畜类。另一方面,D是从两个类A和B中继承而来的。
class E(A, B):
pass
TypeError: Cannot create a consistent method resolution
order (MRO) for bases A, B
11.2 通过集成覆盖方法
我们在P中再写一个函数,然后在其子类中对它进行覆盖。
class P(object):
def foo(self):
print('Hi, Iam P-foo()')
>>> p = P()
>>> p.foo()
Hi, Iam 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
尽管C继承了P的foo()方法,但因为C定义了它自己的foo()方法,所以P中的foo()(Override)。覆盖方法的原因之一是,你的子类可能需要这个方法具有特定或不同的功能。所以,你接下来的问题肯定是:“我还能否调用那个被我覆盖的基类方法呢?”
答案是肯定的,但是这时就需要你去调用一个未绑定的基类方法,明确给出子类的实例,例如下边:
>>> P.foo(c)
Hi, Iam P-foo()
注意,我们上面已经有了一个P的实例p,但上面的这个例子并没有用它。我们不需要P的实例调用P的方法,因为已经有一个P的子类的实例c可用。典型情况下,你不会以这种方式调用父类方法,你会在子类的重写方法里显示地调用基类方法。
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, Iam P-foo()
Hi, I am C-foo()
重写__init__不会自动调用基类的__init__
类似于上面的覆盖非特殊方法,当从一个带构造器 __init()__ 的类派生,如果你不去覆盖 __init__(),它将会被继承并自动调用。但如果你在子类中覆盖了 __init__(),子类被实例化时,基类的 __init__() 就不会被自动调用。
class P(object):
def __init__(self):
print("Calling Ps constructor")
class C(P):
def __init__(self):
print("Calling C's consructor")
class P(object):
def __init__(self):
print("Calling Ps constructor")
class C(P):
def __init__(self):
print("Calling C's consructor")
>>> c = C()
Calling C's consructor
如果你还想调用基类的 __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")