0.说明
阅读一些优秀的Python源代码,会发现面向对象编程的思想无处不在,其实对于任何一门面向对象编程语言来说,面向对象编程都是极其重要的,因此,掌握好一门语言的面向对象编程,将有助于进行更高级的开发。(本文来自香飘叶子51cto博客《Python回顾与整理》系列博文专题)
1.引言
(1)类与实例
类
在Python中有新式类和经典类之分,无非就是有没有显式地继承一个父类:
# 新式类
class MyNewObjectType(bases):
pass
# 经典类
class MyNewObjectType:
pass
object是所有类的父类,如果没有显式继承父类,将以object作为父类。
实例
把类当作是一个实物的模具,而是实例就是该模具的具体产品,这意味着,可以在模具的基础上按需地定制一个生产实物的属性:
myFirstObject = MyNewObjectTypr()
当然不一定要把实例赋值给一个变量,只是如果该实例没有被引用,它就会被垃圾收集器回收。
用类作名称空间容器
每一个类都有自己的名称空间,因为可以使用类来用作名称空间容器,也就是说,只使用类来保存一组特定的变量,而不把类作为其它用途使用。而这些变量,其实就是这个类的属性:
>>> class MyData(object):
... pass
...
>>> mathObj = MyData()
>>> mathObj.x = 4
>>> mathObj.y = 5
>>> mathObj.x + mathObj.y
9
可以看到,这时类中并没有定义任何属性和方法,只在实例中添加私有属性。这里即把类的属性集合作为一个名称空间,所以类就作为一个名称空间容器来使用了。
类和实例都是对象
在Python中,一切皆为对象,所以无论是类还是实例,本质都是对象,只是类和实例本身有着紧密的联系,因为实例就是一个类的具体实现。
(2)方法
一般的方法
给一个类添加功能,其实就是为这个类定义一些方法,在Python中,使用一个类的方法的基本步骤如下:
定义类(和方法)
创建一个实例
用这个实例调用方法
如下定义一个带有方法的类:
>>> class MyDataWithMethod(object):
... def printFoo(self):
... print 'You invoked printFoo()!'
...
其中self代表的是实例对象本身,一般的方法都会需要这个参数,即通过一个实例来进行调用,但是静态方法或类方法不会,其实类方法需要类而不是实例。
实例化这个类并调用方法,如下:
>>> myObj = MyDataWithMethod()
>>> myObj.printFoo()
You invoked printFoo()!
特殊方法:__init__()
__init__()类似于构造器,当实例化一个类时,__init__()方法中的代码就会被执行,以定义额外的行为,即可以认为,实例化是对__init__()的一种隐式调用,因为在实例化时,传给一个类的参数与__init__()接收到的参数是一样的。
(3)创建一个类(类定义)
如下:
class AddrBookEntry(object):
'address book entry class'
def __init__(self, nm, ph):
self.name = nm
self.phone = ph
print 'Created instance for:', self.name
def updatePhone(self, newph):
self.phone = newph
print 'Updated phone# for:', self.name
(4)创建实例(实例化)
如下:
>>> john = AddrBookEntry('John Doe', '408-555-1212')
Created instance for: John Doe
>>> jane = AddrBookEntry('Jane Doe', '650-555-1212')
Created instance for: Jane Doe
这就是实例化调用,它会自动调用__init__(),self把实例对象自动传入__init__()。
(5)访问实例属性
使用句点属性来访问就可以了:
>>> john
<new_class.AddrBookEntry object at 0x7fe2c57bcd10>
>>> john.name
'John Doe'
>>> john.phone
'408-555-1212'
>>> jane.name
'Jane Doe'
>>> jane.phone
'650-555-1212'
(6)方法调用(通过实例)
>>> john.updatePhone('415-555-1212')
Updated phone# for: John Doe
>>> john.phone
'415-555-1212'
(7)创建子类
继承一个类时,子类可以定制只属于它的特定功能,如下:
class EmplAddrBookEntry(AddrBookEntry):
'Employee Address Book Entry class'
def __init__(self, nm, ph, id, em):
AddrBookEntry.__init__(self, nm, ph)
self.empid = id
self.email = em
def updateEmail(self, newem):
self.email = newem
print 'Updated e-mail address for:', self.name
每个子类最好定义它自己的构造器,基类的构造器会被调用,然而,如果子类重写基类的构造器,基类的构造器就不会被自动调用了——这样,基类的构造器就必须显式写出才会被执行,像上面那样,用AddrBookEntry.__init__()设置名字和电话号码。
注意,这里我们要显式传递self实例对象给基类构造器,因为我们不是在该实例中而是在一个子类实例中调用那个方法。因为我们不是通过实例来调用它,这种未绑定的方法调用需要传递一个适当的实例(self)给方法。
(8)使用子类
>>> john = EmplAddrBookEntry('Jhon Doe', '408-555-1212', 42, 'john@spam.doe')
Created instance for: Jhon Doe
>>> john
<new_class.EmplAddrBookEntry object at 0x7f99d99f5d10>
>>> john.name
'Jhon Doe'
>>> john.phone
'408-555-1212'
>>> john.email
'john@spam.doe'
>>> john.updatePhone('415-555-1212')
Updated phone# for: Jhon Doe
>>> john.phone
'415-555-1212'
>>> john.updateEmail('john@doe.spam')
Updated e-mail address for: Jhon Doe
>>> john.email
'john@doe.spam'
命名类、属性和方法: 类名通常是大写字母开头,数据属性应当是数据值的字句,方法名应当指出对应对象或值的行为。可以使用“骆驼记法”来命名,Python推荐使用“骆驼记法”的下划线方式,如"update_phone","update_email"。 另外,类也要细致命名,如"AddrBookEntry","RepairShop"等。 |
2.面向对象编程
面向对象编程可以实现数据与动作的融合:数据层和逻辑层现在由一个可用以创建对象的简单抽象层来描述。
现实世界中的问题和实质完全暴露了本质,从中提供的一种抽象,可以用来进行相似编码,或者编入能与系统中对象进行交互的对象。类提供了这样一些对象的定义,实例即是这些定义的实现。二者对面向对象设计(Object-Oriented Design,OOD)来说都是重要的,OOD仅意味你采用面向对象方式架构来创建系统。(这段话虽然有些抽象,但是却对面向对象概括得很到位)
(1)面向对象设计(OOD)与面向对象编程(OOP)的关系
面向对象设计不会特别要求面向对象编程语言,事实上,OOD可以由纯结构化语言来实现,比如C,但如果想要构造具备对象性质和特点的数据类型,就需要在程序代码上作更多的努力(如果学习过C语言数据结构,就会有所体会)。当一门语言内建面向对象特性时,面向对象编程开发就会更加方便高效。
(2)现实中的问题
使用OOD可以很好地模拟现实世界中的事物。
(3)常用术语
主要是Python中关于OOP的术语:
抽象/实现
抽象指对现实世界和实体的本质表现、行为和特征建模,建立一个相关的子集,可以用于描述程序结构。
封装/接口
封装描述了对数据/信息进行隐藏的观念,它对数据属性提供接口和访问函数。
合成
合成扩充了对类的描述,使得多个不同的类合成为一个大的类,来解决现实问题。
派生/继承/继承结构
派生描述了子类的创建,新类保留已存类类型中所有需要的数据和行为,但允许修改或者创建其它自定义操作,而不会修改原类的定义;继承描述了子类属性从祖先类继承这样一种方式;继承结构表示多“代”派生,可以描述成一个“族谱”,连续的子类,与祖先类都有关系。
多态
多态的概念指出了对象如何通过他们共同的属性和动作来操作及访问,而不需要考虑他们具体的类。
自省/反射
这个性质展示了某对象是如何在运行期取得自身信息的。
3.类
类是一种数据结构,可以以类作为一个蓝图或者模型,来产生真实的物体(实例)。虽然类在Python中本质上也是对象,但是它还不是对象的实现,对象的实现是类所对应的实例。
(1)创建类
如下:
class ClassName(bases):
'class documentation string'
class_suite
(2)声明与定义
与Python函数一样,声明与定义类没什么区别,因为他们是同时进行的。不过需要注意的是,Python并不支持纯虚函数(C++)或者抽象方法(Java),这些都强制程序员在子类中定义方法。作为替代方法,你可以简单地在基类方法中引发NotImplementedError异常,这样可以获得类似的效果。
4.类属性
类的属性主要是下面两种:
对象的数据元素:一种静态的属性,并不依赖于实例,即不通过实例也是可以进行调用的
对象的函数元素:就是类中的方法,一般需要通过具体的实例来进行调用(当然静态方法也可以直接使用)
(1)类的数据属性
这种属性已为OO程序员所熟悉,即静态变量,或者是静态数据。它们表示这些数据是与它们所属的类对象绑定的,不依赖于任何类实例。在Java或C++中,即相当于在一个变量声明前加上static关键字。
举例如下:
>>> class C(object):
... foo = 100
...
>>> print C.foo
100
>>> C.foo = C.foo + 1
>>> print C.foo
101
当然,大多数情况下,可能会考虑使用实例属性。
(2)Methods
方法
方法也是类中的一种属性(函数属性),只是这种属性需要把一个类实例化之后才能使用。如果没有实例化而直接调用类中的方法,会引发TypeError异常:
>>> class MyClass(object):
... def myNoActionMethod(self):
... pass
...
>>> MyClass.myNoActionMethod()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unbound method myNoActionMethod() must be called with MyClass instance as first argument (got nothing instead)
显然,需要将该方法与一个实例进行绑定才能进行调用。
绑定(绑定及非绑定方法)
没有实例,方法是不能被调用的。因此方法必须绑定到一个实例才能直接被调用。没有绑定的方法其实也可以被调用,不过这就需要在声明定义类时明确指出该方法为staticmethod。
(3)决定类的属性
可以使用下面两种方法来查看一个类有哪些属性:
使用dir()内建函数:返回对象的属性的一个名字列表
访问类的字典属性__dict__:返回一个字典,它的key是属性名,value是相应的属性对象的数据值
举例如下:
>>> class MyClass(object):
... 'MyClass class definition'
... myVersion = '1.1'
... def showMyVersion(self):
... print MyClass.myVersion
...
>>>
>>> dir(MyClass)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'myVersion', 'showMyVersion']
>>>
>>> MyClass.__dict__
dict_proxy({'__module__': '__main__', 'showMyVersion': <function showMyVersion at 0x7ffb44788d70>, '__dict__': <attribute '__dict__' of 'MyClass' objects>, 'myVersion': '1.1', '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, '__doc__': 'MyClass class definition'})
如果是经典类,则是这样的:
>>> class MyClass:
... 'MyClass class definition'
... myVersion = '1.1'
... def showMyVersion(self):
... print MyClass.myVersion
...
>>>
>>> dir(MyClass)
['__doc__', '__module__', 'myVersion', 'showMyVersion']
>>> MyClass.__dict__
{'__module__': '__main__', 'myVersion': '1.1', '__doc__': 'MyClass class definition', 'showMyVersion': <function showMyVersion at 0x7ffb44788de8>}
另外使用内建函数vars()也可以查看一个类对象的属性,它返回的是类的__dict__属性的内容:
>>> vars(MyClass)
{'__module__': '__main__', 'myVersion': '1.1', '__doc__': 'MyClass class definition', 'showMyVersion': <function showMyVersion at 0x7ffb44788de8>}
(4)特殊的类属性
对于任何类C,如下:
特殊类属性 | |
C.__name__ | 类C的名字(字符串) |
C.__doc__ | 类C的文档字符串 |
C.__bases__ | 类C的所有父类构成的元组 |
C.__dict__ | 类C的属性 |
C.__module__ | 类C定义所在的模块 |
C.__class__ | 实例C对应的类(仅新式类中) |
使用下面的新式类:
>>> class MyClass(object):
... 'MyClass class definition'
... myVersion = '1.1'
... def showMyVersion(self):
... print MyClass.myVersion
...
说明如下:
C.__name__
>>> MyClass.__name__
'MyClass'
type()返回的是一个类型对象,但是如果我们需要一个字符串来指明类型,而不需要一个对象,这时就显得很有用了:
>>> stype = type('What is your quest?')
>>> stype
<type 'str'>
>>> stype.__name__
'str'
C.__doc__
类的文档字符串,与函数及模块的文档字符串相似,必须紧随头行后的字符串。另外,文档字符串不能被派生类继承,也就是派生类必须含有它们自己的文档字符串。
C.__bases__
>>> MyClass.__bases__
(<type 'object'>,)
C.__dict__
__dict__属性包含一个由类的属性组成的字典。访问一个类属性的时候,Python解释器将会搜索字典以得到需要的属性。如果在__dict__中没有找到,将会在基类的字典中进行搜索,采用“深度优先算法”顺序。基类集的搜索是按顺序的,从左到右,按其在类定义时,定义父类参数时的顺序。对类的修改仅会影响到此类的字典,基类的__dict__属性不会被改动的。
C.__module__
Python支持模块间的类继承,这样类名就完全由模块名所限定:
>>> MyClass.__module__
'__main__'
>>> MyClass
<class '__main__.MyClass'>
因为是在Python交互器中执行,所以此时__module__就为__main__了,假如有一个模块mymod,代码如下:
class C(object):
pass
作如下的导入和测试:
>>> from mymod import C
>>> C
<class 'mymod.C'>
>>> C.__module__
'mymod'
C.__class__
由于类型和类的统一性,当访问任何类的__class__属性时,会发现它就是一个类型对象的实例。也就是说,一个类已是一种类型了。如下:
>>> MyClass.__class__
<type 'type'>
因为经典类并不认同这种等价性(一个经典类是一个类对象,一个类型是一个类型对象),对这些对象来说,这个属性并未定义。
5.实例
类是一种数据结构,它定义了类型,而实例则是这种类型的变量。即类实例化得到实例,该实例的类型就是这个被实例化的类:
>>> class MyClass(object):
... pass
...
>>> myObj = MyClass()
>>> type(myObj)
<class '__main__.MyClass'>
(1)类对象:通过调用类对象来创建实例
因为类和类型已经统一了,因此创建一个类对象,实际上就是创建了一种类型对象,也就是说创建了一种新的类型:
>>> type(MyClass)
<type 'type'>
>>> MyClass.__class__
<type 'type'>
>>>
>>> type(int)
<type 'type'>
>>> int.__class__
<type 'type'>
当然在经典类中则不是这样的,不过只需要记住在新式类中是这样就可以了,因为这是Python面向对象编程发展的趋势。
如果需要实例化一个类,只需要使用函数调用符就可以了,并不需要使用new关键字,这样之后,会完成下面的事情:
Python解释器就会实例化该对象
调用Python所拥有与构造函数最相近的东西(如果有定义)来完成最终的定制工作,比如设置实例属性
最后将这个实例返回给你
(2)__init__()“构造器”方法
如果定义了__init__()方法,那么它将会是实例化类后被调用的第一个方法,它接受实例对象作为第一个参数(self),像标准方法调用一样。调用类时,传进的任何参数都交给了__init__()。
当然如果没有定义__init__()方法,那么就会返回一个实例对象,实例化过程完毕。
(3)__new__()“构造器”方法
与__init__()相比,__new__()方法更像一个真正的构造器。因为类和类型的统一,因此Python用户可以对内建类型进行派生,这就需要一种途径来实例化不可变对象,比如派生字符串、数字等。
在这种情况下,解释器则调用类的__new__()方法,一个静态方法,并且传入的参数是在类实例化操作时生成的。__new__()会调用父类的__new__()方法来创建对象(向上代理)。
(4)__del__()“解构器”方法
由于Python具有垃圾对象回收机制(靠引用计数),这个函数要直到该实例对象所有的引用都被清除掉后才会执行。如下:
>>> class C(object):
... def __init__(self):
... print 'initialized'
... def __del__(self):
... print 'deleted'
...
>>>
>>> c1 = C()
initialized
>>> c2 = c1
>>> c3 = c1
>>> id(c1), id(c2), id(c3)
(140690653257552, 140690653257552, 140690653257552)
>>> del c1
>>> del c2
>>> del c3
deleted
不过在使用__del__()方法时,需要注意下面几点:
如果父类有__del__()方法,不要忘记调用父类的__del__()方法
除非你知道你正在做什么,否则不要去实现__del__()
下面有一个非常好的使用例子,通过类的静态成员和__init__()、__del__()方法来记录类的实例个数:
>>> class InstTrack(object):
... count = 0
... def __init__(self):
... InstTrack.count += 1 #注意是InstTrack.count,而不是self.count
... def __del__(self):
... InstTrack.count -= 1
... def howMany(self):
... return InstTrack.count
...
>>> a = InstTrack()
>>> b = InstTrack()
>>> b.howMany()
2
>>> a.howMany()
2
>>> del b
>>> a.howMany()
1
>>> del a
>>> InstTrack.count
0
6.实例属性
实例仅拥有数据属性,因为严格来说,方法是类属性(类的函数属性)。实例的数据属性只是与某个类的实例相关联的数据值,并且可以通过句点属性标识法来访问。这些值独立于其他实例或类。当一个实例被释放后,它的属性同时也被清除了。
(1)“实例化”实例属性(或创建一个更好的构造器)
设置实例的属性可以在实例创建后任意时间进行,也可以在能够访问实例的代码中进行,而构造器__init__()是设置这些属性的关键点之一。设置实例的属性主要有如下方法:
在构造器中首先设置实例属性
默认参数提供默认的实例安装
举例如下:
>>> class Person(object):
... def __init__(self, name, age, work='student', hobby='IT'):
... self.name = name
... self.age = age
... self.work = work
... self.hobby = hobby
... def showInfo(self):
... print 'Name:%s, age:%s, work:%s, hobby:%s' % \
... (self.name, self.age, self.work, self.hobby)
...
>>> myInfo = Person('xpleaf', '22')
>>> myInfo.showInfo()
Name:xpleaf, age:22, work:student, hobby:IT
__init__()应当返回None
采用函数操作符调用类对象会创建一个类实例,也就是说这样一种调用过程返回的对象就是实例对象:
>>> class MyClass(object):
... pass
...
>>> mc = MyClass()
>>> mc
<__main__.MyClass object at 0x7ff5186a1110>
所以如果定义了构造器,它不应该返回任何对象,因为实例对象是在实例化调用后自动返回的,所以__init__()不应当返回任何对象(即应该返回None),否则就会出现冲突,因为只能返回实例对象。如下:
>>> class MyClass(object):
... def __init__(self):
... print 'initialized'
... return 1
...
>>> mc = MyClass()
initialized
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: __init__() should return None, not 'int'
(2)查看实例属性
内建函数dir()不仅可以显示类属性,还可以显示实例属性:
>>> class C(object):
... pass
...
>>> c = C()
>>> c.foo = 'roger'
>>> c.bar = 'shrubber'
>>> dir(c)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bar', 'foo']
当然也可以使用实例的__dict__属性或者使用内建函数vars:
>>> c.__dict__
{'foo': 'roger', 'bar': 'shrubber'}
>>>
>>> vars(c)
{'foo': 'roger', 'bar': 'shrubber'}
(3)特殊的实例属性
主要是下面两个(如实例对象I为为例):
I.__class__:实例化I的类
I.__dict__:I的属性,键是属性名,值是属性相应的数据值
>>> class C(object):
... pass
...
>>> c = C()
>>> c.__class__
<class '__main__.C'>
>>> c.__dict__
{}
>>> c.foo = 1
>>> c.__dict__
{'foo': 1}
>>>
>>> dir(C) # 类C的属性
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> dir(c) # 实例c的属性
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'foo']
其实从上面也可以看到,__dict__只有实例本身特有的属性,而dir()包含了实例本身和实例化的类的属性,这一点非常重要。
(4)内建类型属性
内建类型也是类,创建一个该内建类型对应的对象,其实就是对该内建类型(它也是一个类,只不过它是“大类”type的一个实例,而内建类型本身和我们自己创建的类就可以认为是“小类”)的实例化。
所以显然内建类型也会有相应的属性,它的实例也是如此:
>>> x = 3 + 0.14j
>>> x.__class__
<type 'complex'>
>>> dir(x)
['__abs__', '__add__', '__class__', '__coerce__', '__delattr__', '__div__', '__divmod__', '__doc__', '__eq__', '__float__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__int__', '__le__', '__long__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__nonzero__', '__pos__', '__pow__', '__radd__', '__rdiv__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rmod__', '__rmul__', '__rpow__', '__rsub__', '__rtruediv__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', 'conjugate', 'imag', 'real']
可以看到有conjugate,imag,real这三个属性或方法,可以尝试访问或调用一下:
>>> x.imag
0.14
>>> x.real
3.0
>>> x.conjugate
<built-in method conjugate of complex object at 0x7ff5187690f0>
>>> x.conjugate()
(3-0.14j)
当然访问__dict__会失败,因为在内建类型中,并没有定义这个属性:
>>> x.__dict__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'complex' object has no attribute '__dict__'
(5)实例属性vs类属性
类属性仅是与类相关的数据值,和实例属性不同,类属性和实例无关。这些值像静态成员那样被引用,即使在多次实例化中调用类,它们的值都保持不变。
另外,类是类属性的名称空间,实例则是实例属性的名称空间,这是非常重要的概念,一定要分清楚。
可以使用类来访问类属性,如果实例没有同名的属性的话,也可以用实例来访问。
(a)访问类属性
类属性可以通过类或实例来访问,在下面的例子中,类C有一个version属性,通过类对象本身就可以对它进行访问。当实例c被创建后,对实例c来说,访问c.version一开始会失败,但由于Python首先会在实例中搜索名字version,然后是类,再就是继承树中的基类,所以最后还是可以访问到version属性。在这个例子中,就是在类中找到version属性的:
>>> class C(object):
... version = 1.2
...
>>> c = C()
>>> C.version
1.2
>>> c.version
1.2
>>> C.version += 0.1
>>> C.version
1.3
>>> c.version
1.3
>>>
>>> dir(c) # dir()会同时显然实例本身的属性和所属类的属性
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'version']
>>> c.__dict__
{}
显然,只有当使用类引用version时,才能更新它的值。如果尝试在实例中设定或更新类属性会创建一个实例属性c.version,后者会阻止对类属性C.version的访问,因为第一个访问的就是c.version,这样可以对实例有效地“遮蔽”类属性C.version,直到c.version被清除掉(类似局部变量与全局变量、局部名称空间与全局名称空间)。
(b)从实例中访问类属性须谨慎
与通常Python变量一样,任何对实例属性的赋值都会创建一个实例属性(如果不存在的话),并且对其赋值。如果类属性中存在同名的属性,那么将会有如下的特性:
试图通过实例属性修改类属性只会创建同名的实例属性
删除同名的实例属性,可以通过实例访问原来的类属性
当然,上面的情况只是针对类属性为不可变对象的情况,如果类属性为可变对象的话,情况又会不一样:
>>> class Foo(object):
... x = {'name': 'xpleaf'}
...
>>> foo = Foo()
>>> foo.x
{'name': 'xpleaf'}
>>> foo.x['loving'] = 'cl'
>>> foo.x
{'name': 'xpleaf', 'loving': 'cl'}
>>> Foo.x
{'name': 'xpleaf', 'loving': 'cl'}
>>> del foo.x # 没有遮蔽,所以不能删除掉
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Foo' object attribute 'x' is read-only
但不管类属性是可变对象还是不可变对象,如果要想对上面的知识有比较深入的理解,应该要把Python对象、可变对象和不可变对象的含义理解到位,同时把深copy和浅copy的概念理解清楚就可以了。
(c)类属性持久性
类属性的修改会影响到所有的实例,因为通过前面概念的理解,很容易知道,如果实例中没有对应同名的实例属性,那么其实通过实例来访问的类属性跟原来的类属性是指向同一个对象的,可以通过下面的例子来进行理解:
>>> class C(object):
... spam = 100
...
>>> c1 = C()
>>> c1.spam
100
>>> id(c1.spam)
25597856
>>> id(C.spam)
25597856
>>> C.spam += 100
>>> C.spam
200
>>> c1.spam
200
>>> id(c1.spam), id(C.spam)
(25601432, 25601432)
>>> c2 = C()
>>> c2.spam
200
>>> del c1
>>> C.spam += 200
>>> C.spam
400
>>> c2.spam
400
因为实例也有它们自己的属性值,所以很显然,修改类属性需要使用类名,而不是实例名。
7.绑定和方法调用
方法是类中定义的函数,它是类属性(类的函数属性),而不是实例属性,不过如果要使用它就需要该类存在一个实例,然后通过该实例就可以调用类的这些方法,因为只有当存在一个实例时,方法才被认为是绑定到那个实例上。
另外,方法的第一个参数是self,它表示的是实例对象本身,不过一个建议是,如果该方法中并没有用到实例对象本身(比如说给该实例对象本身添加一些特定的属性),那么这个时候可以考虑创建一般的常规函数就可以了,或者也可以创建类的静态方法。
(1)调用绑定方法
当调用绑定的方法时,不需要明确地传入self参数,可以认为Python已经自动地帮我们传入这个参数了。不过在调用一些非绑定方法时,我们也需要手动传入这个参数。
(2)调用非绑定方法
调用非绑定方法,一般在继承一个类时才会用到,可以看下面一个例子:
class EmplAddrBookEntry(AddrBookEntry):
'Employee Address Book Entry class'
def __init__(self, nm, ph, em):
AddrBookEntry.__init__(self, nm, ph) # 通过父类名来调用非绑定方法,并且显式传入self参数
self.empid = id
self.email = em
显然,当一个EmplAddrBookEntry类被实例化并且调用__init__()时,并没有AddrBookEntry的实例,然后就通过父类名来调用非绑定方法了。这时候的EmplAddrBookEntry实例其实与AddrBookEntry实例并没有太大的差别,因为还没有执行下面的子类定制语句。
8.静态方法和类方法
静态方法是类中的函数,不需要实例,直接通过类本身就可以进行调用,当然,通过实例对象来调用也是可以的。
在绑定的方法中,实例对象本身(习惯以self作为参数)总是作为方法的第一个参数传给这些方法,而在类方法中,类对象本身总是作为方法的第一个参数传给类方法,与self类似,习惯用cls作用参数,不同的是,调用类方法并不需要将类实例化,当然,通过实例对象来调用也是可以的。
(1)staticmethod()和classmethod()内建函数
创建分别含有静态方法和类方法的类如下:
>>> class TestStaticMethod(object):
... def foo():
... print 'calling static method foo()'
... foo = staticmethod(foo) # 将方法转换为静态方法
...
>>> class TestClassMethod(object):
... def foo(cls):
... print 'calling class method foo()'
... print 'foo() is part of class:', cls.__name__
... foo = classmethod(foo) # 将方法转换为类方法
...
执行如下:
>>> tsm = TestStaticMethod()
>>> TestStaticMethod.foo() # 通过类对象本身调用静态方法
calling static method foo()
>>> tsm.foo() # 通过实例对象调用静态方法
calling static method foo()
>>>
>>> tcm = TestClassMethod()
>>> TestClassMethod.foo() # 通过类对象本身调用类方法
calling class method foo()
foo() is part of class: TestClassMethod
>>> tcm.foo() # 通过实例对象调用类方法
calling class method foo()
foo() is part of class: TestClassMethod
(2)使用函数装饰器
实际上在定义静态方法和类方法时,使用得更多的是函数装饰器,而不是内建函数,上面的两个类,以下面的等价:
>>> class TestStaticMethod(object):
... @staticmethod
... def foo():
... print 'calling static method foo()'
...
>>> class TestClassMethod(object):
... @classmethod
... def foo(cls):
... print 'calling class method foo()'
... print 'foo() is part of class:', cls.__name__
...
显然这样就简洁很多了。
9.组合
为了增加代码的重用性,可以在其它类中使用已经存在的类,主要有下面的两种使用方法:
组合:让不同的类混合并加入到其它类中,来增加功能和代码重用性
派生:子类继承父类的方法
关于组合,可以看下面的一个例子:
class NewAddrBookEntry(object): # class definition 类定义
'new address book entry class'
def __init__(self, nm, ph): # define constructor 定义构造器
self.name = Name(nm) # create Name instance 创建 Name 实例
self.phone = Phone(ph) # create Phone instance 创建 Phone 实例
print 'Created instance for:', self.name
这样的话就很清晰了,NewAddrBookEntry类由它自身和其它类组合而成,它包含有一个Name类实例和一个Phone类实例。
像上面这样,类之间的关系并不太紧密,因此可以使用组合的方法来重用已经定义的类的代码。但是如果类之间的关系比较接近时,就可以考虑派生了,特别是当你需要一些相似的对象,但却有少许不同功能的时候。
10.子类和派生
当类之间有显著的不同,并且较小的类是较大的类所需要的组件时,组合表现得很好,但当你设计“相同的类但有一些不同的功能”时,派生就是一个更加合理的选择了。
从父类可以派生出子类,语法如下:
class SubClassName(ParentClass1[, ParentClass2, ...])
'optional class documentation string'
class_suite
如果我们创建的类没有从任何祖先类派生,对于新式类,可以使用object作为父类的名字。
一个简单的例子如下:
>>> class Parent(object):
... def parentMethod(self):
... print 'calling parent method'
...
>>> class Child(Parent):
... def childMethod(self):
... print 'calling child method'
...
>>> p = Parent()
>>> p.parentMethod()
calling parent method
>>>
>>> c = Child()
>>> c.childMethod()
calling child method
>>> c.parentMethod()
calling parent method
11.继承
继承描述了基类的属性如何“遗传”给派生类。一个子类可以继承它的基类的任何属性,不管是数据属性还是方法。
不过需要注意的是,文档字符串对类,函数/方法,还有模块来说都是唯一的,所以特殊属性__doc__不会从基类中继承过来,这点尤其需要注意:
>>> class P():
... 'class of P'
... pass
...
>>> class P(object):
... 'class of P'
... pass
...
>>> class C(P):
... pass
...
>>> P.__doc__
'class of P'
>>> C.__doc__
>>>
(1)__bases__属性
对于任何(子)类,它是一个包含其父类的集合的元组,不过需要注意的是,这里的父类应该是直接父类,关于这一点,通过下面一个例子就可以很好的理解:
>>> class A(object):
... pass
...
>>> class B(A):
... pass
...
>>> class C(B):
... pass
...
>>> class D(object):
... pass
...
>>> class E(A, D):
... pass
...
>>> A.__bases__
(<type 'object'>,)
>>> B.__bases__
(<class '__main__.A'>,)
>>> C.__bases__
(<class '__main__.B'>,)
>>> E.__bases__
(<class '__main__.A'>, <class '__main__.D'>)
(2)通过继承覆盖方法
一个简单的例子
举例如下:
>>> class P(object):
... def foo(self):
... print 'Hi, I am P-foo()'
...
>>> p = P()
>>> p.foo()
Hi, I am P-foo()
>>>
>>> class C(P):
... def foo(self):
... print 'Hi, I am C-foo()'
...
>>> c = C()
>>> c.foo()
Hi, I am C-foo()
通过子类实例调用被覆盖的父类方法
上面的子类C就通过继承覆盖了父类的foo()方法,不过有些时候我们还是需要调用被覆盖的父类方法,通过子类的实例,可以这样来进行调用:
>>> P.foo(c)
Hi, I am P-foo()
显然,其实这就是在调用一个未绑定的基类方法,原理跟前面提及的是一样的。
在子类重写方法中显式调用基类方法
如下:
>>> class C(P):
... def foo(self):
... P.foo(self)
... print 'Hi, I am C-foo()'
...
不过使用内建函数super()就可以更加简洁和清晰:
>>> class C(P):
... def foo(self):
... super(C, self).foo()
... print 'Hi, I am C-foo()'
...
但不管是哪一种形式,本质上都是在调用未绑定的基类方法。
执行如下:
>>> c = C()
>>> c.foo()
Hi, I am P-foo()
Hi, I am C-foo()
重写__init__不会自动调用基类的__init__
类似于上面的覆盖非特殊方法,当从一个带构造器__init__()的类派生,如果不去覆盖__init__(),它将会被继承并自动调用。但如果在子类中覆盖了__init__(),子类被实例化时,基类的__init__()就不会被自动调用。
举例如下:
>>> class P(object):
... def __init__(self):
... print "calling P's constructor"
...
>>> class C(P):
... def __init__(self):
... print "calling C's constructor"
...
>>> c = C()
calling C's constrctor
但很显然,既然我们需要继承一个父类,说明我们需要使用父类的某些功能,然后再做相应地定制。这样的话,我们应该让父类也进行一定的初始化,以得到父类的初始化属性,来满足我们的需求。这是相当普遍的做法,用来设置初始化基类,然后可以执行子类内部的设置。
因此,可以将上面的子类C修改如下:
>>> class C(P):
... def __init__(self):
... P.__init__(self)
... print "calling C's constructor"
...
>>> c = C()
calling P's constructor
calling C's constructor
显然还是使用了未绑定的基类方法,而为了降低代码的耦合度,可以使用内建函数super()来执行父类的__init__()方法:
>>> class C(P):
... def __init__(self):
... super(C, self).__init__()
... print "calling C's constructor"
...
这样的话,就不需要明确提供父类了,大大降低了代码的耦合度。
(3)从标准类型派生
可以对标准类型进行子类化,这里主要介绍两个相关的例子,一个是可变类型,另一个是不可变类型。
(a)不可变类型的例子
>>> class RoundFloat(float):
... def __new__(cls, val):
... return float.__new__(cls, round(val, 2)) # 使用super(RoundFloat, self).__new__()会更好
...
>>> RoundFloat(1.5955)
1.6
>>> RoundFloat(1.5945)
1.59
>>> RoundFloat(-1.9955)
-2.0
几点说明如下:
所有的__new__()方法都是类方法,要显式地传入类作为第一个参数,这类似于常见的方法如__init__()中需要的self
通过情况下,最好是使用super()内建函数去捕获对应的父类以调用它的__new__()方法
(b)可变类型的例子
创建一个新的字典类型,它的keys()方法会自动排序,如下:
>>> class SortedKeyDict(dict):
... def keys(self):
... return sorted(super(SortedKeyDict, self).keys())
...
执行如下:
>>> d = SortedKeyDict((('zheng-cai', 67), ('hui-jun', 68), ('xin-yi', 2)))
>>> print 'By iterator:'.ljust(12), [key for key in d]
By iterator: ['zheng-cai', 'xin-yi', 'hui-jun']
>>> print 'By keys:'.ljust(12), d.keys()
By keys: ['hui-jun', 'xin-yi', 'zheng-cai']
通过keys迭代过程是以散列顺序的形式,而使用重写的keys()方法则将keys变为字母排序方式了。
(4)多重继承
在使用多重继承时,有下面两个方法需要考虑:
要找到合适的属性
当重写方法时,如何调用对应父类方法以“发挥他们的作用”
下面的例子侧重讨论后者,即方法解析顺序。
(a)方法解释顺序(MRO)
其使用的算法主要如下:
深度优先算法:在经典类中使用,如果在新式类中使用会出现一些问题,所以新式类不使用此算法
广度优先算法:在新式类中使用
(b)简单属性查找示例
举例如下:
class P1:
def foo(self):
print 'called P1-foo()'
class P2:
def foo(self):
print 'called P2-foo()'
def bar(self):
print 'called P2-bar()'
class C1(P1, P2):
pass
class C2(P1, P2):
def bar(self):
print 'called C2-bar()'
class GC(C1, C2):
pass
各类之间的继承关系如下:
在经典类和新式类中,执行如下:
经典类
>>> from MRO_test import *
>>> gc = GC()
>>> gc.foo()
called P1-foo()
>>> gc.bar()
called P2-bar()
很显然是采用了深度优先的算法,虽然C2.bar()比P2.bar()要靠近GC,但因为该算法的原因,找到的是P2.bar()。
新式类:将上面的代码改成新式类的形式
>>> from MRO_test import *
>>> gc = GC()
>>> gc.foo()
called P1-foo()
>>> gc.bar()
called C2-bar()
对比经典类的方法解释顺序,这里使用的是广度优先算法,当然,如果这时候希望执行P2.bar(),那么可以采用未绑定的方式去执行:
>>> P2.bar(gc)
called P2-bar()
即传入一个实例就可以执行未绑定的方法了。另外,新式类有一个__mro__属性,用来告诉我们查找顺序是怎样的:
>>> GC.__mro__
(<class 'MRO_test.GC'>, <class 'MRO_test.C1'>, <class 'MRO_test.C2'>, <class 'MRO_test.P1'>, <class 'MRO_test.P2'>, <type 'object'>)
(c)菱形效应引起MRO问题
如果新式类也是使用深度优先算法,那么就会出现一些问题,具体可以参考书上的例子,也比较容易理解。
(d)总结
经典类,使用深度优先算法。因为新式类继承自object,新的菱形类继承结构出现,问题也就接着而来了,所以必须新建一个MRO。
12.类、实例和其他对象的内建函数
(1)issubclass()
语法
issubclass(sub, sup)
如果sub是sup的子类,则返回True,不过sup也可以是一个由父类组成的元组,如下:
>>> class P(object):
... pass
...
>>> class C(P):
... pass
...
>>> issubclass(C, P)
True
>>> issubclass(C, (P, ))
True
>>> issubclass(P, C)
False
(2)isinstance()
语法
isinstance(obj1, obj2)
如果实例obj1是类obj2或其子类的一个实例,则返回True,如下:
>>> class C1(object):
... pass
...
>>> class C2(object):
... pass
...
>>> c1 = C1()
>>> c2 = C2()
>>> isinstance(c1, C1)
True
>>> isinstance(c2, C2)
True
>>> isinstance(c1, C2)
False
>>> isinstance(C1, c1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: isinstance() arg 2 must be a class, type, or tuple of classes and types
显然,第二个参数应该是一个类或由类组成的元组,因为类和类型的统一,所以也可以是某一类型,即可以使用isinstance()来检查一个对象obj1是否是obj2的类型,举例如下:
>>> isinstance(4, int)
True
>>> isinstance(4, str)
False
>>> isinstance('4', str)
(3)hasattr()、getattr()、setattr()、delattr()
*attr()系列函数可以在各种对象下工作,不限于类和实例,只是因为在类和实例中使用更频繁,所以在这里列出来。
当使用这些函数时,传入正在处理的对象作为第一个参数,但属性名,也就是这些函数的第二个参数,是这些属性的字符串名字,即在操作obj.attr时,就相当于调用*attr(obj, 'attr', ...),其功能分别如下:
hasattr():判断一个对象是否有一个特定的属性,一般用于访问某属性前先作一下检查
getattr():获取一个对象的属性,当属性不存在时,如果没有给定该属性的默认值,就会引发AttributeError异常,类似于dict.get()
setattr():给对象设置属性,如果该属性已经存在,则会取代原来的属性
delattr():删除一个对象的属性
举例如下:
>>> class MyClass(object):
... def __init__(self):
... self.foo = 100
...
>>> myInst = MyClass()
>>> hasattr(myInst, 'foo')
True
>>> getattr(myInst, 'foo')
100
>>> hasattr(myInst, 'bar')
False
>>> getattr(myInst, 'bar')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyClass' object has no attribute 'bar'
>>> getattr(myInst, 'bar', 'oops!')
'oops!'
>>> setattr(myInst, 'bar', 'my attr')
>>> dir(myInst)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bar', 'foo']
>>> getattr(myInst, 'bar')
'my attr'
>>> delattr(myInst, 'bar')
>>> dir(myInst)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'foo']
>>> hasattr(myInst, 'bar')
False
>>> delattr(myInst, 'bar')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: bar
(4)dir()
dir()作用在不同的对象上时,得到的结果的范围是不同的,如下:
dir()作用在实例上:显示实例变量,还有在实例所在的类及所有它的基类中定义的方法和类属性
dir()作用在类(经典类或新式类)上:显示类以及它的所有基类的__dict__中的内容,但不会显示定义在元类(metaclass,元类其实就是type)中的类属性
dir()作用在模块上:显示模块的__dict__的内容
dir()不带参数时:显示调用者的局部变量
(5)super()
super()函数的目的就是帮助程序员找出相应的父类,然后方便调用相关的属性。一般情况下,程序员可能仅仅采用非绑定方式调用祖先类方法,使用super()可以简化搜索一个合适祖先的任务,替你传入实例或类型对象。
super()是一个工厂函数,它返回一个super object,为一个给定的类使用__mro__去查找相应的父类:
>>> class MyClass(object):
... def sayHi(self):
... print 'Hello!'
...
>>> class MyClassSub(MyClass):
... def sayHi(self):
... print 'Hello, xpleaf!'
...
>>> MyClassSub.__mro__
(<class '__main__.MyClassSub'>, <class '__main__.MyClass'>, <type 'object'>)
>>> myInst = MyClassSub()
>>> super(MyClassSub, myInst).sayHi()
Hello!
>>> super(MyClassSub, myInst)
<super: <class 'MyClassSub'>, <MyClassSub object>>
super()的完整语法如下:
super(type[, obj])
其中,对于obj的定义为:
如果obj是一个实例,isinstance(obj, type)就必须返回True
如果obj是一个类或类型,issubclass(obj, type)就必须返回True
这意味着,不传入obj也是可以,得到的也是一个super对象:
>>> super(MyClassSub)
<super: <class 'MyClassSub'>, NULL>
不过这时候的super对象是未绑定的,如果这时去调用父类的方法,会引发AttributeError异常:
>>> super(MyClassSub).sayHi()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'super' object has no attribute 'sayHi'
传入obj后才可以:
>>> super(MyClassSub, myInst)
<super: <class 'MyClassSub'>, <MyClassSub object>>
>>> super(MyClassSub, myInst).sayHi()
Hello!
(6)vars()
vars()内建函数与dir()相信,只是给定的对象参数都必须有一个__dict__属性。vars()返回一个字典,它包含了对象存储于其__dict__中的属性(键)和值。如果提供的对象没有这样的一个属性,则会引发一个TypeError异常。如果没有提供对象作为vars()的一个参数,它将显示一个包含本地名称空间的属性(键)及其值的字典,也就是locals()。如下:
>>> class C(object):
... pass
...
>>> c = C()
>>> c.foo = 100
>>> c.bar = 'Python'
>>> c.__dict__
{'foo': 100, 'bar': 'Python'}
>>> vars(c)
{'foo': 100, 'bar': 'Python'}
>>>
>>> vars()
{'C': <class '__main__.C'>, '__builtins__': <module '__builtin__' (built-in)>, 'c': <__main__.C object at 0x7f0b58b4a1d0>, 'MyClass': <class '__main__.MyClass'>, 'MyClassSub': <class '__main__.MyClassSub'>, '__package__': None, 'myInst': <__main__.MyClassSub object at 0x7f0b58b4a0d0>, 'P': <class '__main__.P'>, 'C2': <class '__main__.C2'>, '__name__': '__main__', 'C1': <class '__main__.C1'>, 'c1': <__main__.C1 object at 0x7f0b58b3ad90>, '__doc__': None, 'c2': <__main__.C2 object at 0x7f0b58b3add0>}
>>> locals()
{'C': <class '__main__.C'>, '__builtins__': <module '__builtin__' (built-in)>, 'c': <__main__.C object at 0x7f0b58b4a1d0>, 'MyClass': <class '__main__.MyClass'>, 'MyClassSub': <class '__main__.MyClassSub'>, '__package__': None, 'myInst': <__main__.MyClassSub object at 0x7f0b58b4a0d0>, 'P': <class '__main__.P'>, 'C2': <class '__main__.C2'>, '__name__': '__main__', 'C1': <class '__main__.C1'>, 'c1': <__main__.C1 object at 0x7f0b58b3ad90>, '__doc__': None, 'c2': <__main__.C2 object at 0x7f0b58b3add0>}
13.用特殊方法定制类
前面提到的__init__()和__del__()只是可自定义特殊方法集中的一部分,较为完整的用来定制类的特殊方法可以参考书上的表格。
(1)简单定制(RoundFloat2)
定制一个类,用来保存浮点型数值对象,四舍五入,保留两位小数,如下:
# !/usr/bin/eny python
class RoundFloatManual(object):
def __init__(self, val):
assert isinstance(val, float), 'Value must be a float!' # 断言引发异常
self.value = round(val, 2) # 保留两位小数
def __str__(self):
return ‘%.2f’ % self.value # print语句执行时的默认方法,即str(obj),否则执行print时是以<object at id>这样的形式输出
__repr__ = __str__ # 在解释器转储(dump)对象时的默认方法,即repr(obj)
执行如下:
>>> from test import RoundFloatManual
>>> rfm = RoundFloatManual(5.5964)
>>> rfm
5.60
>>> print rfm
5.60
如果不定义__repr__方法,在解释器直接输入对象时,显示的将是<object at id>这样的形式,这显然不够友好,而令__repr__ = __str__仅仅是为了想让两者得到相同的输出。
当然,在前面的一个定制类RoundFloat中,并没有担心所有细致对象的显示问题,因为__str__()和__repr__()作为float类的一部分已经为我们定义好了。
(2)数值定制(Time60)
创建一个简单的应用,用来记录小时数和分钟数,如下:
class Time60(object):
def __init__(self, hr, min): # 构造器
self.hr = hr # 给小时赋值
self.min = min # 给分钟赋值
(a)显示
添加一个__str__()方法和__repr__()方法,这样的话在显示我们的实例时,就会得到有意义的输出:
def __str__(self):
return '%d:%d' % (self.hr, self.min)
__repr__ = __str__
执行如下:
>>> from test import Time60
>>> mon = Time60(10, 30)
>>> tue = Time60(11, 15)
>>> mon, tue
(10:30, 11:15)
>>> print mon, tue
10:30 11:15
当然,如果想执行加法操作,就会有问题了:
>>> mon + tue
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'Time60' and 'Time60'
可以看到引发了TypeError异常,那是因为我们并没有给我们的类定义加法的特殊方法。
(b)加法
我们希望我们定义的类可以支持加法,添加__add__()方法如下:
def __add__(self, other):
return self.__class__(self.hr + other.hr, self.min + other.min)
上面的代码中,其实执行self.__class__()与Time60()是一样的,但是这样做可以降低代码的耦合度。
执行如下:
>>> from test import Time60
>>> mon = Time60(10, 30)
>>> tue = Time60(11, 15)
>>> mon + tue
21:45
>>> print mon + tue
21:45
(c)原位加法
在执行+=这样的增量运算时,返回的对象跟原来已经不同了,即新创建了一个对象:
>>> mon = Time60(10, 30)
>>> tue = Time60(11, 15)
>>> id(mon)
140112940170512
>>> mon += tue
>>> id(mon)
140112940171024
原因是,我们并没有给我们的类定义增量赋值运算,所以在执行+=运算时,其实调用的还是原来的加法操作,而从代码中就容易知道,我们定义的__add__()方法,其实是返回了一个新创建的对象。
所以现在来定义一个新的方法,__iadd__()方法,来解决上面的问题:
def __iadd__(self, other):
self.hr += other.hr
self.min += other.min
return self # 这就是秘密所在
执行如下:
>>> from test import Time60
>>> mon = Time60(10, 30)
>>> tue = Time60(11, 15)
>>> mon
10:30
>>> id(mon)
140380691692816
>>> mon += tue
>>> mon
21:45
>>> id(mon)
140380691692816 # 对象id并没有发生改变
可以看到,对象的id在执行增量赋值运算的前后并没有发生变化,说明并没有创建新的对象,而是在原来的对象上进行了修改。而之所以可以这样实现,是因为我们定义了__iadd__()方法,并返回了self,即类实例本身,而变化的仅仅是类中的属性,即hr和min。
关于上面的例子,完整的代码如下:
class Time60(object):
def __init__(self, hr, min):
self.hr = hr
self.min = min
def __str__(self):
return '%d:%d' % (self.hr, self.min)
__repr__ = __str__
def __add__(self, other):
return self.__class__(self.hr + other.hr, self.min + other.min)
def __iadd__(self, other):
self.hr += other.hr
self.min += other.min
return self
(d)进一步优化
当然,上面的例子中还存在很多问题,可以做很多细节上的优化,比如一个比较突出的问题是,不支持60进制:
>>> thu = Time60(10, 30)
>>> fri = Time60(8, 45)
>>> thu + fri
18:75
这都是可以进行改进和优化的,但不管怎么说,通过上面的例子之后,相信对操作符重载、为什么要使用操作符重载以及如何使用特殊方法来实现它我们定制的类,会有一个更好的理解。
(3)迭代器(RandSeq和AnyIter)
(a)RandSeq
如下:
#!/usr/bin/env python
from random import choice
class RandSeq(object):
def __init__(self, seq):
self.data = seq
def __iter__(self):
return self
def next(self):
return choice(self.data)
__iter__()仅返回self,这就是如何将一个对象声明为迭代器的方式,最后调用next()来得到迭代器中连续的值,不同的是,这个迭代器并没有迭代终点,即不会出现StopIteration异常。执行如下:
>>> from randSeq import RandSeq
>>> from time import sleep
>>> for eachItem in RandSeq(('rock', 'paper', 'scissors')):
... print eachItem
... sleep(1)
...
paper
scissors
scissors
paper
rock
paper
scissors
…
>>>
>>> rseq = RandSeq(('rock', 'paper', 'scissors'))
>>> rseq
<randSeq.RandSeq object at 0x7f15243b8d10>
>>> rseq.next()
'paper'
>>> rseq.next()
'rock'
(b)AnyIter
与上面的迭代器不同,这里创建一个迭代器对象,传给next()方法一个参数,控制返回条目的数目,如下:
#!/usr/bin/env python
class AnyIter(object):
def __init__(self, data, safe=False):
self.safe = safe
self.iter = iter(data)
def __iter__(self):
return self
def next(self, howmany=1):
retval = []
for eachItem in range(howmany):
try:
retval.append(self.iter.next())
except StopIteration:
if self.safe:
break
else:
raise # 将异常抛还给调用者raise,只有一个raise,表示触发前一个异常
return retval
执行如下:
>>> from anyIter import AnyIter
>>> a = AnyIter(range(10))
>>> for j in range(1, 5):
... print j, ':', i.next(j)
...
1 : [0]
2 : [1, 2]
3 : [3, 4, 5]
4 : [6, 7, 8, 9]
因为迭代器正好符合项的个数,所以即使没有使用safe参数也不会出现异常。而下面的情况则不一样:
>>> a = AnyIter(range(10))
>>> a.next(14)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "anyIter.py", line 16, in next
retval.append(self.iter.next())
StopIteration
如果使用safe参数,就不会有异常:
>>> a = AnyIter(range(10), True)
>>> a.next(14)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
(4)多类型定制(NumStr)
下面一个例子虽然会有点复杂,但是却非常经典,主要考虑了下面的几点来进行类的定制:
初始化
加法
乘法
False值
比较
代码如下:
#!/usr/bin/env python
class NumStr(object):
def __init__(self, num=0, string=''):
self.__num = num
self.__string = string
def __str__(self):
return '[%d :: %r]' % (self.__num, self.__string)
__repr__ = __str__
def __add__(self, other):
if isinstance(other, NumStr):
return self.__class__(self.__num + other.__num,
self.__string + other.__string)
else:
raise TypeError, 'Illegal argument type for buuilt-in operation'
def __mul__(self, num):
if isinstance(num, int):
return self.__class__(self.__num * num, self.__string * num)
else:
raise TypeError, 'Illegal argument type for built-in operation'
def __nonzero__(self):
return self.__num or len(self.__string)
def __norm_cval(self, cmpres):
return cmp(cmpres, 0)
# the code above equal to:
# if cmpres < 0:
# return -1
# elif cmpres > 0:
# return 1
# else:
# return 0
def __cmp__(self, other):
return self.__norm_cval(cmp(self.__num, other.__num)) + \
self.__norm_cval(cmp(self.__string, other.__string))
执行如下:
>>> from numstr import NumStr
>>> a = NumStr(3, 'foo')
>>> b = NumStr(3, 'goo')
>>> c = NumStr(2, 'foo')
>>> d = NumStr()
>>> e = NumStr(string='boo')
>>> f = NumStr(1)
>>> a
[3 :: 'foo']
>>> b
[3 :: 'goo']
>>> c
[2 :: 'foo']
>>> d
[0 :: '']
>>> e
[0 :: 'boo']
>>> f
[1 :: '']
>>> a < b
True
>>> b < c
False
>>> a == a
True
>>> b * 2
[6 :: 'googoo']
>>> a * 3
[9 :: 'foofoofoo']
>>> b + 3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "numstr.py", line 20, in __add__
raise TypeError, 'Illegal argument type for buuilt-in operation'
TypeError: Illegal argument type for buuilt-in operation
>>> b + e
[3 :: 'gooboo']
>>> e + b
[3 :: 'boogoo']
>>> if d: 'not false'
...
>>> if e: 'not false'
...
'not false'
>>> cmp(a, b)
-1
>>> cmp(a, c)
1
>>> cmp(a, a)
0
上面这个例子非常经典,作如下的说明:
'[%d :: %r]'
因为使用的是%r格式化操作符,所以在输出时会发现字符串都有引号(如:[9 :: 'foofoofoo']),其实这就是repr()的输出,这也是为什么说repr()是更接近官方字符串的原因了。如果换成%s,就不会有引号了,但显然这样做的话就很难通过输出去判断第二个值是否为一个字符串了,因为也有可能是一个变量。
__init__()
属性使用双下划线开头,这样的话,就不可以直接访问到这些数据元素了:
>>> a.__num
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NumStr' object has no attribute '__num'
>>> dir(a)
['_NumStr__norm_cval', '_NumStr__num', '_NumStr__string', '__add__', '__class__', '__cmp__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__mul__', '__new__', '__nonzero__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> a._NumStr__num
3
这是OO设计中的封装特性,这里的话即相当于把属性私有化了,虽然还可以用其它访问来访问。
__add__()
__add__()函数考虑self+other的情况,但我们不需要定义__radd__()来处理other+self,因为这可以由ohter的__add__()去考虑。但后面的__mul__()则有所区别,这是因为重复只允许整型在操作数的右边。
__nonzero__()
Python对象任何时候都有一个布尔值,对标准类型来说,对象有一个False值的情况为:它是一个类似于0的数值,或是一个空序列,或者映射。就我们的类而言,数值为0且字符串为空时,对象的布尔值就为False。
__norm_cval()
它的作用是把cmp()返回的正值传为1,负值转为-1,这是因为cmp()基于比较的结果,通常返回任意的正数或负数(或0),但为了我们的目的,需要严格规定返回值为-1、0和1。它的原理,即等价的代码,在注释中已经给出。
14.私有化
默认情况下,属性在Python中都是“公开的”,但可以通过“访问控制符”来限定成员函数的访问。
(1)双下划线(__)
Python为类元素(属性和方法)的私有性提供初步的形式。由双下划线开始的属性在运行时被“混淆”,所以不能直接访问,因为实际上会有名字前面加上下划线和类名。如前面的self.__num属性,被“混淆后”,用于访问这个数据值的标识就变成了self._NumStr__num。这样做,有下面的好处:
把类名加上后形成的新的“混淆”结果将可以防止在祖先类或子孙类中的同名冲突
为了保护__XXX变量不与父类名称空间相冲突,即父类与子类都可以同时存在__XXX变量,而不会相互影响,因为被“混淆”之后,它们的访问标识已经发生了改变
(2)单下划线(_)
简单的模块级私有化只需要在属性名前使用一个单下划线字符。这就防止模块的属性用"from mymodule import *"来加载。这是严格基于作用域的,所以这同样适合于函数。
尽管Python没有在语法上把private, protected等特征内建于语言中,但是可以按照我们自己的需求严格地定制访问权。
15.授权
(1)包装
我们知道,对于一个类,如果需要对其进行定制,以修改、增加其功能,可以通过派生来实现。在Python 2.2以前,因为类和类型还没有统一,Python标准类型子类化或派生类都是不允许的,所以那时候只能通过包装来实现类似类的派生功能,不过之后因为类和类型的统一,通过派生也可以实现上面的功能,不过,习惯上,对于标准类型,我们还是用包装,而不是派生。
包装是对一个已存在的对象进行包装,一般是对标准类型进行包装,从而对其增加新的、删除不要的或修改其它已存在的功能,而包装包括定义一个类,它的实例拥有标准类型的核心行为。
当然也可以包装类,但是对于我们自己定义的类来说,更好的方法是派生。
(2)实现授权
首先需要理解下面的概念:
什么是授权
授权是包装的一个特性,可用于简化处理相关命令×××,采用已存在的功能以达到最大限度的代码重用。
授权的过程
包装一个类型通常是对已存在的类型做一些定制,而授权的过程,即是所有更新的功能都是由新类的某部分来处理,但已存在的功能就授权给对象的默认属性。
授权的关键
实现授权的关键点就是覆盖__getattr__()方法,在代码中包含一个对getattr()内建函数的调用。
授权与属性查找
引用一个属性时,Python解释器首先在局部名称空间中查找该属性,比如一个自定义的方法或局部实例属性;如果没有在局部名称空间中找到,则搜索类名称空间,即在类属性中查找;最后,如果两类搜索都失败了,搜索则对原对象开始授权请求,此时__getattr__()会被调用,然后调用getattr()得到一个对象的默认行为。
当然,理解上面的概念都会比较抽象,因为没有一个实际的例子来说明,下面就通过举一些例子来说明。
(a)包装对象的简例
下面一个类,几乎可以包装任何对象,提供基本功能,如repr()和str()来处理字符串表示法。另外定制由get()方法处理,它可以返回原始对象。其它保留的功能都授权给对象的本地属性,在必要时通过__getattr__()获得。如下:
class WrapMe(object):
def __init__(self, obj):
self.__data = obj
def get(self):
return self.__data
def __repr__(self):
return repr(self.__data)
def __str__(self):
return str(self.__data)
def __getattr__(self, attr): # 实现授权
return getattr(self.__data, attr)
使用上面的包装类来包装复数,因为在所有Python数值类型中,只有得数拥有属性:数据属性和内建方法conjugate()内建方法。执行如下:
>>> from test import WrapMe
>>> wrappedComplex = WrapMe(3.5+4.2j)
>>> wrappedComplex # 包装的对象:repr()
(3.5+4.2j)
>>> wrappedComplex.real # 实部属性
3.5
>>> wrappedComplex.imag # 虚部属性
4.2
>>> wrappedComplex.conjugate() # conjugate()方法
(3.5-4.2j)
>>> wrappedComplex.get() # 实际对象
(3.5+4.2j)
>>>
>>> type(wrappedComplex.get())
<type 'complex'>
>>> type(wrappedComplex)
<class 'test.WrapMe'>
从上面的例子可以看到,对于未在包装类中定义的属性,是通过getattr()方法,授权给对象。
下面是包装列表类型的例子:
>>> from test import WrapMe
>>> wrappedList = WrapMe([123, 'foo', 45.67])
>>> wrappedList.append('bar')
>>> wrappedList.append(123)
>>> wrappedList
[123, 'foo', 45.67, 'bar', 123]
>>> wrappedList.index(45.67)
2
>>> wrappedList.count(123)
2
>>> wrappedList.pop()
123
>>> wrappedList
[123, 'foo', 45.67, 'bar']
>>>
>>> wrappedList[3]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'WrapMe' object does not support indexing
>>> wrappedList.get()[3]
'bar'
刚开始一切正常,而后来尝试使用切片操作时,就会报错,那是因为:特殊行为没有在类型的方法列表中,不能被访问,因为它们不是属性,比如对列表的切片操作,它是内建于类型中的,而不是像append()方法那样作为属性存在的,只有已存在的属性是在此代码中授权的。但是后面我们通过返回原始对象来进行切片操作。
而下面一个例子,通过对文件进行操作,来理解一些信息片段从何而来,从而利用新得到的知识来重复功能:
>>> f = WrapMe(open('/tmp/motd'))
>>> f
<open file '/tmp/motd', mode 'r' at 0x7f2966d3ec90>
>>> f.get()
<open file '/tmp/motd', mode 'r' at 0x7f2966d3ec90>
>>> f.readline()
'Python is a great!\n'
>>> f.tell()
19
>>> f.seek(0)
>>> print f.readline(),
Python is a great!
>>> f.close()
>>> f.get()
<closed file '/tmp/motd', mode 'r' at 0x7f2966d3ec90>
>>> print '<%s file %s, mode %s at %x>' % (f.closed and 'closed' or 'open', f.name, f.mode, id(f.get()))
<closed file /tmp/motd, mode r at 7f2966d3ec90>
(b)更新简单的包裹类
包装标准类型
为对象加上创建时间、修改时间和访问时间这几个属性,说明如下:
创建时间(ctime):实例化一个类的时间
修改时间(mtime):核心数据升级的时间(通常会调用新的set()方法)
访问时间(atime):最后一次对象的数据值被获取或者属性被访问时的时间戳
程序代码如下:
#!/usr/bin/env python
from time import time, ctime
class TimedWrapMe(object):
def __init__(self, obj):
self.__data = obj
self.__ctime = self.__mtime = self.__atime = time()
def get(self):
self.__atime = time()
return self.__data
def gettimeval(self, t_type):
if not isinstance(t_type, str) or t_type[0] not in 'cma':
raise TypeError, "argument of 'c' ,'m', or 'a' req 'd'"
return getattr(self, '_%s__%stime' % (self.__class__.__name__, t_type[0])) # 私有属性的引用方法
def gettimestr(self, t_type):
return ctime(self.gettimeval(t_type))
def set(self, obj):
self.__data = obj
self.__mtime = self.__atime = time()
def __repr__(self):
self.__atime = time()
return repr(self.__data)
def __str__(self):
self.__atime = time()
return str(self.__data)
def __getattr__(self, attr):
self.__atime = time()
return getattr(self.__data, attr)
代码不做过多的解释,只要把前面的内容理解了,其实并不难理解。前面已经知道授权是如何工作的,下面包装一个没有属性的对象,来突出刚加入的新的功能,包装一个整型的例子如下:
>>> from twrapme import TimedWrapMe
>>> timeWrappedObj = TimedWrapMe(932)
>>> timeWrappedObj.gettimestr('c')
'Tue May 10 10:03:27 2016'
>>> timeWrappedObj.gettimestr('m')
'Tue May 10 10:03:27 2016'
>>> timeWrappedObj.gettimestr('a')
'Tue May 10 10:03:27 2016'
>>> timeWrappedObj
932
>>> timeWrappedObj.gettimestr('c')
'Tue May 10 10:03:27 2016'
>>> timeWrappedObj.gettimestr('m')
'Tue May 10 10:03:27 2016'
>>> timeWrappedObj.gettimestr('a')
'Tue May 10 10:03:58 2016'
可以看到,一开始三个时间都是一样的,而访问一次对象后,atime就发生了改变。如果使用set()来置换对象,则mtime和atime都会被更新:
>>> timeWrappedObj.set('time is up!')
>>> timeWrappedObj.gettimestr('m')
'Tue May 10 16:34:06 2016'
>>> timeWrappedObj
'time is up!'
>>> timeWrappedObj.gettimestr('c')
'Tue May 10 10:03:27 2016'
>>> timeWrappedObj.gettimestr('m')
'Tue May 10 16:34:06 2016'
>>> timeWrappedObj.gettimestr('a')
'Tue May 10 16:34:30 2016'
改进包装一个特殊对象
下面是一个包装文件对象的类,实现的功能为,所有写入文件的文本会自动转化为大写:
#!/usr/bin/env python
class CapOpen(object):
def __init__(self, fn, mode='r', buf=-1):
self.file = open(fn, mode, buf)
def __str__(self):
return str(self.file)
def __repr__(self):
return repr(self.file)
def __iter__(self):
return self.file
def write(self, line): # 除了write(),所有属性都已授权给文件对象
self.file.write(line.upper())
def __getattr__(self, attr):
return getattr(self.file, attr)
执行如下:
>>> from capOpen import CapOpen
>>> f = CapOpen('/tmp/test', 'w')
>>> f.write('delegation example\n')
>>> f.write('faye is good\n')
>>> f.write('at delegating\n')
>>> f.close()
>>> f
<closed file '/tmp/test', mode 'w' at 0x7f770cb43c90>
>>>
>>> f = CapOpen('/tmp/test', 'r')
>>> type(f)
<class 'capOpen.CapOpen'>
>>> for eachLine in f:
... print eachLine,
...
DELEGATION EXAMPLE
FAYE IS GOOD
AT DELEGATING
需要注意的是,如果不给CapOpen包装添加添加__iter__()的特殊方法,那么得到的包装后的对象会提示是不可迭代的。如果希望返回的是self,而不是self.file,即希望让CapOpen对象本身也是一个可迭代的对象,不仅仅要为其定义__iter__方法,还要定义next方法,否则是无法迭代其内容的,如下:
def __iter__(self):
return self
def next(self):
return self.file.next()
16.新式类的高级特性(Python 2.2+)
(1)新式类的通用特性
主要如下:
可以子类化Python数据类型:在之前只能通过包装来扩展标准类型
Python内建的转换函数都是工厂函数:当这些函数被调用时,实际上是对相应的类型进行实例化
(2)__slots__类属性
__dict__属性跟踪所有实例属性,假如有一个实例inst,它有一个属性foo,使用inst.foo和inst.__dict__['foo']来访问是一致的。
但现在的问题是,由于字典本身的数据结构问题,其会占用比较大的内存空间,因此如果有一个类,它需要多个实例,这时候就需要考虑性能上的问题了。
那么这个时候可以使用__slots__属性来替代__dict__属性。__slots__是一个类变量,由一序列对象组成,由所有合法标识构成的实例属性的集合来表示,它可以是一个列表、元组或可迭代对象,当然也可以是标识实例能拥有的唯一的属性的简单字符串。任何试图创建一个其名不在__slots__中的名字的实例属性都将导致AttributeError异常:
>>> class SlottedClass(object):
... __slots__ = ('foo', 'bar')
...
>>> c = SlottedClass()
>>> c.foo = 42
>>> c.xxx = "don't think so"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'SlottedClass' object has no attribute 'xxx'
>>> c.__dict__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'SlottedClass' object has no attribute '__dict__'
>>> c.__slots__
('foo', 'bar')
(3)__getattribute__()特殊方法
类中的__getattr__()特殊方法,仅当属性不能在实例的__dict__或它的类(类的__dict__),或者祖先类(其__dict__)中找到时,才被调用。
如果需要一个适当的函数来执行每一个属性的访问,不管该属性是否存在,那么就可以使用__getattribute__()特殊方法了,直接举例如下:
>>> class Test(object):
... def __getattribute__(self, attr):
... if attr == 'a':
... return 'OK'
... b = 3
...
>>> t = Test()
>>> t.b
>>> t.a
'OK'
不过还不知道可以用在什么地方,需要使用到时再做深入了解即可。
(4)描述符
书本上讲解的描述符相对来说会深入很多,如果初次学习,会非常难以理解,可以先参考下面一篇文章来作为入门学习:
当然,看完之后就可以尝试看书上的例子了,也是非常经典和深入的。
核心的要点是:描述符可以用来做什么——它们提供了一种方法将property的逻辑隔离到单独的类中来处理。(其实这只是属性描述符的内容)
更深入的目前还未能去理解,可能就需要以后对Python作更加深入的学习和研究了,不过如果把上面的内容弄懂了,轻松使用Python描述符还是没有问题的。另外还有一点是,在书本上说明的一点:静态方法 、类方法、属性,甚至所有的函数都是描述符。从这里看出,Python描述符深入下去学习的话还有很多的内容需要了解。它的工作方式是这样的:函数本身就是一个描述符,函数的__get__()方法用来处理调用对象,并将调用对象返回给你。这样一来的话,对函数的调用就有更加深入的理解了,因为确实来说,对于每一个函数,都有一个__get__()方法 。
关于上面的内容,可以参考下面的一个完整的例子:
#!/usr/bin/env python
import os
import pickle
class FileDescr(object):
saved = []
def __init__(self, name=None):
self.name = name
def __get__(self, obj, typ=None):
if self.name not in FileDescr.saved:
raise AttributeError, \
"%r used before assignment" % self.name
try:
f = open(self.name, 'r')
val = pickle.load(f)
f.close()
return val
except(pickle.UnpicklingError, IOError,
EOFError, AttributeError), e:
raise AttributeError, \
"could not read %r: %s" % self.name
def __set__(self, obj, val):
f = open(self.name, 'w')
try:
pickle.dump(val, f)
FileDescr.saved.append(self.name)
except(TabError, pickle.UnpicklingError), e:
raise AttributeError, \
"could not pickle %r" % self.name
finally:
f.close()
def __delete__(self, obj):
try:
os.unlink(self.name)
FileDescr.saved.remove(self.name)
except(OSError, ValueError), e:
pass
执行如下:
>>> from descr import FileDescr
>>> class MyFileVarClass(object):
... foo = FileDescr('foo')
... bar = FileDescr('bar')
...
>>> fvc = MyFileVarClass()
>>> print fvc.foo
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "descr.py", line 16, in __get__
"%r used before assignment" % self.name
AttributeError: 'foo' used before assignment
>>>
>>> fvc.foo = 42
>>> fvc.bar = 'leanna'
>>> print fvc.foo, fvc.bar
42 leanna
>>>
>>> del fvc.foo
>>> print fvc.foo, fvc.bar
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "descr.py", line 16, in __get__
"%r used before assignment" % self.name
AttributeError: 'foo' used before assignment
属性和property()内建函数
通过使用property()内建函数,就可以写一个和属性有关的函数来处理实例属性的获取(getting)、赋值(setting)、删除(deleting)操作,property()有四个参数:
property(fget=None, fset=None, fdel=None, doc=None)
property()的一般用法是,将它写在一个类定义中,property()接受一些传进来的函数(其实是方法 )作为参数。实际上,property()是在它所在的类被创建时被调用的,这些传进来的方法是非绑定的,所以这些方法其实就是函数。
可以先看下面的一个例子:
# 例子1
class ProtectName(object):
def __init__(self, name='xpleaf'):
self.__name = name
def get_name(self):
return self.__name
def set_name(self, name):
self.__name = name
name = property(get_name)
执行如下:
>>> from pro3 import ProtectName
>>> inst = ProtectName()
>>> inst.name
'xpleaf'
>>> inst.name = 'cl'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
可以看到,并不能修改name属性,是因为没有在property()函数中传入修改属性值的函数,进一步修改为如下:
# 例子2
class ProtectName(object):
def __init__(self, name='xpleaf'):
self.__name = name
def get_name(self):
return self.__name
def set_name(self, name):
self.__name = name
name = property(get_name, set_name)
执行如下:
>>> from pro4 import ProtectName
>>> inst = ProtectName()
>>> inst.name
'xpleaf'
>>> inst.name = 'cl'
>>> inst.name
'cl'
对于上面的例子,通过使用装饰器,更流行的写法如下:
# 例子3
class ProtectName(object):
def __init__(self, name='xpleaf'):
self.__name = name
@property
def name(self):
return self.__name
@name.setter
def name(self, new_name):
print 'Warnning: You have changed your name!'
self.__name = new_name
执行如下:
>>> from pro5 import ProtectName
>>> inst = ProtectName()
>>> inst.name
'xpleaf'
>>> inst.name = 'cl'
Warnning: You have changed your name!
>>> inst.name
'cl'
在生产环境中,这种方法常用如下:
class User(UserMixin, db.Model):
……
password_hash = db.Column(db.String(128))
@property
def password(self):
raise AttributeError('password is not a readable attribute')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
即在保存用户密码时,保存到数据库中的并非是原来的密码,而是经过哈希之后的值,所以使用这种方法就可以设置密码,同时也可以防止访问原来的密码(该密码在数据库中并没有保存)。
另外,为了说明property()传入的参数是函数而不是方法,可以看下面的一个例子:
from math import pi
def get_pi(dummy):
return pi
class PI(object):
pi = property(get_pi, doc='Constant "pi"')
执行如下:
>>> from pro6 import PI
>>> inst = PI()
>>> inst.pi
3.141592653589793
(5)元类和__metaclass__
元类
元类是一个类,它是类中的大类,它的实例是其他的类,它可以让你来定义某些类是如何被创建的。当创建一个新类时,就是在使用默认的元类,它是一个类型对象,而对于传统的类来说,它们的元类是types.ClassType:
>>> class C(object):
... pass
...
>>> class CC:
... pass
...
>>> type(C)
<type 'type'>
>>> type(CC)
<type 'classobj'>
>>> import types
>>> type(CC) is types.ClassType
True
元类的使用
元类一般用于创建类。在执行类定义时,解释器必须知道这个类的正确的元类,确定一个类的元类的顺序如下:
解释器会先寻找类属性__metaclass__,如果此属性存在,就将这个属性赋值给此类作为它的元类;
如果此属性没有定义,它会向上查找父类中的__metaclass__,对于新式类,它的元类其实就是type(因为对于新式类的父类object,type(object)是类型);
如果还没有发现__metaclass__属性,解释器会检查名为__metaclass__的全局变量;
如果__metaclass__全局变量不存在,这个类就是一个传统类,并用types.ClassType作为元类;
从上面的顺序可知,如果定义了一个传统类,并且设置它的__metaclass__=type,那么其实就是将它升级为一个新式类。在执行类定义的时候,将检查此类正确的元类,元类通常传递三个参数到构造器:类名、从基类继承数据的元组和类的属性字典。
另外对于元类的使用,在现实情况使用也许不多,因为创建元类用于改变类的默认行为和创建方式,但大多数时候,创建新式类或传统类的通用做法都是使用系统自己所提供的元类。不过了解元类的知识可以让我们更好地知道类的基本工作方式 。下面会有几个基本的例子来帮助理解:
元类示例1
程序代码如下:
#!/usr/bin/env python
from time import ctime
print '*** Welcome to Metaclasses!'
print '\tMetaclass declaration first.'
class MetaC(type):
def __init__(cls, name, bases, attrd):
super(MetaC, cls).__init__(name, bases, attrd)
print '*** Created class %r at: %s' % (name, ctime())
print '\tClass "Foo" declaration next.'
class Foo(object):
__metaclass__ = MetaC
def __init__(self):
print '*** Instantiated class %r at: %s' % (
self.__class__.__name__, ctime())
print '\tClass "Foo" instantiation next.'
f = Foo()
print '\tDONE'
执行如下:
/usr/bin/python2.7 /home/xpleaf/PycharmProjects/Test/meta1.py *** Welcome to Metaclasses! Metaclass declaration first. Class "Foo" declaration next. *** Created class 'Foo' at: Mon Jun 20 18:02:06 2016 Class "Foo" instantiation next. *** Instantiated class 'Foo' at: Mon Jun 20 18:02:06 2016 DONE
元类示例2
创建一个元类,要求程序员在他们写的类中提供一个__str__()方法的实现,如果没有在类中覆盖__repr__()方法,元类会提示你这么做;如果未实现__str__()方法,将会引发一个TyperErrorr异常。程序代码如下:
from warnings import warn
class ReqStrSugRepr(type):
def __init__(cls, name, bases, attrd):
super(ReqStrSugRepr, cls).__init__(name, bases, attrd)
if '__str__' not in attrd:
raise TypeError("Class requires overriding of __str__()")
if '__repr__' not in attrd:
warn('Class suggests overriding of __repr__()\n', stacklevel=3)
class Foo(object):
__metaclass__ = ReqStrSugRepr
def __str__(self):
return 'Instance of class:', self.__class__.__name__
def __repr__(self):
return self.__class__.__name__
class Bar(object):
__metaclass__ = ReqStrSugRepr
def __str__(self):
return 'Instance of class:', self.__class__.__name__
class FooBar(object):
__metaclass__ = ReqStrSugRepr
执行如下:
/usr/bin/python2.7 /home/xpleaf/PycharmProjects/Test/meta2.py
sys:1: UserWarning: Class suggests overriding of __repr__()
Traceback (most recent call last):
File "/home/xpleaf/PycharmProjects/Test/meta2.py", line 33, in <module>
class FooBar(object):
File "/home/xpleaf/PycharmProjects/Test/meta2.py", line 10, in __init__
raise TypeError("Class requires overriding of __str__()")
TypeError: Class requires overriding of __str__()
17.相关模块和文档
主要是介绍了operator模块,它提供了Python中大多数标准操作符的函数版本,在某些情况下,这种接口类型比标准操作符的硬编码方式更通用:
>>> import operator
>>> dir(operator)
['__abs__', '__add__', '__and__', '__concat__', '__contains__', '__delitem__', '__delslice__', '__div__', '__doc__', '__eq__', '__floordiv__', '__ge__', '__getitem__', '__getslice__', '__gt__', '__iadd__', '__iand__', '__iconcat__', '__idiv__', '__ifloordiv__', '__ilshift__', '__imod__', '__imul__', '__index__', '__inv__', '__invert__', '__ior__', '__ipow__', '__irepeat__', '__irshift__', '__isub__', '__itruediv__', '__ixor__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__name__', '__ne__', '__neg__', '__not__', '__or__', '__package__', '__pos__', '__pow__', '__repeat__', '__rshift__', '__setitem__', '__setslice__', '__sub__', '__truediv__', '__xor__', '_compare_digest', 'abs', 'add', 'and_', 'attrgetter', 'concat', 'contains', 'countOf', 'delitem', 'delslice', 'div', 'eq', 'floordiv', 'ge', 'getitem', 'getslice', 'gt', 'iadd', 'iand', 'iconcat', 'idiv', 'ifloordiv', 'ilshift', 'imod', 'imul', 'index', 'indexOf', 'inv', 'invert', 'ior', 'ipow', 'irepeat', 'irshift', 'isCallable', 'isMappingType', 'isNumberType', 'isSequenceType', 'is_', 'is_not', 'isub', 'itemgetter', 'itruediv', 'ixor', 'le', 'lshift', 'lt', 'methodcaller', 'mod', 'mul', 'ne', 'neg', 'not_', 'or_', 'pos', 'pow', 'repeat', 'rshift', 'sequenceIncludes', 'setitem', 'setslice', 'sub', 'truediv', 'truth', 'xor']
那么关于Python面向对象编程的内容就到这里了,其实知识量和难度相对都是比较高的,但不管怎么说,可以先过一遍,以后在实际项目中如果有需要用到时,那么当作文档来查阅也是非常不错的,因为在写这个内容的时候,自己也有了一定的印象,到时相对来说就会轻松很多。
转载于:https://blog.51cto.com/xpleaf/1767733