面向对象编程
编程的发展已经从简单控制流中按步的指令序列进入到更有组织的方式中,依靠代码块可以形成命名子程序和完成既定的功能。结构化的或过程性编程可以让我们把程序组织成逻辑快,以便重复或重用。创造程序的过程变得更具逻辑性;选出的行为要符合规范,才可以约束创建的数据。迪特尔父子认为结构化编程是“面向行为”的,因为事实上,即使没有任何行为的数据也必须“规定”逻辑性。
然而,如果我们能对数据加上动作呢?如果我们所创建和编写的数据片段,是真实生活中实体的模型,内嵌数据体和动作呢?我们通过一系列已定义的接口(又称存取函数集合)访问数据属性,我们就有一个“对象”系统,从大的方面来看,每一个对象既可以与自身进行交互,也可以与其他对象进行交互。
面向对象编程踩上了进化的步伐,增强了结构化编程,实现了数据与动作的融合:数据层和逻辑层由一个可用以创建这些对象的简单抽象层来描述。
面向对象设计(OOD)与面向对象编程(OOP)
OOD不会特别要求支持OO的语言,事实上,OOD可以由纯结构化语言来实现,比如C,但如果想构造具备对象性质和特点的数据类型,就需要在程序上多做努力,如果一门语言内建OO特性,那么OOP开发就会更加方便高效。
另一方面,一门面向对象的语言不一定会强制写OO方面的程序。例如CPP被认为更好的C。
现实世界中的问题
用OOD的思维来抽象问题,是因为它直接提供建模和解决世界问题及情形的途径。我们按照现实生活中物体或者事件的行为来定义类和实例的行为和属性。
常用术语
- 抽象/实现
抽象是指对现实世界问题和实体的本质表现、行为和特征建模,建立一个相关的自己,可以用于描述程序结构,从而实现这种模型。
对某种抽象的实现就是对此数据及与之相关接口的现实化。
- 封装/接口
封装描述了对数据信息进行隐藏的观念,它对数据属性提供接口和访问函数。客户端不需要知道封装后数据属性是如何组织的,只对数据提供相应接口一面客户程序通过不规范的操作来存取封装的数据属性。
- 合成
合成描述了一个异常复杂的系统,比如一个类由其他类组成,更小的组件也可能是其他的类,数据属性及行为,所有这些合在一起,称之为合成。
- 派生/继承/继承结构
派生描述了子类的创建,新类保留已存类类型中所余姚的数据和行为,但允许修改或者其他的自定义操作,都不会修改原类的定义。
继承描述了子类属性从祖先类继承这样一种方式。
继承结构表示多“代”派生,可以描述成一个“族谱”。
- 泛化/特化
泛化便是所有子类与其父类及祖先类有一样的特点,所以子类可以认为同祖先类“是一个”(is-a)的关系,因为一个派生对象(实例)是祖先类的一个“例子”。
特化描述所有子类的自定义,也就是,一些属性让它与其祖先类不同。
- 多态
多态的概念指出了对象如何通过他们共同的属性和动作来操作及访问,而不需要考虑他们具体的类。多态表明了动态(后来又称运行时)绑定的存在,允许重载及运行时类型确定和验证。
- 自省/反射
自省表示给予你某种能力来进行像“手工类型检查”的工作,它也被称为反射。
类
类是一种数据结构,可以用它来定义对象,后者把数据值和行为特性融合在一起。
Python中,类声明与函数声明很相似:
class ClassName(object): ‘class documentation string‘ class_suite
类像一个Python的容器类型。尽管类本身是一个对象,但正被定义的时候,它们还不是对象的实现。类的实例才是对象的实现。
声明与定义类没有什么区别,他们是同时进行的,定义紧跟在声明和可选的文档字符串后面。同时,所有的方法也IXUS同时被定义。
- 类属性
属性就是属于另外一个对象的数据或者函数元素,可以通过我们熟悉的句点属性标识符来访问。一些Python类型有数据属性,一些有函数属性(方法)。
有些属性本身也是对象,拥有自己的属性,可以访问,导致一个属性连。
类属性仅与其被定义的类相绑定,并且因为实例对象在日常OOP中用得最多,实例数据属性是会一直用到的主要数据属性。类的数据属性仅当需要有更加“静态”数据类型时才变得有用。
类的数据属性
数据属性是所定义的类的变量。它们可以像任何其他变量一样在类创建后被使用,并且,要么是由类中的方法来更新,要么是在主程序其他地方更新。
类的数据属性是一种静态数据属性。在介绍实例属性时,会详细对比这些。
- 方法
>>> class MyClass(object): def myMethod(self): pass >>> mc = MyClass() >>> mc.myMethod()
上述代码中,myMethod就是MyClass的方法。通过句点属性标识符与它的实例绑定。
如果直接调用:
>>> myMethod() Traceback (most recent call last): File "<pyshell#6>", line 1, in <module> myMethod() NameError: name ‘myMethod‘ is not defined
引发了NameError,因为在全局名字中,没有这样的函数存在。
如果是由类对象调用:
>>> MyClass.myMethod() Traceback (most recent call last): File "<pyshell#7>", line 1, in <module> MyClass.myMethod() TypeError: unbound method myMethod() must be called with MyClass instance as first argument (got nothing instead)
会引发TypeError异常。
- 绑定(绑定及非绑定方法)
为与OOP惯例保持一致,Python严格要求,没有实例,方法是不能被调用的。这种限制即Python所描述的绑定概念(binding),方法必须绑定(到一个实例)才能直接被调用。非绑定的方法可能可以被调用,但实例对象一定要明确给出,才能确保调用成功。
- 决定类的属性
要知道一个类有哪些属性,有两种方法,一种是使用dir()内建函数,另外是访问类的字典属性__dict__,这是所有类都有的特殊属性之一。
>>> class MyClass(object): ‘MyClass class definition‘ myVersion = ‘1.1‘ def showMyVersion(self): print MyClass.myVersion >>> dir(MyClass) [‘__class__‘, ‘__delattr__‘, ‘__dict__‘, ‘__doc__‘, ‘__format__‘, ‘__getattribute__‘, ‘__hash__‘, ‘__init__‘, ‘__module__‘, ‘__new__‘, ‘__reduce__‘, ‘__reduce_ex__‘, ‘__repr__‘, ‘__setattr__‘, ‘__sizeof__‘, ‘__str__‘, ‘__subclasshook__‘, ‘__weakref__‘, ‘myVersion‘, ‘showMyVersion‘] >>> MyClass.__dict__ dict_proxy({‘__module__‘: ‘__main__‘, ‘showMyVersion‘: <function showMyVersion at 0x02ABAEF0>, ‘__dict__‘: <attribute ‘__dict__‘ of ‘MyClass‘ objects>, ‘myVersion‘: ‘1.1‘, ‘__weakref__‘: <attribute ‘__weakref__‘ of ‘MyClass‘ objects>, ‘__doc__‘: ‘MyClass class definition‘})
从上面可以看到,dir()放回的仅是对象的属性的一个名字列表,而__dict__返回的一个字典,key是属性名,value是属性对象的数据值。
- 特殊的类的属性
C.__name__ | 类C的名字(字符串) |
C.__doc__ | 类C的文档字符串 |
C.__bases__ | 类C的所有父类构成的元组 |
C.__dict__ | 类C的属性 |
C.__module__ | 类C定义所在的模块(1.5 版本新增) |
C.__subclasshook__ | 实例C对应的类(仅新式类中) |
实例
如果说类是一种数据结构定义类型,那么实例则声明了一个这种类型的变量。
也就是说实例时有声明的类。
- 初始化
通过调用类对象来创建实例
很多OO语言都提供new关键字,通过new可以创建类的实例。Python的方法更简单,一旦定义一个类,通过使用函数操作符就可以实例化类:
>>> class MyClass(object): >>> mc = MyClass()
当我"call"一个类的时候,解释器就会实例化该对象,并且调用Python所拥有与构造函数最相近的东西来执行最终的定制工作,最后将这个实例返回给你。
- __init__()“构造器”方法
当类被调用,实例化的第一步是创建实例对象。创建实例对象是调用__init__()方法。默认情况下,如果没有定义(或覆盖)特殊方法__init__(),对实例不会施加任何特别的操作。
- __new__()构造器方法
与__init__()相比,__new__()方法更像一个真正的构造器。类型和类在Python2.2就统一了,所以需要一种途径来实例化不可变对象,比如派生字符串、数字等。
在这种情况下,解释器则调用类的__new__()方法,一个静态方法,并且传入的参数是在类实例化操作时生成的。__new__()会调用父类的__new__()来创建对象。
为什么__new__()比__init__()更像构造器呢?这是因为__new__()必须返回一个合法的实例,这样解释器调用__init__()时,就可以把这个实例作为self传给它。调用父类的__new__()来创建对象,正像其它语言中使用new关键字一样。
- __del__()解构器方法
有一个相应的书结构器方法名为__del__()。然而,由于Python具有垃圾对象回收机制,这个函数要知道该实例对象所有的引用都被清除掉后才会执行。Python中的解构器是在实例释放前提供特殊处理功能的方法,通常没有被实现,因为实例很少显式释放。
实例属性
- 实例化实例属性
设置实例的属性可以在实例创建后任意时间进行,也能在访问实例的代码中进行。
能够在运行时创建实例属性,是Python类的优秀特性之一,Python不仅是动态类型,而且在运行时,允许这些对象属性的动态创建。当然这样的属性必须谨慎。
一个缺陷是,属性在条件语句中创建,如果该条件语句块未被执行,属性也不存在。
- 查看实例属性
用内建函数dir()可以显示类属性,同时用__dict__属性也可以查看实例属性的字典。
- 内建类型属性
内建类型也是类,所以对内建类型也可以使用dir(),与其他任何类型对象一样,可以得到一个包含它属性名字的表:
>>> x = 3+0.14j >>> x.__class__ <type ‘complex‘> >>> dir(x) [‘__abs__‘, ‘__add__‘, ‘__class__‘, ‘__coerce__‘, ‘__delattr__‘, ‘__div__‘, ‘__divmod__‘, ‘__doc__‘, ‘__eq__‘, ‘__float__‘, ‘__floordiv__‘, ‘__format__‘, ‘__ge__‘, ‘__getattribute__‘, ‘__getnewargs__‘, ‘__gt__‘, ‘__hash__‘, ‘__init__‘, ‘__int__‘, ‘__le__‘, ‘__long__‘, ‘__lt__‘, ‘__mod__‘, ‘__mul__‘, ‘__ne__‘, ‘__neg__‘, ‘__new__‘, ‘__nonzero__‘, ‘__pos__‘, ‘__pow__‘, ‘__radd__‘, ‘__rdiv__‘, ‘__rdivmod__‘, ‘__reduce__‘, ‘__reduce_ex__‘, ‘__repr__‘, ‘__rfloordiv__‘, ‘__rmod__‘, ‘__rmul__‘, ‘__rpow__‘, ‘__rsub__‘, ‘__rtruediv__‘, ‘__setattr__‘, ‘__sizeof__‘, ‘__str__‘, ‘__sub__‘, ‘__subclasshook__‘, ‘__truediv__‘, ‘conjugate‘, ‘imag‘, ‘real‘]
我们知道x是复数属性后,可以访问它的数据属性
>>> x.imag 0.14 >>> x.real 3.0 >>> x.conjugate() (3-0.14j) >>> x.__dict__ Traceback (most recent call last): File "<pyshell#23>", line 1, in <module> x.__dict__ AttributeError: ‘complex‘ object has no attribute ‘__dict__‘
但是访问__dict__会失败,因为内建类型中不存在这个属性。
实例属性 VS 类属性
类属性仅是与类相关的数据值,和实例属性不同,类属性和实例无关。这些值像静态成员那样被引用,即使再多次实例化中调用类,他们的值都保持不变。除非实例中显式改变它们的值,否则不会因为实例而改变它们的值。
类和实例都是名字空间,类是类属性的名字空间,实例则是实例属性的。
1.访问类属性
类属性可通过类或实例访问。
>>> class Class(object): version = 1.0 >>> c = Class() # 实例化 >>> Class.version # 通过类来访问 1.0 >>> c.version # 通过实例来访问 1.0 >>> c.version += 0.1 # 通过实例来更新 >>> c.version # 实例访问已经更新 1.1 >>> Class.version # 并没有改变类访问的值 1.0 >>> Class.version += 0.5 #此时更新类访问的值 >>> Class.version # 类访问 1.5 >>> c.version # 实例访问也没有改变 1.1 >>> d = Class() # 实例化 >>> d.version # 实例访问 1.5 >>> Class.version += 0.5 # 类更新 >>> Class.version # 类访问已更新 2.0 >>> d.version # 实例访问已更新 2.0
2.从实例中访问类属性须谨慎
与通常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
如果我们把这个实例属性删掉,会怎么样呢?
>>> del foo.x >>> foo.x 1.5
很神奇吧
所以,给一个类属性同名的实例属性赋值,我们会有效地“隐藏”类属性,但一旦我们删除了实例属性,类属性又重建天日。
接下来我们通过另一个例子探讨另一个有趣的现象:
>>> class Foo(object): x = {2003: ‘poe‘} >>> foo = Foo() >>> foo.x {2003: ‘poe‘} >>> foo.x[2004] = ‘valid path‘ >>> foo.x {2003: ‘poe‘, 2004: ‘valid path‘} >>> Foo.x {2003: ‘poe‘, 2004: ‘valid path‘} >>> del foo.x Traceback (most recent call last): File "<pyshell#8>", line 1, in <module> del foo.x AttributeError: ‘Foo‘ object attribute ‘x‘ is read-only
我们知道,假如类属性是不可变对象,我们对实例属性修改是不会改变类属性的值的,但是,当类属性是可变对象时,比如说字典。我们修改类属性就等同于修改了实例属性,因为我们没有改变它引用的形式。所以实例属性在修改后没有“遮盖”类属性,也就不能被删除。
3.类属性持久性
静态成员,顾名思义,任凭整个实例如何进展,它都独立于实例。同时,当一个实例在类属性被修改后才创建,那么更新的值就将生效。类属性的修改会影响所有的实例,而实例的修改不影响类。
这一点,之前的例子里已经有提到,不予赘述。