python学习笔记14-类总结
一、类简单介绍
1、介绍
类是Python面向对象程序设计(OOP)的主要工具,类建立使用class语句,通过class定义的对象。
类和模块的差异,类是语句,模块是文件.
类和实例
实例:代表程序领域中具体的元素。
类是生产实例的工厂,实例就像带有“数据"的记录,而类是处理这些记录的“程序”.
类是实例工程,类的属性提供行为(数据以及函数[方法]),所有从类产生的实例都继承该类的属性。
类的一个实例可以通过:实例.属性,实例.方法,获得类的属性和方法调用。类支持继承,当我们对class语句产生的对象使用object.attribute这种方式时,这个表达式会在Python中启动搜索--搜索对象连接树,搜索对象所属的子类,然后搜索到子类继承的多个超类,直到寻找attribute首次出现的对象。
超类,子类。子类继承超类,也能在树中较低位置重新定义超类的变量名,从而覆盖超类定义的行为(方法)
类方法调用,也就是附属于类的函数属性。
编写类树
以class语句和类调用来构造一些树和对象。简单概述:
*每个class语句会生成一个新的类对象
*每次类调用时,就会生成一个新的实例对象
*实例自动连接至创建了这些实例的类
*类连接至其超类的方式是:将超类列在类头部的括号内。其从左到右的顺序会决定树中的次序。
class C2:
class C3:
class C1(C2,C3):
....
I1=C1()
I2=C2()
这里例子是多重继承,也就是说在类数中,类有一个以上的超类,在Python中,如果class语句中的小括号
内有一个以上的超类(像这里的C1),它们由左至右的次序会决定超类搜索的顺序。
附加在实例的属性只属于那些实例,但附加在类上的属性则由所有子类及其实例共享。
*属性通常是class语句中通过赋值语句添加在类中,而不是嵌入在函数的def语句内。
*属性通常是类中,对传递给函数的特殊参数(也就是self),做赋值运算而添加在实例中。【*】
类通过函数为实例提供行为,因为这类嵌套的def会在类中对变量名进行赋值,实际效果就是把属性添加在
类对象之中了,从而可以由所有实例和子类继承。
>>> class T:
... def setname(self,who):
... self.name=who
...
>>> I1=T()
>>> I1.setname('diege')
>>> print I1.name
diege
第一个特殊参数:self。
这个参数提供了被处理的实例的参照值。
因为类是多个实例的工厂,每当需要取出或设定正由某个方法调用所处理的特定的实例的属性时,那些方法通常都会自动传入参数self.
当方法对self属性进行赋值时,会创建或修改类树底端实例内的属性,因为self自动引用正在处理的实例。self相当于实例本身。
__init__ 构造器方法
上面的例子中,在setname方法调用前,T类都不会把name属性附加在实例之上。事实上,调用I1.setname前引用I1.name
会产生未定义变量名错误。如果类想确保name这样的变量名一定会在其实例中设置,通常都会在构造时填好这个属性。
>>> class T:
... def __init__(self,who):
... self.name=who
>>> I1=T('diege')
>>> print I1.name
diege
写好并继承后,每次从类产生实例时,Python都会自动地调用名为__init__的方法。新实例会如往常那样传入__init__的self参数,
而列在小括号内任何值会成为第二以及其后的参数。其效果就是在创建实例时初始化了这个实例,而不要额外的方法调用。
这样就不用在取得实例属性前,调用一个其他方法实现,__init__相当初始化,默认之意。
__init__构造器方法,除了明确传入类的名字的任何参数外,还隐性的传入新实例。T('diege') ,隐性传入了self(self代表新实例)
T('diege')创建了一个新的对象(实例对象),I1赋值语句引用这个对象,通过I1.name就取得这个对象的name属性。
OOP为了代码重用
OOP就是代码重用:分解代码,最小化代码的冗余以及对现在的代码进行定制来编写程序,而不是实地修改代码或从头开始。
2、类代码编写基础
类支持多个对象的产生,命名空间的继承,运算符重载
1)、类产生多个实例对象
Python OOP模型中的两种对象:类对象和实例对象。
类对象提供默认的行为,是实例对象的工厂。实例对象是程序处理的实际对象:各自都有独立的命名空间。类对象来至于语句,而实例来至于调用。每次调用一个类,就会得到这个类的新实例。
a、类对象提供默认行为
b、实例对象是具体的元素
2)、类通过继承进行定制
a、类通过继承进行定制
b、类是模块内部的属性
3)、类可以截获Python运算符
二、class语句
一般形式
class <name>(superclass,...):
data=value
def mothod(self,...):
self.member=value
在class语句内,任何赋值语句都会产生类属性。
类几乎就是命名空间,也就是定义变量名(属性)的工具,把数据和逻辑导出给客户端。
怎么样从class语句得到命名空间的呢?过程如下。
就像模块文件,位于class语句主体中的语句会建立起属性。当python执行class语句时(不是调用类)
会从头到尾执行其主体内的所有语句。在这个过程中,进行赋值运算会在这个类的作用域中创建变量名,从而成为对应
类对象中的属性。因为,类就像模块和函数:
*就像函数一样。class语句是作用域,由内嵌的赋值语句建立变量名,就存在这个本地作用域内。
*就像模块内的变量名,在class语句内赋值的变量名会变成类对象中的属性。
class是复合语句,任何种类的语句都可以位于其主体内:print ,=,if,def等。当class语句自身运行时,class语句内的所有
语句都会执行。在class语句内赋值的变量名会创建类属性,而内嵌的def语句则会创建类方法,其他的赋值语句也可以制作属性。
class顶层的赋值语句定义的属性可以用于管理贯穿所有实例的信息。
三、方法
方法调用需要通过实例,一般是通过实例调用的
instance.method(arg...)
这会自动翻译成以下形式的类方法函数调用:
class.method(instance,args...)
class通过Python继承搜索流程找出方法名称所在之处.
类方法的第一个参数通常称为self。这个参数提供方法一个钩子,从而返回调用的主体,也就是实例对象:
第一个参数self需明确
这个变量名的存在,会让你明确脚本中使用的是实例属性名称,而不是本地作用域或全局作用域中的变量名
1、调用超类的构造器
像其他属性一样___init__方法是由继承进行查找。也就是说,在构造时,Python会找出并且只调用一个__init__(树中最低的那个)。如果要保证子类的构造方法也执行超类构造器的逻辑(启用超类的构建代码),一般都必须通过子类手动明确地调用超类的__init__方法(通过超类名称名称来调用,手动传入到self实例:Superclass.__init__(self,...) )。
class Super:
def __init__(self,x):
...default code...
class Sub(Super):
def __init__(self,x,y):
Super.__init__(self,x) ###还是有用到的地方。
...custom code...
如果真的想运行超类的构造方法并做适当的修改,自然只能用这种方法进行调用:没有这样的调用,子类会完全取代(覆盖)超类的构造器,或者子类没有设置__init__构造器的情况下完全继承超类的构造器方法(__init__方法也会继承)。
2、其他方法调用的可能。
这种通过类调用方法的模式(类中调用类的方法(不一定自己)),是扩展继承方法行为(而不是完全取代).
3、继承
像class语句这样的命名空间工具的重点就是支持变量名继承。这里扩展关于属性继承的一些机制和角色。在Python中,当对对象进行点号运算时,就会发生继承,而且涉及到搜索属性定义树(一或多个命名空间)。每次使用obecj.attr形式的表达式时(objecj是实例或类对象),Python会从头到尾搜索命名空间树,先从对象开始,找到第一个attr为止。这包括在方法中对self属性的引用。因为树中较低的定义会覆盖较高的定义,继承构成了专有化的基础。
4、属性树的构造
命名空间树构造以及填入变量名的方式,通常来说:
【*】实例属性是由对方法内self属性进行赋值运算而生成的
【*】类属性是通过class语句内顶层的语句(赋值语句)而生成的
【*】超类链接通过class语句首行的括号内列出类而生成的
5、继承方法的专有化- 重载-回调
继承树搜索模式,变成了将系统专有化的最好方式。因为继承会先在子类寻找变量名,然后才查找超类,子类就可以对超类的属性重新定义来取代默认的行为。把系统做成类的层次,再新增外部的子类来对其进行扩展,而不是在原处修改已存在的逻辑。
重新定义继承变量名的概念引出了各种专有化技术。
当子类重新定义一个方法时又调用了超类同样的方法来实现这个子类方法的部分功能,这里就用到回调技术。
这样实现了默认的行为,换句话说,Sub.mothod只是扩展了Super.mothod的行为,而不是完全取代他。
6、抽象类
抽象类就是会调用方法的类,但没有继承或定义该方法,而是期待该方法由子类填补。当行为无法预测,非得等到更为具体的子类编写时才知道,可用这种方式把类通用化。这种“填空”的代码结构一般就是OOP软件的框架。从delegate方法的角度来看,这个例子中的超类有时也称作是抽象类--也就是类的部分行为默认是由其子类所提供的。如果预期的方法没有在子类定义,当继承搜索失败时,Python会引发为定义,变量名的异常。类的编写者偶尔会使用assert语句,使这种子类需求更为明显,或者引发内置的异常NotImplementedError
class Super:
def method(self):
print "in Super.method"
def delegate(self):
self.action()
class Provider(Super):
def action(self):
print "in Provider.method"
类有特殊的属性__name__类的名字,就像模块一样有__name__属性模块的名字。类中默认为类行首行中的类名称的字符串。
四、运算符重载
重载的关键概念
*运算符重载让类拦截常规的Python运算。
*类可重载所有Python表达式运算。
*类可重载打印,函数调用,属性点号运算等运算。
*重载使类实例的行为像内置类型。
*重载是通过提供特殊名称的类方法来实现的。
如果类中提供了某些特殊名称的方法,当类实例出现在运算有关的表达式的时候,Python就会自动调用这些方法。
1、 常见的运算符重载方法
方法 重载 调用
__init__ 构造器方法 对象建立:X=Class()
__del__ 析构方法 对象收回
__add__ 运算符+ X+Y,X+=Y
__sub__ 运算符- X-Y,X-=Y
__or__ 运算符|(位OR) X|Y X|=Y
__repr__,__str__ 打印,转换 print X【__str__】、repr(X)、str(X)
__call__ 函数调用 X()
__getattr__ 点号运算 X.undefined
__setattr__ 属性赋值语句 X.any=Value
__getitem__ 索引运算 X[key],没有__iter__时的for循环和其他迭代器
__setitem__ 索引赋值语句 X[key]=value
__len__ 长度 len(X),真值测试
__cmp__ 比较 X==Y,X
__lt__ 特定的比较 X<Y(or else __cmp__)
__eq__ 特定的比较 X==Y(or else __cmp__)
__radd__ 左侧加法 + Noninstance + X
__iadd__ 实地(增强的)的加法 X+=Y(or else __add__)
__iter__ 迭代环境 用于循环,测试,列表,映射及其他
所有重载方法的名称前后都有两个下划线字符,以便把同类中定义的变量名区别开来。特殊方法名称和表达式或运算的映射关系,是由Python语言预先定义好的。
所有运算符重载的方法都是选用的:如果没有写某个方法,那么定义的类就不支持该运算。多数重载方法只用在需要对象行为表现得就像内置函数一样的高级程序中。然而,__init__构造方法常出现在绝大多数类中。
__getitem__拦截索引运算
__getitem__方法拦截实例的索引运算。当实例X出现X[i]这样的索引运算中时,Python会调用这个实例继承的__getitem__方法。
(如果有),把X作为第一个参数传递,并且放括号内的索引值传递给第二个参数
2、__iter__和_getitem__实现迭代
for循环的作用是从0到更大的索引值,重复对序列进行索引运算,直到检测到超出边界的异常。
__getitem__也可以是Python中一种重载迭代的方式,如果定义了这个方法,for循环每次循环时都会调用类的__getitem__
任何支持for循环的类也会自动支持Python所有迭代环境,包括成员关系测试in,列表解析,内置函数map,列表和元组赋值运算以及类型构造方法也会自动调用__getitem__(如果定义的话)。如今,Python中所有的迭代环境都会先尝试__iter__方法,再尝试__getitem__。如果对象不支持迭代协议,就会尝试索引运算。
从技术角度来将,迭代环境是通过调用内置函数iter去尝试寻找__iter__方法来实现的,而这种方法应该返回一个迭代器对象。
如果已经提供了,Python就会重复调用这个迭代器对象的next方法,直到发生StopIteration异常。如果没有找到__iter__方法
,Python会改用__getitem__机制,就像之前那样通过偏移量重复索引,直到引发IndexError异常。
迭代器对象就是实例self,因为next方法是这个类的一部分。在较为复杂的的场景中,迭代器对象可定义为个别的类或对象,有自己的状态信息,对相同数据支持多种迭代。以Python的raise语句发出信号表示迭代结束。__iter__对象会在调用过程中明确地保留状态信息。所以比__getitem__具体更好的通用性。__iter__迭代器比__getitem__更复杂和难用。迭代器是用来迭代,不是随机的索引运算。事实上,迭代器根本没有重载索引表达式.
__iter__机制是在__getitem__中所见到的其他所有迭代环境的实现方式(成员关系测试,类型构造器,序列赋值运算)。和__getitem__不同的是,__iter__只循环一次,而不是循环多次,循环之后就变为空,每次新的循环,都得创建一个新的迭代器对象。
3、其他常用运算符重载
__del__是析构器
每当实例产生时,就会调用__init__构造方法,每当实例空间被收回执行__del__方法
__setattr__会拦截所有赋值语句,一般不用。
__repr__,__str__ 打印,转换 print X、repr(X)、str(X)
__call__拦截调用:如果定义了,Python就会为实例应用函数调用表达式运行__call__方法。
当需要为函数的API编写接口时,__call__就变得很用有:这可以编写遵循所需要的函数来调用接口对象。
四、命名空间:完整的内容
点号和无点号的变量,会用不同的方式处理,而有些作用域是用于对对象命名空间做初始设定的。
*无点号运算的变量名(例如,X)与作用域相对应
*点号的属性名(如object.X)使用的是对象的命名空间。
*有些作用域会对对象的命名空间进行初始化(模块和类)
1、简单变量名:如果赋值就不是全局变量
无点号的简单运算名遵循函数LEGB作用域法则:
赋值语句(X=value)
使变量名为本地变量:在当前作用域内,创建或改变变量名X,除非声明它是全局变量。如在函数的内的赋值语句。
引用(X)
在当前作用域内搜索变量名X,之后是在任何以及所有的嵌套函数中,然后是在当前的全局作用域中搜索,最后在内置作用域中搜索。
2、属性名称:对象命名空间
点号的属性名称指的是特定对象的属性,并且遵守模块和类的规则。就类和实例对象而言,引用规则增加了继承搜索这个流程。
赋值语句(object.X=value)
在进行点号运算的对象的命名空间内创建或修改属性名X,并没有其他作用。继承树的搜索只发生在属性引用时,而不是属性的赋值运算时
引用(object.X)
就基于类的对象而言,会在对象内搜索属性名X,然后是其上所有可读取的类(使用继承搜索流程).对于不是基于类的对象而言,例如模块,则是从对象中直接读取X(可能是的属性包括,变量名,函数,类)。
3、命名空间:赋值的位置将变量名分类
在Python中,赋值变量名的场所相当重要:这完全决定了变量名所在作用域或对象。一下实例总结了命名空间的概念。
# vim manynames.py
X=11 #模块属性 全局
def f():
print X #函数(本地)作用域内没有X,嵌套函数没有X变量,当前全局作用域(模块的命名空间内)有,显示全局
def g():
X=22 #定义本地作用域变量X
print X #搜索函数(本地)作用域内变量X,有打印
class C:
X=33 #定义的类属性,类的命名空间
def m(self):
X=44 #貌似在这里没有什么意义
self.X=55 #定义类实例的属性,实例的命名空间
if __name__=='__main__':
print X #打印模块属性 结果11
f() #调用f(),f()返回模块全局变量的X 11
g() #调用g(),g()返回函数内局部变量X 22
print X #打印 模块全局变量的里变量,模块的属性 11
obj=C() #调用类的方法产生实例
print obj.X #打印实例的属性X X继承类的属性,所以为33
obj.m() #实例调用类的m方法,
print obj.X #显示这个X属性 因为上一步m方法设置了实例的属性X,为55
# python manynames.py
11
11
22
11
33
55
作用域总是由源代码中赋值语句的位置来决定,而且绝不会受到其导入关系的影响。属性就像是变量,在赋值之后才会存在。
而不是在赋值前。通常情况下,创建实例属性的方法是在类的__init__构造器方法内赋值。
通常说来,在脚本内不应该让每个变量使用相同的命变量名。
4、命名空间字典
模块的命名空间实际上是以字典的形式实现的,并且可以由内置属性__dict__显示这一点。类和实例对象也是如此:属性点号运算
其内部就是字典的索引运算,而属性继承其实就是搜索链接的字典而已。实际上,实例和类对象就是Python中带有链接的字典而已,
>>> class Super():
... def hello(self):
... self.data1='diege'
...
>>> class Sub(Super):
... def hola(self):
... self.data2='eggs'
...
制作子类的实例时,该实例一开始会是空的命名空间字典,但是有链接会指向它的类,让继承搜索能顺着寻找。
实际上,继承树可在特殊的属性中看到,你可以进行查看。实例中有个__class__属性链接到了它的类,而类有个__base__属性。
就是元组,其中包含了通往更高的超类的连接。
>>> X=Sub()
>>> X.__dict__
{}
>>> X.__class__
<class __main__.Sub at 0x2850353c>
>>> Y=Super()
>>> Y.__dict__
{}
>>> Y.__class__
<class __main__.Super at 0x285034ac>
>>> Sub.__bases__
(<class __main__.Super at 0x285034ac>,)
>>> Super.__bases__
()
当类为self属性赋值时,会填入实例对象。也就是说,属性最后会位于实例的属性命名空间字典内,而不是类的。
实例对象的命名空间保存了数据,会随实例的不同而不同,而self正是进入其命名空间的钩子。
>>> Y=Sub()
>>> X.hello()
>>> X.__dict__
{'data1': 'diege'}
>>> X.hola()
>>> X.__dict__
{'data1': 'diege', 'data2': 'eggs'}
>>> Sub.__dict__
{'__module__': '__main__', '__doc__': None, 'hola': <function hola at 0x284954c4>}
>>> Super.__dict__
{'__module__': '__main__', 'hello': <function hello at 0x28495f0c>, '__doc__': None}
>>> Sub.__dict__.keys(),Super.__dict__.keys()
(['__module__', '__doc__', 'hola'], ['__module__', 'hello', '__doc__'])
>>> Y.__dict__
{}
Y是这个类的第2个实例。即时X的字典已由方法内的赋值语句做了填充,Y还是空的命名空间字典。每个实例都有独立的命名空间字典,一开始是空的,可以记录和相同类的其他实例命名空间字典中属性,完全不同的属性。
因为属性实际上是python的字典键,其实有两种方式可以读取并对其进行赋值:通过点号运算,或通过键索引运算。
>>> X.data1,X.__dict__['data1']
('diege', 'diege')
>>> X.data3='lily'
>>> X.__dict__
{'data1': 'diege', 'data3': 'lily', 'data2': 'eggs'}
>>> dir(X)
['__doc__', '__module__', 'data1', 'data2', 'data3', 'hello', 'hola']
>>> dir(Sub)
['__doc__', '__module__', 'hello', 'hola']
>>> dir(Super)
['__doc__', '__module__', 'hello']
对实例赋值,只影响实例,不会影响实例的类和超类
5、命名空间连接
__class__和__bases__这些属性可以在程序代码内查看继承层次。可以用他来显示类树
五、类设计
1、Python和OOP
Python和OOP实现可以概括为三个概念。
继承
继承是基于Python中属性查找(在X.name表达式中)
多态
在X.method方法中,method的意义取决于X的类型(类)
封装
方法和运算符实现行为,数据隐藏默认是一种惯例
类和继承:是“一个”关系 (is a)
从程序员的角度来看,继承是由属性点号运算启动的,由此触发实例,类以及任何超类中变量名搜索。
从设计师的角度看,继承是一种定义集合成员关系的方式:类定义了一组内容属性,可由更具体的集合(子类)继承和定制。
子类和超类的继承是1对1的关系
类和组合:”有一个“关系 (has a)
从程序员的角度来看,组合设计到把其他对象嵌入到容器对象内,并使其实现容器方法。
对设计师来说,组合是另一种表示问题领域中的关系的方法。
但是组合不是集合的成员关系,而是组件,也是整体的组成部分。
组合也反映了个组成部分之间的关系,通常称为“有一个”(has a)关系。Python中,“组合”(聚合)就是指内嵌对象集合体。
类和持续性
持续性:保证数据的持续性,将数据保存在文件或者数据库,继续保存。
pickle和shelve模块和类实例结合起来使用效果很好,通过单个步骤存储到硬盘上。
pickle机制把内存中的对象转换成序列化的字节流,可以保存在文件中。
Shelve会自动把对象pickle生成按键读取的数据库,而此数据库导出类似于字典的接口.
pickle
>>> pickle.dump(obj.server,file) #写入
>>> objread=pickle.load(file) #读取
shelve
>>> dbase=shelve.open('datafile')
>>> dbase['key']=obj.server 写入
>>> shtest=shelve.open('datafile')
>>> shtest['name'] #读取\
2、OOP和委托
所谓的委托,通常就是指控制器对象内嵌其他对象,而把运算请求传给那些对象。控制器负责管理工作。
在Python中,委托通常是以__getattr__钩子方法实现的,因为这个方法会拦截对不存在属性的读取,包装类(代理类)可以使用__getattr__把任意读取转发给包装的对象。包装类包有被包括对象的接口。而且自己也可以增加其他运算。
class wrapper:
def __init__(self,object):
self.wrapped=object
def __getattr__(self,attrname): #__getattr__点号运算,这里重载内置getattr方法打印传入类执行的方法,并把属性请求传入给对象,使用对象默认的方法。委托
print 'Trace:',attrname
return getattr(self.wrapped,attrname)
3、多重继承
在class语句中,首行括号内可以列出一个以上的超类。当这么做时,就在使用所谓的多重继承:类和其实例继承了列出的所有超类的变量。搜索属性时,Python会由左到右搜索类首行中的超类,直到找到相符者。
通常意义上讲,多重继承是模拟属于一个集合以上的对象的好办法,例如一个人可以是工程师,作家,音乐家。因为,可以继承这些集合的特性。
多重继承最常见的用户是作为“混合”超类的通用方法。这类超类一般都称呼混合类:他们提供方法,可以通过继承将其加入应用类。
每个实例都有内置的__class__属性,引用了它所继承的类,而每个类都有__name__属性,用用了首行中的变量名,所以self.__class__.__name__是取出实例的类的名称
>>> x.__class__.__module__
'trac'
>>> x.__module__
'trac
而是用self.__module__或 self.__class__.__module__则取出实例引用模块的名称
4、类是对象:通用对象的工厂
类是对象,因此它很容易在程序中进行传递,保存在数据库结构中。也可以把类传给产生任意种类对象的函数。这类函数在OOP设计领域偶尔称为工厂。
工厂式的函数或程序代码,在一些情况下很方便,因为他们可以让我们取出并传入没有预先在程序代码中硬编码的类。实际上,这些类在编写程序时可能还不存在。抽象类。
>>> def factory(aClass,*args):
... return apply(aClass,args)
...
这里定义了一个对象生成器函数,称为factory.它预期传入的是类对象(任何对象都行),还有该类构造器的一个或多个参数。这个函数使用apply调用该函数并返回实例。
5、方法是对象:绑定或无绑定
方法也是一种对象,很像函数。类方法能有实例或类来读取。实际上Python中就有两种方式。
无绑定类方法对象:无self
通过对类进行点号运算从而获取类的函数属性,会传回无绑定(unboud)方法对象。调用该方法时,【必须明确提供实例对象】作为第一个参数。
绑定实例方法对象:
self+函数对通过对实例进行全运算从而获取类的函数属性,会传回绑定(bound)方法对象。Python在绑定方法对象中自动把实例和函数打
包,所以,不用传递实例去调用该方法。实例已拥有该方法。
这两种方法都是功能齐全的对象,可四处传递,保持在列表内等。执行时两者都需要第一参数的实例(也就是self的值).
6、类和模块
都是命名空间
模块
* 是数据/逻辑套件
* 由Python文件或C扩展编写成
* 通过导入使用
类
*实现新的对象
*由class语句创建
*通过调用使用
*总是存在于模块中。
类支持其他模块不支持的功能。例如,运算符重载,产生多个实例以及继承。