第三章 类型和对象(续)

 

1.6. 特殊方法

 

所有内建的数据类型都拥有一些特殊方法。特殊方法的名字总是由两个下划线(__)开头和结尾。在程序运行时解释器会根据你的代码隐式调用这些方法来完成你想要的功能。例如运行z = x + y 这个代码,解释器内部执行的就是 z= x.__add__(y)。b=x[k] 语句解释器就会执行 b =  x.__getitem__(k)。每个数据类型的行为完全依赖于这些特殊方法的具体实现。

内建类型的特殊方法都是只读的,所以我们无法改变内建类型的行为。虽然如此,我们还是能够使用类定义新的类型,并让它具有象内建类型那样的行为。要做到这一点也不难,只要你能实现本章介绍的这些特殊方法就可以喽!

 

1.6.1. 对象创建、销毁及表示

 

表 3.7 中列出的方法用于初始化、销毁及表示对象。 __init__()方法初始化一个对象,它在一个对象创建后立即执行。 __del__()方法在对象即将被销毁时调用,也就是该对象完成它的使命不再需要时调用。需要注意的是语句 del x 只是减少对象 x 的引用计数,并不调用这个函数。

Table 3.7. 对象创建,删除,表示使用的特殊方法

方法                               描述
__init__(self[,args]) 初始化self
__del__(self) 删除self
__repr__(self) 创建self的规范字符串表示
__str__ (self) 创建self的信息字符串表示
__cmp__(self,other) 比较两个对象,返回负数,零或者正数
__hash__(self) 计算self的32位哈希索引
__nonzero__(self) 真值测试,返回0或者1

 

__repr__()和__str__()方法都返回一个字符串来表示 self 对象。通常情况,__repr__()方法会返回的这样一个字符串:通过对该字符串取值(eval)操作将会重新得到这个对象。如果一个对象拥有__repr__方法,当对该对象使用repr()函数或后引号(`)操作时,就会调用这个函数做为返回值。例如:

<script type="text/javascript"></script> Toggle line numbers Toggle line numbers
   1 a = [2,3,4,5]           # 创建一个列表
2 s = repr(a) # s = '[2, 3, 4, 5]'
3 # 注: 也可以使用 s = `a`
4 b = eval(s) # 再转换为一个列表

 

如果`re[r()不能返回这样一个字符串,那它应该返回一个格式为<...message...>的字符串,例如:

<script type="text/javascript"></script> Toggle line numbers Toggle line numbers
   1 f = open("foo")
2 a = repr(f) # a = "<open file 'foo', mode 'r' at dc030>"

 

当调用str()函数或执行print语句时,python会自动调用被操作(或打印)对象的__str__()方法。与__repr__()相比,__str__()方法返回的字符快通常更简洁易读,内容一般是该对象的描述性信息。如果一个对象没有被定义该函数,Python就会调用__repr__()方法。

__cmp__(self,other)方法用于与另一对象进行比较操作。如果 self < other ,它返回一个负值;若self == other,返回零;若self > other,返回一个正数。如果一个对象没有定义该函数,对象就改用对象的标识进行比较。另外,一个对象可以给每个相关操作定义两个比较函数(正向反向),这通常被称为rich comparisons。__nonzero__()方法用于对自身对象进行真值测试,应该返回0或1,如果这个方法没有被定义,Python将调用__len__()方法来取得该对象的真值。最后__hash__()方法计算出一个整数哈希值以便用于字典操作。(内建函数hash()也可以用来计算一个对象的哈希值)。相同对象的返回值是相等的。注意,可变对象不能定义这个方法,因为对象的变化会改变其哈希值,这会造成它不能被定位和查询。一个对象在未定义 cmp() 方法的情况下也不能定义 hash()。

 

1.6.2. 属性访问

 

表 3.8列出了读取、写入、或者删除一个对象的属性的方法.

Table 3.8. 访问属性的方法

方法                                    描述
__getattr__(self , name) 返回属性 self.name
__setattr__(self , name , value) 设置属性 self.name = value
__delattr__(self , name) 删除属性 self .name
例如:

a = x.s # 调用 __getattr__(x,"s")
x.s = b # 调用 __setattr__(x,"s", b)
del x.s # 调用 __delattr__(x,"s")

对于类实例,__getattr__()方法只在类例字典及相关类字典内搜索属性失败时才被调用。这个方法会返回属性值或者在失败时引发AttributeError异常。

 

1.6.3. 序列和映射的方法

 

表 3.9中介绍了序列和映射对象可以使用的方法。

Table 3.9. 序列和映射的方法

方法                                        描述
__len__(self) 返回self的长度 len(someObject) 会自动调用 someObject的__len__()
__getitem__(self , key) 返回self[key]
__setitem__(self , key , value) 设置self[key] = value
__delitem__(self , key) 删除self[key]
__getslice__(self ,i ,j) 返回self[i:j]
__setslice__(self ,i ,j ,s) 设置self[i:j] = s
__delslice__(self ,i ,j) 删除self[i:j]
__contains__(self ,obj) 返回 obj 是否在 self 中

例如:
a = [1,2,3,4,5,6]
len(a) # __len__(a)
x = a[2] # __getitem__(a,2)
a[1] == 7 # __setitem__(a,1,7)
del a[2] # __delitem__(a,2)
x = a[1:5] # __getslice__(a,1,5)
a[1:3] = [10,11,12] # __setslice__(a,1,3,[10,11,12])
del a[1:4] # __delslice__(a,1,4)

内建函数len(x)调用对象 x 的__len__()方法得到一个非负整数。如果一个对象没有定义__nonzero__()方法,就由 __len__()这个函数来决定其真值。 __getitem__(key)方法用来访问个体元素。对序列类型,key只能是非负整数,对映射类型,关键字可以是任意Python不变对象。 __setitem__()方法给一个元素设定新值。__delitem__()方法和__delslice__()方法在使用del语句时被自动调用。 切片方法用来支持切片操作符 s[i:j]。__getslice__(self,i,j)方法返回一个self类型的切片,索引 i 和 j 必须是整数,索引的含义由__getslice__()方法的具体实现决定。如果省略 i,i就默认为 0,如果省略 j,j 就默认为 sys.maxint。 __setslice__()方法给为一个切片设定新值。__delslice__()删除一个切片中的所有元素。__contains__()方法用来实现 in 操作符。

除了刚才描述过的方法之外,序列以及映射还实现了一些数学方法,包括__add__(), __radd__(), __mul__(), 和 __rmul__(),用于对象连接或复制等操作。下面会对这些方法略作介绍。

Python还支持扩展切片操作,这对于操作多维数据结构(如矩阵和数组)会很方便。你可以这样使用扩展切片:

<script type="text/javascript"></script> Toggle line numbers Toggle line numbers
   1 a = m[0:100:10]          # 步进切片 (stride=10)
2 b = m[1:10, 3:20] # 多维切片
3 c = m[0:100:10, 50:75:5] # 多维步进切片
4 m[0:5, 5:10] = n # 扩展切片分配
5 del m[:10, 15:] # 扩展切片删除

 

扩展切片的一般格式是i:j stride, srtide是可选的。和普通切片一样,你可以省略每个切片的开始或者结束的值。另外还有一个特殊的对象--省略对象。写做 (...),用于扩展切片中表示任何数字:

<script type="text/javascript"></script> Toggle line numbers Toggle line numbers
   1 a = m[..., 10:20]        # 利用省略进行的扩展切片操作
2 m[10:20, ...] = n

 

当进行扩展切片操作时,__getitem__(), __setitem__(), 和 __delitem__()方法分别用于实现访问、修改、删除操作。然而在扩展切片操作中,传递给这些方法的参数不是一个整数,而是一个包含切片对象的元组(有时还会包括一个省略对象)。例如:

a = m[0:10, 0:100:5, ...]

上面的语句会以下面形式调用__getitem__():

a = __getitem__(m, (slice(0,10,None), slice(0,100,5), Ellipsis))

 

 

        注意:在Python1.4版开始,就一直有扩展切片的语法,却没有任何一个内建类型支持扩展切片操作。
Python 2.3改变了这个现状。从Python 2.3开始,内建类型终于支持扩展切片操作了,这要感谢 Michael Hudson。

 

 

1.6.4. 数学操作

 

表3.10 列出了与数学运算相关的特殊方法。数学运算从左至右进行,执行表达式 x + y 时,解释器会试着调用 x.add(y)。以 r 开头的特殊方法名支持以反转的操作数进行运算。它们在左运算对象未实现相应特殊方法时被调用,例如 x + y 中的 x 若未提供 __add__() 方法,解释器就会试着调用函数 y.__radd__(x)

表 3.10. 数学操作的方法

Method                          Result
__add__(self ,other) self + other
__sub__(self ,other) self - other
__mul__(self ,other) self * other
__div__(self ,other) self / other
__mod__(self ,other) self % other
__divmod__(self ,other) divmod(self ,other)
__pow__(self ,other [,modulo]) self ** other , pow(self , other , modulo)
__lshift__(self ,other) self << other
__rshift__(self ,other) self >> other
__and__(self ,other) self & other
__or__(self ,other) self | other
__xor__(self ,other) self ^ other
__radd__(self ,other) other + self
__rsub__(self ,other) other - self
__rmul__(self ,other) other * self
__rdiv__(self ,other) other / self
__rmod__(self ,other) other % self
__rdivmod__(self ,other) divmod(other ,self)
__rpow__(self ,other) other ** self
__rlshift__(self ,other) other << self
__rrshift__(self ,other) other >> self
__rand__(self ,other) other & self
__ror__(self ,other) other | self
__rxor__(self ,other) other ^ self
__iadd__(self ,other) self += other
__isub__(self ,other) self -= other
__imul__(self ,other) self *= other
__idiv__(self ,other) self /= other
__imod__(self ,other) self %= other
__ipow__(self ,other) self **= other
__iand__(self ,other) self &= other
__ior__(self ,other) self |= other
__ixor__(self ,other) self ^= other
__ilshift__(self ,other) self <<= other
__irshift__(self ,other) self >>= other
__neg__(self) -self
__pos__(self) +self
__abs__(self) abs(self)
__invert__(self) ~self
__int__(self) int(self)
__long__(self) long(self)
__float__(self) float(self)
__complex__(self) complex(self)
__oct__(self) oct(self)
__hex__(self) hex(self)
__coerce__(self ,other) Type coercion

 

__iadd__(), __isub__()方法用于实现原地运算(in-place arithmetic),如 a+=b 和 a-=b (称为增量赋值)。原地运算与标准运算的差别在于原地运算的实现会尽可能的进行性能优化。举例来说,如果 self 参数是非共享的,就可以原地修改 self 的值而不必为计算结果重新创建一个对象。

int(), long(), float()和complex() 返回一个相应类型的新对象,oct() 和 hex()方法分别返回相应对象的八进制和十六进制的字符串表示。

x.coerce(self,y) 用于实现混合模式数学计算。这个方法在需要时对参数 self (也就是x) 和 y 进行适当的类型转换,以满足运算的需要。如果转换成功,它返回一个元组,其元素为转换后的 x 和 y ,若无法转换则返回 None。在计算x op y时(op是运算符),使用以下规则:

        1.若 x 有__coerce__()方法,使用x.__coerce__(y)返回的值替换 x 和 y, 若返回None,转到第 3 步。
2.若 x 有__op __()方法,返回 x.__op __(y)。否则恢复 x 和 y 的原始值,然后进行下一步。
3.若 y 有__coerce__()方法,使用y.__coerce__(x)返回的值替换 x 和 y。若返回None,则引发异常。
4.若 y 有__rop __()方法,返回y.__op __(x),否则引发异常.

 

虽然字符串定义了一些运算操作,但 ASCII字符串和Unicode字符串之间的运算并不使用 coerce()方法。

在内建类型中,解释器只支持少数几种类型进行混合模式运算。常见的有如下几种:

·如果 x 是一个字符串, x % y 调用字符串格式化操作,与 y 的类型无关
·如果 x 是一个序列, x + y 调用序列连结
·如果 x 和 y 中一个是序列,另个是整数, x * y调用序列重复

 

 

1.6.5. 比较操作

 

表 3.11 列出了实现分别各种比较操作(<, >, <=, >=, ==, !=)的对象特殊方法,这也就是 rich comparisons。这个概念在Python 2.1中被第一次引入。这些方法都使用两个参数,根据操作数返回适当类型对象(布尔型,列表或其他Python内建类型)。举例来说,两个矩阵对象可以使用这些方法进行元素智能比较,并返顺一个结果矩阵。若比较操作无法进行,则引发异常.

表 3.11. 比较方法

方法                            操作
__lt__(self ,other ) self < other
__le__(self ,other ) self <= other
__gt__(self ,other ) self > other
__ge__(self ,other ) self >= other
__eq__(self ,other ) self == other
__ne__(self ,other ) self != other

 

 

1.6.6. 可调用对象

 

最后,一个对象只要提供 __call__(self[,args]) 特殊方法,就可以象一个函数一样被调用。举例来说,如果对象 x 提供这个方法,它就可以这样调用:x(arg1 , arg2 , ...)。解释器内部执行的则是 x .__call__(self , arg1 , arg2 , ...)

 

1.7. 性能及内存占用

 

所有的Python对象至少拥有一个整型引用记数、一个类型定义描述符及真实数据的表示这三部分。对于在32位计算机上运行的C语言实现的Python 2.0,表 3.12 列出了常用内建对象占用内存的估计大小,对于解释器的其它实现或者不同的机器配置,内存占用的准确值可能会有不同。你可能从来不考虑内存占用问题,不过当 Python在要求高性能及内存紧张的环境下运行时,就必须考虑这个问题。下边这个表可以有效的帮助程序员更好地规划内存的使用:

表 3.12. 内建数据类型使用的内存大小

类型              大小

Integer 12 bytes
Long integer 12 bytes + (nbits/16 + 1)*2 bytes
Floats 16 bytes
Complex 24 bytes
List 16 bytes + 4 bytes(每个元素)
Tuple 16 bytes + 4 bytes(每个条目)
String 20 bytes + 1 byte(每个字符)
Unicode string 24 bytes + 2 bytes(每个字符)
Dictionary 24 bytes + 12*2n bytes, n = log2(nitems)+1
Class instance 16 bytes 加一个字典对象
Xrange object 24 bytes

 

由于字符串类型太常用了,所以解释器会特别优化它们。可以使用内建函数intern(s)来暂存一个频繁使用的字符串 s。这个函数首先在内部哈希表中寻找字符串 s 的哈希值,如果找到,就创建一个到该字符串的引用,如果找不到,就创建该字符串对象并将其哈希值加入内部哈希表。只要不退出解释器,被暂存的字符串就会一直存在。如果你关心内存占用,那你就一定不要暂存极少使用的字符串。为了使字典查询更有效率,字符串会缓存它们最后的哈希值。

一个字典其实就是一个开放索引的哈希表。The number of entries allocated to a dictionary is equal to twice the smallest power of 2 that’s greater than the number of objects stored in the dictionary. When a dictionary expands, its size doubles. On average, about half of the entries allocated to a dictionary are unused.

一个Python程序的执行首先是一系列的函数调用(包括前面讲到的特殊方法),之后再选择最高效的算法。搞懂 Python 的对象模型并尽量减少特殊方法的调用次数,可以有效提高你的程序的性能。特别是改良类及模块的名字查询方式效果卓著。看下面的代码:

<script type="text/javascript"></script> Toggle line numbers Toggle line numbers
   1 import math
2 d = 0.0
3 for i in xrange(1000000):
4 d = d + math.sqrt(i)

 

在上面的代码中,每次循环调用都要进行两次名称查询。第一次在全局名称空间中定位math模块,第二次是搜寻一个名称是sqrt的函数对象。 我们将代码改成下面这样:

<script type="text/javascript"></script> Toggle line numbers Toggle line numbers
   1 from math import sqrt
2 d = 0.0
3 for i in xrange(1000000):
4 d = d + sqrt(i)

 

这个代码每次循环只需要进行一次名称查询。就这样一个简单的调整,在作者的 200 MHz PC上运行时,这个简单的变化会使代码运行速度提高一倍多。

注:      200 MHz的机器我没有,但在我的2000 MHz机器上效果没有这么明显,不过仍然有10%以上的提高  --Feather
那时作者用的是 Python 2.0,现在你用的是 2.4。 Python一直在不断进步嘛! --WeiZhong

 

在Python程序设计中,应该仅在必要时使用临时变量,尽可能的避免非必要的序列或字典查询。下面我们看看 Listing 3.2中的这两个类:

Listing 3.2 计算一个平面多边形的周长

 

<script type="text/javascript"></script> Toggle line numbers Toggle line numbers
   1 class Point:
2 def __init__(self,x,y,z):
3 self.x = x
4 self.y = y
5 #低效率的示例
6 class Poly:
7 def __init__(self):
8 self.pts = []
9 def addpoint(self,pt):
10 self.pts.append(pt)
11 def perimeter(self): #计算周长
12 d = 0.0
13 self.pts.append(self.pts[0]) # 暂时封闭这个多边形
14 for i in xrange(len(self.pts)-1):
15 d2 = (self.pts[i+1].x - self.pts[i].x)**2 + (self.pts[i+1].y - self.pts[i].y)**2
16 d = d + math.sqrt(d2)
17 self.pts.pop() # 恢复原来的列表
18 return d

 

Poly类中的 perimeter() 方法,每次访问 self.pts[i]都会产生两次查询--一次查询名字空间字典,另一次查询 pts 序列。

下面我们改写一下代码,请看 Listing 3.3:

Listing 3.3 Listing 3.2的改良版本

<script type="text/javascript"></script> Toggle line numbers Toggle line numbers
   1 class Poly:
2 ...
3 def perimeter(self):
4 d = 0.0
5 pts = self.pts #提高效率的关键代码
6 pts.append(pts[0])
7 for i in xrange(len(pts)-1):
8 p1 = pts[i+1]
9 p2 = pts[i]
10 d += sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2)
11 pts.pop()
12 return d

这个代码的关键之处在于用一个局部变量引用了一个类属性,尽管这样的修正对效率提高的并不是很多(15-20%),了解这些并在你的常规代码中留意这些细节就能够帮助你写出高效的代码。当然,如果对性能要求极高,你也可以 C 语言编写 Python 扩展。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值