面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。
面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。
在Python中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class)的概念。
类最终解释了面向对象编程思想(OOP)
类与实例相互关联着:类是对象的定义,而实例是"真正的实物",它存放了类中所定义的对象的具体信息。
下面的示例展示了如何创建一个新式类:
class MyNewObjectType(bases):'define MyNewObjectType class'class_suite #类体
关键字是 class,紧接着是一个类名。随后是定义类的类体代码。这里通常由各种各样的定义和声明组成。新式类和经典类声明的最大不同在于,所有新式类必须继承至少一个父类,参数 bases可以是一个(单继承)或多个(多重继承)用于继承的父类。而经典类不需要继承。
object 是“所有类之母”。如果你的类没有继承任何其他父类,object 将作为默认的父类。它位于所有类继承结构的最上层。如果你没有直接或间接的子类化一个对象,那么你就定义了一个经典类:
class MyNewObjectType:'define MyNewObjectType classic class'class_suite
如果你没有指定一个父类,或者如果所子类化的基本类没有父类,你这样就是创建了一个经典类。很多 Python 类都还是经典类。即使经典类已经过时了,在以后的 Python 版本中,仍然可以使用它们。
创建一个实例的过程称作实例化,过程如下(注意:没有使用 new 关键字):
myFirstObject = MyNewObjectType()
类名使用我们所熟悉的函数操作符(()),以“函数调用”的形式出现。然后你通常会把这个新建的实例赋给一个变量。赋值在语法上不是必须的,但如果你没有把这个实例保存到一个变量中,它就没用了,会被自动垃圾收集器回收,因为任何引用指向这个实例。这样,你刚刚所做的一切,就是为那个实例分配了一块内存,随即又释放了它。
只要你需要,类可以很简单,也可以很复杂。最简单的情况,类仅用作名称空间(namespaces)。这意味着你把数据保存在变量中,对他们按名称空间进行分组,使得他们处于同样的关系空间中-----所谓的关系是使用标准 Python 句点属性标识。比如,你有一个本身没有任何属性的类,使用它仅对数据提供一个名字空间,这样的类仅作为容器对象来共享名字空间。
示例如下:
class MyData(object):pass
注意有的地方在语法构成上需要有一行语句,但实际上不需要做任何操作,这时候可以使用 pass语句。这种情况,必要的代码就是类体,但我们暂不想提供这些。上面定义的类没有任何方法或属性。下面我们创建一个实例,它只使用类作为名称空间容器。
>>>mathObj =MyData()
>>>mathObj.x= 4
>>>mathObj.y= 5
>>>mathObj.x+ mathObj.y
9
>>>mathObj.x * mathObj.y
20
我们当然也可以使用变量“x”,“y”来完成同样的事情,但本例中,实例名字 mathObj 将 mathObj.x和 mathObj.y 关联起来。这就是我们所说的使用类作为名字空间容器。mathObj.x 和 mathObj.y 是实例属性,因为它们不是类 MyData 的属性,而是实例对象(mathObj)的独有属性。本章后面,我们将看到这些属性实质上是动态的:你不需要在构造器中,或其它任何地方为它们预先声明或者赋值
我们改进类的方式之一就是给类添加功能。类的功能有一个更通俗的名字叫方法。在 Python中,方法定义在类定义中,但只能被实例所调用。也就是说,调用一个方法的最终途径必须是这样的:
(1)定义类(和方法),
(2)创建一个实例
(3)最后一步,用这个实例调用方法。
例如:
class MyDataWithMethod(object): # 定义类def printFoo(self):# 定义方法print 'You invoked printFoo()!'
你可能注意到了 self
参数,它在所有的方法声明中都存在。这个参数代表实例对象本身,当你用实例调用方法时,由解释器悄悄地传递给方法的,所以,你不需要自己传递 self 进来,因为它是自动传入的。
举例说明一下,假如你有一个带两参数的方法,所有你的调用只需要传递第二个参数,Python 把 self 作为第一个参数传递进来,如果你犯错的话,也不要紧。Python 将告诉你传入的参数个数有误。总之,你只会犯一次错,下一次———你当然就记得了!
现在我们来实例化这个类,然后调用那个方法:
>>> myObj = MyDataWithMethod() # 创建实例
>>> myObj.printFoo() # 现在调用方法
You invoked printFoo()!
对于已熟悉面向对象编程的人来说,__init__()
类似于类构造器。如果你初涉 OOP 世界,可以认为一个构造器仅是一个特殊的方法,它在创建一个新的对象时被调用。在 Python 中,__init__()
实际上不是一个构造器。你没有调用“new”来创建一个新对象。(Python 根本就没有“new”关键字)。
取而代之,Python 创建实例后,在实例化过程中,调用__init__()
方法,当一个类被实例化时,就可以定义额外的行为,比如,设定初始值或者运行一些初步诊断代码———主要是在实例被创建后,实例化调用返回这个实例之前,去执行某些特定的任务或设置
class AddrBookEntry(object): # 类定义'address book entry class'def __init__(self, nm, ph): # 定义构造器self.name = nm # 设置 nameself.phone = ph # 设置 phoneprint 'Created instance for:', self.namedef updatePhone(self, newph): # 定义方法self.phone = newphprint 'Updated phone# for:', self.name
在 AddrBookEntry 类的定义中,定义了两个方法:__init__()
和updatePhone()。__init__()
在实例化时被调用,即,在 AddrBookEntry()被调用时。你可以认为实例化是对__init__()
的一种隐式的调用,因为传给 AddrBookEntry()的参数完全与__init__()
接收到的参数是一样的(除了 self,它是自动传递的)。
>>> john = AddrBookEntry('John Doe', '408-555-1212') #为 John Doe 创建实例
>>> jane = AddrBookEntry('Jane Doe', '650-555-1212') #为 Jane Doe 创建实例
如果不存在默认的参数,那么传给__init__()
的两个参数在实例化时是必须的。
访问实例属性
>>> john
<__main__.AddrBookEntry instance at 80ee610>
>>> john.name
'John Doe'
>>> john.phone
'408-555-1212'
>>> jane.name
'Jane Doe'
>>> jane.phone
'650-555-1212'
>>> john.updatePhone('415-555-1212') #更新 John Doe 的电话
>>> john.phone
'415-555-1212'
靠继承来进行子类化是创建和定制新类类型的一种方式,新的类将保持已存在类所有的特性,而不会改动原来类的定义。对于新的类类型来说,
这个新的子类可以定制只属于它的特定功能。除了与父类或基类的关系外,子类与通常的类没有什么区别,也像一般类一样进行实例化。注意下面,子类声明中提到了父类:
class EmplAddrBookEntry(AddrBookEntry):'Employee Address Book Entry class'#员工地址本类def __init__(self, nm, ph, id, em):AddrBookEntry.__init__(self, nm, ph)self.empid = idself.email = emdef updateEmail(self, newem):self.email = newemprint 'Updated e-mail address for:', self.name
现在我们创建了第一个子类,EmplAddrBookEntry。Python 中,当一个类被派生出来,子类继承了基类的属性,所以,在上面的类中,我们不仅定义了__init__()
和updatEmail()方法,而且EmplAddrBookEntry 还从 AddrBookEntry 中继承了 updatePhone()方法。
如果需要,每个子类最好定义它自己的构造器,不然,基类的构造器会被调用。然而,如果子类重写基类的构造器,基类的构造器就不会被自动调用了–这样,基类的构造器就必须显式写出才会被执行,像我们上面那样,用 AddrBookEntry.__init__()
设置名字和电话号码。我们的子类在构造器后面几行还设置了另外两个实例属性:员工 ID 和 E-mail 地址。
注意,这里我们要显式传递 self 实例对象给基类构造器,因为我们不是在其实例中调用那个方法而是在一个子类实例中调用那个方法。因为我们不是通过实例来调用它,这种未绑定的方法调用需要传递一个适当的实例(self)给方法。
>>> john = EmplAddrBookEntry('John Doe', '408-555-1212',42, 'john@spam.doe')
Created instance for: John Doe #给 John Doe 创建实例
>>> john
<__main__.EmplAddrBookEntry object at 0x62030>
>>> john.name
'John Doe'
>>> john.phone
'408-555-1212'
>>> john.email
'john@spam.doe'
>>> john.updatePhone('415-555-1212') Updated phone# for: John Doe
>>> john.phone
'415-555-1212'
>>> john.updateEmail('john@doe.spam') Updated e-mail address for: John Doe
>>> john.email
'john@doe.spam'
像函数一样,Python中的类方法也是一种对象。由于既可以通过实例也可以通过类来访问方法,所以在Python里有两种风格:
两种方法都是对象,它们可以被传递、存入列表等待。两者运行时都需要一个实例作为第一参数(传一个self值),但当通过一个实例调用一个绑定方法时Python自动会提供一个。例如我们运行如下的代码:
class Test:def func(self,message):print messageobject1=Test()
x=object1.func
x('绑定方法对象,实例是隐含的')t=Test.func
t(object1,'未绑定的方法对象,需要传递一个实例') #t('未绑定的方法对象,需要传递一个实例') #错误的调用
object1=Test()生成一个实例,object1.func返回一个绑定的方法,把实例object1和方法func绑定。
Test.func是用类去引用方法,我们得到一个未绑定的方法对象。要调用它就得传一个实例参数,如t(object1,‘未绑定的方法对象,需要传递一个实例’) 。
大多数时候,我们都直接调用方法,所以一般不会注意到方法对象。但是如果开始写通用的调用对象的代码时,需要特别仔细地注意未绑定方法,它们需要地传一个实例参数
类名通常由大写字母打头。这是标准惯例,可以帮助你识别类,特别是在实例化过程中(有时看起来像函数调用)。还有,数据属性听起来应当是数据值的名字,方法名应当指出对应对象或值的行为。
另一种表达方式是:数据值应该使用名词作为名字,方法使用谓词(动词加对象)。数据项是操作的对象、方法应当表明程序员想要在对象进行什么操作。在上面我们定义的类中,遵循了这样的方针,数据值像“name”,“phone”和“email”,行为如“updatePhone”,“updateEmail”。这就是常说的“混合记法(mixedCase)”或“骆驼记法(或驼峰记法,camelCase)”。Python规范推荐使用骆驼记法的下划线方式,比如,“update_phone”,“update_email”。类也要细致命名,像“AddrBookEntry”,“RepairShop”等等就是很好的名字。
编程的发展已经从简单控制流中按步的指令序列进入到更有组织的方式中,依靠代码块可以形成命名子程序和完成既定的功能。结构化的或过程性编程可以让我们把程序组织成逻辑块,以便重复或重用。创建程序的过程变得更具逻辑性;选出的行为要符合规范,才可以约束创建的数据。迪特尔父子(这里指 DEITEL 系列书籍作者 Harvey M.Deitel 和 Paul James Deitel 父子)认为结构化编程是“面向行为”的,因为事实上,即使没有任何行为的数据也必须“规定”逻辑性。
然而,如果我们能对数据加上动作呢?如果我们所创建和编写的数据片段,是真实生活中实体的模型,内嵌数据体和动作呢?如果我们能通过一系列已定义的接口(又称存取函数集合)访问数据属性,像自动取款机卡或能访问你的银行帐号的个人支票,我们就有了一个“对象”系统,从大的方面来看,每一个对象既可以与自身进行交互,也可以与其它对象进行交互
面向对象编程踩上了进化的步伐,增强了结构化编程,实现了数据与动作的融合:数据层和逻辑层现在由一个可用以创建这些对象的简单抽象层来描述。现实世界中的问题和实体完全暴露了本质,从中提供的一种抽象,可以用来进行相似编码,或者编入能与系统中对象进行交互的对象中。类提供了这样一些对象的定义,实例即是这些定义的实现。二者对面向对象设计(object-oriented design,OOD)来说都是重要的,OOD 仅意味来创建你采用面向对象方式架构来创建系统。
面向对象设计(OOD)不会特别要求面向对象编程语言。事实上,OOD 可以由纯结构化语言来实现,比如 C,但如果想要构造具备对象性质和特点的数据类型,就需要在程序上作更多的努力。当一门语言内建 OO 特性,OO 编程开发就会更加方便高效。
另一方面,一门面向对象的语言不一定会强制你写 OO 方面的程序。例如 C++可以被认为“更好的 C”;而 Java,则要求万物皆类,此外还规定,一个源文件对应一个类定义。然而,在 Python 中,类和 OOP 都不是日常编程所必需的。尽管它从一开始设计就是面向对象的,并且结构上支持 OOP,但Python 没有限定或要求你在你的应用中写 OO 的代码。OOP 是一门强大的工具,不管你是准备进入,学习,过渡,或是转向 OOP,都可以任意支配。
考虑用 OOD 来工作的一个最重要的原因,在于它直接提供建模和解决现实世界问题和情形的途径。比如,让你来试着模拟一台汽车维修店,可以让你停车进行维修。我们需要建两个一般实体:处在一个“系统”中并与其交互的人类,和一个修理店,它定义了物理位置,用于人类活动。因为
前者有更多不同的类型,我将首先对它进行描述,然后描述后者。在此类活动中,一个名为 Person的类被创建以用来表示所有的人。Person 的实例可以包括消费者(Customer),技工(Mechanic),还可能是出纳员(Cashier)。这些实例具有相似的行为,也有独一无二的行为。比如,他们能用声音进行交流,都有 talk()方法,还有 drive_car()方法。不同的是,技工有 repair_car()方法,而出纳有 ring_sale()方法。技工有一个repair_certification 属性,而所有人都有一个 drivers_license属性。
最后,所有这些实例都是一个检查(overseeing)类 RepairShop 的参与者,后者具有一个叫operating_hours 的数据属性,它通过时间函数来确定何时顾客来修车,何时职员技工和出纳员来上班。RepairShop 可能还有一个 AutoBay 类,拥有 SmogZone,TireBrakeZone 等实例,也许还有一个叫GeneralRepair 的实例。
我们所编的 RepairShop 的一个关键点是要展示类和实例加上它们的行为是如何用来对现实生活场景建模的。同样,你可以把诸如机场,餐厅,晶蕊,医院,其至一个邮订音乐公司想像为类,它们完全具备各自的参与者和功能性。
对于已熟悉有关 OOP 术语的朋友来说,看 Python 中是怎么称呼的:
抽象指对现实世界问题和实体的本质表现,行为和特征建模,建立一个相关的子集,可以用于描绘程序结构,从而实现这种模型。抽象不仅包括这种模型的数据属性,还定义了这些数据的接口。对某种抽象的实现就是对此数据及与之相关接口的现实化(realization)。现实化这个过程对于客户程序应当是透明而且无关的。
封装描述了对数据/信息进行隐藏的观念,它对数据属性提供接口和访问函数。通过任何客户端直接对数据的访问,无视接口,与封装性都是背道而驰的,除非程序员允许这些操作。作为实现的一部分,客户端根本就不需要知道在封装之后,数据属性是如何组织的。
在 Python 中,所有的类属性都是公开的,但名字可能被“混淆”了,以阻止未经授权的访问,但仅此而已,再没有其他预防措施了。这就需要在设计时,对数据提供相应的接口,以免客户程序通过不规范的操作来存取封装的数据属性
合成扩充了对类的描述,使得多个不同的类合成为一个大的类,来解决现实问题。合成描述了一个异常复杂的系统,比如一个类由其它类组成,更小的组件也可能是其它的类,数据属性及行为,所有这些合在一起,彼此是“有一个”的关系。比如,RepairShop“有一个”技工(应该至少有一个
吧),还“有一个”顾客(至少一个)。
这些组件要么通过联合关系组在一块,意思是说,对子组件的访问是允许的(对 RepairShop 来说,顾客可能请求一个 SmogCheck,客户程序这时就是与 RepairShop 的组件进行交互),要么是聚合在一起,封装的组件仅能通过定义好的接口来访问,对于客户程序来说是透明的。
继续我的例子,客户程序可能会建立一个 SmogCheck 请求来代表顾客,但不能够同 RepairShop 的 SmogZone 部分进行交互,因为 SmogZone 是由 RepairShop 内部控制的,只能通过 smogCheckCar()方法调用。Python支持上述两种形式的合成。
派生描述了子类的创建,新类保留已存类类型中所有需要的数据和行为,但允许修改或者其它的自定义操作,都不会修改原类的定义。继承描述了子类属性从祖先类继承这样一种方式。从前面的例子中,技工可能比顾客多个汽车技能属性,但单独的来看,每个都“是一个”人,所以,不管对谁而言调用 talk()都是合法得,因为它是人的所有实例共有的。继承结构表示多“代”派生,可以描述成一个“族谱”,连续的子类,与祖先类都有关系。
泛化表示所有子类与其父类及祖先类有一样的特点,所以子类可以认为同祖先类是“是一个”的关系,因为一个派生对象(实例)是祖先类的一个“例子”。比如,技工“是一个”人,车“是一个”交通工具,等等。在上面我们间接提到的族谱图中,我们可以从子类到祖先类画一条线,表示“是一个”的关系。特化描述所有子类的自定义,也就是什么属性让它与其祖先类不同。
多态的概念指出了对象如何通过他们共同的属性和动作来操作及访问,而不需考虑他们具体的类。多态表明了动态(又名,运行时)绑定的存在,允许重载及运行时类型确定和验证。
不同功能的函数可以使用相同的函数名,这样就可以用一个函数名调用不同内容的函数。在面向对象方法中一般是这样表述多态性:向不同的对象发送同一条消息,不同的对象在接收时会产生不同的行为(即方法)。也就是说,每个对象可以用自己的方式去响应共同的消息。所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。
自省表示给予你,程序员,某种能力来进行像“手工类型检查”的工作,它也被称为反射。这个性质展示了某对象是如何在运行期取得自身信息的。如果传一个对象给你,你可以查出它有什么能力,这样的功能不是很好吗?这是一项强大的特性,在本章中,你会时常遇到。Python 支持某种形式的自省功能,dir()和 type()内建函数。请密切关注这些调用,还有那些特殊属性,像__dict__,__name__
及__doc__
。可能你对其中一些已经很熟悉了!
上一篇:OI生成随机题目数据,测试点集锦【C++】【有待完善】
下一篇:excel函数记录