Python学习笔记整理(十五)类的编写

2023-01-31 02:01:37 编写 整理 学习笔记

类代码编写细节
一、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顶层的赋值语句定义的属性可以用于管理贯穿所有实例的信息。

二、方法
方法位于class语句的主体内是由def语句建立的函数对象。抽象角度,方法替实例对象提供了要继承的行为。程序角度,
方法的工作方式与简单函数完全一致,只有一个重要差异:方法的第一个参数总是接受方法调用的隐形主体,也就是实例对象。
Python会自动把实例方法的调用对应到类方法函数。
方法调用需要通过实例,如:
instance.method(arg...)
这会自动翻译成以下形式的类方法函数调用:
class.method(instance,args...)
class通过Python继承搜索流程找出方法名称所在之处。事实上,这两种调用形式在Python都有效。
类方法的第一个参数通常称为self。这个参数提供方法一个钩子,从而返回调用的主体,也就是实例对象:
因为类可以产生许多实例对象,所以需要这个参数来管理每个实例彼此各不相同的数据。
Python中self一定要在程序代码中明确地写出:方法一定要通过self来取出或修改由当前方法调用或正在处理的实例属性。
这个变量名的存在,会让你明确脚本中使用的是实例属性名称,而不是本地作用域或全局作用域中的变量名。

1、调用超类的构造器
方法一般是通过实例调用的。不过通过类调用【class.method(instance实例,args...)】方法也扮演了一些特殊角色。
常见的如构造器方法。像其他属性一样___init__方法是由继承进行查找。也就是说,在构造时,Python会找出并且只调用
一个__init__。如果要保证子类的构造方法也会执行超类构造器的逻辑,一般都必须通过类明确地调用超类的__init__方法。
class Super:
    def __init__(self,x):
        ...default code...
class Sub(Super):
    def __init__(self,x,y):
        Super.__init__(self,x)  ###还是有用到的地方。
        ...custom code...
I=Sub(1,2)
疑问:子类__init__方法不会继承超类的吗?需要是明确手动继承?这个是重载吧?
动手验证
>>> class Super:
...     def __init__(self):
...             self.name='diege'
...
>>> class Sub(Super):
...     def setage(self,age):
...             self.age=age
...
>>> x=Sub() 
>>> x.name
'diege
实验证明子类的__init__方法也会继承,没有任何特殊,超类的任何属性子类都会继承,前面的例子是重载。
前面的例子是代码有可能直接调用运算符重载方法的环境之一。
如果真的想运行超类的构造方法并做适当的修改,自然只能用这种方法进行调用:没有这样的调用,子类会
完全取代(覆盖)超类的构造器,或者子类没有设置__init__构造器的情况下完全继承超类的构造器方法。

2、其他方法调用的可能。
这种通过类调用方法的模式(类中调用类的方法(不一定自己)),是扩展继承方法行为(而不是完全取代)的
一般基础。Python2.2新增的选项:静态方法、可以编写不预期第一个参数为实例对象的方法。这类方法可像简单
的无实例的函数那样运作,其变量名属于其所在类的作用域。不过,这是高级的选用扩展功能。通常情况,一定要
为方法传入实例,无论通过实例还是类调用。

3、继承
像class语句这样的命名空间工具的重点就是支持变量名继承。这里扩展关于属性继承的一些机制和角色。
在Python中,当对对象进行点号运算时,就会发生继承,而且涉及到搜索属性定义树(一或多个命名空间)。每次
使用obecj.attr形式的表达式时(objecj是实例或类对象),Python会从头到尾搜索命名空间树,先从对象开始,
找到第一个attr为止。这包括在方法中对self属性的引用。因为树中较低的定义会覆盖较高的定义,继承构成了
专有化的基础。

4、属性树的构造
命名空间树构造以及填入变量名的方式,通常来说:
【*】实例属性是由对方法内self属性进行赋值运算而生成的
【*】类属性是通过class语句内顶层的语句(赋值语句)而生成的
【*】超类链接通过class语句首行的括号内列出类而生成的。


5、继承方法的专有化
继承树搜索模式,变成了将系统专有化的最好方式。因为继承会先在子类寻找变量名,然后才查找超类,子类就可以对超类的属性重新定义来取代默认的行为。把系统做成类的层次,再新增外部的子类来对其进行扩展,而不是在原处修改已存在的逻辑。重新定义继承变量名的概念因出了各种专有化技术。
>>> class Super:
...     def method(self):
...             print 'in Super.method'
...
>>> class Sub(Super):                 
...     def method(self):             
...             print 'start Sub.method'
...             Super.method(self)    #直接调用超类的方法
...             print 'ending Sub.method'    
...
>>> Z=Super()
>>> Z.method()
in Super.metho
>>> X=Sub()
>>> X.method()
start Sub.method
in Super.method
ending Sub.method
直接调用超类的方法是这里的重点。Sub类以其专有化的版本取代了Super的方法函数。但是取代时,Sub又回调了Super所导出的版本,从而实现了默认的行为,换句话说,Sub.mothod只是扩展了Super.mothod的行为,而不是完全取代他。这种扩展编码模式常常用于构造器方法。
6、类接口技术
class Super:
        def method(self):
                print "in Super.method"
        def delegate(self):
                self.action()
class Inheritor(Super):
        pass
class Replacer(Super):
        def method(self):
                print "in Replacer.method"
class Extender(Super):
        def method(self):
                print "starting Extender.method"
                Super.method(self)
                print "ending Extender.method"
class Provider(Super):
        def action(self):
                print "in Provider.method"
if __name__=='__main__':
        for C in (Inheritor,Replacer,Extender):
                print '\n'+C.__name__+'...'
                C().method()     #C后面的括号表面是类时实例,这里是创建实例和方法调用一起了。分解C=Inheritor(),C.method()
               
                print '\nProvider...'
                x=Provider()    #创建实例对象
                x.delegate()    #实例对象条用delegate方法,delegate方法通过实例的action方法实现
结果
Inheritor...
in Super.method
Provider...
in Provider.method
Replacer...
in Replacer.method
Provider...
in Provider.method
Extender...
starting Extender.method
in Super.method
ending Extender.method
Provider...
in Provider.method
说明
Super
    定义了一个method函数和一个delegate函数
Inheritor
    没有提供任何新的变量名,因此获得Super中定义的一切内容
Replacer
    用自己的版本覆盖Super的method
Extender
    覆盖并回调默认的method,从而定制Super的method
Provider
    现实Super的delegate方法预期的action方法.这里有点不好理解
Provider继承了Super的method和delegate方法并增加了action方法,而delegate方法是调用实例的action方法实现的。
尾部的自我测试程序代码在for循环中建立了三个不同的实例。因为类是对象,可以将他们放入元组中,并可以通过这样的方式创建实例。类有特殊的属性__name__类的名字,就像模块一样有__name__属性模块的名字。类中默认为类行首行中的类名称的字符串
7、抽象超类
上例中Provider类如何工作的?当通过Provider类的实例调用delegate方法时,两个独立的继承搜索会发生:
(1)最初x.delegate的调用中,Pythn会搜索Provider实例和它上层的对象。知道在Super中找到delegate方法。实例x
会像往常一样传递给这个方法self参数
(2)Super.delegate方法中,self.action会对self及其它上层的对象启动新的独立继承搜索,因为self指的是Provider
实例,就会找到Provider中的action方法。
抽象类就是会调用方法的类,但没有继承或定义该方法,而是期待该方法由子类填补。当行为无法预测,非得等到更为具体的子类编写时才知道,可用这种方式把类通用化。这种“填空”的代码结构一般就是OOP软件的框架。从delegate方法的角度来看,这个例子中的超类有时也称作是抽象类--也就是类的部分行为默认是由其子类所提供的。如果预期的方法没有在子类定义,当继承搜索失败时,Python会引发为定义
变量名的异常。类的编写者偶尔会使用assert语句,使这种子类需求更为明显,或者引发内置的异常NotImplementedError
class Super:
        def method(self):
                print "in Super.method"
        def delegate(self):
                self.action()
        def action(self):
                assert 0, 'action must be defind'
如果表达式运算结构为假,就会引发带有错误信息的异常。在这里表达式总是为假(0)。因为如果没有方法重新定义,
继承就会找到这里的版本,触发错误信息。

三、运算符重载
重载的关键概念
*运算符重载让类拦截常规的Python运算。
*类可重载所有Python表达式运算。
*类可重载打印,函数调用,属性点号运算等运算。
*重载使类实例的行为像内置类型。
*重载是通过提供特殊名称的类方法来实现的。
如果类中提供了某些特殊名称的方法,当类实例出现在运算有关的表达式的时候,Python就会自动调用这些方法。
类Number放到number.py模块中:
class Number():
        def __init__(self,start):
                self.data=start
        def __sub__(self,other):
                return Number(self.data-other)
>>> from number import Number
>>> X=Number(5)
>>> Y=X-2      
>>> Y.data
3
>>> Z=X+5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'instance' and 'int'
这个例子Number类提供一个方法来拦截实例的构造器(__init__),此外还有一个方法捕捉减法表达式(__sub__).
这种特殊的方法是钩子,可与内置运算相绑定。
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、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作为第一个参数传递,并且放括号内的索引值传递给第二个参数。
>>> class index:
...     def __getitem__(self,index):
...             return index**2 
...
>>> X=index()
>>> X[2]
4
>>> for i in range(5):
...     print   X[i],
...
0 1 4 9 16
__getitem__和__iter__实现迭代
for循环的作用是从0到更大的索引值,重复对序列进行索引运算,直到检测到超出边界的异常。
__getitem__也可以是Python中一种重载迭代的方式,如果定义了这个方法,for循环每次循环时都会调用类的__getitem__
>>> class stepper:
...     def __getitem__(self,i):
...             return self.data[i]
...
>>> X=stepper
>>> X=stepper()
>>> X.data='diege'
>>> X[1]
'i'
>>> for item in X:
...     print item,
...
d i e g e
任何支持for循环的类也会自动支持Python所有迭代环境,包括成员关系测试in,列表解析,内置函数map,列表和元组赋值运算以及类型构造方法也会自动调用__getitem__(如果定义的话)。如今,Python中所有的迭代环境都会先尝试__iter__方法,再尝试__getitem__。如果对象不支持迭代协议,就会尝试索引运算。
从技术角度来将,迭代环境是通过调用内置函数iter去尝试寻找__iter__方法来实现的,而这种方法应该返回一个迭代器对象。
如果已经提供了,Python就会重复调用这个迭代器对象的next方法,直到发生StopIteration异常。如果没有找到__iter__方法
,Python会改用__getitem__机制,就像之前那样通过偏移量重复索引,直到引发IndexError异常。
class Squares:
        def __init__(self,start,stop):
                self.value=start-1
                self.stop=stop
        def __iter__(self):
                return self
        def next(self):
                if self.value==self.stop:
                        raise StopIteration
                self.value+=1
                return self.value**2
>>> from test29 import Squares
>>> for i in Squares(1,5):   
...     print i,         
...
1 4 9 16 25
迭代器对象就是实例self,因为next方法是这个类的一部分。在较为复杂的的场景中,迭代器对象可定义为个别的类或对象,有自己的状态信息,对相同数据支持多种迭代。以Python raise语句发出信号表示迭代结束。__iter__对象会在调用过程中明确地保留状态信息。所以比__getitem__具体更好的通用性。__iter__迭代器比__getitem__更复杂和难用。迭代器是用来迭代,不是随机的索引运算。事实上,迭代器根本没有重载索引表达式.
>>> X=Squares(1,5)
>>> X[1]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: Squares instance has no attribute '__getitem__'
__iter__机制是在__getitem__中所见到的其他所有迭代环境的实现方式(成员关系测试,类型构造器,序列赋值运算)。和__getitem__不同的是,__iter__只循环一次,而不是循环多次,循环之后就变为空,每次新的循环,都得创建一个新的。
>>> X=Squares(1,5)
>>> [n for n in X]
[1, 4, 9, 16, 25]
>>> [n for n in X]
[]
如果使用生成器函数编写,这个例子可能更简单
>>> from __future__ import generators
>>> def gsquares(start,stop):       
...     for i in range(start,stop+1):
...             yield i**2           
...
>>> for i in gsquares(1,5):
...     print i,          
...
1 4 9 16 25
和类不同的是,这个函数会自动在迭代中存储存其状态。这是假设的例子,实际上,可以跳过这两种技术,只用for循环,map或列表解析
一次创建这个列表。
>>> [x**2 for x in range(1,6)]
[1, 4, 9, 16, 25
有多个迭代器的对象。
__getattr__和__setattr__捕捉属性的引用。
__getattr__方法是拦截属性点号运算。__getattr__可以作为钩子来通过通用的方式相应属性请求。
>>> class empty:
...     def __getattr__(self,attrname):
...             if attrname=="age":
...                     return 40
...             else:
...                     raise AttributeError,attrname
...
>>> X=empty()
>>> X.age
40
这里,empty类和其实例X本身并没有属性。所以对X.age的存取会转换至__getattr__方法,self则赋值为实例(X),
而attrname则赋值为未定义的属性名称字符串("age"),这个类传回一个实际值作为X.age点号表达式的结果(40),
让age看起来像实际的属性。实际上,age变成了动态计算的属性。
__setattr__会拦截所有赋值语句,一般不用。
__repr__,__str__ 打印,转换    print X、repr(X)、str(X)
__call__拦截调用:如果定义了,Python就会为实例应用函数调用表达式运行__call__方法。
当需要为函数的api编写接口时,__call__就变得很用有:这可以编写遵循所需要的函数来调用接口对象。
函数接口和回调代码
__del__是析构器
每当实例产生时,就会调用__init__构造方法,每当实例空间被收回执行__del__方法。

>>> x.__class__
<class trac.wrapper at 0x28503f8c>
>>> x.__class__.__name__
'wrapper'
每个实例都有内置的__class__属性,引用了它所继承的类,而每个类都有__name__属性,用用了首行中的变量名,所以self.__class__.__name__
是取出实例的类的名称
四、命名空间:完整的内容
点号和无点号的变量,会用不同的方式处理,而有些作用域是用于对对象命名空间做初始设定的。
*无点号运算的变量名(例如,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__这些属性可以在程序代码内查看继承层次。可以用他来显示类树
6、一个较复杂的例子
vim person.py
class GenericDisplay:
        def gatherAttrs(self):
                attrs='\n'
                for key in self.__dict__:
                        attrs+='\t%s=%s\n' % (key,self.__dict__[key])
                return attrs
        def __str__(self):
                return '<%s:%s>' % (self.__class__.__name__,self.gatherAttrs())

class Person(GenericDisplay):
        def __init__(self,name,age):
                self.name=name
                self.age=age
        def lastName(self):
                return self.name.split()[-1]
        def birthDay(self):
                self.age+=1
class Employee(Person):
        def __init__(self,name,age,job=None,pay=0):
                Person.__init__(self,name,age)
                self.job=job
                self.pay=pay
        def birthDay(self):
                self.age+=2
        def giveRaise(self,percent):
                self.pay*=(1.0+percent)
if __name__=='__main__':
        diege=Person('diege wang',18)
        print diege
        print diege.lastName()
        diege.birthDay()
        print diege

        sue=Employee('Sue Jones',44,job='dev',pay=1000)
        print sue
        print sue.lastName()
        sue.birthDay()
        sue.giveRaise(.10)
        print sue
# python person.py
<Person:
        age=18
        name=diege wang
>
wang
<Employee:
        job=dev
        pay=1000
        age=44
        name=Sue Jones
>
Jones
<Employee:
        job=dev
        pay=1100.0
        age=46
        name=Sue Jones
>
可以作为模块导入测试
>>> from person import Person
>>> dir(Person)
['__doc__', '__init__', '__module__', '__str__', 'birthDay', 'gatherAttrs', 'lastName']
>>> diege.__dict__
>>> diege=Person('diege wang',18)
{'age': 18, 'name': 'diege wang'}
>>> print diege
<Person:
        age=18
        name=diege wang
>
>>> print diege.lastName()
wang
>>> diege.age
18
>>> diege.birthDay()
>>> diege.age
19

相关文章