Python2.6-原理之类和oop(上)
来自《python学习手册第四版》第六部分
一、oop:宏伟蓝图(26章)
在这之前的部分中,经常会使用“对象”这个词,其实,到目前为止都是以对象为基础的,在脚本中传递对象、用在表达式中和调用对象的方法等。不过要让代码真正归类于oo,那么对象一般也需要参与到所谓的继承层次中。这一章来探索python中的类:类是在python实现支持继承的新种类的对象的部件。类似面向对象程序设计的主要工具。oop提供的设计方法可以来分解代码,把代码的冗余度降至最低,并且通过定制现有的代码来编写新的程序,而不是在原处修改。
1、类建立使用一条新的语句:class语句。通过class定义的对象,看起来很像本书之前研究过的内置类型。事实上,类其实是只运用并扩展之前的一些想法。也就是类是一些函数的包,这些函数大量使用并处理内置对象类型。不过,类的设计是为了创建和管理心得对象,并且它们也支持继承;在python中oop完全是可选的,并且初学者不要求使用类。实际上,可以用简单的结构代替。因为使用类需要一些预先的规划,采用战术的人(希望短期出成品)一般使用简单的结构;而采用战略的人(长期产品开发)对类会更感兴趣一些。
2、类就是python的程序组成单元,就像函数和模块一样:类是封装逻辑和数据的另一种方式,实际上,类也定义新的命名空间,在很大程度上就像模块。类有三个独到之处:a、多重实例,每次调用一个类,就会产生一个独立命名空间的新对象,不同的对象之间互不干扰;b、通过继承进行定制,可以在类的外部重新定义其属性从而扩充这个类。更通用的,类可以建立命名空间的层次结构,而这种层次结构可以定义该结构中类创建的对象所使用的变量名;c、运算符重载。
3、属性继承搜索:相比cpp和java,python的oop理解和使用都很简单。实际上,python中大多数oop的都简化成了这个表达式:。这个是用来读取模块的属性,调用对象的方法等。然而,当对class语句产生的对象使用这种方式时,这个表达式会在python中启动搜索----搜索对象连接的树,来寻找attribute首次出现的对象。当类启用时,上边的表达式实际上等于下面的自然语言:造出attribute首次出现的地方,先搜索object,然后是该对象之上的所有类,由下至上,由左至右。也就是属性只是简单的搜索“树”而已,称这种搜索程序为继承,因为树中位值较低的对象继承了树中位值较高的对象拥有的属性。当从下至上进行搜索时,连接至树中的对象就是树种所有上层对象所对应一的所有属性的集合体,直到树的最顶端。
4、在python中,通过代码建立连接对象树,每次使用object.attribute表达式时,就是在运行期间去“爬树“,来搜索属性:
图解:类树,底端有两个实例(I1和 I2),在它上有个类(C1),而顶端有两个超类(C2和C3),所有这些对象都是命名空间(变量的封装),而继承就是由下至上搜索此树,来寻找属性名称所出现的最低的地方。代码隐含了这种树的形状。
上图中包含了5个对象树,而对象都标识为变量,这些对象全都有相应的属性,可进行搜索。更明确的说,此树把三个类的对象(椭圆的C1、C2、C3)和两个实例对象(矩形的 I1 和 I2)连接至继承搜索树。ps:在python对象模型中,类和通过类产生的实例是两种不同的对象类型:a、类,类的属性提供了行为(数据以及函数),所有从类产生的实例都继承该类的属性;b、实例,代表程序中具体的元素。就搜索树而言,实例从它的类继承属性,而类是从搜索树中所有比它更上一层的类中继承属性。
5、 较高的类成为超类,也叫做基类(C2、C3);较低的类成为了子类,也叫做派生类(C1)。超类提供了所有子类共享的行为,但是因为 搜索是由下而上,子类可能会在树中较低位置重新定义超类的变量名,从而覆盖超类定义的行为。在扩展这个概念之后,假设创建了图25-1(就是上面4的那个图)。然后定义:I2.w。这个代码会立即启用继承。这是一个object.attribute表达式,于是会触发树的搜索:python会查看I2和其上的对象来搜索属性w。并以厦门这个顺序搜索链接的对象:I2、C1、C2、C3。找到首个w之后就会停止搜索,找不到就出错。类中继承了四个属性:w、x、y、z。其他属性引用规则会循着树中其他路径进行:a、I1.x和I2.x两者都会在C1中找到x并停止搜索,因为C1比C2位置更低;b、I1.y和I2.y两者都会在C1中找到y,因为这里是y唯一出现的地方;c、I1.z和I2.z两者都会在C2中找到z,因为C2比C3更靠左侧;d、I2.name会找到I2中的name。不需要爬树。
6、 虽然类和实例是两种不同的对象类型,但在树中,几乎完全相同:每种类型的主要用途都是用来作为另一种类型的命名空间(变量的封装,也就是我们可以附加属性的地方)。因此,如果类和实例听起来像模版,那也是如此;然而,类树中的对象也有对其他命名空间对象的自动搜索连接,而类对应的是语句,并不是整个文件。类和实例的差异在于:类是一种产生实例的工厂;类和模版的另一个差异:内存中特定模块只有一个实例(所以得重载模块以取得其新代码);从操作的角度来看,类通常都有函数,而实例有其他版本的数据项,类的函数中使用了这些数据。
7、类方法调用:如果I2.w引用是一个函数调用,就是“调用C3.w函数处理I2”,也就是会自动将I2.w()调用映射为C3.w(I2),传入该实例作为继承的函数的第一个参数。事实上,每当我们调用附属于类的函数时,总会隐含着这个类的实例,这也就是称之为面向对象模型的一部分原因:当运算执行时,总是有个主体对象。python把隐含的实例传入方法中的第一个特殊的参数,习惯称之为self(在cpp和java中,self相当于this,但是python中的sekf一定是明确写出来的,使得属性的读取更为明显)。
8、编写类树,内容如下:a、每个class语句会生成一个新的类对象;b、每次类调用时,就会生成一个新的实例对象;c、实例自动链接至创建了这些实例的类;d、类连接至其超类的方式是,将超类列在类头部的括号内,其从左至右的顺序会决定树中的次序。 例如,要简历图25-1(上面4的图),可以用这种形式的代码(省略了class内部的内容):
这里通过运行三个class语句创建三个类对象,然后通过两次调用类C1,创建两个实例对象,就好像它是一个函数一样。实例记住了它们来自哪个类,类C1也记住了所列出的超类。在python中,如果class语句中小括号有一个以上的超类,它们由左至右的次序会决定超类搜索的顺序。所以这是很重要的:这决定了变量名的作用域。附加在实例上的属性只属于那些实例,但附加在类上的属性则由所有子类及其实例共享。在后面的代码中可以发现:a、属性通常是在class语句中通过赋值语句添加在类中,而不是潜入在函数的def语句内;b、属性通常是在类内,对传给函数的特殊参数(self),做赋值运算而添加在实例中。
9、接上8.类通过函数(在class中由def语句编写而成)为实例提供行为。因为这类嵌套的def会在类中对变量名进行赋值,实际效果就是把属性添加在了类对象之中,从而可以由所有实例和子类继承:
这样的环境中,def的语句没有特别之处,从操作角度看,当def出现在这种类的内部时,称之为方法,而且会自动接收第一个特殊参数(self),这个参数提供了被处理的实例的参照值。就像简单变量一样,类和实例属性并没有事先声明,而是在首次赋值时它的值才会存在。当方法对self属性进行赋值时,会创建惑修改类树底端实例内的属性,因为self自动引用正在处理的实例。事实上,类树中所有对象都不过是命名空间对象,我们可以通过恰当的变量名读取惑设置其任何属性。只要变量名C1和I2都位于代码的作用域内,写C1.setname和 I1.setname 等效的。目前来说,直到setname方法调用前,C1类都不会把name属性附加在实例之上。事实上,调用I1.setname前引用I1.name会产生未定义变量名的错误,如果类想确保像name这样的变量名一定会在其实例中设置,通常都会在构造时填好这个属性:
写好并继承后,每次从类产生实例时,python会自动调用名为__init__的方法。新实例会如往常那样传入__init__的self参数,而列在类调用小括号内的任何值会成为第二以及其后的参数。其效果就是在创建实例时初始化了这个实例,而不需要额外的方法调用。由于__init__方法的运行时机,也被称为构造函数。这是所谓的运算符重载方法这种较大类型方法中最常用的代表。这种方法会像往常一样在类树中被继承,而且在变量名开头和结尾都有两个下划线以使其变得特别。当能够支持通信操作的实例出现在对应的运算时,python就会自动运行它们,而且它们是使用简单方法调用最常用的替代方法。这类方法也是可选的:省略时,不支持这类运算。例如,实现集合交集,类可能会提供intersect的方法,或者重载表达式运算符&,也就是编写名为__and__的方法来处理所需要的逻辑。因为运算符机制让实例的用法和外观类似于内置类型,可以让有些类提供一致而自然的接口,从而可以与预期的内置类型的代码兼容。
10、从基本上说,类其实就是由函数和其他变量名所构成的包,很像模块。然后,我们从类得到的自动属性继承搜索,支持软件高层次的定制,而这是模块和函数做不到的。而且类提供了自然的结构,让代码可以把逻辑和变量名区域化,这样也有助于程序的调试。假设这里要写一个员工的数据库应用程序,作为一个oop程序员,可能会先写一个通用的超类:
一旦编写了这样的通用行为,就可以针对特定种类的员工进行定制,体现不同类型和一般情况的差异,例如工程师的薪资计算规则,就可以在子类中取代这个方法:
因为这里的computerSalary在类树的下面,会取代Employee中的通用版本。然后在简历员工所属的员工累种类的实例的时候,就能获取不同的行为了:
当想查看员工的薪资的时候,可根据创建这个对象的类来计算,这也是基于继承搜索的原理:
这是基于第4、16章介绍过的多态概念的又一实例,多态是只运算的意义取决于运算对象。这里computerSalary方法在调用前,会通过继承搜索在每个对象中找到。在其他应用中,多态可用于隐藏(封装)接口差异性。例如,处理数据流的程序可以写成预期有输入和输出方法的对象,而不关心那些方法实际在做什么:
将针对各种数据来源所需读取和写入方法接口定制的子类的实例传入后,都可以重用这个processor函数,无论什么时候都可以让它来处理所需使用的任何数据来源:
因为读取和写入方法的内部实现已经分解至某个独立的位置,修改这些代码不会与正在使用的代码产生冲突。实际上,processor函数本身也可以是类,让转换器的转换逻辑通过继承添加,并让读取器和写入器能够通过组合方式嵌入。
二、类代码编写基础(27章)
类有三个主要的不同之处,从最底层来看,类几乎就是命名空间,很像第5部分中的模块,但是和模块不同的是,类支持多个对象的产生、命名空间继承以及运算符重载。类与模块的区别在于,类是产生多个实例的工厂,反之,每个模块只有一个副本会导入某个程序中(事实上,必须调用reload来更新单个模块对象,反映出来对该模块的修改,这就是原因之一)。正如本部分介绍的,从某种程度上看,python的类和def及模块很相似,但是它在其他语言中用过的相比可能就大不同了。
1、类对象提供默认行为:执行class语句,就会得到类对象。类的主要特性有:a、class语句创建类对象并将其赋值给变量名,就像函数def语句,class语句也是可执行语句。执行时,会产生新的类对象,并将其赋值给class头部的变量名。此外,就像def应用,class语句一般是在其所在文件导入时执行的;b、class语句内的赋值语句会创建类的属性,就像模块文件一样,class语句内的顶层的赋值语句(不是在def内)会产生类对象中的属性。从技术角度看,class语句的作用域会变成类对象的属性的命名空间,就像模块的全局作用域一样。执行class语句后,类的属性可由变量名点号运算获取object.name;c、类属性提供对象的状态和行为,类对象的属性记录状态信息和行为,可由这个类所创建的所有实例共享,位于类中的函数def语句会生成方法,方法将会处理实例。
2、实例对象是具体的元素:调用类对象时,就得到了实例对象。类的实例内含的重点概要:a、像函数那样调用类对象会创建新的实例对象,每次类调用时,都会建立并返回新的实例对象。实例代表了程序领域中的具体元素;b、每个实例对象继承类的属性并获得了自己的命名空间,由类所创建的实例对象是新的命名空间。一开始是空的,但是会继承创建该实例的类对象内的属性;c、在方法内对self属性做赋值运算会产生每个实例自己的属性,在类方法函数内,第一个参数(self)会引用正处理的实例对象,对self的属性做赋值运算,会创建惑修改实例内的数据,而不是类的数据
3、例子:首先定义一个名为FirstClass的类,通过交互模式运行class语句:
这里是交互模式,不过一般情况下,应该是当其所在的模块文件导入时运行的。就像通过def建立的函数,这个类在python执行语句之前是不会存在的。和所有的复合语句一样,class开头一行会列出类的名称,后面接一个惑多个内嵌并且缩进的语句的主体。
4、接上面3,def也是赋值运算,这里是将函数对象赋值给变量名setdata,而且displat位于class范围内,因此会产生附加在类上的属性:FIrstClass.setdata和FirstClass.display。事实上,在类嵌套的代码块中顶层的赋值的任何变化名,都会变成类的属性。位于类中的函数称为方法,方法是普通def。在方法函数中,调用时,第一个参数自动接收隐含的实例对象:调用的主体。例如:
以此方式调用类时(注意小括号),会产生实例对象,也就是可读取类属性的命名空间。准确的说,这时候有三个对象:两个实例和一个类。其实是有三个链接命名空间。如图26-1所示,以oop观点来说,X是一个FirstClass对象,Y也是:
这两个实例一开始是空的,但是它们被连接到创建它们类。如果对实例以及类对象内的属性名称进行点号运算,python会通过继承搜索从类取得变量名(除非该变量名位于实例内):
x或 y 本身都没有setdata属性,为了寻找这个属性,python会顺着实例到类的连接搜索。这就是所谓的继承:继承是在属性点号运算时发生的,而且只与查找连接对象内的变量名有关。在FirstClass的setdata函数中,传入的值会赋给self.data。在方法中,self 会自动引用正在处理的实例(x或 y)所以赋值语句会把值存储在实例的命名空间,而不是类的命名空间(这是26-1中变量名data的创建的方式)。因为类会产生多个实例,方法必须经过self参数才能获取正在处理的实例,当调用类的display方法来打印self.data时,会发现每个实例的值都不同。另外,变量名display在 x 和 y 之内都相同,因为它是来自于(继承自)类的:
。ps:在每个实例内的data成员存储了不同对象类型(字符串和浮点数)。就像python中的其他事物,实例属性(有时候被称作成员)并没有声明。首次赋值后,实例就会存在,就像简单的变量。事实上,在调用setdata之前,就对某一实例调用display,则会触发未定义变量名的错误:data属性以set打他方法赋值前,是不会在内存中存在的。另一种正确判断这个模型动态方式的途径是,考虑一下我们可以在类的内部或外部修改实例属性。在类内时,通过方法内对self进行赋值运算;而在类外时,则可以通过对实例对象进行赋值运算:
虽然比较少见,通过在类方法函数外对变量名进行赋值运算,甚至可以在实例命名空间内产生全新的属性:
这样会增加一个名为anothername的新属性,实例对象 x 的任何类方法都可以使用它,也可以不使用它。类通常是通过对self参数进行赋值运算从而建立实例的所有属性的,但不是必须如此。程序可以取出、修改或创建其所引用的任何对象的属性。
5、类通过继承进行定制:类可以引入新组件(子类)来进行修改,而不是对现有组件进行原地的修改。由类产生的实例对象会继承该类的属性。python也可让类继承其他类,因而开启了编写类层次结构的方向,在阶层较低的地方覆盖现有的属性,让行为特定化。实际上,向层次的下端越深入,软件就会变得越特定。这里和模块并不一致:模块的属性存在于一个单一、平坦的命名空间之内(这个命名空间不接受定制化)。在python中,实例从类中继承,而类继承于超类。下面是继承机制的核心观点:a、超类列在了类开头的括号中,要继承另一个类的属性,把该类列在class语句开头的括号中就可以了。含有继承的类称为子类,而子类所继承的类就是其超类;b、类从其超类中继承属性,就像实例继承其类中所定义的属性名一样,类也会继承其超类中定义的所有属性名称。当读取属性时,如果它不存在于子类中,python会自动搜索这个属性;c、实例会继承所有可读取类的属性,每个实例会从创建它的类中获取变量名,此外,还有该类的超类。寻找变量名时,python会检查实例,然后是它的类,最后是所有超类;d、每个object.attribute都会开启新的独立搜索,python会对每个属性取出表达式进行对类树的独立搜索,这包括在class语句外对实例和类的引用(例如:X.attr),以及在类方法函数内对self实例参数属性的引用。方法中每个self.attr表达式都会开启对self及其上层的类的attr属性的搜索;e、逻辑的修改时通过创建子类,而不是修改超类。
6、这是第二个例子,建立在上面那个例子上,首先,定义个新的类SecondClass,继承FirstClass所有变量名,并提供其自己的一个变量名:
这里因为SecondClass中的变量名display覆盖了FirstClass中的display。把这种在树中较低处发生的重新定义的、取代属性的动作称为重载。结果就是SecondClass改变了方法display的行为,把FirstClass特定化了。另外SecondClass(以及任何实例)依然会继承FirstClass的setdata方法。用一个例子来说明:
在调用SecondClass创建了其实例对象。setdata依然是执行FirstClass中的版本,但是这一次display属性是来自SecondClass,并打印定制的内容。图26-2描绘了其中涉及的命名空间:
这里的SecondClass引入的专有化是在FirstClass外部完成的。也就是说,不会影响当前存在的或未来的FirstClass对象。
7、类是模块内的属性:类的名称没有什么神奇的地方,当class语句执行时,这真是赋值给对象的变量,而对象可以用任何普通表达式引用,例如,如果FIrstClass是写在模块文件内,而不是在交互模式下输入的,就可以将其导入,在类开头的那行可以正常的使用它的名称:
等效于:
类名称总是存在于模块中,所以还是要遵循第五部分学到的所有规则。例如,单一模块文件内可以有一个以上的类,就像模块内其他语句,class语句会在导入时执行已定义的变量名,而这些变量名会变成独立的模块属性。更通用的情况是,每个模块可以任意混合任意数量的变量、函数以及类,而模块内的所有变量名的行为都相同。文件food.py如下:
如果模块和类碰巧有相同名称,也是如此,例如文件person.py:
需要像往常一样通过模块获取类;
虽然这看上去很多余,不过却是必须的:person.person指的是person模块内的person类,只写person只会取得模块,而不是类,除非使用from语句。实际上,python中的通用惯例指出,类名应该以一个大写字母开头,以使得它们更为清晰:
模块反映了整个文件,而类只是文件内的语句。
8、类可以截获python运算符:类和模块的第三个差别:运算符重载。简单来说,运算符重载就是让类写成的对象,可截获并响应用在内置类型上的运算:加法、切片、打印和点号运算等。这只是自动分发机制:表达式和其他内置运算流程要经过类的实现来控制。类与模块相比,模块可以实现函数调用,而不是表达式的行为。重载运算符主要概念的特点:a、以双下划线命名的方法(__X__)是特殊钩子。python运算符重载的实现是提供特殊命名的方法来拦截运算。python语句在每种运算和特殊命名的方法之间定义固定不变的映射关系;b、当实例出现在内置运算时,这类方法会自动调用,例如,如果实例对象继承了__add__方法,当对象出现在+表达式内时,该方法就会调用。该方法的返回值会变成相应的表达式的结果;c、类可覆盖多数内置类型运算;d、运算符覆盖方法没有默认值,而且也不需要,如果类没有定义或继承运算符重载方法,就是说相应的运算在类实例中并不支持,例如__add__没有的话,+表达式就会引发异常;e、运算符可让类与python的对象模型相集成,重载类型运算时,以类实现的用户定义对象的行为就会像内置对象一样,提供了一致性,以及与预期接口的兼容性。
9、第三个例子:这一次定义SecondClass的子类,实现三个特殊名称的属性,让python自动进行调用:a、当新的实例构造时,会调用__init__(self是新额ThirdClass对象);b、当ThirdClass实例出现在+表达式时,则会调用__add__;c、当打印一个对象的时候(当通过str内置函数或者其python内部的等价形式来将其转换为打印字符串的时候),运行__str__。新的子类也定义了一个常规命名的方法,叫做mul,它在原创修改该实例的对象。如下是个新的子类:
ThirdClass是一个SecondClass对象,所以其实例会继承SecondClass的display方法。但是ThridClass生成的调用现在会传递一个参数,这是传给__init__构造函数内的参数value的,并将其赋值给self.data。直接效果就是,ThirdClass计划在构建时自动设置data属性,而不是在构建之后请求setdata调用。此外,ThirdClass对象现在可以出现在+表达式和print调用中,对于+,python把左侧的实例对象传给__add__中的self参数,而把右边的值传给other,如图26-3所示,__add__返回的内容称为+表达式的结果。对于print,python把要打印的对象传递给__str__中的self;该方法返回的字符串看作是对象的打印字符串。使用__str__,我们可以用一个常规的print来显示该类的对象,而不是调用特殊的display方法:
__init_-、__add__、__str__这样的特殊命名的方法会由子类和实例继承,就像这个类中赋值的其他变量名。如果方法没有在类中编写,python会在其所有超类内寻找这类变量名。运算符重载方法的名称并不是内置变量惑保留字,只是当对象出现在不同的环境时python会去搜索的属性。python通常会自动进行调用,但偶尔也能由程序代码调用(例如,__init__通常可手动调用来触发超类的构造函数)。注意__add__方法创建并返回这个类的新的实例对象(通过它的结果值调用ThirdClass)。但是,mul会在原处修改当前的实例对象(通过重新赋值self属性)。我们可以通过重载*表达式来实现后者,但是这一点和内置类型的行为不同。就像数字和字符串,总是替*运算符创建新对象。通常的实践说明,从在的运算符应该以与内置的运算符实现同样的方式工作,因为运算符重载其实只是表达式对方法的分发机制,可以在自己的类对象中以任何喜欢的方式解释运算符
10、世界上最简单的python类:类产生的基本的继承模型非常简单:所涉及的就是在链接的对象树中搜索属性。实际上,我们建立的类中可以什么东西都没有。下列语句建立一个类,其内完全没有附加的属性(空的命名空间对象):
因为没有写任何方法,所以需要无操作的pass语句。在交互模式下执行这句之后,建立这个类后,就可以完全在最初的class语句外,通过赋值变量名给这个类增加属性:
通过赋值语句创建这些属性后,就可以用一般的语法将它们取出。这样用时,类差不多就像C的struct或者pascal的record:这种对象就是有字段附加在它的上边:
注意:其实该类还没有实例,也只能这样用。类本身也是对象,也是没有 实例。事实上,类只是独立完备的命名空间,只要有类的引用值,就可以在任何时刻设定或修改其属性。不过,当建立两个实例时,看看会发生什么:
这些实例最初完全是空的命名空间对象。不过,因为它们知道创建它们的类,所以会因继承并获取附加在类上的属性:
其实,这些实例本身没有属性,只是从类对象哪里取出name属性。不过,把一个属性赋值给一个实例,就会在该对象内创建(或修改)该属性,而不会因属性的引用而启动继承搜索,因为属性赋值运算只会影响属性赋值所在的对象。在这里,x得到自己的name,但是y依然继承附加在它的类上的name:
不过在下一章就可以看到,命名空间对象的属性通常都是以字典的形式实现的,而类继承树只是连接至其他字典的字典而已。例如,__dict__属性是针对大多数基于类的对象的命名空间字典(一些类也可能在__slots__中定义了属性,这是个高级而少用的功能,在第30、31章介绍)。如下的代码在3.0中运行:名称和_X_内部名称集合所出现的顺序可能随着版本的不同而有所不同。但是赋值的名称全部给出:
在这里,类的字典显示出赋值了的name和age属性,x有自己的name,而y依然是空的,不过,每个实例都连接至其类以便继承,如果想查看的话,连接叫做__class__:
类也有一个__bases__属性,它是其超类的元组:
这两个属性是python在内存中类树常量的表示方式。
11、接上10:python的类模型相当动态。类和实例只是命名空间对象,属性是通过赋值语句动态建立。恰巧这些赋值语句往往在class语句中,只要能引用树中任何一个对象的任意地方,都可以发生。即使是方法(通常是在类中通过def创建)也可以完全独立的在任意类对象的外部创建。例如:下列在任意类之外定义了一个简单函数,并带有一个参数:
这里与类完全没有关系,只要我们传进一个带有name属性的对象(变量名self并没有使这变的特别):
如果将这个简单函数的赋值成类的属性,就会变成方法,可以由任何实例调用(并且通过类名称本身,只要手动传入一个实例):
通常情况下,类是由class语句填充的,而实例的属性则是通过在方法函数内对self属性进行赋值运算而创建的,不过重点在于python中的eoop其实就是在已连接的命名空间对象内寻找属性而已。
12、类和字典的关系:这里先po一段字典的代码和一段类的代码:
,
左边的是利用键来记录属性;右边的使用一个空的class语句来产生一个空的命名空间对象。一旦产生了空类,随着时间就用赋值类属性来填充它,这和前面一样。但是对于将需要的每一条不同的记录,都需要一个新的class语句。更通俗的说,可以产生一个空类的实例来表示每条不同的记录:
这里从相同的类产生两条记录。实例开始的时候为空,就像类一样。然后通过对属性赋值来填充记录,不过这次,有两个分开的对象,由此有两个不同的name属性。实际上,同一个类的实例甚至不一定必须具有相同的一组属性名称;在这个示例中,其中一个就有唯一的age名称。实例实际上是不同的名称空间,因此,每个实例都有一个不同的属性字典。尽管他们通常由类方法一致的填充,但他们比预想的更灵活。最后写一个更完整的类来实现记录和和处理:
这一方案也产生多个实例,但是这次类不是空的,:已经添加了逻辑(方法)在构建的时候来初始化实例并把属性收集到一个元组中这里,构造函数在实例上强制一个写一致性,通过总是设置name和job属性。此外,类的方法和实例属性创建了一个包,它组合数据和逻辑。最后,可能吧该类连接到一个更大的层级中,以通过类的自动属性搜索来继承已有的一组方法,或者甚至可能把类的实例存储到一个文件中,该文件带有python对象pickle化以使其持久。实际上,下一章中,将用一个更加实际的运行实例来展示类基础知识的引用,从而实现类和记录的类比。尽管类型像字典一样灵活,但类允许我们以内置类型和简单函数不能直接支持的方式为对象添加行为。尽管可以把函数存储到字典中,但却没有类这么自然。
三、更多实例
这部分中,将会构建一组类来做些更加具体的事情,将会看到,在python编程中我们所谓的实例和类,往往与传统的术语记录和程序扮演着相同的角色。在本章中,我们将编写两个类:a、Person--创建并处理关于人员的信息的一个类;b、Manager--一个定制的Person,修改了继承的行为。在编写之后,将实例存储到一个shelve的面向对象数据库中,使它们持久化。通过这种方式,可以把这些代码用作模板,从而发展为完全用python编写的一个完备的个人数据库。这里为了考虑到人们在从头编写类的困难的基础上,会在这里介绍每个步骤,帮助学习基础知识;逐渐的构建类,以便可以看到其功能如何组合到一个完整的程序中。不管语法细节如何,python的类系统实际上很大程度上就是在一堆对象中查找属性,并为函数给定一个特殊的第一个参数。
1、步骤1:创建实例:先编写主类Person。在python中,模块名使用小写字母开头,而类名使用一个大写字母开头,这是通用的惯例:就好像方法中使用self作为参数名,这可能不是语言所要求的,但是,这种违背管理的做法会给随后看代码的人造成混淆。新的模块文件命名为person.py。将其中的类命名为Person,如下所示:
在本部分前面所有的工作都在这个文件中完成。在python中单个模块文件中,可以编写任意多个函数和类,而且当模块拥有一个单一、一致的用途的时候,它们会工作的更好。
2、编写构造函数:在Person类中第一件事就是记录关于人员的基本信息,如果可以的话,就填充记录字段。在python的术语中,这叫做实例对象的属性,并且它们通常通过给类方法函数中的self属性赋值来创建。赋给实例属性第一个值的通常方法是在__init__构造函数方法中将它们赋给self,构造函数方法包含了每次创建一个实例的时候python会自动运行的代码:
传入的数据作为构造函数方法的参数附加到一个实例上,并且将它们赋给self以保持持久。在oo中,self就是新创建的实例对象,而name、job和pay变成了状态信息,即保存在对象中供随后使用的描述性数据。尽管其他的技术(例如,封装作用域引用)也可以保存细节,但实例属性使得这个过程很明确而且易于理解。这里参数名出现了两次,看上去有冗余 ,但是其实不是,例如,job参数在__init__函数的作用域中是一个本地变量,但是self.job是实例的一个属性,两个是不同的变量。在产生一个实例的时候,__init__会自动被调用并且有特殊的第一个参数。虽然名字怪异,但是仍然是一个常规的函数,并且支持已经介绍的所有函数特性。例如,可以为它的参数提供哦你默认值,从而当参数的值不可用的情况下,就不必提供参数值。便于说明,那么这里先把job参数成为可选的参数,将默认为None,而且在job没有的情况下,pay也可以默认为0,实际上,必须要为pay指定一个默认值,因为在python语法规则中,一个函数定义中,第一个拥有默认值的参数值和的任何参数,都必须有默认值:
这段代码意味着在创建Person的时候,需要给name传入值,但是job和pay是可选的;通常self参数由pyahton自动填充以引用实例对象,把值赋给self属性就会将值赋给新的实例。
3、用python编程其实就是一种增量原型,编写一些代码,测试它,编写更多的代码,再次测试,以此类推。由于python提供交互式绘画,并且几乎在代码修改之后立即转变,所以在进行中测试比编写大量代码后再一次性测试要更加自然。所以这里先测试下当前写好的代码:生成类的几个实例,并且显示构造函数所创建的它们的属性。不过在每次开始一个新的测试会话的时候都需要重新导入模块和重新输入测试实例,更常见的做法就是使用交互式来进行简单的一次性测试,通过包含测试对象的文件底部编写代码来进行更多的大量测试:
这里bob对象针对job和pay接受了默认值,但是sue显式的提供了值,还要注意到,在创建sue的时候,是如何使用关键字参数;可以根据位置来传递,但是关键字随后可以提醒我们这些数据是什么(并且它允许按照从左到右的顺序传递参数)。当这个文件作为脚本运行时,底部的测试代码创建了类的两个实例,并且打印出每个实例的属性(name和pay):
也可以在python的交互提示模式下输入这个文件的测试代码(假设这里首先导入了Person类),但是,像这样把负责测试的代码封装到模块文件中,使得将来可以更容易的再次运行它们。从技术上说,bob和sue都是命令空间对象,像所有的类实例一样,它们每个都拥有各自类所创建的状态信息的独立副本。由于类的每一个实例都有自己的一组self属性,类通过这种方式来记录多个对象是很自然的事情;就像是内置类型一样,类充当一种对象工程。其他的比如函数和模块,没有这样的概念。
4、在2.6和3.0中的print可以使用较新的方法:
5、步骤2:添加行为方法。现在,类基本上就是一个记录工厂,创建并填充记录的字段(也就是实例的属性),而且如果已经知道python的简单的核心类型,那么就已经知道类的大部分用法;类其实只是一个最小的结构性扩展。如因为name只是一个简单的字符串,所以还是可以用空格和索引处分隔来从对象提取姓氏:
与之类似的还有pay,可以通过赋值来修改状态信息的当前状态,不管对象是独立的还是嵌入类结构中的:
所以可以如上面一样来处理bob.name和sue.pay:
这里添加了最后两行,当他们运行时,使用基本的字符串和列表操作提取了bob的姓氏,并且通过基本的数字操作修改sue的pay属性,从某种意义上说,sue也是一个可修改对象,原处修改其状态就好像是对一个列表进行append操作:
对于老手来说,代码的一般方法在实际中并非好方法,像这样在类之外的硬编码操作可能会导致以后的维护问题。因为它们可能位于多个文件中,分散到多个单独的步骤中。
6、编写方法:这里涉及到封装的软件设计概念。就是将操作逻辑包装到界面之后,这样每种操作在程序里只编码一次。也就是修改也只需要修改一个版本。python的术语角度来说,想要操作对象的代码位于类方法中,而不是分散在整个程序中,构造代码以删除冗余,并且由此优化维护。作为额外的好处,把操作放入方法中,还会使得这些操作可以应用于类的任何实例,而不是只能用于把它们硬编码来处理的那些对象。并且修改底部的self测试代码以使用所创建的新的方法,而不是硬编码操作。如下面:
方法只是附加给类并旨在处理那些类的实例的常规函数。实例是方法调用的主体,并且会自动传递给方法的self参数。结果如下:
这里注意sue的pay在修改之后仍然是一个整数,这里是通过方法中调用内置的int函数来把结果转换为整数。把值修改为int或float,在对于一个正式的系统中,可能就是需要解决的舍入问题。在后面会介绍函数装饰器,并且介绍python的assert语句,这些机制可以在开发中自动为我们进行验证性测试。
7、步骤3:运算符重载。当前来说,在跟踪对象的时候,还是需要手动接受和打印单个的属性(例如:bob.name,sue.pay)。如果一次显示一个实例的话,默认情况下显示的是对象的类名及其在内存中的地址。例如将上面的代码最后改成print(sue):
可以通过使用运算符重载,在一个类中编写这样的方法,当在类的实例上运行时,方法截获并处理内置的操作,特别是可以使用可能是python中第二常用的运算符重载方法。即上一部分介绍过的继__init__:之后的__str__方法。每次一个实例转换为其打印字符串的时候,__str__都会自动运行。因为这是打印一个对象会做的事情,所以直接效果就是,打印一个对象会显示对象的__str__方法所返回的内容,要么自己定义一个该方法,要么从一个超类继承一个该方法(__双下划线的名称和任何其他名称一样继承)。__init__的编写也算是运算符重载,它在构建的时候自动运行,以初始化一个新创建的实例。像__str__这样更加专注的方法,允许我们输入专门的操作,并且当我们的对象用于这样的环境中的时候会提供专门的行为。下面的代码扩展了类以给出一个定制的显示,当类的实例作为一个整体显示的时候会列出属性,而不是依赖于用处较少的默认显示:
这里,在__str__中使用字符串%格式来构建显示字符串,在代码底部,类使用像这样的内置的类型对象和操作来完成任务。关于内置类型和函数,之前学习的所有内容都适用于基于类的代码。类在很大程度上只是添加了额外的一层结构,它把函数和数据包装在一起并且支持扩展。这里直接把self测试代码修改为打印对象,而不是打印单个属性。新的__str__返回“【...】”行,该函数由打印操作自动运行:
这里值得注意的一个地方,一个相关重载方法__repr__提供对象的一种代码低层级显示。有的时候,类提供一个__str__以便实现用户友好的显示,同时也提供了一个__repr__以便让开发者看到额外的细节。由于打印运行__str__并且交互提示模式使用__repr__回应结果。所以可以为两种用户都提供合适的显示。不过在类中,__str__就足够了。
8、步骤4:通过子类定制行为。上面的三个步骤已经包含了:创建实例、在方法中提供行为、并且还有运算符重载,就还剩下通过继承来定制化。来完成oop的概念。
9、接8:编写子类,先定义一个Person的子类,叫做manager,它用一个更加特殊的版本替代了继承的giveRaise方法,新类如下:
这意味着定义一个名为manager的新类,它继承自超类Person并且可能向Person添加一些定制。然后在该子类中定义giveRaise行为,这样就能代替父类:
10、接9。扩展的方法:不好的方式:就是复制和粘贴Person中的giveRaise的代码,然后针对Manager修改如下:
这会和预想的一样工作,但是问题在于如果说在未来需要修改工资的计算方法,那么就需要修改两个地方的代码(父类和子类);好的方式:所以可以直接基于父类来进行扩展:
这段代码利用了这样个事实:类方法总是可以在一个实例中调用(这是通常的方式,python吧该实例自动的发送给self参数),或者通过类来调用(较少的方式,其中,必须手动的传递实例)。如下是常规调用:
由python自动的转换为如下的同等形式:
其中,包含了要运行的方法的类,由应用于该方法的继承搜索规则来确定。这两种形式之间略有差异--如果直接通过类来调用,必须手动的传递实例。不过不管哪种方式,方法总是需要一个主体实例,并且python只是对通过实例调用的方式自动提供实例。通过对类名调用的方式,需要自己给self发送一个实例,对于giveRaise这样的一个方法的内部代码,self已经是调用的主体,并且由此将实例传递过去。通过类直接调用会扰乱了继承,并且把调用沿着类树向上传递以运行一个特定的版本。在这个例子中,可以使用这一技术来调用Person中默认的giveRaise,即便该方法在manager层级已经重新定义了。某种情况下,必须这样通过Person的方式调用,因为,Manager的giveRaise代码内部的self.giveRaise()可能会循环---由于self已经是一个Manager,self.giveRaise()将会再次解析为Manager.giveRaise,以此类推,直到可用内存耗尽。“好”的代码意味着会考虑到未来的代码维护问题:
为了测试Manager子类定制,还添加了self测试代码,它创建了一个Manager,调用其他方法,并且打印它。这里是新版本的输出:
bob和sue和以前一样,并且当Manager tom加薪10%的时候,实际得到了20%(50k-60k),因为Manager中定制的giveRaise只是对他运行,还要注意,测试代码的末尾如何把tom作为一个整体打印出来,按照Person的__str__中定义的漂亮格式来显示:Manager对象获取lastName,而__init_-构造函数方法的代码通过继承直接从Person得到。
11、多态的作用。为了使得这个从继承获取的行为更加惊人,在文件的末尾添加了如下代码:
输出结果如下:
在添加的代码中,对象是一个Person惑Manager,python自动的运行相应的giveRaise。这是python中所谓的多态。多态是python灵活性的核心。例如,把3个对象中的任何一个传递给调用了giveRaise方法的一个函数,将会有同样的效果:根据所传递的对象的类型,将会自动运行相应的版本。另一方面,对于3个对象,打印都运行相同的__str__,因为其代码在Person中只出现一次。Manager既应用最初在Person中编写的代码,也对这些代码进行特殊化。这里已经算是利用oop的特性实现了代码定制和复用。
12、继承、定制和扩展:通常,类可以继承、定制惑扩展超类中已有的代码。这里giveRaise重新定义了一个超类方法以定制它,但someThingElse定义了一些新的内容已进行扩展:
13、oop:大思路。在示例中,可能理论上已经实现了一个定制的giveRaise操作而没有子类化。但是,没有其他的选项能够产生像这样的代码那样优化的代码:a、尽管可以从头开始编写Manager的全新的、独立的代码,但是必须重新实现Person中所有那些与Manager相同的行为;b、尽管可以直接原处修改已有的Person类来满足Manager的giveRaise的需求,但这么做可能会使需要原来的Person行为的地方无法满足需求;c、尽管可以直接完整的复制Person类,将副本重新命名为Manager,并且修改其giveRaise,这么做将会引入代码冗余性,这会使我们将来的工作很麻烦。可以通过类来构架的可定制层级,为那些将会随着时间而发展的计划提供更好的解决方案。
14、步骤5:定制构造函数。上述示例的代码虽然正常工作,但是还是有些地方不太好:当创建Manager对象的时候,必须为它提供一个mgr工作名称似乎是没有意义的。所以可以通过重新定义__init__方法,从而提供mgr字符串。而且和giveRaise的定制一样还想通过类名的调用来运行Person中最初的__init__,下面的代码就是编写了新的Manager构造函数,并且把创建tom的调用修改为不传入mgr工作名称:
这里,再次使用前面相同的形式来扩展__init__构造函数--通过类名直接调用并显式传递self 实例,从而运行超类的版本。由于我们也需要运行Person的构造函数逻辑(来初始化实例属性),我们实际上必须以这种方式调用它;否则,实例就不会附加任何的属性。以这种方式调用重定义的超类构造函数,在python中是一种很常用的编码模式,在构造的时候,python自身使用继承来查找并调用唯一的一个__init__方法,也就是类树中最低的一个;
15、组合类的其他方式:除了继承之外,还有其他方式组合类,例如,一种常用的编码模式是把对象彼此嵌套以组成复合对象。这里举例为编写Manager扩展的代码,将它嵌入一个Person中,而不是继承Person。厦门的替代方法使用__getattr__运算符重载方法来做到这点,(将在第29章介绍使用内置函数getattr来拦截未定义属性的访问,并将它们委托给嵌入的对象)。这里的giveRaise方法仍然实现定制,通过修改传递到嵌入的对象的参数。实际上,Manager变成了控制层,它把调用向下传递到嵌入的对象,而不是向上传递到父类的方法:
这个Manager替代方案是一种叫做委托的常用代码模式的一个代表,委托是一种基于复合的结构,它管理一个包装的对象并且把方法调用传递给它。但是它需要大约两倍的代码,并且对于我们想要表示的直接定制来说,它没有继承更合适。在这里Manager不是一个真正的Person,因此,需要额外的代码手动为嵌入的对象分派方法;像__str__这样的运算符重载方法必须重新定义,并且由于状态信息是删除的一层,所以添加新的Manager行为不那么直接。然而,当嵌入的对象比直接定制隐藏需要与容器之间有更多有限的交互时,对象嵌入以及基于其上的设计模式还是很适合的。例如,如果想要跟踪惑验证对另一个对象的方法的调用,像这里的替代Manager这样的一个控制器可能会更方便。此外,像下面这样的一个假设的Department可能聚合其他的对象,以便将它们当作一个集合对待。将这段代码添加到person.py文件的底部并自己尝试:
有趣的是,这里代码使用了继承和复合,Department是嵌入并控制其他对象的聚合的一个复合体,但是嵌入的Person和Manager对象自身使用继承来定制。
16、在3.0中(2.6中使用新样式的类),刚刚写的,替代的基于委托的Manager类,不重新定义它们是不能够截取并委托像__str_-这样的运算符重载方法属性。尽管我们知道,__str__是唯一的用于我们特定例子中的这样名字,但这对于基于委托的类是一个通用的问题。像打印和索引这样的内置操作都隐式的调用__str__和__getitem__这样的运算符重载方法。在3.0中,像这样的内置操作无法通过通用属性管理器找到它们的隐式属性:__getattr__(针对未定义的属性运行)及其近亲__getattribute__(针对所有属性运行)都不会调用。这就是为什么必须在替代的Manager中冗余的重定义__str__,为了确保在3.0中运行的时候,打印能够找到嵌入的Person对象。从技术角度上说,会发生这样的情况是因为传统的类通常会在运行时在实例中查找运算符重载名,但是,新式的类不会这样,它们完全略过实例而在类中查找这样的方法。在2.6的传统类中,内置方法不会像通常一样查找属性,例如,打印通过__getattr__找到__str__。新式的类也继承一个默认的__str__,__getattr__无法找到它,但在3.0中__getattribute__也不会截取这个名字。
17、步骤6:使用內省工具。虽然现在类是完整的并且有了大多数基本的oop。但是仍然有两个问题:a、首先,如果现在查看对象的显示,会发现,当打印tom的时候,Manager会把它标记为Person,因为Manager是一个定制的和特殊化的Person。然而,尽可能的用最确切(最底层)的类来显示对象,这可能会更准确;b、当前的显示格式只是显示了包含在__str__中的属性,而没有考虑未来的目标。例如,还无法通过Manager的构造函数验证tom工作名已经正确的设置为mgr,因为为Person编写的__str__没有打印出这一字段。更糟糕的是,如果我们改变或者修改了在__init__中分配给对象的属性集合,那么还必须记得也要更新__str__以显示新的名字,否则,将无法随着时间的推移而同步。这也意味着,未来在代码中引入冗余性的时候,必须自己做潜在的额外工作。由于__str__中的任何不一致都会反映到程序的输出中,所以这种冗余性可能比前面所解决的额其他形式更为明显,不过,避免未来的额外工作总是好事。
18、特殊类属性:可以使用python的内省工具来解决这两个问题,它们是特殊的属性和函数,允许我们访问对象实现的一些内部机制。这部分知识较为高级,当然了解基本的还是有用的,因为这允许编写以通用方式处理类的代码。例如,,在代码中,有两个钩子可以帮助解决问题,在上一部分快结束的时候说过;a、内置的instance.__class__属性提供了一个从实例到创建它的类的链接。类反过来有一个__name__(就像模版一样),还有个__base__序列,提供了超类的访问,可以使用这些来打印创建一个实例的类的名字,而不是通过硬编码来做到;b、内置的object.__dict__属性提供了一个字典,带有一个键/值对,以便每个属性都附加到一个命名控件对象(包括模块、类和实例)。由于它是字典,因此我们可以获取键的列表、按照键来索引、迭代其键,等等,从而广泛的处理所有的属性。使用这些来打印出任何实例的每个属性,而不是在定制显示中硬编码,下面的是在交互模式下的代码:
正如上一部分说的,如果一个实例的类定义了__slots__,而实例可能没有存储在__dict__字典中,但实例的一些属性也是可以访问的额,这是新式类(3.0中所有类)的一项可选的和相对不太明确的功能,即把属性存储在数组中,并且我们将在第30、31章讨论,既然slots其实属于类而不是实例,并且它们在任何事件中极少用到,那么就可以在这里忽略它们而关注常规的__dict__。
19、一种通用显示工具:在文本编辑器中打开一个新的文件,并叫做classtools.py,其中只实现一个类,因为其__str__ print重载用于通用的內省工具,它将会对任何实例有效,不管实例的属性集合是什么,并且由于这是一个类,所以它自动变成一个公用的工具:得益于继承,它可以混合到想要使用它显示格式的任何类中。如果想要改变实例的显示,只需要修改这个类,于是,在其下一次运行的时候,继承其__str__的每一个类都将自动选择新的格式:
这里的文档字符串,作为通用的工具,文档字符串可以放在简单函数和模块的顶部,并且也可以放在类机器方法的开始处;help函数和PyDoc工具会自动的提取和显示它们。直接运行的时候,这个模块的self测试会创建两个实例并打印它们。这里定义的__str__显示了实例的类,及其所有的属性名和值,按照属性名排序:
20、实例与类属性的关系:看上面classtools模块的self测试代码,其类只显示实例属性,属性附加到继承树底部的self对象;这就是self的__dict__所包含的内容。继承的类属性只是附加到了类,而没有向下复制到实例。如果确实想要包含继承属性,可以把__class__链接爬升到实例的类,使用这里的__dict__去获取类属性,然后迭代类的__bases__属性爬升到甚至更高的超类,如果喜欢简单的代码,在实例上运行一个内置的dir调用而不是使用__dict__并爬升,因为dir结果在排序的结果列表中包含了继承的名称:
这里的2.6和3.0的输出有所不同,因为3.0中的dict.keys不是一个列表,并且3.0的dir返回了确切的类类型实现属性。从技术上说,3.0的dir返回的更多,因为类都是"新式"的并且从类类型那里继承了很大一组运算符重载名称。实际上,可能想要过滤掉3.0的dir结果中的大多数__X__名称,因为它们都是内部实现细节,而不是通常想要显示的内容。
21、工具类的命名考虑:由于classtools模块中的AttrDisplay类旨在和其他任意类混合的通用性工具,所以必须注意与客户类潜在的无意的命名冲突。为此,必须假设客户子类可能想要使用其__str__和gatherAttrs,但是后者可能比一个子类的期待还要多,如果一个子类无意的自定义一个gatherAttrs名称,它可能会破坏我们的类,因为可能会使用子类中的这个低版本。可以在文件的self测试代码中为TopTest添加一个gatherAttrs,除非新的方法是相同的,或者有意定制了最初的方法,我们的工具类将不再像计划的那样工作:
这不一定是坏事,有时候希望其他的方法可供子类使用,要么直接调用,要么定制。如果真的只想提供一个__str_-,这还不够理想。为了减少名称冲突的机会,python程序员常常对不想做其他用途的方法添加一个单下划线的前缀:比如_gatherAttrs。一种更好但是不太常用的方法就是只在方法名前面使用两个下划线符号:__gatherAttrs,python会自动扩展这样的名称,以包含累的名称,从而使它们变得真正唯一。这一功能通常叫做伪私有类属性。将在第30章介绍
22、类的最终形式:要在类中使用这一通用工具,只需要从其模块中导入它,使用继承将其混合到顶层类中,并且删除掉之前编写的更专门的__str__方法。新的打印重载方法将会由Person的实例继承,Manager的实例也会继承;Manager从Person继承__str__,现在Person从另一个模块的AttrDisplay获取它。下面是经过修改后的person.py文件的最终版本:
在最后的版本中,添加一些新的注释来记录所做的工作和每个最佳实践管理,使用了功能性描述的文档字符串和用于简短注释的#。现在运行这段代码,将会看到对象的所有属性,而不是最初的__str__中直接编码的那些属性。并且,最终的问题也解决了:由于AttrDisplay直接从self实例中提取了类名,所以每个对象都显示其最近的(最低的)类的名称,tom现在显示为Manager,而不是Person。并且下面是结果:
从更大角度来说,属性显示类已经变成了一个通用工具,可以通过继承将其混合到任何类中,从而利用它所定义的显示格式。此外,其所有客户豆浆自动获取工具中的未来修改。
23、步骤7:把对象存储到数据库中。现在实现了一个两模块的系统,不仅实现了显示人员的最初设计目标,而且提供了一个通用的属性显示工具。还缺少将创建的对象记录成真正的数据库记录。可以通过使用python的一项叫做对象持久化的功能,让对象在创建它们的程序退出后依然存在。
24、对象持久化通过3个标准的库模块来实现:a、pickle,任意python对象和字节串之间的序列化;b、dbm,(在2.6中叫做anydbm),实现一个可通过键访问的文件系统,以存储字符串;c、shelve,使用另两个模块按照键把python对象存储到一个文件中。
25、pickle模块是一种非常通用的对象格式化和解格式化工具:对于内存中几乎任何的python对象,它都能聪明的把对象转换为字节串,这个字节串可以随后用来在内存中重新构架最初的对象。pickle模块几乎可以处理我们所能够创建的任何对象,包括列表、字典、嵌套组合以及类实例。后者对于pickle来说特别有用,因为它们提供了数据(属性)和行为(方法),实际上,组合几乎等于“记录”和“程序”。由于pickle通用,所以可以不用编写代码来创建和解析对象的定制文本文件表示,它可以完全替代这些代码。通过在文件中存储一个对象的pickley字符串,可以有效的使其持久化:随后直接载入它并进行unpickle操作,就可以重新创建最初的对象。
26、虽然pickle本身把对象存储为简单的普通文件并随后载入它们是很容易的,但shelve模块提供了一个额外的层结构,允许按照键来存储pickle处理后的对象。Shelve使用pickle把一个对象转换为其pickle化的字符串,并将其存储在一个dbm文件中的键之下;随后载入的时候,shelve通过键获取pickle化的字符串,并用pickle在内存中重新创建最初的对象。对于脚本来说,一个shelve的pickle化的对象看上去就像是字典,也就是通过键索引来访问、指定键来存储,并且使用len、in和dict.keys这样的字典工具来获取信息。shelve自动把字典操作映射到存储在文件中的对象。对于脚本来说,一个shelve和一个常规的字典之间唯一的编码区别在于,一开始必须打开shelve并且在修改之后必须关闭它。实际效果就是,一个shelve提供了一个简单的数据库来按照键存储和获取本地的python对象,并由此使它们跨程序运行而且保持持久化。它不支持SQL这样的查询工具,并且缺乏企业级数据库中可用的某些高级功能。但是一旦使用键获取存储在shelve中的本地python对象,就可以使用python的所有功能来处理它。
27、在shelve数据库中存储对象。有关pickle和shelve可以在标准库手册或者《programming python》中看更详细的介绍。这里先编写一个新的脚本,将类的对象存储到shelve中,打开一个新的文本文件,命名为makedb.py,并导入类以便创建一些实例来存储。可以像之前使用from载入一个类,或者import:
在新的脚本中,一旦有了一些实例,将它们存储到shelve中。可以直接导入shelve模块,用一个外部文件名打开一个新的shelve,把对象赋给shelve中的键,当操作完毕之后,关闭这个shelve:
这里使用对象的名字做键,从而把它们赋给shelve,这只是为了方便。在shelve中,键可以是任何的字符串,包括处理的ID和时间戳(可以在os和time标准库模块中使用)来创建唯一的字符串。这样就可以针对每个键只存储一个对象(虽然对象可以是包含多个对象的一个列表或字典)。存储在键之下的值可以是几乎任何类型的python对象:像字符串、列表和字典这样的内置对象,用户定义的类实例,以及所有这些嵌套式的组合。如果这段脚本运行的时候没有输出,意味着它可能有效,这里没有打印任何内容,只是创建和存储对象:
28、交互的探索shelve:这时候当前的目录下会有一个或多个文件,名字都是以"persondb"开头。实际创建的文件可能根据每个平台而有所不同,与内置的open函数一样,shelve.open()中的文件名也是相对于当前目录的,除非包含了一个目录路径。不管文件存储的位置,这些文件实现为一个通过键访问的文件,其中包含了3个python对象的pickle化的表示。不要删除这些文件,这些是我们的数据库,并且是我们备份惑移动存储的时候需要复制和转移的内容。在3.0并且没有安装额外的软件的情况下,数据库存储在3个文件中(在2.6中只是一个文件persondb,因为bsdbd扩展模块在python中为shelve预安装了;在3.0中,bsdbd是一个第三方开源插件):
这些内容并非无法解读(查看shelve文件的时候,从windows资源管理器或者python shell中都是二进制散列文件)但是它们在不同的平台上有所不同,并且无法确切的等同于一个用户友好的数据库界面。可以通过编写另一个脚本,或者在交互模式下浏览shelve,可以通过常规的python语法和开发模式来处理它。这里,交互模式有效的称为一个数据库客户端:
这里为了载入或使用存储的对象,不一定必须导入Person惑Manager类。比如,可以自由的调用bob的lastName方法,并且自动获取其定制的打印显示格式,即使在作用域中没有Person类,也可以。这之所以起作用,是因为Python对一个类实例进行pickle操作,它记录了其self实例属性,以及实例所创建于的类的名字和类的位置。当随后从shelve中获取bob并对其unpickle的时候,python将自动的重新导入该类并且将bob连接到它。这种方法结果就是,类实例在未来导入的时候,会自动地获取其所有的类行为。只有在创建新实例的时候,才需要导入自己的类,而不是处理已有实例的时候也要这么做。虽然这是一项成熟的功能,可是:a、缺点是当随后载入一个实例的时候,类及其模块的文件都必须导入。更正式的说,可以pickle的类必须在一个模块文件的顶部编码,而这个模块文件可以通过sys.path模块的查找路径所列出的目录来访问(并且,该模块文件不该在大多数脚本文件的模块__main__中,除非它们在使用的时候总是位于该模块中)。由于这一外部模块文件的需求,因此一些应用程序选择pickle更简单的对象,例如,字典惑列表,特别是如果它们要通过internet传递的时候;b、优点是,当该类的实例再次载入的时候,对类的源代码文件的修改会自动选取;这往往不需要更新存储的对象本身,因为更新它们的类代码就会改变它们的行为。
29、更新shelve中的对象:这里是最后一段脚本:编写一个程序,让每次运行的时候更新一个实例(记录),以证实此时我们的对象真的是持久的(例如,每次一个python程序运行的时候,它们的当前值都是可用的)。如下的文件updatedb.py打印出数据库,并且每次把存储的对象之一增加一次。如果跟踪这里所发生的事情,就会注意到,发现可以使用很多工具,自动使用通用的__str__重载方法打印对象,调用giveRaise方法增加之前写入的值。这些对基于oop继承模型上的对象“就有效了”,即便当它们位于一个文件中:
由于这段脚本启动的时候会打印数据库,必须运行几次才能看得到对象的改变。:
这里看到了一个从python中得到的成品的shelve和pickle工具,并且它具备自己在类中编写的行为。再一次,可以在交互模式中验证脚本的作用:
在第30章给出了另一个对象持久化的例子,使用pickle而不是shelve在一个普通文件中存储一个更大的复合对象,但是,效果是类似的。
四、类代码编写细节(28章)
这部分及后面的部分会深入的研究之前介绍过的概念,从另一个角度看待类机制。这里将继续学习类、方法和继承,正式讲解第26章额一些编写类概念,并进行扩展。类是最后一个命名空间工具,所以这里也会总结命名空间的概念。
1、class语句:python中class与cpp中是有不同的,python中class不是声明式的,就像def一样,class语句是对象的创建者并且是一个隐含的赋值运算,执行时,会产生类对象,并把其引用值存储在前面所使用的变量名,此外,像def一样,class语句也是真正的可执行代码,直到python到达并运行定义的class语句前,类都是不存在的(一般都是在其所在的模块被导入时,在这之前都不会存在)。
2、一般形式:class是符合语句,其缩进语句的主体一般都出现在头一行下边。在头一行中,超类列在类名称之后的括号内,由逗号隔开。列出一个以上的超类会引起多重继承,下面是class的一般形式:
在class语句内,任何赋值语句都会产生类属性,而且还有特殊名称方法重载运算符。例如,名为__init__的函数会在实例对象构造时调用(如果定义过的话)。
3、例子:类几乎就是命名空间,也就是定义变量名(属性)的工具,把数据和逻辑导出给客户端。那么如何从class语句得到命名空间的呢?就像模块文件,位于class语句主体中的语句会建立其属性。当python执行class语句时(不是调用类),会从头至尾执行其主体内的所有语句。在这个过程中,进行的赋值运算会在这个类作用域中创建变量名,从而成为对应的类对象内的属性。因此类就像模块和函数:a、就像函数一样,class语句是本地作用域,由内嵌的赋值语句建立的变量名,就存在于这个本地作用域内;b、就像模块内的变量名,在class语句内赋值的变量名会变成类对象中的属性。类的主要的不同之处在于其命名空间也是python继承的基础。在类或实例对象中找不到的所引用的属性,就会从其他类中获取。因为class是复合语句,所以任何种类的语句都可位于其主体内:print、=、if、def等等。当class语句自身运行时(不是调用类来创建实例的时候),class语句内的所有语句都会执行。在class语句内赋值的变量名,会创建类属性,而内嵌的def则会创建类方法,但是其他赋值语句也可制作属性。例如,把简单的非函数的对象赋值给类属性,就会产生数据属性,由所有实例共享:
在这里,变量名spam是在class语句的顶层进行赋值的,所以会附加在这个类中,从而为所有的实例共享。可以通过类名修改它或者通过实例或类引用它:
这种类属性可以用于管理贯穿所有实例的信息。例如:所产生的实例的数目的计数器(在第31章接着说),现在假设通过实例而不是类给变量名spam赋值:
对实例的属性进行赋值运算会在该实例内创建惑修改变量名,而不是在共享的类中。通常情况下,继承搜索只会在属性引用时发生,而不是在赋值运算时发生:对对象属性进行赋值总是会修改该对象,除此之外没有其他影响。例如:y.spam会通过继承而在类中查找,但是对x.spam进行赋值运算则会把该变量名附加在x本身上。下面的例子,是将相同的变量名存储在两个位置。假设执行下列类:
这个类有两个def,把类属性与方法函数绑定在一起。此外,也包含一个=赋值语句。因为赋值语句是在类中赋值变量名data,该变量名会在这个类的作用域内存在,变成类对象的属性。就像所有类属性,这个data会被继承,从而被所有没有自己的data属性的类的实例所共享。当创建这个类的实例的时候,变量名data会在构造函数方法内对self.data进行赋值运算,从而把data附加到这些实例上:
结果就是data存在两个地方;a、在实例对象内(由__init__中的self.data赋值运算所创建)以及在实例继承变量名的类中(由类中的data赋值运算所创建)。类的display方法打印了这两个版本。先以点号运算得到self实例的属性,然后才是类。利用这些技术把属性存储在不同对象内,可以决定其可见范围。附加在类上时,变量名是共享的;附加在实例上时,变量名是属于每个实例的数据,而不是共享的行为或数据。虽然继承搜索会查找变量名,但总是可以通过直接读取所需要的对象,而获得树中任何地方的属性。
4、从抽象视角看,方法替实例对象提供了要继承的行为。从程序设计的角度看,方法的工作方式与简单函数完全一致,只是有个重要差异:方法的第一个参数总是接收方法调用的隐性主体,也就是实例对象。也就是python会自动把实例方法的调用对应到类方法函数。如下所示:会自动翻译成:。
5、调用超类构造函数:在构造时,python会ton通过继承找出所有属性__init__方法并且只调用一个。如果保证子类的构造函数也会执行超类构造时的逻辑,一般都必须通过类明确的调用超类的__init__方法:
这是代码有可能直接调用运算符重载方法的环境之一。如果真的想运行超类的构造方法,自然只能用这种方式进行调用:没有这样的调用,子类会完全取代超类的构造函数。
6、其他方法调用的可能性:通过类调用方法的模式,是扩展继承方法行为(不是完全取代)的一般基础。在第31章中,我们也会遇到2.2新增的选项:静态方法,可以让编写不期望第一参数为实例对象的方法。这类方法可像简单的无实例的函数那样运行,其变量名属于其所在类的作用域,并且可以用来管理类数据。一个相关的概念,类方法,当调用的时候接受一个类而不是一个实例,并且它可以用来管理基于每个类的数据。不过,这是高级的选用扩展功能,通常来说,一定要为方法传入实例,无论通过实例还是类调用都行。
7、属性树的构造,图28-1总结命名空间树构造以及填入变量名的方式。通常来说:a、实例属性是由对方法内self属性进行赋值运算而生成的;b、类属性是通过class语句内的语句(赋值语句)而生成的;c、超类的连接是通过class语句首行的括号内列出类而生成的:
结果就是连接实例的属性命名空间树,到产生它的类、再到类首行中所列出的所有超类,每次以点号运算从实例对象取出属性名称时,python会向上搜索树,从实例直到超类。
8、类接口技术:扩展只是一种与超类接口的方式。下面所展示的specialize.py文件定义了多个类,示范了一些常用技巧:a、Super:定义一个method函数以及在子类中期待一个动作的delegate;b、Inheritor:没有提供任何新的变量名,因此会获得Super中定义的一切内容;c、Replacer:用自己的版本覆盖Super的method;d、Extender:覆盖并回调默认method,从而定制Super的method;e、Provider:实现Super的delegate方法预期的action方法。研究这些子类来了解它们定制的共同的超类的不同途径。下面就是文件:
首先,这个例子末尾的自我测试程序代码会在for循环中建立三个不同类实例,因为类是对象,可以将它们放在元组中,并可以通过通用方式创建实例。类也有特殊的__name__属性,就像模块。它默认为类首行中的类名称的字符串。下面是结果:
9、抽象超类:注意上一个例子中的Provider类是如何工作的。当通过Provider实例调用delegate方法时,有两个独立的继承搜索:a、在最初x.delegate调用中,Python会搜索Provider实例和它上层的对象,知道在Super中找到delegate方法。实例x会像往常一样传递给这个方法的self参数;b、在Super.delegate方法中,self.action会对self以及它上层的对象启动新的独立继承搜索。因为self指的是Provider实例,在Provider子类中就会找到action方法。从delegate方法的角度来看,这个例子中的超类有时也称作抽象超类,也就是类的部分行为默认是由其子类所提供的。如果预期的方法没有在子类中定义,当继承搜索失败时,会引发未定义变量名的衣长。类的编写者偶尔会使用assert语句,使这种子类需求更为明显,或者引发内置的衣长NotImplementedError。下面是assert方法的例子:
只要表达式为假,就会触发出错信息,而且,有些类只在该类的不完整方法中直接产生NotImplementedError异常:
对于子类的实例,我们将得到异常,除非子类提供了期待的方法来替代超类中的默认方法:
10、2.6和3.0的抽象超类:前面的抽象超类,需要由子类填充的方法,它们也可以以特殊的类语法来实现。编写的代码的这种方法根据版本不同而有所变化。在3.0中,在一个class头部使用一个关键字参数,以及特殊的@装饰器语法,:
,
左边是3.0的;右边是2.6的,它们的效果是相同的,也就是不产生一个实例,除非在类树的较低层级定义了该方法。例如,在3.0中,与前一小节的例子等价的特殊语法如下:
这样的方式编写代码,带有一个抽象方法的类是不能继承的(即不能通过调用它来创建一个实例),除非其所有的抽象方法都已经在子类中定义了。尽管这需要更多的代码,但是这种方法的优点是,当视图产生该类的一个实例时,由于没有方法会产生错误,可以提前预警。不过这方法也依赖于两种高级语言工具,第31、38、39章涉及的元类声明。
11、命名空间:完整的内容。首先记住,点号和无点号的变量名,会用不同的方式处理,而有些作用域是用于对对象命名空间做初始设定的:a、无点号运算的变量名(例如:X)与作用域相对应;b、点号的属性名(例如:object.X)使用的是对象的命名空间;c、有些作用域会对对象的命名空间进行初始化(模块和类)。
12、简单变量名:如果赋值就不是全局变量。无点号的简单变量名遵循第17章中的函数LEGB作用域法则,具体如下:a、赋值语句(X = value),使变量名成为本地变量:在当前作用域内,创建或改变变量名X,除非声明它是全局变量;b、引用(X),在当前作用域内搜索变量名X,之后是在任何以及所有的嵌套的函数中,然后是在当前的全局作用域中搜索,最后在内置作用域中搜索。
13、属性名称:对象命名空间。点号的属性名指的是特定对象的属性,并且遵循模块和类的规则。就类和实例对象而言,引用规则增加了继承搜索这个流程:a、赋值语句(object.X = value)在进行点号运算的对象的命名空间内创建惑修改属性名X,并没有其他作用。继承树的搜索只发生在属性引用时,而不是属性的赋值运算时;b、引用(object.X)就基于类的对象而言,会在对象内搜索属性名X,然后是其上所有可读取的类(使用继承搜索流程)。对于不是基于类的对象而言(例如,模块),则是从对象中直接读取X。
14、python命名空间的"禅":赋值将变量名分类。点号和无点号的变量名有不同的搜索流程,再加上两者都有多个搜索层次,有时很难看出变量名最终属于何处。在python中,赋值变量名的场所相当重要:这完全决定了变量名所在的作用域对象。下面的manynames.py示范了这条原则是如何变成代码的,并总结了本身遇到的命名空间的概念:
这个文件分别五次给相同的变量名X赋值。从上至下,对X的赋值语句会产生:模块属性(11)、函数内的本地变量(22)、类属性(33)、方法中的本地变量(44)以及实例属性(55)。下面是源代码的其余部分:
文件执行时打印的输出就在程序代码的注释中,可以跟踪这些输出来了解每次读取的变量X是哪个。ps:可以通过类来读取其属性(C.X),但无法从def语句外读取函数惑方法内的局部变量。局部变量对于在def内的其余代码才是可见的。事实上,只有当函数调用惑方法执行时,才会存在于内存中。这个文件定义的其中一些变量名可以让文件以外的其他模块看见。但是,我们在另一个文件内读取这些变量名前,总是需要先导入的。
ps:manynames.f()是怎样打印manynames中的X的,而不是打印本文件中赋值的X。作用域总是由源代码中的赋值语句位置来决定的(也就是语句),而且绝不会受到其导入关系的影响。而且直到调用I.m()前实例的X都不会创建:属性就像是变量,在赋值之后才会存在,而不是在赋值前。在通常情况下,创建实例属性的方法是在类的__init__构造函数内进行赋值的,但这不是唯一的选择。正如在第17章看到的,一个函数在其外部修改名称也是可能的,使用global和(3.0中的)nonlocal语句:
15、命名空间字典:在19章,模块的命名空间实际上是以字典的形式实现的,并且可以由内置属性__dict__显示这一点。类和实例对象也是如此:属性点号运算其实内部就是字典的索引运算,而属性继承其实就是搜索链接的字典而已。实际上,实例和类对象就是python中带有链接的字典而已。python暴露这些字典,还有字典间的链接,以便在高级角色中使用。为了了解python内部属性的工作方式,通过交互式加入类,来跟踪命名空间自动的增长方式。首先定义一个超类和一个带方法的子类,而这些方法会在实例中保存数据:
当制作子类的实例时,该实例开始会是空的命名空间字典,但是有链接会指向它的类,让继承搜索能顺着寻找。继承树可在特殊的属性中看到,可以进行查看。实例中有个__class__属性连接到了它的类,而类有个__bases__属性,是一个元组,其中包含了通往更高的超类的链接(在3.0中运行):
当类为self属性赋值时,会填入实例对象,即属性最后会位于实例的属性命名空间字典内,而不是类的。实例对象的命名空间保存了数据,会随着实例不同而不同,而self正是进入命名空间的钩子:
类字典内的其他含有下划线变量名,python会自动设置这些变量,它们中大多数都不会在一般程序中用到,但是有些工具会使用其中的一些变量(例如__doc__控制第15章讨论的文档字符串)。因为属性实际上是python的字典键,所以有两种方式可以读取并对其进行赋值:通过点号运算或者键索引运算:
这种等效关系只适用于实际中附加在实例上的属性。因为属性点号运算也会执行继承搜索,所以可以存取命名空间字典索引运算无法读取的属性。例如:继承的属性X.hello无法由X.__dict__['hello']读取。下面的代码是用dir的实例dir(object)类似于object__dict__.keys()调用。在2.2中,dir会自动收集继承的属性,3.0中它包含从所有类的隐含超类object类继承的名称:
16、命名空间链接:上一节介绍了“实例和类的特殊属性__class__和__bases__”,但是没有例子说明为什么留意这些属性。简单来说,这些属性可以在程序代码内查看继承层次。例如,可以用它们来显示类树,就像下面例子:
(class与instancetree同一层级)
此脚本中的classtree函数是递归的:它会使用__name__打印类的名称,然后调用自身从而运行超类。这样可以让函数遍历任意形状的类树。递归会运行到顶端,然后在具有空的__bases__属性组超类停止。当使用递归的时候,一个函数的每个活动层级都获取本地作用域的自己的副本;在这里,这意味着cls和indent在每个classtree层级都是不同的。这个文件的大部分内容都是自我测试程序代码。在3.0下独立执行时,会创建空的类树,从中产生两个实例,并打印其类树结构:
在3.0下运行时,包含了隐藏object超类的树会自动添加到独立的类上,因为所有的类在3.0中都是“新式的”:
这里由点号表示的缩进用来代表类树的高度的。可以在任何想很快得到类树显示的地方导入这些函数:
这个例子示范了能够利用的多种特殊属性中的一种,而这些属性显示出解释器内部细节。在第30章还会接着介绍。
17、文档字符串也可以用于类的部分。在第15章详细介绍了文档字符串,它是出现在各种结构的顶部的字符串常量,由python在相应对象的__doc__属性自动保存。它适用于模块文件、函数定义,以及类和方法。下面的文件docstr.py提供了一个快速但全面的示例,来概况文档字符串可以在代码中出现的位置。所有这些都是可以三重引号的块:
文档字符串的主要优点是,它们在运行时能够保持。因此,如果它们已经编写为文档字符串,可以用其__doc__属性来获取文档:
第15章讨论了PyDoc工具,该工具知道如何格式化报表中的所有这些字符串。下面是在2.6下运行代码的情况(3.0还显示从新式类模式的隐含object超类继承来的额外属性):
文档字符串在运行时可用,但是从语法上比#注释要缺乏灵活性。针对功能性文档(对象做什么)使用文档字符串;针对更加微观的文档(令人费解的表达式是如何工作的)使用#注释。
18、类与模块的关系:模块:a、是数据/逻辑包;b、通过编写Python文件或C扩展来创建;c、通过导入来使用; 类:a、实现新的对象;b、由class语句创建;c、通过调用来使用;d、总是位于一个模块中。