title: 对象 create: 2016.12.7 modified: 2016.12.7 tags: python 多态 封装 方法
5
[TOC]
之前介绍了Python主要的内建对象类型(数字int、字符串str、列表list、元组tuple和字典dict),以及内建函数和标准库的用法,还有自定义函数的方式。接下来将介绍如何创建自己的对象? 为什么要自定义对象呢?使用字典、序列、数字和字符串来创建函数,完成这项工作还不够吗?这样做当然可以,但是创建自己的对象(尤其是类型或者被称为类的对象)是Python的核心概念,事实上,Python被称为面向对象的语言(和C++、Java一样)。接下来会介绍如何创建对象,以及多态、封装、方法、属性、父类以及继承的概念。
In [1]:
'abc'.count('a')
Out[1]:
In [2]:
[1,2,'a'].count('a')
Out[2]:
对于变量x来说,不需要知道它是字符串还是列表,就可以调用它的count方法—不用管它是什么类型(只要提供一个字符作为参数即可)。
任何不知道对象到底是什么类型,但是又要对对象“做点什么”的时候,都会用到多态。这不仅限于方法,很多内建运算符和函数都有多态的性质:
In [3]:
1+2
Out[3]:
In [4]:
'Fish '+'license'
Out[4]:
In [11]:
__metaclass__=type #确定使用新式类
class Person:
def setName(self, name):
self.name=name
def getName(self):
return self.name
def greet(self):
print "Hello, world! I'm %s" % self.name
foo=Person()
foo.setName('Luke Skywalker')
foo.greet()
注意 尽管可能使用的是新版的Python,但一些功能不会在旧式类上起作用。为了确保类是新型的,需要在模块或者脚本开始的地方放置赋值语句__metaclass__=type,或者继承新式类(比如object类,也就是子类化内建类object)。新式类必然包含了更多的功能,也是之后推荐的写法,从写法上区分的话,如果当前类或者父类继承了object类,那么该类便是新式类。
在调用foo的setName和greet函数时,foo自动将自己作为第一个参数传人函数中—因此形象地命名为self。显然这就是self的用处和存在的必要性。没有它,成员方法就没法访问它们要对其属性进行操作的对象本身了。
属性是可以在外部访问的:
In [8]:
foo.name
Out[8]:
2.2.1 私有化
默认情况下,程序可以从外部访问一个对象的属性。但是有时候需要使用私有属性,这是外部对象无法访问的,但是通过getName和setName等访问器(accessor)能够访问这些私有属性。
为了让方法或者属性变为私有,只要在它的名字前面加上双下划线即可:
In [3]:
class Secretive:
def __inaccessible(self):
print "Hello, world!"
def accessible(self):
print "The secret message is: "
self.__inaccessible()
现在__inaccessible从外界是无法访问的,而在内部还能使用(比如从accessible)访问:
In [4]:
s=Secretive()
s.__inaccessible()
In [5]:
s.accessible()
类的内部定义中,所有以双下划线开始的名字(方法或属性)都被“翻译”成前面加上单下划线和类名的形式。在了解了这些幕后的事情后,实际上还是能在类外访问这些私有方法,尽管不应该这么做:
In [7]:
s._Secretive__inaccessible()
简而言之,确保其他人不会访问对象的方法和属性是不可能的,但是通过这类“名称变化术”就是他们不应该访问这些方法和属性的强有力信号。
2.2.2 访问器方法
访问器是一个简单的方法,它能使用getHeight、setHeight这样的名字来得到或者重绑定一些属性:
In [26]:
class Rectangle:
def __inf__(self):
self.width=0
self.height=0
def setSize(self,size):
self.width,self.height=size
def getSize(self):
return self.width,self.height
r=Rectangle()
r.width=10
r.height=5
r.getSize()
Out[26]:
In [27]:
r.setSize((150,100))
r.width
Out[27]:
在上面的例子中,getSize和setSize方法是一个名为size的假想属性的访问器方法,size是由width和height构成的元组。如果有一天要改变类的实现,将size变成一个真正的属性,这样width和height就可以动态算出,那么就要把它们放到一个访问器方法中去。但如果有很多简单的属性,那么就不现实了。如果那么做就得写很多访问器方法。那么怎么解决呢?这就需要用到property函数。
property函数
property函数的使用很简单。延续上面的Rectangle类,只要增加一行代码(子类化object,或者使用__metaclass__=type):
In [2]:
__metaclass__=type
class Rectangle:
def __inf__(self):
self.width=0
self.height=0
def setSize(self,size):
self.width,self.height=size
def getSize(self):
return self.width,self.height
size=property(getSize,setSize)
在新版的Rectangle中,property函数创建了一个属性size,其中访问器方法被当做参数(先是取值,然后是赋值)。
In [33]:
w=Rectangle()
w.width=10
w.height=5
w.size
Out[33]:
In [34]:
w.size=150,100
w.width
Out[34]:
很显然,size属性仍然取决于getSize和setSize中的计算。但它看起来就像普通的属性一样。实际上,property函数可以用fget,fset,fdel和doc-这四个参数来调用。如果没有参数,产生的属性既不可读,也不可写。如果只使用一个参数调用(一个取值方法),产生的属性是只读的。第三个参数(可选)是一个用于删除属性的方法。第四个参数(可选)是一个文档字符串。
2.2.3 特殊方法
在Python中,有的名称(方法名)在前面和后面都加上两个下划线,比如__future__,这样拼写表示名字有特殊含义,所以绝不要在自己的程序中使用这种名字。由这些名字组成的集合所包含的方法称为特殊方法。如果对象实现了这些方法的某一个,那么这个方法会在特殊的情况下被Python调用。而几乎没有直接调用它们的必要。
(1) 构造方法
首先要讨论的第一个特殊方法是构造方法。构造方法是一个很奇怪的名字,它代表着类似于以前例子中使用过的那种名为init的初始化方法。但构造方法和其他普通方法不同的地方在于,当一个对象被创建后,会立即调用构造方法。
In [25]:
class FooBar:
def __init__(self):
self.somevar=42
f=FooBar()
f.somevar
Out[25]:
(2) 重写一般方法和特殊的构造方法
如果一个方法在B类的一个实例中被调用(或一个属性被访问),但在B类中没有找到该方法,那么就会去它的父类A里面找:
In [7]:
class A:
def hello(self):
print "hello, I'm A"
class B(A):
pass
a=A()
b=B()
a.hello()
In [8]:
b.hello()
在子类中增加功能最基本的方式就是增加方法。但是也可以重写一些父类的方法来自定义继承的行为。B类也能重写这个方法。
In [9]:
class B(A):
def hello(self):
print "hello, I'm B"
b=B()
b.hello()
重写是继承机制中的一个重要内容,但是对于构造方法尤其重要。构造方法用来初始化新创建对象的状态,大多数子类不仅要拥有自己的初始化代码,还要拥有父类的初始化代码。虽然重写的机制对于所有方法来说都是一样的,但是当重写构造方法时,更可能遇到特别的问题:如果一个类的构造方法被重写,那么就需要调用父类的构造方法,否则对象可能不会被正确的初始化。如下:
In [24]:
class Bird:
def __init__(self):
self.hungry=True
def eat(self):
if self.hungry:
print 'Aaaah...'
self.hungry=False
else:
print 'No,thanks!'
b=Bird()
b.eat()
In [15]:
b.eat()
可以看到,鸟吃过了以后,就不会再饥饿。现在考虑子类SongBird,它添加了唱歌的行为。
In [18]:
class SongBird(Bird):
def __init__(self):
self.sound='Squawk!'
def sing(self):
print self.sound
sb=SongBird()
sb.sing()
因为SongBird是Bird的一个子类,它继承了eat方法,但如果调用eat方法,就会产生一个问题:
In [19]:
sb.eat()
异常很清楚地说明了错误:SongBird没有hungry属性。原因是:在SongBird中,构造方法被重写,但新的构造方法没有任何关于初始化hungry属性的代码。为了达到预期的效果,SongBird的构造方法必须调用其父类Bird的构造方法来确保进行基本的初始化。有两种方法能达到这个目的,如下:
调用未绑定的父类构造方法
In [28]:
class SongBird(Bird):
def __init__(self):
Bird.__init__(self)
self.sound='Squawk!'
def sing(self):
print self.sound
sb=SongBird()
sb.sing()
In [21]:
sb.eat()
In [22]:
sb.eat()
通过将当前的实例作为self参数提供给未绑定方法,SongBird就能够使用其父类构造方法的所有实现,也就是说属性hungry能被设置。
使用super函数
super函数只能在新式类中使用。当前的类和对象可以作为super函数的参数使用,调用函数返回的是父类的方法,而不是当前类的方法。如下:
In [25]:
__metaclass__=type
class SongBird(Bird):
def __init__(self):
super(SongBird,self).__init__()
self.sound='Squawk!'
def sing(self):
print self.sound
sb=SongBird()
sb.sing()
In [26]:
sb.eat()
In [27]:
sb.eat()
(3) 成员访问方法
接下来介绍一些处理对象访问的方法,这些方法允许你创建自己的序列或者映射。
基本的序列和映射规则很简单,但如果要实现它们全部功能就需要实现很多特殊函数。下面将会说到:
基本的序列和映射规则
序列和映射是对象的集合。为了实现它们基本的行为(规则),如果对象是不可变的(如字符串和元组)。那么就需要使用两个特殊方法,如果是可变的(列表和字典),则需要使用4个。
a. __len__(self):这个方法返回集合中所含对象的数量。对于序列来说,这就是元素的个数;对于映射来说,则是键-值对的数量。
b. __getitem__(self,key):这个方法返回与所给键对应的值。对于序列来说,键应该是一个0~n-1的整数(或者像后面所说的负数);对于映射来说,可以使用任何种类的键。
c. __setitem__(self,key,value):这个方法按一定的方式存储和key关联的value,该值随后可使用 __getitem__来获取。当然,只能为可以修改的对象定义这个方法。
d. __delitem__(self,key):这个方法在对一部分对象使用del语句时被调用,同时删除和键关联的值。这个方法也是为可修改的对象定义的。
对这些方法的附件要求:
a. 对于一个序列来说,如果键是负整数,那么要从末尾开始计数。换句话说就是x[-n]和x[len(x)-n]是一样的;
b. 如果键是不合适的类型(例如,对序列使用字符串作为键),会引发一个TypeError异常;
c. 如果序列的索引是正确的类型,但超出了范围,会引发一个IndexError异常。
让我们实践一下—看看如果创建一个无穷序列,会发生什么:
In [3]:
def checkIndex(key):
"""所给的键能接受索引吗?
为了能被接受,键应该是一个非负的整数,如果它不是一个整数,比如是字符串,会引发TypeError;
如果它是负数,则会引发IndexError(因为序列是无限长的)。
"""
if not isinstance(key,(int,long)):
raise TypeError
if key<0:
raise IndexError
class ArithmeticSequence:
def __init__(self,start=0,step=1):
"""初始化算数序列
初始值-序列中的第一个值
步长-两个相邻值之间的差别
改变-用户修改的值的字典
"""
self.start=start
self.step=step
self.changed={} #没有项被修改
def __getitem__(self,key):
"""Get an item from the arithmetic sequence.
"""
checkIndex(key)
try:
return self.changed[key] #修改了吗?
except KeyError: #否则...
return self.start+key*self.step #...计算值
def __setitem__(self,key,value):
"""修改算术序列中的一个项
"""
checkIndex(key)
self.changed[key]=value
s=ArithmeticSequence(1,2)
s[4]
Out[3]:
In [4]:
s[4]=2
s[4]
Out[4]:
In [5]:
s[5]
Out[5]:
注意,没有实现__del__方法的原因是我希望删除元素是非法的:
In [6]:
del s[4]
这个类没有__len__方法,因为它是无限长的。
索引检查是通过用户自定义的checkIndex函数实现的。如果使用了一个非法类型的索引,就会引发TypeError异常,如果索引的类型是正确的但超出了范围,则会引起IndexError异常:
In [7]:
s['four']
In [8]:
s[-4]
(4) __getattr__和__setattr__
拦截(intercept)对象的所有属性访问是可能的,这样可以用旧式类实现属性。为了在访问属性的时候可以执行代码,必须使用一些特殊方法。下面的4种方法提供了需要的功能(在旧式类中只需要后3个)
a. __getattribute__(self,name):当属性name被访问时自动被调用(只能在新式类中使用);
b. __getattr__(self,name):当属性name被访问且对象没有相应的属性时被自动调用;
c. __setattr__(self,name,value): 当试图给属性name赋值时会被自动调用;
d. __delattr__(self,name): 当试图删除属性name时会被自动调用。
尽管和使用property函数相比有点复杂(而且在某些方面效率更低),但是这些特殊方法是很强大的,因为可以对处理很多属性的方法进行再编码。
下面还是Rectangle的例子,但这次使用的是特殊方法:
In [73]:
class Rectangle:
def __init__(self):
self.width=0
self.height=0
def __setattr__(self,name,value):
if name =='size':
self.width,self.height=value
else:
self.__dict__[name]=value
def __getattr__(self,name):
if name =='size':
return self.width,self.height
else:
raise AttributeError
In [74]:
w=Rectangle()
w.size
Out[74]:
In [75]:
w.__dict__
Out[75]:
In [76]:
w.size=(2,6)
w.size
Out[76]:
In [77]:
w.width
Out[77]:
In [78]:
hasattr(w,'size')
Out[78]:
In [79]:
w.age=28
w.age
Out[79]:
In [80]:
w.__dict__
Out[80]:
注意: __setattr__方法在所涉及的属性不是size时也会被调用。如果属性是size,那么就像前面那样执行操作,否则就要使用特殊方法__dict__,该方法包含一个字典,字典里是所有实例的属性;
__getattr__方法只在普通的属性没有被找到的时候调用。
(5) 迭代器
迭代的意思是重复做一些事很多次—就像在循环中做的那样。到现在为止只有在for循环中对序列和字典进行迭代,但实际上也能对其他的对象进行迭代:实现__iter__特殊方法的对象。
__iter__方法返回一个迭代器(iterator),所谓的迭代器就是具有next方法(这个方法在调用时不需要任何参数)的对象。在调用next方法时,迭代器会返回它的下一个值。
迭代规则的关键是什么?为什么不使用列表?因为列表的杀伤力太大。如果有可以一个接一个地计算值的函数,那么在使用时可能是计算一个值时获取一个值-而不是通过列表一次性获取所有值。如果有很多值,列表就会占用太多的内存。另外,使用迭代器更通用、更简单、更优雅。让我们看看一个不使用列表的例子,因为要用的话,列表的长度必须无限。
这里的“列表”是一个斐波那契数列。使用的迭代器如下:
In [84]:
class Fibs:
def __init__(self):
self.a=0
self.b=1
def next(self):
self.a,self.b=self.b,self.a+self.b
return self.a
def __iter__(self):
return self
fibs=Fibs()
for f in fibs:
if f>10:
print f
break
在很多情况下,__iter__被放到会在for循环中使用的对象中。
注意 正式的说法是,一个实现了__iter__方法的对象是可迭代的,一个实现了next方法的对象则是迭代器。
内建函数iter可以从可迭代的对象中获得迭代器:
In [86]:
a=[1,2,3]
a.next()
In [93]:
it=iter([1,2,3])
it.next()
Out[93]:
In [94]:
it.next()
Out[94]:
从迭代器中得到序列:
使用list函数显式地将迭代器转化为列表。
In [100]:
it=iter([1,2,3])
it
Out[100]:
In [101]:
list(it)
Out[101]:
In [10]:
class C:
print 'Class C being defined...'
从上可以看出,类的定义其实就是执行代码块,这一点很有用,比如,在类的定义区并不只限使用def语句:
In [8]:
class MemberCounter:
members=0
def init(self):
MemberCounter.members+=1
m1=MemberCounter()
m1.init()
MemberCounter.members
Out[8]:
In [9]:
m2=MemberCounter()
m2.init()
MemberCounter.members
Out[9]:
上面的代码中,在类作用域内定义了一个可供所有成员(实例)访问的变量,用来计算类的成员数量。
就像方法一样,类作用域内的变量也可以被所有实例(对象)访问:
In [11]:
m1.members
Out[11]:
In [12]:
m2.members
Out[12]:
那么在实例中重绑定members属性呢?
In [13]:
m1.members='Two'
m1.members
Out[13]:
In [14]:
m2.members
Out[14]:
In [3]:
class Filter:
def init(self):
self.blocked=[]
def filter(self,sequence):
return [x for x in sequence if x not in self.blocked]
class SPAMFilter(Filter):
def init(self):
self.blocked=['SPAM']
In [4]:
f=Filter()
f.init()
f.filter([1,2,3])
Out[4]:
Filter类的用处在于它可以用作其他类的父类,比如SPAMFilter类,可以将序列中“SPAM”过滤出去。
In [3]:
s=SPAMFilter()
s.init()
s.filter(['SPAM','SPAM','SPAM','SPAM','eggs','bacon'])
Out[3]:
2.4.1 子类化列表,字典和字符串
如果希望实现一个和内建对象类型(例如列表,字符串和字典)行为相似的序列或映射,可以使用子类化内建类型。
注意 当子类化一个内建类型,比如list的时候,也就间接的将object子类化了。因此该类就自动成为新式类,意味着可以使用像super函数这样的特性了。
看看下面的例子-带有访问计数的列表。
In [10]:
class CounterList(list):
def __init__(self,*args):
super(CounterList,self).__init__(*args)
self.counter=0
def __getitem__(self,index):
self.counter+=1
return super(CounterList,self).__getitem__(index)
CounterList类严重依赖于它的子类化父类(list)的行为。CounterList类没有重写任何的方法,能直接调用列表的任何方法(如append、extend、index)。在两个被重写的方法中,super方法被用来调用相应的父类的方法,只有在__init__中添加了所需的初始化counter属性的行为,并在__getitem__中更新了counter属性。
In [17]:
c1=CounterList('aaa')
c1
Out[17]:
In [18]:
c1=CounterList((1,2,3))
c1
Out[18]:
In [20]:
c1=CounterList({'first':1,'second':2})
c1
Out[20]:
In [28]:
c1=CounterList(range(10))
c1
Out[28]:
In [29]:
c1.reverse()
c1
Out[29]:
In [30]:
del c1[3:6]
c1
Out[30]:
In [31]:
c1.counter
Out[31]:
In [33]:
c1[0]+c1[1]+c1[2]
Out[33]:
In [34]:
c1.counter
Out[34]:
可以看到,CounterList在很多方面和列表的作用一样,但它有一个counter属性(被初始化为0),每次列表元素被访问时,它都会自增。
In [4]:
issubclass(SPAMFilter,Filter)
Out[4]:
In [5]:
issubclass(Filter,SPAMFilter)
Out[5]:
如果想要知道已知类的父类(们),可以直接使用它的特殊属性__bases__:
In [7]:
SPAMFilter.__bases__
Out[7]:
In [8]:
Filter.__bases__
Out[8]:
同样,还能使用isinstance函数检测一个对象是否是一个类的实例:
In [5]:
s=SPAMFilter()
isinstance(s,SPAMFilter)
Out[5]:
In [10]:
isinstance(s,str)
Out[10]:
如果只想知道一个对象属于哪个类,可以使用__class__属性或type函数:
In [11]:
s.__class__
Out[11]:
In [6]:
type(s)
Out[6]:
In [10]:
type([1,2])
Out[10]:
In [16]:
class Calculator:
def calculate(self,expression):
self.value=eval(expression)
class Talker:
def talk(self):
print 'Hi,my value is ',self.value
class TalkingCalculator(Calculator,Talker):
pass
子类(TalkingCalculator)自己不做任何事,它从自己的父类继承所有的行为。这样它就成了会说话的计算器(talking calculator)。
In [17]:
tc=TalkingCalculator()
tc.calculate('1+2+3')
tc.talk()
这种行为称为多重继承(multiple inheritance),是个非常有用的工具。
一般来说,对于对象不用探讨过深。程序员可以靠多态调用自己需要的方法。不过如果想要知道对象到底有什么方法和属性,有些函数可以帮助完成这项工作。如下可以检查对象的方法或属性是否已经存在:
In [18]:
hasattr(tc,'talk')
Out[18]:
In [19]:
hasattr(tc,'fnord')
Out[19]:
In [21]:
getattr(tc,'talk','None') #获得对象属性的值,可选择提供默认值,以便在属性不存在时使用
Out[21]:
In [24]:
getattr(tc,'value','None')
Out[24]:
In [22]:
setattr(tc,'name','Mr. Gumby') #与getattr相对应的函数是setattr,用来设置对象的属性及值
tc.name
Out[22]:
如果要查看对象内所有存储的值,那么可以使用__dict__属性。
In [23]:
tc.__dict__
Out[23]:
In [116]:
def flatten(nested):
for sublist in nested:
for element in sublist:
yield element
nested=[[1,2],[3,4],5]
任何包含yield语句的函数称为生成器。除了名字不同以外,它的行为和普通的函数也有很大的差别。这就在于它不像return语句那样返回值,而是每次产生一个值。每次产生一个值(使用yield语句),函数就会被冻结:即函数停在那点等待被激活。函数被激活后就从停止的那点开始执行。
接下来可以通过在生成器上迭代来使用所有的值:
In [118]:
flatten(nested)
Out[118]:
In [119]:
for num in flatten(nested):
print num
从上可以看到,试图对一个数值5进行迭代会引发一个TypeError异常。
生成器由两部分组成:生成器的函数和生成器的迭代器。生成器的函数是用def语句定义的,包含yield部分,生成器的迭代器是这个函数返回的部分。
In [120]:
nested=[[1,2],[3,4],[5]]
In [121]:
list(flatten(nested))
Out[121]:
In [123]:
def flatten(nested):
try:
for sublist in nested:
for element in flatten(sublist):
yield element
except TypeError:
yield nested
当flatten被调用时,有两种可能性(处理递归时大部分都是这种情况):基本情况和需要递归的情况。在基本的情况中,函数被告知展开一个元素(比如一个数字),这种情况下,for循环会引发一个TypeError异常(因为试图对一个数字进行迭代),生成器会产生一个元素。如果展开的是一个列表,那么就要进行特殊处理。程序必须遍历所有的子列表,并对他们调用flatten。然后使用另一个for循环来产生被展开的子列表的所有元素。
In [124]:
list(flatten([[[1],2],3,4,[5,[6,7]],8]))
Out[124]:
到目前为止,Python语言的大部分知识都介绍了。