|
1.5.9. 模块类型
模块是容器对象。import语句用来将其它模块中包含的对象导入当前模块。举例来说,语句 import foo 中的 foo 就是一个模块对象。模块拥有自己的名字空间,这是通过模块的一个字典属性来实现的。这个名字空间可以通过模块对象的dict属性来访问。当一个模块的属性被访问(使用点操作符)时,比如访问 m.x,Python 会自动的去访问 m.__dict__["x"]。同样的,赋值操作 m[x]=y 在内部被执行的其实是 m.__dict__[x]=y。 模块对象拥有以下属性:
属性 描述 m.__dict__ 保存模块名字空间的字典 m.__doc__ 模块的文档字符串 m.__name__ 模块名字 m.__file__ 模块的文件名 m.__path__ 当一个模块通过一个包被引用时,__path__是包的名字
注1:所有内建模块拥有没有__file__ 属性的特权。 注2:如果一个模块拥有 __path__ 属性,import 语句就会认为它是一个包(package)。当从一个包中 import 一个子模块时,将使用包的__path__属性而不是sys.path。
1.5.10. 类 类型
class语句用来创建类,第七章详细介绍了类。和模块类似,类也使用一个字典属性来维护自己的名称空间。访问类的属性时,比如 c.x 在执行行将被翻译成 c.__dict__["x"]。如果在类的 __dict__里没有找到属性x,那么就会到该类的父类中寻找。如果有多个父类,则搜索按照父类(base class)在类定义中顺序从左至右,深度优先。属性赋值如 c.y = 5,则总是更新 c 的__dict__属性,而不会更新某个父类的字典。
class对象定义的属性:
属性 描述 c.__dict__ 类 c 的名字空间 c.__doc__ 类 c 的文档字符串 c.__name__ 类 c 的名字 c.__module__ 类 c 的定义所在的模块 c.__bases__ 类 c 的所有父类(这是一个元组)
1.5.11. 类实例 类型
调用一个类就会生成该类的一个实例。每个实例也有独立的名字空间(也是dict字典,注意不要与类的名字空间混淆)。类实例有以下属性:
属性 描述 x.__dict__ 实例 x 的名字空间 x.__class__ 实例 x 所属的类
访问一个类实例 x 的属性时,比如 x.a,解释器会先查找 x.__dict__["a"],若没有找到,则接着查询 x.__class__.__dict__["a"],如果还没找到,则按照上面提到的搜索顺序继续查询该类的父类们的名字空间,如果还是没有找到,就要查看该类是否定义了__getattr__()方法,如果有这个方法就使用这个方法继续查找。如果经过以上种种手段仍然没有找到这个属性,就引发AttributeError异常。属性赋值如 x.y = 5,则总是更新实例 x 的__dict__属性,而不会更新其所属的类或其某个父类的__dict__字典。
1.5.12. 文件类型
一个文件对象就是一个打开的文件,调用内建函数open()成功则返回一个文件对象。更多关于文件类型的细节在第九章。
1.5.13. 内部类型
解释器内部使用的一系列对象,它们属于内部类型(用户通常不会遇到它们,不过必要时使用它们会解决一些棘手问题)。内部使用对象包括调试对象(traceback objects),代码对象 (code objects),frame objects,切片对象(slice objects)及 省略对象(Ellipsis object)。
1.5.13.1. 代码对象
调用内建函数compile()返回一个代码对象。它表示原始字节编译码或称为字节码。代码对象和函数对象相似,但它不保存被编译代码的上下文信息(被编译代码所在的名称空间及参数的默认值等)。代码对象是不可变对象,而函数对象是可变对象。一个代码对象 c 拥有如下只读属性:
属性 描述 c.co_argcount 参数的个数(不包括 * 或 ** 参数) c.co_code 原始字节码字符串 c.co_consts 字节代码用到的常量 c.co_filename 对象 c 所在的文件 c.co_firstlineno 被编译源代码第一行行号 c.co_flags 解释器标志: 1=优化 | 2=newlocals | 4=*arg | 8=**arg c.co_lnotab 源代码行号=>字节码偏移量 这是一个映射字典 c.co_name 该代码对象的名字 c.co_names 字节代码用到的局部变量名 这是一个元组 c.co_nlocals 字节代码用到的局部变量个数 c.co_stacksize 需要的虚拟机堆践大小(包含内部变量) c.co_varnames 一个元组,包括全部的局部变量名和参数名
1.5.13.2. Frame 对象
Frame 对象表示执行 frame。通常在 traceback对象中会遇到这个对象。 它拥有以下只读属性:
属性 描述 f.f_back 下一个外部frame对象(对当前frame的调用者来说) 如果已到栈底的话 它的值就是 None f.f_code 当前frame中正在执行的代码对象 f.f_locals 当前frame可见的局部变量的字典 f.f_globals 当前frame可见的全局变量的字典 f.f_builtins 当前frame可见的内建名字的字典 f.f_restricted 是否在受限模式下运行 0:不受限 | 1:受限 f.f_lineno 源代码当前行号 f.f_lasti 字节码当前指令索引
下边是frame对象的可写属性(通过调试器或其他工具可以改变下面属性的值):
f.f_trace 当前frame的跟踪函数(供调试器使用) 或 None f.f_exc_type 当前frame发生的异常类型 或 None f.f_exc_value 当前frame发生的异常的值 或 None f.f_exc_traceback 当前framev发生的 traceback 或 None
1.5.13.3. traceback 对象
traceback 对象保存异常的栈追踪信息。只要发生异常就会创建 traceback对象。当一个异常被处理时,可以通过 sys.exc_info() 函数输出异常的堆栈追踪信息。traceback 对象 t 有以下只读属性:
属性 描述 t.tb_next 栈追踪的下一级 (对发生异常的 frame 来说) 或 None t.tb_frame 当前级正在执行的 frame 对象 t.tb_lineno 引发异常的源代码行号 t.tb_lasti 正在执行的指令索引
1.5.13.4. 切片对象
切片对象用于表示在扩展切片语法中的切片。如a [i :j :stride ], a [i :j , n :m ], 或者 a […, i :j ]。切片对象也可以使用内建函数slice([i,] j [,stride])创建。切片对象有下列只读属性: 属性 描述 s.start 切片的下边界,省略时返回None s.stop 切片的上边界,省略时返回None s.step 切片的步进值,省略时返回None
1.5.13.5. 省略对象
省略对象用于表示在一个切片中出现了省略(...)。这个类型只有一个对象,通过内建名称Ellipsis来访问这个对象。它没有任何属性。它的布尔值为 True。
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()函数或后引号(`)操作时,就会调用这个函数做为返回值。例如:
- a = [2,3,4,5] # 创建一个列表
- s = repr(a) # s = '[2, 3, 4, 5]'
- # 注: 也可以使用 s = `a`
- b = eval(s) # 再转换为一个列表
复制代码
如果`re[r()不能返回这样一个字符串,那它应该返回一个格式为<...message...>的字符串,例如:
- f = open("foo")
- 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还支持扩展切片操作,这对于操作多维数据结构(如矩阵和数组)会很方便。你可以这样使用扩展切片:
- a = m[0:100:10] # 步进切片 (stride=10)
- b = m[1:10, 3:20] # 多维切片
- c = m[0:100:10, 50:75:5] # 多维步进切片
- m[0:5, 5:10] = n # 扩展切片分配
- del m[:10, 15:] # 扩展切片删除
复制代码
扩展切片的一般格式是i:j stride, srtide是可选的。和普通切片一样,你可以省略每个切片的开始或者结束的值。另外还有一个特殊的对象--省略对象。写做 (...),用于扩展切片中表示任何数字:
- a = m[..., 10:20] # 利用省略进行的扩展切片操作
- 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 的对象模型并尽量减少特殊方法的调用次数,可以有效提高你的程序的性能。特别是改良类及模块的名字查询方式效果卓著。看下面的代码:
- import math
- d = 0.0
- for i in xrange(1000000):
- d = d + math.sqrt(i)
复制代码
在上面的代码中,每次循环调用都要进行两次名称查询。第一次在全局名称空间中定位math模块,第二次是搜寻一个名称是sqrt的函数对象。 我们将代码改成下面这样:
- from math import sqrt
- d = 0.0
- for i in xrange(1000000):
- 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 计算一个平面多边形的周长
- class Point:
- def __init__(self,x,y,z):
- self.x = x
- self.y = y
- #低效率的示例
- class Poly:
- def __init__(self):
- self.pts = []
- def addpoint(self,pt):
- self.pts.append(pt)
- def perimeter(self): #计算周长
- d = 0.0
- self.pts.append(self.pts[0]) # 暂时封闭这个多边形
- for i in xrange(len(self.pts)-1):
- d2 = (self.pts[i+1].x - self.pts[i].x)**2 + (self.pts[i+1].y - self.pts[i].y)**2
- d = d + math.sqrt(d2)
- self.pts.pop() # 恢复原来的列表
- return d
复制代码
Poly类中的 perimeter() 方法,每次访问 self.pts都会产生两次查询--一次查询名字空间字典,另一次查询 pts 序列。
下面我们改写一下代码,请看 Listing 3.3:
Listing 3.3 Listing 3.2的改良版本
- class Poly:
- ...
- def perimeter(self):
- d = 0.0
- pts = self.pts #提高效率的关键代码
- pts.append(pts[0])
- for i in xrange(len(pts)-1):
- p1 = pts[i+1]
- p2 = pts[i]
- d += sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2)
- pts.pop()
- return d
复制代码
这个代码的关键之处在于用一个局部变量引用了一个类属性,尽管这样的修正对效率提高的并不是很多(15-20%),了解这些并在你的常规代码中留意这些细节就能够帮助你写出高效的代码。当然,如果对性能要求极高,你也可以 C 语言编写 Python 扩展。 |
|