Python学习笔记整理(十四)类基础

时间:2023-02-25 18:13:10

一、OOP介绍
1、类的基本概念
类是Python面向对象程序设计(OOP)的主要工具。
类建立使用class语句,通过class定义的对象,很像内置类型。类就是一些函数包,这些函数大量使用并处理内置对象类型。
不过,类型的设计为了创建和管理新的对象,并且也支持继承。类就是一种定义新种类的东西的方式,它反映了在程序领域中的真实对象。
继承和组合这些一般性的OOP概念,适用于能够分解成一组对象的任何应用程序。
程序观点:类是Python程序的组成单元,就像函数和模块一样,类是封装逻辑和数据的另一种方式。也定义新的命名空间。
特点:多重实例,通过继续进行定制,运算符重载(类创建的对象进行切片,级联和索引运算。

2、概览OOP
1)属性搜索继承
object.attribute
使用这个表达式来读取模块的属性,调用对象的方法等。然后,当我们对class语句产生的对象使用这种方式时,
这个表达式会在Python中启动搜索--搜索对象连接树,来寻找attribute首次出现的对象。当类启用时,这个
Python表达式实际上就等于下列自然语言:
找出attribute首次出现的地方,先搜索object,然后改对象之上的所有类,由下至上,由左到右2。
换句话说,取出属性只是简单的搜索“树”而已。我们称这种搜索为继承。因为树中位置低的对象继承了数中较高
位置对象拥有的属性。当从下至上的进行搜索时,连接至树中对象就是树中所有上层对象所定义的所有属性的集合体,
直到树的最顶端。
类:类是实例工程,类的属性提供行为(数据以及函数),所有从类产生的实例都继承该类的属性。
实例:代表程序领域中具体的元素。
超类,子类。子类继承超类,也能在树中较低位置重新定义超类的变量名,从而覆盖超类定义的行为(方法)。
类和模块的差异,类是语句,模块都文件
类和实例
类是生产实例的工厂,实例就像带有“数据"的记录,而类是处理这些记录的“程序”.
类方法调用
调用方法,也就是附属于类的函数属性。
如果I2.w是引用函数调用,它代表的就是:调用C3.w函数来处理I2",也就是python的会自动把I2.w调用对应为C3.w(I2)调用。
将实例当成第一个参数传递给继承的函数。
I2实例,C3超类,w方法。理解为类中附属的函数就是方法,这种方法对一种类型都有效。
每当我们调用附属于类的函数时,总会隐含着这个类的实例。这个隐含的主体或环境就是将其称之为面向对象模型的一部分原因。
当运算执行时,总是有个主体对象。Python把隐含的实例传进方法的第一个特殊的参数,习惯上将其称为self.
方法能通过实例,如bob.giveRaise()或者Employee.giveRaise(bob)进行调用。
2)、编写类树
以class语句和类调用来构造一些树和对象。简单概述:
*每个class语句会生成一个新的类对象
*每次类调用时,就会生成一个新的实例对象
*实例自动连接至创建了这些实例的类
*类连接至其超类的方式是:将超类列在类头部的括号内。其从左到右的顺序会决定树中的次序。
class     C2:
class    C3:
class    C1(C2,C3):
....
I1=C1()
I2=C2()
这里例子是多重继承,也就是说在类数中,类有一个以上的超类,在Python中,如果class语句中的小括号
内有一个以上的超类(像这里的C1),它们由左至右的次序会决定超类搜索的顺序。
附加在实例的属性只属于那些实例,但附加在类上的属性则由所有子类及其实例共享。
深入研究属性发现:
*属性通常是在class语句中通过赋值语句添加在类中,而不是嵌入在函数的def语句内。
*属性通常在类中,对传给函数的特殊参数(也就是self),做赋值运算而添加在实例中的。
例如,类通过函数为实例提供行为。因为类嵌套的def会在类中对变量名,进行赋值,实际效果就是把
属性添加在了类对象之中,而可以由所有实例和子类继承。
class C1(C2,C3):
    def setname(self,who):
        self.name=who
I1=C1()
I1.setname('bob')
print I1.name
>>> class C1:       
...     def setname(self,who):
...             self.name=who        
...
>>> I1=C1()
>>> I1.setname('diege')
>>> print I1.name
diege
在这样的环境下,def语法没有什么特别之处,从操作的角度来看,当def出现在像这样的类内部的时候,通常称为方法,
而且会自动接收第一个特殊参数(通常称为self),这个参数提供了处理的实例的参照值。
因为类是多个实例的工厂,每当需要取出或设置正由某个方法调用所处理的特定的实例的属性时,那些方法通常都会
通过这个自动传入的参数self.在之前的代码中,self是用来存储两个实例之一的内部变量名。

在这个例子中,在setname方法调用前,C1类都不会把name属性附加在实例之上。事实上,调用I1.setname前引用I1.name
会产生未定义变量名错误。如果类想确保name这样的变量名一定会在其实例中设置,通常都会在构造时填好这个属性。
>>> class C1:
...     def __init__(self,who):
...             self.name=who
...
>>> I1=C1('diege')
>>> print I1.name
diege
>>> class pers:
...     def __init__(self,name,age,sex,level):
...             self.name=name
...             self.age=age    
...             self.sex=sex  
...             self.level=level  
...
>>> diege=pers('diege',18,'m',99)
>>> diege.name
'diege'
>>> diege.age
18
>>> diege.sex
'm'
>>> diege.level
99
如果构造器不需要求一次性设置这么多属性。类的实例应该是diege=pers()
>>> class pers:              
...     def setname(self,name):                 
...             self.name=name
...     def setage(self,age):       
...             self.age=age 
...
>>> Ptest()     #错误语法
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: pers instance has no __call__ method
>>> Ptest.age
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: pers instance has no attribute 'age'
>>> Ptest.setage(18)        
>>> Ptest.age
18
如果所有实例具有一些公共属性,则可以直接self赋值而不需参数输入。比如一个年纪一个班的同学作为一个class
>>> class student:              
...     def __init__(self):                 
...             self.level=2012
                self.class=02
这样所有的类的实例都具有这两个相同的属性。

__init__ 构造??
这样每次从类产生实例时,Python会自动调用名为__init__的方法。新的实例如往常那样传入__init__的self参数,
而列在类调用小括号内的任何值都会成为第二,以及其后的参数,其效果就是在创建实例时初始化了这个实例,而
不要额外的方法调用。
由于__init__ 方法的运行时机,它也称作构造器。这是所谓的运算符重载方法这种较大类型方法中最常用的代表。
这种方法像往常一样在类树总被继承。这类方法是可选的:省略时,不支持这类运算。
3、OOP为了代码重用
OOP就是代码重用:分解代码,最小化代码的冗余以及对现在的代码进行定制来编写程序,而不是实地地修改代码或
从头开始。
类其实就是由函数和其他变量名所构成的包,很像模块。然后从类得到自动属性继承搜索,支持软件的高层次定制,
而通过函数和模块做不到。
举例:任务是实现员工的数据库应用程序。
先写一个通过用的员工超类,定义组织中所有员工默认的通用行为
class Emloyee:
    defc info(self):...
    defc computeSalary(self):...
    defc giveRaise(self):...
    defc retire(self):...
然后可以编写子类,定制每种类型的员工中不同的行为。这个类型的员工的其他行为则会继承通用化的超类。
例如工程是有独特的薪资计算规则,就可以在子类中取代这一个方法。
class Engineer(Emloyee):
    def computeSalary(self):...
因为这里computeSalary版本在类树的下面出现,所以会取代(覆盖)超类(Emloyee)中的通用版本。
然后可以建立员工所属的员工种类的实例。
bob=Emloyee()
diege=Engineer()
当稍后要取得这些员工的薪资时,额可以根据创建这个对象的类来计算。这也是基于继承搜索的原理。
company=[diege,bob]
fro emp in company:
    print emp.computeSalary()

二、类代码编写基础
类支持多个对象的产生,命名空间的继承,运算符重载
1、类产生多个实例对象
Python OOP模型中的两种对象:类对象和实例对象。
类对象提供默认的行为,是实例对象的工厂。实例对象是程序处理的实际对象:各自都有独立的命名空间。类对象来至于
语句,而实例来至于调用。每次调用一个类,就会得到这个类的新实例。
1)类对象提供默认行为
执行class语句,就会得到类对象。以下是Python类的主要特性,
*class语句创建类对象并将其赋值给变量名。
class是可执行语句,执行时,产生新的类对象,并将其赋值给class头部的变量,class如def一样是在其所在文件导入时执行。
*class语句内的赋值语句会创建类的属性。
就像模块文件一样,class语句内的顶层赋值语句(不在def之内)会产生类对象中的属性。从技术角度,class语句的作用域会变成
类对象的属性的命名空间,就像模块的全局作用域一样。执行class语句后,类的属性可由变量名点号运算获取object.name
类的属性和类实例的属性不一样。类实例的属性在def中定义。
*类对象提供对象的状态和行为。
类对象的属性记录状态信息和行为,可由这个类所创建的所有实例共享。位于类中的函数def语句会生成方法,方法将会处理实例。
2)实例对象是具体的元素
当调用类对象时,我们得到实例对象。
*像函数那样调用类对象创建新的实例对象。
每次类调用时,都会建立并返回新的实例对象。实例代表了程序领域中的具体元素。
*每个实例对象继承类的属性并获得了自己的命名空间。
由类创建的实例对象是新的命名空间。一开始是空的,但是会继承创建该实例的类对象内的属性。
*在方法内对self属性做赋值运算会产生每个实例自己的属性。
在类方法函数内,第一个参数(惯例为self)会引用正处理的实例对象(也就是实例本身)。对self的属性做赋值运算,会创建或
修改实例内的数据,而不是类的数据。
>>> class  T:                 
...     name='diege'              
...     def __init__(self,sex):
...             self.sex=sex
...
>>> T.name     #类的属性
'diege'
>>> T    
<class __main__.T at 0x2850353c>
>>> T('m')
<__main__.T instance at 0x28508fec>
>>> I1=T('m')  #相同的对象每赋值一次0x28508d0c都有变化。
#这里是类创建了一个实例对象,变量名连接到这个实例对象
>>> print I1
<__main__.T instance at 0x28508d0c>
>>> print I1.sex  #实例的属性
m
>>> print I1.name #实例继承类的属性
diege
第一个实例
>>> class FirstClass:          
...     def setdata(self,value):
...             self.data=value
...     def display(self):
...             print self.data
这个类的两个属性:FirstClass.setdata,FirstClass.display.在类代码顶层的赋值的任何变量名,都会成为类的属性。
位于类中的函数通常称为方法,也是属性的一种。方法是普通的def。在方法函数中,调用时,第一个参数自动接收隐含的实例对象(self):调用主体
>>> X=FirstClass()   
>>> Y=FirstClass()   
错误理解:
#这样是变量X直接连接到类对象了,而不是类产生出来的实例对象。好像理解错误了,也是实例对象。
#注意这里的括号 如果没有括号则是变量X直接引用class对象,有括号就是产生实例对象了,也就是可读取类属性的命名空间。
这时有三个对象:两个实例和一个类。从OOP观点来看,X和Y都是FirstClass对象。
>>> X.display
<bound method FirstClass.display of <__main__.FirstClass instance at 0x28508f6c>>
>>> X.setdata
这个两个对象一开始是空的,但是她们被连接到创建它们的类、
如果对实例以及类对象内的属性名称进行点运算,Python会通过搜索从类取得变量名(除非该变量名位于实例内)
>>> X.setdata('king diege')
>>> Y.setdata(3.14)
X或Y本身没有setdata属性,为了寻找这个属性,Python会顺着实例到类的连接搜索。这就是Python的继承:继承是在属性
点号运算时发生的,而且只查找连接对象内变量名。
在FirstClass的setdata函数中,传入的值会赋值给self.data。在方法中,self(惯例,这是最左侧参数的名称)会自动引用
正在处理的实例(X或Y),所以赋值语句会把值存储在实例的命名空间,而不是类的命名空间。
因为类产生多个实例,方法必须经过self参数才能获取正在处理的实例。当调用类display方法来打印self.data时,会发现
每个实例的值都不同,另外变量名display在X和Y中都相同,因为它是来至于(继承自)类的:
>>> X.display()
king diege
>>> Y.display()
3.14
注意:在每个实例内的data成员存储不同对象类型(字符串和浮点).
这个模型动态性的方式是:考虑可以在类的内部或外部修改实例属性,在类中时。通过方法内的self进行赋值运算,
而在类的外部时,可以通过对实例对象进行赋值运算。
>>> X.data    
'king diege'
>>> X.data='New value'
>>> X.display()
New value
>>> Y.display() 
3.14
对实例赋值,不影响其他实例和类。
这个比较少见,不过甚至可以在实例命名空间内产生全新的属性。
>>> X.other='diege test'   
>>> X.other            
'diege test'
疑问解决:当一个类的属性名(赋值语句)和一个类实例的属性名一样,会怎么样?
结论:说明类的属性和类实例的属性是不同的命名空间,即时属性名相同也不会影响。
>>> class T:                  
...     name='diege'          
...     def __init__(self,who):        
...             self.name=who
...
>>> T.name
'diege'
>>> Y=T()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() takes exactly 2 arguments (1 given)
#这里报错是因为构造器,必须让输入第2个参数
NameError: name 'diege' is not defined
>>> Y=T('diege')
>>> Y.name
'diege'
>>> Y=T('lily')     
>>> Y.name    
'lily'
>>> T.name
'diege'
说明类的属性和类实例的属性是不同的命名空间,即时属性名相同也不会影响。
当类和类的实例定义同名属性时,实例的属性覆盖继承类的,当实例没定义这个属性时继承这个类的属性。
2、类通过继承进行定制
1)类通过继承进行定制
除了作为工厂来生成多个实例对象外,类也可以引入新组件(子类)来进行修改,而不对现有组件进行实地的修改。
在阶层较低的地方覆盖现有的属性,让行为特定化。
在Python中,实例从类中继承,而类继承与超类。属性继承机制的核心观点为:
*超类列在了类开头的括号中。
*类从其超类中继承属性。
就像实例继承其类中所定义的属性名一样,类继承其超类中定义的所有属性名。当读取属性时,
如果它不存在于子类中,Python会自动搜索这个属性。
*实例会继承所有可读取类的属性。
每个实例会从创建它的类中获取变量名以及该类的超类。寻找变量名时,Python会检查实例,然后是它的类,最后是所有超类。
*每个object.attribute都会开启新的独立搜索。
这包括在class语句外对实例和类的引用(例如,X.attr),以及类方法函数内对self实例参数属性的引用。
方法中每个self.attr表达式都会开启self及其上层的类的attr属性的搜索。
*逻辑的修改是通过创建子类,而不是修改超类。
在树中层次较低的子类中重新定义超类的变量名,子类就可以取代并定制所继承的行为。

这种搜索的结果主要目的就是,类支持程序的分解和定制。让我们对现有的程序代码进行定制,而不是实地进行修改或从头开头。
第二个例子
这个例子建立在上一个例子上。定义一个新的类SecondClass继承FirstClass所有变量名,并提供其自己的一个变量名。
>>> class FirstClass():
...     def     setdata(self,value):
...                     self.data=value
...     def     display(self):
...             print self.data
...
>>> class SecondClass(FirstClass):
...     def     display(self):
...             print 'Current value="%s"' % self.data
继承搜索从实例到子类,从子类到超类,从左到右。SecondClass覆盖了FirstClass中的display.有时候,我们把这种在树中较低处
发生的重新定义,取代属性的动作称为【重载】
结果就是SecondClass改变了方法display的行为。
>>> Z=SecondClass()
>>> Z.setdata
<bound method SecondClass.setdata of <__main__.SecondClass instance at 0x28508e6c>>
>>> Z.setdata(42)
>>> Z.display()
Current value="42"
>>> X.setdata(42)
>>> X.display()  
42
注意:SecondClass引入的专有化完全是在FirstClass外部完成的,也就是说,不会影响当前存在的
或未来的FirstClass对象。
2)类是模块内部的属性
在非交互模式下FirstClass是写在模块文件内。可以将其导入,就可有正常使用它的名称。
from modulename import FirstClass
class SecondClass(FirstClass):
    def    display(self):
        print 'Current value="%s"' % self.data
等效于
import modulename
class SecondClass(modulename.FirstClass):
    def    display(self):
        print 'Current value="%s"' % self.data
类名称总是存在于模块中。就像模块其他语句一样,class语句会在导入时执行已定义的变量名,而这些变量名会变成独立的
模块属性。每个模块可以混合任意数量的变量(赋值语句),函数和类。这些变量名都是模块的属性,行为都相同。
模块和类同名也是如此
class  person:
    ...
import person
x=person.person()

from person import person
x=person()
3、类可以截获Python运算符。
类和模块的第三个主要差别(1:类代码,模块是文件,2:?):运算符重载。简而言之,运算符重载就是让类写成的对象,
可以截获并响应用在内置类型上的运算:加法,切片,打印,点号运行等。这只是自动分发机制:表达式和其他内置运算流程
要经过类的实现来控制。模块可以实现函数调用,而不是表达式行为。
运算符重载,让我们自己的对象行为就像内置对象那样,可以让类对象由预期的内置类型接口代码处理。
重载运算符主要概念:
*以双下划线命名的方法(__X__)是特殊的钩子。
python运算符重载的实现是提供特殊的命名方法来拦截运算。Python替每种运算和特殊命名的方法之间,定义了固定不变的映射关系。
*当实例出现在内置运算时,这类方法自动调用。
例如,如果实例对象继承__add__方法,当对象出现在+表达式内时,该方法自动调用。该方法的返回值会变成相应表达式的结果。
*类可以覆盖多数内置类型运算。
有几十种特殊运算符重载的方法名称,几乎可截获并实现内置类型的所有运算。它不仅包括表达式,而且像打印和对象建立这类
基本运算也包括在内。
*运算符覆盖方法没有默认值,而且也不需要
如果类没有定义或继承运算符重载方法,就是说相应的运算在类的实例中并不是支持。例如,如果没有__add__,+表达式就会引发异常。
*运算符可以让类与Python的对象模型相继承、
重载类型运算时,以类实现的用户定义对象的行为就像内置对象一样。因此,提供了一致性,以及与预期的接口兼容性。

运算符重载是可选的功能,主要提其他Python程序员开发工具的人在使用,而不是那些应用程序开发人员在使用。
运算符重载不要轻易使用(__init__例外)。

第三个例子
定义SecondClass的子类,实现三个特殊名称的属性。
让Python自动进行调用:当新的实例构造时,会调用__init__(self是新的ThirdClass对象),而当ThirdClass实例出现在+
或*表达式中,则分别调用__add__和__mull_.
>>> class ThirdClass(SecondClass):
...     def __init__(self,value):                  
...             self.data=value                    
...     def __add__(self,other):                   
...             return ThirdClass(self.data + other)   #有没有发现类中的方法调用类的默认方法self.data + other表达式就是value
...     def __mul__(self,other):                      
...             self.data=self.data * other                   
...
>>> dir(ThirdClass)
>>> dir(ThirdClass)
['__add__', '__doc__', '__init__', '__module__', '__mul__', 'display', 'setdata']
>>> a=ThirdClass()
Traceback (most recent call last):#构造器必须输入第二个参数
  File "<stdin>", line 1, in <module>
TypeError: __init__() takes exactly 2 arguments (1 given)
>>> a=ThirdClass('efg')
>>> a.display()
Current value="efg"
>>> b=a+'zyx' #b连接到表达式a+'zyx'生成的对象
>>> b.display()
Current value="efgzyx"
>>> a * 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for *: 'instance' and 'int
__mul_写成了__mull__
修改后
>>> a*3
>>> a.display()
Current value="efgefgefg"
子类是超类的一个对象,ThirdClass是一个SecondClass对象。子类增加一个方法(属性)就像实例增加一个属性一样。不过子类可以增加
变量(赋值语句产生的属性)和方法(def语句产生的属性)。而实例则为实例新属性赋值: X.other='diege test'  (较少见)【类也可以
在外部这样为类添加属性-也少见,这种属性赋值运算只会影响赋值所在的对象,不会影响类和其他实例,即使方法也可以完全独立的在任意类对象的外部创建。】
__init__和__add__这样的特殊命名的方法会由子类和实例继承,就像这个类中赋值的其他变量名。运算符重载方法的名称并不是内置变量
或保留字,他们只是当对象出现在不同的环境时,Python会去搜索的属性。Python通常会自动进行调用,但偶尔也能由程序代码调用(
例如,__init__通常可手动调用来触发超类的构造器)
注意:__add__方法创建并返回这个类的新的实例对象(通过ThirdClass)。__mul__方法会在原处修改当前实例对象(通过重新赋值self属性)
这点和内置类型的行为不同,就像数字和字符串,总是替*运算符制作新的对象。因为运算符重载其实只是表达式对方法的的分发机制。
可以在自己的类对象中以任何喜欢的方式解释运算符。
为什么要使用运算符重载?
可以选择使用或不使用运算符重载。决定取决于有多想让对象的方法和外观看起来更香内置类型。如果省略运算符重载,并且不从
超类中继承该方法,实例就不支持相应的运算。如果尝试使用这个实例进行运算,会抛出异常。
几乎每个实际的类都会出现一个重载方法是:__init__构造器方法。因为这可以让类立即在其新创建的实例内添加属性。

最简单的python类
>>> class res:pass
其内完全没有附加的属性(空的命名空间对象)
在class语句外,通过赋值变量名给这个类增加属性
>>> dir(res)
['__doc__', '__module__']
>>> res.name='diege'
>>> res.age=18
>>> dir(res)       
['__doc__', '__module__', 'age', 'name']
>>> res.name
'diege'
>>> res.age
18
实例
>>> x=res()
>>> y=res()
>>> x.name,y.name
('diege', 'diege')
修改实例属性
>>> x.name='lily'
>>> res.name,x.name,y.name
('diege', 'lily', 'diege')
这种属性赋值运算只会影响赋值所在的对象,不会影响类和其他实例。
命名空间对象的属性通常都是以字典的形式显示的。而类继承只是连接至其他字典的字典而已。
__dict__属性是大多数基于类的对象命名空间字典
>>> res.__dict__.keys()
['age', '__module__', '__doc__', 'name']
>>> x.__dict__.keys()  
['name']
>>> y.__dict__.keys()
[]
>>> dir(res)
['__doc__', '__module__', 'age', 'name']
>>> dir(x) 
['__doc__', '__module__', 'age', 'name']。
每个实例都连接至其类以便继承。如果想查看,这个连接叫做__class__.
类似的__base__属性,也是其超类构成的运算。这两个属性是Python在内存中类树常量的标识方式。
奥秘的重点是,Python类模型相当动态。类和实例只是命名空间对象。
属性是通过赋值语句动态建立。这些赋值语句往往在class语句内。
即使方法也可以完全独立的在任意类对象的外部创建。
接着添加这个前面res类的方法
>>> def upperName(self):
...     return self.name.upper()
res.method=upperName
>>> x.method()
'LILY'
>>> y.method()
'DIEGE
通常情况下类是有class语句填充的,而实例的属性则是通过在方法函数内对self属性进行赋值运算而创建的。
不过重点不在于必须如此。OOP其实就是是在已连接命名空间对象内寻找属性而已。
为了方便管理代码,最好还是在class语句类定义。

本文出自 “diege” 博客,请务必保留此出处http://ipseek.blog.51cto.com/1041109/798993