Python 核心编程(第二版)——面向对象编程
1.介绍
如何创建一个类?关键字是class,紧接着是一个类名。随后是定义类的类体代码。ex:
class MyNewObjectType(bases): 'define MyNewObjectType class' class_suite #类体
新式类和经典类声明的最大不同在于,所有新式类必须继承至少一个父类,参数bases可以是一个(单继承)或多个(多重继承)用于继承的父类。
object 是“所有类之母”。如果你的类没有继承任何其他父类,object 将作为默认的父类。它位于所有类继承结构的最上层。如果你没有直接或间接的子类化一个对象,那么你就定义了一个经典类:
class MyNewObjectType: 'define MyNewObjectType classic class' class_suite
如果你没有指定一个父类,或者如果所子类化的基本类没有父类,你这样就是创建了一个经典类。
创建一个实例的过程称作实例化,过程如下(注意:没有使用new 关键字):
myFirstObject = MyNewObjectType()
类名使用我们所熟悉的函数操作符(()),以“函数调用”的形式出现。然后你通常会把这个新建的实例赋给一个变量。赋值在语法上不是必须的,但如果你没有把这个实例保存到一个变量中,它就没用了,会被自动垃圾收集器回收,因为任何引用指向这个实例。
注意有的地方在语法构成上需要有一行语句,但实际上不需要做任何操作,这时候可以使用pass语句。
方法——在Python中,方法定义在类定义中,但只能被实例所调用。也就是说,调用一个方法的最终途径必须是这样的:(1)定义类(和方法),(2)创建一个实例(3)最后一步,用这个实例调用方法。
class MyDataWithMethod(object): # 定义类 def printFoo(self): # 定义方法 print 'You invoked printFoo()!'
注意:self 参数,它在所有的方法声明中都存在,这个参数代表实例对象本身。
在Python 中,Python 创建实例后,在实例化过程中,调用__init__()方法,当一个类被实例化时,就可以定义额外的行为。
1 创建一个类(类定义) 2 class AddrBookEntry(object): # 类定义 3 'address book entry class' 4 5 def __init__(self, nm, ph): # 定义构造器 6 self.name = nm # 设置 name 7 self.phone = ph # 设置 phone 8 print 'Created instance for:', self.name 9 10 def updatePhone(self, newph): # 定义方法 11 self.phone = newph 12 print 'Updated phone# for:', self.name
创建实例(实例化) >>> john = AddrBookEntry('John Doe', '408-555-1212') #为John Doe 创建实例 >>> jane = AddrBookEntry('Jane Doe', '650-555-1212') #为Jane Doe 创建实例
实例化调用,它会自动调用__init__()。self 把实例对象自动传入__init__()。你可以在脑子里把方法中的self 用实例名替换掉。如果不存在默认的参数,那么传给__init__()的两个参数在实例化时是必须的。
创建子类——靠继承来进行子类化是创建和定制新类类型的一种方式,新的类将保持已存在类所有的特性,而不会改动原来类的定义(指对新类的改动不会影响到原来的类,译者注)。对于新的类类型来说,这个新的子类可以定制只属于它的特定功能。
如果需要,每个子类最好定义它自己的构造器,不然,基类的构造器会被调用。然而,如果子类重写基类的构造器,基类的构造器就不会被自动调用了--这样,基类的构造器就必须显式写出才会被执行。
核心笔记:命名类、属性和方法
类名通常由大写字母打头。这是标准惯例,可以帮助你识别类,特别是在实例化过程中(有时看起来像函数调用)。还有,数据属性(译者注:变量或常量)听起来应当是数据值的名字,方法名应当指出对应对象或值的行为。另一种表达方式是:数据值应该使用名词作为名字,方法使用谓词(动词加对象)。数据项是操作的对象、方法应当表明程序员想要在对象进行什么操作。Python 规范推荐使用骆驼记法的下划线方式。
2. 面向对象编程
面向对象编程思想(OOP)、面向对象设计(object-oriented design,OOD)
抽象/实现:抽象指对现实世界问题和实体的本质表现,行为和特征建模,建立一个相关的子集,可以用于描绘程序结构,从而实现这种模型。抽象不仅包括这种模型的数据属性,还定义了这些数据的接口。对某种抽象的实现就是对此数据及与之相关接口的现实化(realization)。现实化这个过程对于客户程序应当是透明而且无关的。
封装/接口:封装描述了对数据/信息进行隐藏的观念,它对数据属性提供接口和访问函数。
合成:合成扩充了对类的描述,使得多个不同的类合成为一个大的类,来解决现实问题。合成描述了一个异常复杂的系统,比如一个类由其它类组成,更小的组件也可能是其它的类,数据属性及行为,所有这些合在一起,彼此是“有一个”的关系。
派生/继承/继承结构:派生描述了子类的创建,新类保留已存类类型中所有需要的数据和行为,但允许修改或者其它的自定义操作,都不会修改原类的定义。继承描述了子类属性从祖先类继承这样一种方式。
泛华/特化:泛化表示所有子类与其父类及祖先类有一样的特点,所以子类可以认为同祖先类是“是一个”的关系,因为一个派生对象(实例)是祖先类的一个“例子”。
多态:多态的概念指出了对象如何通过他们共同的属性和动作来操作及访问,而不需考虑他们具体的类。多态表明了动态(又名,运行时)绑定的存在,允计重载及运行时类型确定和验证。
自省/反射:自省表示给予你,程序员,某种能力来进行像“手工类型检查”的工作,它也被称为反射。这个性质展示了某对象是如何在运行期取得自身信息的。
3. 类
在Python 中,类声明与函数声明很相似,头一行用一个相应的关键字,接下来是一个作为它的定义的代码体,如下所示:
def functionName(args): 'function documentation string' #函数文档字符串 function_suite #函数体 class ClassName(object): 'class documentation string' #类文档字符串 class_suite #类体
二者都允许你在他们的声明中创建函数,闭包或者内部函数(即函数内的函数),还有在类中定义的方法。最大的不同在于你运行函数,而类会创建一个对象。类就像一个Python 容器类型。类还允许派生。你可以创建一个子类,它也是类,而且继续了父类所有的特征和属性。
创建类:Python 类使用class 关键字来创建。简单的类的声明可以是关键字后紧跟类名:
class ClassName(bases): 'class documentation string' #'类文档字符串' class_suite #类体
基类是一个或多个用于继承的父类的集合;类体由所有声明语句,类成员定义,数据属性和函数组成。类通常在一个模块的顶层进行定义,以便类实例能够在类所定义的源代码文件中的任何地方被创建。
声明与定义:对于Python 函数来说,声明与定义类没什么区别,因为他们是同时进行的,定义(类体)紧跟在声明(含class 关键字的头行[header line])和可选(但总是推荐使用)的文档字符串后面。同时,所有的方法也必须同时被定义。
4. 类属性
什么是属性呢?属性就是属于另一个对象的数据或者函数元素,可以通过我们熟悉的句点属性标识法来访问。属性的一个有趣的地方是,当你正访问一个属性时,它同时也是一个对象,拥有它自己的属性,可以访问,这导致了一个属性链。类属性仅与其被定义的类相绑定。
类的数据属性——数据属性仅仅是所定义的类的变量。它们可以像任何其它变量一样在类创建后被使用,并且,要么是由类中的方法来更新,要么是在主程序其它什么地方被更新。
静态变量(静态数据)表示这些数据是与它们所属的类对象绑定的,不依赖于任何类实例。
决定类的属性——要知道一个类有哪些属性,有两种方法。最简单的是使用dir()内建函数。另外是通过访问类的字典属性__dict__,这是所有类都具备的特殊属性之一。
特殊的类属性
特殊类属性 | |
C.__name__ | 类C的名字(字符串) |
C.__doc__ | 类C的文档字符串 |
C.__bases__ | 类C的所有父类构成的元组 |
C.__dict__ | 类C的属性 |
C.__module__ | 类C定义所在的模块(1.5 版本新增) |
C.__class__ | 实例C对应的类(仅新式类中) |
__name__是给定类的字符名字。它适用于那种只需要字符串(类对象的名字),而非类对象本身的情况。甚至一些内建的类型也有这个属性。
__doc__是类的文档字符串,与函数及模块的文档字符串相似,必须紧随头行(header line)后的字符串。文档字符串不能被派生类继承,也就是说派生类必须含有它们自己的文档字符串。
__bases__用来处理继承,它包含了一个由所有父类组成的元组。
__dict__属性包含一个字典,由类的数据属性组成。访问一个类属性的时候,Python 解释器将会搜索字典以得到需要的属性。如果在__dict__中没有找到,将会在基类的字典中进行搜索,采用“深度优先搜索”顺序。基类集的搜索是按顺序的,从左到右,按其在类定义时,定义父类参数时的顺序。对类的修改会仅影响到此类的字典;基类的__dict__属性不会被改动的。
Python 支持模块间的类继承。引入__module__,这样类名就完全由模块名所限定。
5. 实例
>>> class MyClass(object): # define class 定义类 ... pass >>> mc = MyClass() # instantiate class 初始化类
仅调用("calling")类:MyClass(),就创建了类MyClass 的实例mc。返回的对象是你所调用类的一个实例。
__init__() "构造器" 方法——当类被调用,实例化的第一步是创建实例对象。一旦对象创建了,Python 检查是否实现了__init__()方法。默认情况下,如果没有定义(或覆盖)特殊方法__init__(),对实例不会施加任何特别的操作.任何所需的特定操作,都需要程序员实现__init__(),覆盖它的默认行为。如果__init__()没有实现,则返回它的对象,实例化过程完毕。如果__init__()已经被实现,那么它将被调用,实例对象作为第一个参数(self)被传递进去,像标准方法调用一样。调用类时,传进的任何参数都交给了__init__()。
与__init__()相比,__new__()方法更像一个真正的构造器。为何我们认为__new__()比__init__()更像构造器呢?这是因为__new__()必须返回一个合法的实例,这样解释器在调用__init__()时,就可以把这个实例作为self 传给它。调用父类的__new__()来创建对象,正像其它语言中使用new 关键字一样。
有一个相应的特殊解构器(destructor)方法名为__del__()。然而,由于Python 具有垃圾对象回收机制(靠引用计数),这个函数要直到该实例对象所有的引用都被清除掉后才会执行。
要注意,解构器只能被调用一次,一旦引用计数为0,则对象就被清除了。这非常合理,因为系统中任何对象都只被分配及解构一次。
总结:
不要忘记首先调用父类的__del__()。
调用 del x 不表示调用了x.__del__() -----前面也看到,它仅仅是减少x 的引用计数。
如果你有一个循环引用或其它的原因,让一个实例的引用逗留不去,该对象的__del__()可
能永远不会被执行。
__del__()未捕获的异常会被忽略掉(因为一些在__del__()用到的变量或许已经被删除了)。
不要在__del__()中干与实例没任何关系的事情。
除非你知道你正在干什么,否则不要去实现__del__()。
如果你定义了__del__,并且实例是某个循环的一部分,垃圾回收器将不会终止这个循环—
—你需要自已显式调用del。
核心笔记:跟踪实例
Python 没有提供任何内部机制来跟踪一个类有多少个实例被创建了,或者记录这些实例是些什么东西。如果需要这些功能,你可以显式加入一些代码到类定义或者__init__()和__del__()中去。最好的方式是使用一个静态成员来记录实例的个数。靠保存它们的引用来跟踪实例对象是很危险的,因为你必须合理管理这些引用,不然,你的引用可能没办法释放(因为还有其它的引用)
6. 实例属性
设置实例的属性可以在实例创建后任意时间进行,也可以在能够访问实例的代码中进行。构造器__init()__是设置这些属性的关键点之一。
核心笔记:实例属性
能够在“运行时”创建实例属性,是Python 类的优秀特性之一,Python 不仅是动态类型,而且在运行时,允许这些对象属性的动态创建。一个缺陷是,属性在条件语句中创建,如果该条件语句块并未被执行,属性也就不存在。
在构造器中首先设置实例属性——构造器是最早可以设置实例属性的地方,因为__init__()是实例创建后第一个被调用的方法。一旦__init__()执行完毕,返回实例对象,即完成了实例化过程。
默认参数提供默认的实例安装——在实际应用中,带默认参数的__init__()提供一个有效的方式来初始化实例。需要明白一点,默认参数应当是不变的对象;像列表(list)和字典(dictionary)这样的可变对象可以扮演静态数据,然后在每个方法调用中来维护它们的内容。
__init__()应当返回None——如果定义了构造器,它不应当返回任何对象,因为实例对象是自动在实例化调用后返回的。相应地,__init__()就不应当返回任何对象(应当为None);否则,就可能出现冲突,因为只能返回实例。
内建函数dir()可以显示类属性,同样还可以打印所有实例属性。
特殊的实例属性 | |
I.__class__ | 实例化I的类 |
I.__dict__ | I的属性 |
__dict__属性由一个字典组成,包含一个实例的所有属性。键是属性名,值是属性相应的数据值。字典中仅有实例属性,没有类属性或特殊属性。
核心风格:修改__dict__
对类和实例来说,尽管__dict__属性是可修改的,但还是建议你不要修改这些字典,除非你知道你的目的。这些修改可能会破坏你的OOP,造成不可预料的副作用。使用熟悉的句点属性标识来访问及操作属性会更易于接受。需要你直接修改__dict__属性的情况很少,其中之一是你要重载__setattr__特殊方法。
内建类型属性——对内建类型也可以使用dir(),与任何其它对象一样,可以得到一个包含它属性名字的列表。在内建类型中,不存在这个__dict__属性。
实例属性 VS 类属性——类属性仅是与类相关的数据值,和实例属性不同,类属性和实例无关。这些值像静态成员那样被引用,即使在多次实例化中调用类,它们的值都保持不变。不管如何,静态成员不会因为实例而改变它们的值,除非实例中显式改变它们的值。
类和实例都是名字空间。类是类属性的名字空间,实例则是实例属性的。可采用类来访问类属性,如果实例没有同名的属性的话,你也可以用实例来访问。
类属性可通过类或实例来访问。
从实例中访问类属性须谨慎——与通常Python 变量一样,任何对实例属性的赋值都会创建一个实例属性(如果不存在的话)并且对其赋值。如果类属性中存在同名的属性,有趣的副作用即产生。(经典类和新式类都存在)。给一个与类属性同名的实例属性赋值,我们会有效地“隐藏”类属性,但一旦我们使用(del语句)删除了这个实例属性,类属性又重见天日。
类属性持久性——静态成员,如其名所言,任凭整个实例(及其属性)的如何进展,它都不理不采(因此独立于实例)。同时,当一个实例在类属性被修改后才创建,那么更新的值就将生效。类属性的修改会影响到所有的实例。
核心提示:使用类属性来修改自身(不是实例属性)
使用实例属性来试着修改类属性是很危险的。原因在于实例拥有它们自已的属性集,在Python 中没有明确的方法来指示你想要修改同名的类属性,修改类属性需要使用类名,而不是实例名。
7. 绑定和方法调用
方法仅仅是类内部定义的函数。(这意味着方法是类属性而不是实例属性)。其次,方法只有在其所属的类拥有实例时,才能被调用。最后,任何一个方法定义中的第一个参数都是变量self,它表示调用此方法的实例对象。
核心笔记:self 是什么?
self 变量用于在类实例方法中引用方法所绑定的实例。因为方法的实例在任何方法调用中总是作为第一个参数传递的,self 被选中用来代表实例。你必须在方法声明中放上self(你可能已经注意到了这点),但可以在方法中不使用实例(self)。如果你的方法中没有用到self , 那么请考虑创建一个常规函数,除非你有特别的原因。毕竟,你的方法代码没有使用实例,没有与类关联其功能,这使得它看起来更像一个常规函数。在其它面向对象语言中,self 可能被称为 this。
调用绑定方法——方法,不管绑定与否,都是由相同的代码组成的。唯一的不同在于是否存在一个实例可以调用此方法。记得self 在每一个方法声明中都是作为第一个参数传递的。当你在实例中调用一个绑定的方法时,self 不需要明确地传入了。这算是"必须声明self 作为第一个参数"对你的报酬。当你还没有一个实例并且需要调用一个非绑定方法的时候你必须传递self 参数。
调用非绑定方法——调用非绑定方法并不经常用到。需要调用一个还没有任何实例的类中的方法的一个主要的场景是:你在派生一个子类,而且你要覆盖父类的方法,这时你需要调用那个父类中想要覆盖掉的构造方法。
8. 静态方法和类方法
一对内建函数被引入,用于将作为类定义的一部分的某一方法声明“标记”(tag),“强制类型转换”(cast)或者“转换”(convert)为这两种类型的方法之一。通常的方法需要一个实例(self)作为第一个参数,并且对于(绑定的)方法调用来说,self 是自动传递给这个方法的。而对于类方法而言,需要类而不是实例作为第一个参数,它是由解释器传给方法。
9. 组合
一个类被定义后,目标就是要把它当成一个模块来使用,并把这些对象嵌入到你的代码中去,同其它数据类型及逻辑执行流混合使用。有两种方法可以在你的代码中利用类。第一种是组合(composition)。就是让不同的类混合并加入到其它类中,来增加功能和代码重用性。你可以在一个大点的类中创建你自已的类的实例,实现一些其它属性和方法来增强对原来的类对象。另一种方法是通过派生。
10. 子类和派生
OOP 的更强大方面之一是能够使用一个已经定义好的类,扩展它或者对其进行修改,而不会影响系统中使用现存类的其它代码片段。OOD 允许类特征在子孙类或子类中进行继承。这些子类从基类(或称祖先类,超类)继承它们的核心属性。而且,这些派生可能会扩展到多代。在一个层次的派生关系中的相关类(或者是在类树图中垂直相邻)是父类和子类关系。从同一个父类派生出来的这些类(或者是在类树图中水平相邻)是同胞关系。父类和所有高层类都被认为是祖先。
创建子类——创建子类的语法看起来与普通(新式)类没有区别,一个类名,后跟一个或多个需要从其中派生的父类:
class SubClassName (ParentClass1[, ParentClass2, ...]): 'optional class documentation string' class_suite
如果你的类没有从任何祖先类派生,可以使用object 作为父类的名字。经典类的声明唯一不同之处在于其没有从祖先类派生---此时,没有圆括号:
class ClassicClassWithoutSuperclasses: pass
11. 继承
继承描述了基类的属性如何“遗传”给派生类。一个子类可以继承它的基类的任何属性,不管是数据属性还是方法。
需要注意的是文档字符串对类,函数/方法,还有模块来说都是唯一的,所以特殊属性__doc__不会从基类中继承过来。
__bases__类属性,对任何(子)类,它是一个包含其父类(parent)的集合的元组。注意,我们明确指出“父类”是相对所有基类(它包括了所有祖先类)而言的。那些没有父类的类,它们的__bases__属性为空。
通过继承覆盖(Overriding)方法——super()不但能找到基类方法,而且还为我们传进self。
核心笔记:重写__init__不会自动调用基类的__init__
当从一个带构造器 __init()__的类派生,如果你不去覆盖__init__(),它将会被继承并自动调用。但如果你在子类中覆盖了__init__(),子类被实例化时,基类的__init__()就不会被自动调用。Python 使用基类名来调用类方法,对应在JAVA 中,是用关键字super 来实现的,这就是super()
1 class P(object): 2 def __init__(self): 3 print "calling P's constructor" 4 5 class C(P): 6 def __init__(self): 7 print "calling C's constructor" 8 9 class C(P): 10 def __init__(self): 11 P.__init__(self) 12 print "calling C's constructor" 13 >>> c = C() 14 calling P's constructor 15 calling C's constructor 16 17 class C(P): 18 def __init__(self): 19 super(C, self).__init__() 20 print "calling C's constructor"
使用super()的重点,是你不需要明确提供父类。这意味着如果你改变了类继承关系,你只需要改一行代码(class 语句本身)而不必在大量代码中去查找所有被修改的那个类的名字。
多重继承——当使用多重继承时,有两个不同的方面要记住。首先,还是要找到合适的属性。另一个就是当你重写方法时,如何调用对应父类方法以“发挥他们的作用”,同时,在子类中处理好自己的义务。
核心笔记:Python 2.2 使用一种唯一但不完善的MRO
在2.2 版本中,算法基本思想是根据每个祖先类的继承结构,编译出一张列表,包括搜索到的类,按策略删除重复的。然而,在Python 核心开发人员邮件列表中,有人指出,在维护单调性方面失败过(顺序保存),必须使用新的C3 算法替换,也就是从2.3 版开始使用的新算法。
经典类使用的解释顺序,深度优先,从左至右。新式方法的查询:它首先查找同胞兄弟,采用一种广度优先的方式。新式类也有一个__mro__属性,告诉你查找顺序是怎样。
12. 类、实例和其他对象的内建函数
issubclass() 布尔函数判断一个类是另一个类的子类或子孙类。它有如下语法:
issubclass(sub, sup)
issubclass() 返回True 的情况:给出的子类sub 确实是父类sup 的一个子类(反之,则为False)。这个函数也允许“不严格”的子类,意味着,一个类可视为其自身的子类,所以,这个函数如果当sub 就是sup,或者从sup 派生而来,则返回True。(一个“严格的”子类是严格意义上的从一个类派生而来的子类。)
从Python 2.3 开始,issubclass()的第二个参数可以是可能的父类组成的tuple(元组),这时,只要第一个参数是给定元组中任何一个候选类的子类时,就会返回True。
isinstance() 布尔函数在判定一个对象是否是另一个给定类的实例时,非常有用。它有如下语法:
isinstance(obj1, obj2)
isinstance()在obj1 是类obj2 的一个实例,或者是obj2 的子类的一个实例时,返回True(反之,则为False)。
注意:第二个参数应当是类;不然,你会得到一个TypeError。但如果第二个参数是一个类型对象,则不会出现异常。这是允许的,因为你也可以使用isinstance()来检查一个对象obj1 是否是obj2 的类型。
同issubclass()一样,isinstance()也可以使用一个元组(tuple)作为第二个参数。如果第一个参数是第二个参数中给定元组的任何一个候选类型或类的实例时,就会返回True。
hasattr()函数是Boolean 型的,它的目的就是为了决定一个对象是否有一个特定的属性,一般用于访问某属性前先作一下检查。getattr()和setattr()函数相应地取得和赋值给对象的属性,getattr()会在你试图读取一个不存在的属性时,引发AttributeError 异常,除非给出那个可选的默认参数。setattr()将要么加入一个新的属性,要么取代一个已存在的属性。而delattr()函数会从一个对象中删除属性。
dir():
dir()作用在实例上(经典类或新式类)时,显示实例变量,还有在实例所在的类及所有它的基类中定义的方法和类属性。
dir()作用在类上(经典类或新式类)时,则显示类以及它的所有基类的__dict__中的内容。但它不会显示定义在元类(metaclass)中的类属性。
dir()作用在模块上时,则显示模块的__dict__的内容。(这没改动)。
dir()不带参数时,则显示调用者的局部变量。(也没改动)。
对于那些覆盖了__dict__或__class__属性的对象,就使用它们;出于向后兼容的考虑,如果已定义了__members__和__methods__,则使用它们。
super():super()函数在Python2.2 版本新式类中引入。这个函数的目的就是帮助程序员找出相应的父类,然后方便调用相关的属性。一般情况下,程序员可能仅仅采用非绑定方式调用祖先类方法。使用super()可以简化搜索一个合适祖先的任务,并且在调用它时,替你传入实例或类型对象。
文档解释顺序(MRO),用于在祖先类中查找属性。对于每个定义的类,都有一个名为__mro__的属性,它是一个元组,按照他们被搜索时的顺序,列出了备搜索的类。语法如下:
super(type[, obj])
给出type,super()“返回此type 的父类”。如果你希望父类被绑定,你可以传入obj 参数(obj必须是type 类型的).否则父类不会被绑定。obj 参数也可以是一个类型,但它应当是type 的一个子类。通常,当给出obj 时:
如果 obj 是一个实例,isinstance(obj,type)就必须返回True
如果 obj 是一个类或类型,issubclass(obj,type)就必须返回True
事实上,super()是一个工厂函数,它创造了一个super object,为一个给定的类使用__mro__去查找相应的父类。super() 的主要用途, 是来查找父类的属性。
vars():vars()内建函数与dir()相似,只是给定的对象参数都必须有一个__dict__属性。vars()返回一个字典,它包含了对象存储于其__dict__中的属性(键)及值。如果提供的对象没有这样一个属性,则会引发一个TypeError 异常。如果没有提供对象作为vars()的一个参数,它将显示一个包含本地名字空间的属性(键)及其值的字典,也就是,locals()。
类,实例及其它对象的内建函数 | |
内建函数 | 描述 |
issubclass(sub, sup) | 如果类sub 是类sup 的子类,则返回True,反之,为False。 |
isinstance(obj1, obj2) |
如果实例obj1 是类obj2 或者obj2 子类的一个实例;或者如果obj1是obj2 的类型,则返回True;反之,为False。 |
hasattr(obj, attr) |
如果obj 有属性attr(用字符串给出),返回True,反之,返回表类,实例及其它对象的内建函数(续) |
内建函数 | 描述 |
getattr(obj, attr[, default]) | 获取obj 的attr 属性;与返回obj.attr 类似;如果attr不是obj 的属性,如果提供了默认值,则返回默认值;不然,就会引发一个AttributeError 异常。 |
setattr(obj, attr, val) | 设置obj 的attr 属性值为val,替换任何已存在的属性值;不然,就创建属性;类似于obj.attr=val |
delattr(obj, attr) | 从obj 中删除属性attr(以字符串给出);类似于delobj.attr。 |
dir(obj=None) | 返回obj 的属性的一个列表;如果没有给定obj,dir()则显示局部名字空间空间中的属性,也就是locals().keys() |
super(type, obj=None) |
返回一个表示父类类型的代理对象;如果没有传入obj,则返 回的super 对象是非绑定的;反之,如果obj 是一个type , issubclass(obj,type) 必为True ; 否则,isinstance(obj,type)就必为True。 |
vars(obj=None) |
返回obj 的属性及其值的一个字典;如果没有给出obj,vars()显示局部名字空间字典(属性及其值),也就是locals()。 |
13. 用特殊方法定制类
方法的两个重要方面:首先,方法必须在调用前被绑定(到它们相应类的某个实例中);其次,有两个特殊方法可以分别作为构造器和析够器的功能,分别名为__init__()和__del__()。
用来定制类的特殊方法 | |
特殊方法 | 描述 |
基本定制型 | |
C.__init__(self[, arg1, ...]) | 构造器(带一些可选的参数) |
C.__new__(self[, arg1, ...]) | 构造器(带一些可选的参数);通常用在设置不变数据类型的子类。 |
C.__del__(self) | 解构器 |
C.__str__(self) | 可打印的字符输出;内建str()及print 语句 |
C.__repr__(self) | 运行时的字符串输出;内建repr() 和‘‘ 操作符 |
C.__unicode__(self) | Unicode 字符串输出;内建unicode() |
C.__call__(self, *args) | 表示可调用的实例 |
C.__nonzero__(self) | 为object 定义False 值;内建bool() (从2.2 版开始) |
C.__len__(self) | “长度”(可用于类);内建len() |
对象(值)比较 | |
C.__cmp__(self, obj) | 对象比较;内建cmp() |
C.__lt__(self, obj) and | 小于/小于或等于;对应<及<=操作符 |
C.__gt__(self, obj) and | 大于/大于或等于;对应>及>=操作符 |
C.__eq__(self, obj) and | 等于/不等于;对应==,!=及<>操作符 |
属性 | |
C.__getattr__(self, attr) | 获取属性;内建getattr();仅当属性没有找到时调用 |
C.__setattr__(self, attr, val) | 设置属性 |
C.__delattr__(self, attr) | 删除属性 |
C.__getattribute__(self, attr) | 获取属性;内建getattr();总是被调用 |
C.__get__(self, attr) | (描述符)获取属性 |
C.__set__(self, attr, val) | (描述符)设置属性 |
C.__delete__(self, attr) | (描述符)删除属性 |
定制类/模拟类型 | |
数值类型:二进制操作符 | |
C.__*add__(self, obj) | 加;+操作符 |
C.__*sub__(self, obj) | 减;-操作符 |
C.__*mul__(self, obj) | 乘;*操作符 |
C.__*div__(self, obj) | 除;/操作符 |
C.__*truediv__(self, obj) | True 除;/操作符 |
C.__*floordiv__(self, obj) | Floor 除;//操作符 |
C.__*mod__(self, obj) | 取模/取余;%操作符 |
C.__*divmod__(self, obj) | 除和取模;内建divmod() |
C.__*pow__(self, obj[, mod]) | 乘幂;内建pow();**操作符 |
C.__*lshift__(self, obj) | 左移位;<<操作符 |
特殊方法 | 描述 |
定制类/模拟类型 | |
数值类型:二进制操作符 | |
C.__*rshift__(self, obj) | 右移;>>操作符 |
C.__*and__(self, obj) | 按位与;&操作符 |
C.__*or__(self, obj) | 按位或;|操作符 |
C.__*xor__(self, obj) | 按位与或;^操作符 |
数值类型:一元操作符 | |
C.__neg__(self) | 一元负 |
C.__pos__(self) | 一元正 |
C.__abs__(self) | 绝对值;内建abs() |
C.__invert__(self) | 按位求反;~操作符 |
数值类型:数值转换 | |
C.__complex__(self, com) | 转为complex(复数);内建complex() |
C.__int__(self) | 转为int;内建int() |
C.__long__(self) | 转为long;内建long() |
C.__float__(self) | 转为float;内建float() |
数值类型:基本表示法(String) | |
C.__oct__(self) | 八进制表示;内建oct() |
C.__hex__(self) | 十六进制表示;内建hex() |
数值类型:数值压缩 | |
C.__coerce__(self, num) | 压缩成同样的数值类型;内建coerce() |
C.__index__(self) | 在有必要时,压缩可选的数值类型为整型(比如:用于切片索引等等) |
序列类型 | |
C.__call__(self, *args) | 序列中项的数目 |
C.__getitem__(self, ind) | 得到单个序列元素 |
C.__setitem__(self, ind,val) | 设置单个序列元素 |
C.__delitem__(self, ind) | 删除单个序列元素 |
特殊方法 | 描述 |
序列类型 | |
C.__getslice__(self, ind1,ind2) | 得到序列片断 |
C.__setslice__(self, i1, i2,val) | 设置序列片断 |
C.__delslice__(self, ind1,ind2) | 删除序列片断 |
C.__contains__(self, val) | 测试序列成员;内建in 关键字 |
C.__*add__(self,obj) | 串连;+操作符 |
C.__*mul__(self,obj) | 重复;*操作符 |
C.__iter__(self) | 创建迭代类;内建iter() |
映射类型 | |
C.__len__(self) | mapping 中的项的数目 |
C.__hash__(self) | 散列(hash)函数值 |
C.__getitem__(self,key) | 得到给定键(key)的值 |
C.__setitem__(self,key,val) | 设置给定键(key)的值 |
C.__delitem__(self,key) | 删除给定键(key)的值 |
C.__missing__(self,key) | 给定键如果不存在字典中,则提供一个默认值 |
14. 私有化
Python 为类元素(属性和方法)的私有性提供初步的形式。由双下划线开始的属性在运行时被“混淆”,所以直接访问是不允许的。实际上,会在名字前面加上下划线和类名。把类名加上后形成的新的“混淆”结果将可以防止在祖先类或子孙类中的同名冲突。这种名字混淆的另一个目的,是为了保护__XXX 变量不与父类名字空间相冲突。如果在类中有一个__XXX 属性,它将不会被其子类中的__XXX 属性覆盖。
15. 授权
“包装”在Python 编程世界中经常会被提到的一个术语。它是一个通用的名字,意思是对一个已存在的对象进行包装,不管它是数据类型,还是一段代码,可以是对一个已存在的对象,增加新的,删除不要的,或者修改其它已存在的功能。
授权是包装的一个特性,可用于简化处理有关dictating 功能,采用已存在的功能以达到最大限度的代码重用。
记住,属性可以是数据属性,还可以是函数或方法。
创建时间,修改时间,及访问时间是文件的几个常见属性。创建时间(或'ctime')是实例化的时间,修改时间(或'mtime')指的是核心数据升级的时间[通常会调用新的set()方法],而访问时间(或'atime')是最后一次对象的数据值被获取或者属性被访问时的时间戳。
16. 新式类的通用特性
isinstance()内建函数:
1 OLD (not as good): 2 if type(obj) == type(0)… 3 if type(obj) == types.IntType… 4 5 BETTER: 6 if type(obj) is type(0)… 7 8 EVEN BETTER: 9 if isinstance(obj, int)… 10 if isinstance(obj, (int, long))… 11 if type(obj) is int…
记住:尽管isinstance()很灵活,但它没有执行“严格匹配”比较----如果obj 是一个给定类型的实例或其子类的实例,也会返回True。但如果想进行严格匹配,你仍然需要使用is 操作符。
__dict__属性跟踪所有实例属性。字典会占据大量内存,如果你有一个属性数量很少的类,但有很多实例,那么正好是这种情况。为内存上的考虑,用户现在可以使用__slots__属性来替代__dict__。
基本上,__slots__是一个类变量,由一序列型对象组成,由所有合法标识构成的实例属性的集合来表示。它可以是一个列表,元组或可迭代对象。也可以是标识实例能拥有的唯一的属性的简单字符串。任何试图创建一个其名不在__slots__中的名字的实例属性都将导致AttributeError 异常。
这种特性的主要目的是节约内存。其副作用是某种类型的"安全",它能防止用户随心所欲的动态增加实例属性。带__slots__属性的类定义不会存在__dict__了。
Python 类有一个名为__getattr__()的特殊方法,它仅当属性不能在实例的__dict__或它的类(类的__dict__),或者祖先类(其__dict__)中找到时,才被调用。 __getattribute__()使用起来,类似__getattr__(),不同之处在于,当属性被访问时,它就一直都可以被调用,而不局限于不能找到的情况。如果类同时定义了__getattribute__()及__getattr__()方法,除非明确从__get-attribute__()调用,或__getattribute__()引发了AttributeError 异常,否则后者不会被调用。为了安全地访问任何它所需要的属性,你总是应该调用祖先类的同名方法。
描述符是Python 新式类中的关键点之一。它为对象属性提供强大的API。你可以认为描述符是表示对象属性的一个代理。当需要属性时,可根据你遇到的情况,通过描述符(如果有)或者采用常规方式(句点属性标识法)来访问它。
__get__(),__set__(),__delete__()特殊方法,,这三个特殊方法充当描述符协议的作用。__get__()可用于得到一个属性的值,__set__()是为一个属性进行赋值的,在采用del 语句(或其它,其引用计数递减)明确删除掉某个属性时会调__delete__()方法。
使用描述符的顺序很重要,有一些描述符的级别要高于其它的。整个描述符系统的心脏是__getattribute__(),因为对每个属性的实例都会调用到这个特殊的方法。这个方法被用来查找类的属性,同时也是你的一个代理,调用它可以进行属性的访问等操作。
优先级别的排序:
类属性(最高的优先级)
数据描述符(__get__() 和 __set__())
实例属性(__dict__())
非数据描述符
默认为__getattr__()
通常,函数是非绑定的。虽然静态方法是在类中被定义的,它也是非绑定的。但方法必须绑定到一个实例上,类方法必须绑定到一个类上,对不?一个函数对象的描述符可以处理这些问题,描述符会根据函数的类型确定如何“封装”这个函数和函数被绑定的对象,然后返回调用对象。它的工作方式是这样的:函数本身就是一个描述符,函数的__get__()方法用来处理调用对象,并将调用对象返回给你。
属性是一种有用的特殊类型的描述符。“一般”情况下,当你使用点属性符号来处理一个实例属性时,其实你是在修改这个实例的__dict__属性。
property()内建函数有四个参数,它们是 :
property(fget=None, fset=None, fdel=None, doc=None)
property()的一般用法是,将它写在一个类定义中,property()接受一些传进来的函数(其实是方法)作为参数。实际上,property()是在它所在的类被创建时被调用的,这些传进来的(作为参数的)方法是非绑定的,所以这些方法其实就是函数!
在你写的类定义中创建描述符方法的一个弊端是它会搞乱类的名字空间。解决这个问题的办法:
“借用”一个函数的名字空间
编写一个用作内部函数的方法作为 property()的(关键字)参数
(用 locals())返回一个包含所有的(函数/方法)名和对应对象的字典
把字典传入 property(),然后
去掉临时的名字空间
这样,方法就不会再把类的名字空间搞乱了,因为定义在内部函数中的这些方法属于其它的名字空间。由于这些方法所属的名字空间已超出作用范围,用户是不能够访问这些方法的,所以通过使用属性property()来访问属性就成为了唯一可行的办法。
元类(Metaclasses):元类是来定义某些类是如何被创建的,从根本上说,赋予我们如何创建类的控制权。元类一般用于创建类。在执行类定义时,解释器必须要知道这个类的正确的元类。解释器会先寻找类属性__metaclass__,如果此属性存在,就将这个属性赋值给此类作为它的元类。如果此属性没有定义,它会向上查找父类中的__metaclass__。 所有新风格的类如果没有任何父类,会从对象或类型中继承。(type (object) 当然是类型)。如果还没有发现__metaclass__属性,解释器会检查名字为__metaclass__的全局变量,如果它存在,就使用它作为元类。否则, 这个类就是一个传统类,并用types.ClassType 作为此类的元类。(注意:在这里你可以运用一些技巧... 如果你定义了一个传统类,并且设置它的__metaclass__ =type,其实你是在将它升级为一个新风格的类!)
在执行类定义的时候,将检查此类正确的(一般是默认的)元类,元类(通常)传递三个参数(到构造器):类名,从基类继承数据的元组,和(类的)属性字典。
最重要的请铭记在心的是,元类的最终使用者不是用户,正是程序员自己。你通过定义一个元类来“迫使”程序员按照某种方式实现目标类,这将既可以简化他们的工作,也可以使所编写的程序更符合特定标准。
创建一个新风格的类或传统类的通用做法是使用系统自己所提供的元类的默认方式。
17. 相关模块和文档
与类相关的模块 | |
模块 | 说明 |
UserList | 提供一个列表对象的封装类 |
UserDict | 提供一个字典对象的封装类 |
UserString | 提供一个字符串对象的封装类;它又包括一个MutableString 子类,如果有需要,可以提供有关功能 |
types | 定义所有Python 对象的类型在标准Python 解释器中的名字 |
operator | 标准操作符的函数接口 |