我们将族群或类别称作类型(class),将个体叫做实例(instance)。类型持有同族个体的共同行为和共享状态,而实例仅保存私有特性而已。
上面这局话都类与实例的概括是确实太精准了。
任何类型都是其祖先类型的子类,同样对象也可以被判定为其祖先类型的实例。
Python中所有的类都是object的子类,同样也属于object的实例,但object哪里来,由type造出来,在Python中type就是造物主。
In [123]: isinstance(type,object) Out[123]: True In [124]: issubclass(type,object) Out[124]: True In [125]: isinstance(object,type) Out[125]: True In [126]:
简单理解,祖宗里面最高的是object,他是最高的,就好比树根。但祖宗确是造物主type创建,但type也是继承与祖宗,有点拗口。
单就类型对象而言,其本质就是用来存储方法和字段成员的特殊容器,用同一份设计来实现才是正常思路。
类型对象属于创建者这样的特殊存在。默认情况下,它们由解释器在首次载入时自动生成,生命周期与进程相同,且仅存在一个实例
In [135]: type('123') is 'abc'.__class__ is str Out[135]: True
名字
在通常认知里,变量是一段具有特定格式的内存,变量名则是内存别名。因为在编码阶段,无法确定内存的具体位置,故使用名称符号代替
静态编译和动态解释型语言对于变量名的处理方式也完全不同。静态编译器和链接器会以固定地址,或直接、间接寻址指令代替变量名。也就是说变量名
不参与执行过程,可被删除。但在解释型动态语言里,名字和对象通常史两个运行期实体。名字不但有自己的类型,还需分配内存,并介入执行过程。
甚至可以说,名字才是动态模型的基础。
赋值步骤:
1准备好右边值目标对象
2准好变量名
3在名字空间里为两者建立关联。
即便如此,名字与目标对象之间也仅是引用关联。名字只负责找人,但对于此人一无所知。
鉴于在运行期才能知道名字引用的目标类型,所以说Python是一种动态类型语言。
名字空间
名字空间默认使用字典(dict)数据结构,由多个键值对(key/value)组成。
内置函数globals和locals分别返回全局名字空间和本地名字空间字典
In [138]: globals() is locals() Out[138]: True In [139]:
在主模块中运行,locals()与glocals()是相等的
In [140]: def test(): ...: x = 'hello' ...: print(locals()) ...: print('local', id (locals())) ...: print('global', id(globals())) ...: In [141]: test() {'x': 'hello'} local 4547282256 global 4510296176 In [142]:
globals总是固定指向模块名字空间,而locals则指向当前作用域环境。
可以直接修改名字空间来建立关联引用。
In [142]: globals()['name'] = 'sidian' In [143]: name Out[143]: 'sidian' In [144]:
正因为名字空间的特性,赋值操作仅是名字在名字空间里重新关联,而非修改原对象。
命名习惯建议
1 类名称使用CapWords格式
2模块文件名、函数、方法成员等使用lower_case_with_underscores格式
3全局常量使用UPPER_CASE_WITH_UNDERSCORES格式
4避免与内置函数或标准库的常用类型同名,因为这样容易误导
模块与类还是由一些相同的地方的
模块成员以但下划线开头(_x),属私有成员,不会被*号导入
类型成员以双下划线揩油,但无结尾,属自动命名私有成员
以双下划线开头和结尾,通常是系统成员,应避免使用
在交互模式下,单下划线(_)返回最后一个表达式结果。
In [146]: 1+2 Out[146]: 3 In [147]: _ Out[147]: 3 In [148]:
内存
Python没有值类型、引用类型之分。事实上,每个对象都很重。即便是简单的数字,也由标准对象头,以及保存类型指针和引用计数等信息。
弱引用
如果说,名字与目标对象关联构成强引用关系,会增加引用计数,进而影响期生命周期,那么弱引用(weak reference)就是简配版,骑在保留引用前提下,不增加计数,也不阻止目标被回收
不是所有的类型支持弱引用,比如int,tuple,看有没有__weakref__属性
Python 3.7.4 (default, Jul 9 2019, 18:13:23) Type 'copyright', 'credits' or 'license' for more information IPython 7.7.0 -- An enhanced Interactive Python. Type '?' for help. PyDev console: using IPython 7.7.0 Python 3.7.4 (default, Jul 9 2019, 18:13:23) [Clang 10.0.1 (clang-1001.0.46.4)] on darwin class X: ...: def __del__(self): ...: print(id(self), 'dead') ...: a = X() import sys sys.getrefcount(a) Out[5]: 2 import weakref w = weakref.ref(a) id(w()) Out[8]: 4411485904 sys.getrefcount(a) Out[9]: 2 del a 4411485904 dead
weakref内置的一些方法
w() a = X() w = weakref.ref(a) weakref.getweakrefcount(a) Out[14]: 1 weakref.getweakrefs(a) Out[15]: [<weakref at 0x106f25bf0; to 'X' at 0x106f16f50>] hex(id(w)) Out[16]: '0x106f25bf0'
弱引用可用于一些特定场合,比较缓存,监控等。这类"外挂"场景不应该影响目标对象,不能阻止它们被回收。
弱引用的另外一个典型应用就是实现Finalizer,也就是在对象被回收时执行额外的"清理操作"(有点像回调函数)
a = X() w = weakref.ref(a, lambda x:print(x,x() is None)) del a 4377854224 dead <weakref at 0x107083770; dead> True
当删除a的时候,执行了red里面定义的函数。
书中说明了为什么不用__del__
因为析构方法作为目标成员,其用途是完成对象内部资源清理。它无法感知,也不应该处理与之无法的外部场景。
但在实际开发中,外部关联场景有很多,那么用Finalizer才是合理设计,因为这样只有一个不会侵入的观察员存在。
注意回调函数参数为弱引用而废目标对象。回调函数执行时,目标已无法访问。
使用weakref.proxy可以使弱引用对象的使用与原名字语法一致。
a = X() a.name = 'sidian' w = weakref.ref(a) w.name Traceback (most recent call last): File "/usr/local/lib/python3.7/site-packages/IPython/core/interactiveshell.py", line 3326, in run_code exec(code_obj, self.user_global_ns, self.user_ns) File "<ipython-input-32-df0c13a86467>", line 1, in <module> w.name AttributeError: 'weakref' object has no attribute 'name' w().name Out[33]: 'sidian' p = weakref.proxy(a) p Out[35]: <__main__.X at 0x104f256b0> p.name Out[36]: 'sidian' p.age=80 a.age Out[38]: 80 w().age Out[40]: 80 del a 4377856016 dead
对象复制
复制分浅拷贝(shallow copy)和深度拷贝(deep copy)两种。
对于对象内部成员,浅拷贝仅复制名字引用,而深拷贝会递归复制所有成员。
pick.dumps 与pick.loads可以实现深拷贝
循环引用垃圾回收
当两个或更多对象构成循环引用(reference cycle)时,该机制就会遭遇麻烦。因为彼此引用导致计数永不归零,从而无法触发回收操作,形成内存泄露。
为此,另有一套专门用来处理循环引用的垃圾回收器(gc)作为补充
单个对象也能构成循环引用,比如列表把自身引用作为元素存储。
In [184]: l = [1,2] In [185]: l[1] = l In [186]: l Out[186]: [1, [...]]
对垃圾回收器进行操作
class X: ...: def __del__(self): ...: print(id(self), 'dead') ...: gc.disable() a = X() b = X() a.x=b b.x=a del a del b gc.enable() 4595851024 dead 4595950928 dead gc.collect() Out[21]: 233
对于某些性能优先的算法,在确保没有循环引用的前提下,临时关闭gc可获得更好的性能。
甚至在某些极端优化策略里,会完全屏蔽垃圾回收,以重启进程来回收资源。
做性能测试(timeit)会关闭gc,避免垃圾回收对执行计时造成影响
编译
源码先编译成字节码,才能交由解释器以解释方式执行。这也时Python性能为人诟病的一个重要原因。
字节码(byte code)时中间代码,面向后端编译器或解释器。要么解释执行,要么二次编译成机器代码(native code)执行。
字节码指令通常基于栈式虚拟机(stack_based vm)实现,没有寄存器等复杂结构,实现简单。
且其具备重中立性,与硬件架构、操作系统等无关,便于将编译和平台实现分离,式跨平台语言的主流方案。
Python3使用专门的保存字节码缓存文件(__pycache__/*.pyc)
除了执行指令的字节码,还有很多数据,共同组成执行单元。
从这些元数据里,可以获得参数、闭包等诸多信息。
In [188]: def add(x, y): ...: return x + y ...: In [189]: add.__code__ Out[189]: <code object add at 0x10d1bba50, file "<ipython-input-188-5fcdd2924cd8>", line 1> In [190]: add.__code__.co_varnames Out[190]: ('x', 'y') In [191]: add.__code__.co_code Out[191]: b'|x00|x01x17x00Sx00' In [192]: In [192]: import dis In [193]: dis.dis(add) 2 0 LOAD_FAST 0 (x) 2 LOAD_FAST 1 (y) 4 BINARY_ADD 6 RETURN_VALUE In [194]:
上面简单的运行了dis进行了反汇编(disassembly)
某些时候,需要手工完成编译操作。
In [197]: source = ''' ...: print('hello, world') ...: print(1+2) ...: ''' In [198]: code = compile(source,'demo','exec') In [199]: dis.show_code(code) Name: <module> Filename: demo Argument count: 0 Kw-only arguments: 0 Number of locals: 0 Stack size: 2 Flags: NOFREE Constants: 0: 'hello, world' 1: 3 2: None Names: 0: print In [200]: dis.dis(code) 2 0 LOAD_NAME 0 (print) 2 LOAD_CONST 0 ('hello, world') 4 CALL_FUNCTION 1 6 POP_TOP 3 8 LOAD_NAME 0 (print) 10 LOAD_CONST 1 (3) 12 CALL_FUNCTION 1 14 POP_TOP 16 LOAD_CONST 2 (None) 18 RETURN_VALUE In [201]: exec(code) hello, world 3 In [202]:
除compile函数外,标准库还有编译原码文件的相关操作。
分别为py_compile,compileall,后续用到再学
执行
不管代码如何生成,最终要么以模块导入执行,要么调用eval,exec执行。这两个内置函数使用简单,eval执行单个表达式,exec执行代码块,接收字符串或已编译好的代码对象(code)作为参数。
如果是字符串,就会检查是否符合语法规则。
In [202]: eval('(1+2)+3') Out[202]: 6 In [203]: s = ''' ...: def test(): ...: print("hello,world") ...: test() ...: ''' In [204]: exec(s) hello,world In [205]:
无论选择那种方式执行,都必须由相应的上下文环境。默认直接使用当前全局和本地名字空间。
如同不同代码一样,从中读取目标对象,或写入新值。
In [205]: x= 100 In [206]: def test(): ...: y = 200 ...: print(eval('x+y')) ...: In [207]: test() 300 In [209]: def test(): ...: print('test:', id(globals()), id(locals())) ...: exec('print("exec", id(globals()),id(locals()))') ...: In [210]: test() test: 4510296176 4537806512 exec 4510296176 4537806512 In [211]:
有了操作上下文名字空间的能力,动态代码就可向外部环境注入新的成员,比如说构建新的类型,导入新的算法,最终达到将动态逻辑或其结果融入,成为当前体系组成部分的设计目标。
In [211]: s =''' ...: class My_X:... ...: def hello(): ...: print('hello, world') ...: ''' In [212]: exec(s) In [213]: My_X Out[213]: __main__.My_X In [214]: hello() hello, world In [215]:
某些时候,动态代码来源不确定,基于安全考虑,必须对执行过程进行隔离,阻止其直接读写环境。
如此,就需显式传入容器对象作为动态代码的专用名字空间,以类似建议沙箱(sandbox)方式执行。
根据需要,分别提供globals,locals参数,也可公用同一个空间字典。
为保证代码正确执行,解释器会自动导入__bultins__模块,以便调用内置函数。
In [215]: g = {'x':100} In [216]: l = {'y':200} In [217]: eval('x+y',g,l) Out[217]: 300 In [218]: In [218]: ns = {} In [219]: exec('class XXX:...',ns) In [220]: ns
同时提供两个名字空间参数时,默认总是在locals优先。
In [221]: s = ''' ...: print(x) ...: global y ...: y += 100 ...: z = x+y ...: ''' In [222]: g = {'x':10,'y':20} In [223]: l = {'x':1000} In [224]: exec(s,g,l) 1000
In [226]: l Out[226]: {'x': 1000, 'z': 1120}
前面的s定义中y定义为全局变量,所以没有输出到l里面
前面提及,在函数作用域内,locals函数总是返回执行栈帧(stack frame)名字空间。
因此就式显式提供locals名字空间,也无法将其注入倒台代码的函数内
In [227]: s = ''' ...: print(id(locals())) ...: def test(): ...: print(id(locals())) ...: test() ...: ''' In [228]: ns = {} In [229]: id(ns) Out[229]: 4536481344 In [230]: ns2 = {} In [231]: id(ns2) Out[231]: 4540089840 In [232]: exec(s,ns,ns2) 4540089840 4544788512
In [234]: ns2 Out[234]: {'test': <function test()>}
明显沙盒传入的,默认情况下,所有信息都保存在locals()里面,除非定义了global,会传入globals()
内置类型
与自定义类型(user-defined)相比,内置类型(built-in)算是特权阶层。除了它们是符合数据结构的基本构成单元以外,最重要的式被编译器和解释器特别对待。
比如核心级别的指令和性能优化,专门设计的高效缓存,等等。
内置类型主要的有int,float,str,bytes,bytearray,list,tuple,dict,set,frozenset,其中bytearray,list,dict,set为可变类型。
标准库collections.abc列出了相关类型的抽象基类,可据此判断其基本行为方式
In [10]: import collections.abc In [11]: issubclass(dict, collections.abc.Sequence) Out[11]: False In [12]: issubclass(dict, collections.abc.MutableSequence) Out[12]: False In [13]: issubclass(dict, collections.abc.Mapping) Out[13]: True In [14]:
整数
In [15]: import sys In [16]: x= 1 In [17]: sys.getsizeof(x) Out[17]: 28 In [18]: y=1<<10000 In [19]: sys.getsizeof(y) Out[19]: 1360 In [20]:
Python中int的变长结构允许我们创建超大的天文数字,理论上仅收可分配内存大小的限制。
对于长数字,可以用下划线当做分隔符,且不定位置。
In [21]: 2_3_4 Out[21]: 234 In [22]: 23_345_123 Out[22]: 23345123 In [23]:
另外进制的也可以用
In [23]: 0x23_34_12_2 Out[23]: 36913442 In [24]: 0b01_10 Out[24]: 6 In [25]:
0b,0x 0o分别代码2进制,16进制,8进制的数字
转换
In [31]: eval(bin(100)) Out[31]: 100 In [32]: bin(100) Out[32]: '0b1100100' In [33]: hex(100) Out[33]: '0x64' In [34]: oct(100) Out[34]: '0o144' In [35]: int(bin(100),2) Out[35]: 100 In [36]: int(hex(100),16) Out[36]: 100 In [37]: int(' 100 ') Out[37]: 100 In [38]: int(' 100 ') Out[38]: 100 In [39]:
通过一些命令,可以十进制数字转换为指定的进制字符字符串,也可以通过int还原,第二参数为第一输入参数的进制,输出都式10进制的。
当然也可以通过eval完成,单相比与直接用C实现的转换函数,其性能要差很多,毕竟动态运行需要额外编译和执行开销。
还有一种转换操作式将整数转换为字节数组,这常用于二进制网络协议和文件读写。在这里需要指定字节序,也就是常说的大小端。
目前使用较多的Intel x86 、AMD 64 采用小端。ARM则两种都支持,可自行设定。另外,TCP/IP网络字节,采用大端,这属于协议定义,与硬件结构与操作系统无法。
In [56]: x = 0x1234 In [57]: n = (x.bit_length() +8 -1) //8 In [58]: n Out[58]: 2 In [59]: x=0x1234 In [60]: n = (x.bit_length() + 8 -1)//8 In [61]: b = x.to_bytes(n, sys.byteorder) In [62]: b Out[62]: b'4x12' In [63]: b.hex() Out[63]: '3412' In [64]: hex(int.from_bytes(b,sys.byteorder)) Out[64]: '0x1234'
书中的代码执行完毕以后,我对b的输出其实卡住了一会而,为什么直接输出b是b'4x12',不是应该b'x34x12'吗?
在Python中对于字节码x的输出,如果x的字节码对应asci码有对应值,x就会自动转换成相应的ASCI字符.
In [79]: b'x34' Out[79]: b'4' In [80]:
在电脑数据中一个字节等于8个bit位,对应的16进制刚好为x12,其中的1为高位的4个bit,2对应为低位的4个bit
所以x12的一个数据刚好为一个字节,Python中显示器的为Unicode,对应2个字节,16位。这些计算机的基础,我薄弱啊。
所以在计算机中数据都喜欢用x的形式表示,因为计算机的最小单位为字节,用二进制表示需要8个占位00000000,但转换为16进制,刚好一个x表示。
In [94]: name = '中' In [95]: name Out[95]: '中' In [96]: name.encode('gbk') Out[96]: b'xd6xd0' In [97]: name.encode('utf8') Out[97]: b'xe4xb8xad' In [98]: ord(name) Out[98]: 20013 In [99]: hex(ord(name)) Out[99]: '0x4e2d' In [100]: 'u4e2d' Out[100]: '中' In [101]: chr(20013) Out[101]: '中' In [102]: chr(0x4e2d) Out[102]: '中' In [103]:
ord() 函数是 chr() 函数(对于8位的ASCII字符串)或 unichr() 函数(对于Unicode对象)的配对函数,它以一个字符(长度为1的字符串)作为参数,返回对应的 ASCII 数值,或者 Unicode 数值。
上面的列子中,很好的展示了解码与编码,Python编辑器,对于unicode字符是直接显示的。从字符解码也可以看出来,对于中文的解码,utf8需要三个字节,gbk两个字节,unicode也才两个字节。
In [103]: 0x123 Out[103]: 291 In [104]: 0b0101001 Out[104]: 41 In [105]: 0o5432 Out[105]: 2842 In [106]:
在终端中,无论你输入那种类型的数字,终端会自动转换成10进制的输出,这次学习让我对字符编码又有了更好的认识。
运算符
比较有意思的是一个
In [111]: divmod(5,2) Out[111]: (2, 1)
返回了一个元祖,一个是商,一个属余数。
布尔
布尔是整数的子类型,也就是说True和False可被当做数字直接使用
In [112]: True.__class__ Out[112]: bool In [113]: True.__class__.__mro__ Out[113]: (bool, int, object) In [114]:
In [114]: True == 1 Out[114]: True In [115]: False == 0 Out[115]: True In [117]: False + 1 Out[117]: 1 In [118]:
在进行布尔转换时,数字值、空值(None)、空序列,空字典、空集合,等都被视为False
对于自定义类型,可通过重写__boll__或__len__方法影响bool转换结果。
枚举
首相枚举操作的对象是一个类,是类,不时实例对象。Enum是一个类,继承元类EnumMeta
操作的对象,属于类属性。Enum()调用的是EnumMeta的__call__方法,整个模块有1000行代码,是在没信心看了。用的又是类元编程。
In [187]: Color = Enum('Color','BLACK, YELLOW BLUE RED') In [188]: black = Color.BLACK In [189]: isinstance(black, Color) Out[189]: True In [190]: black.name Out[190]: 'BLACK' In [191]: black.value Out[191]: 1 In [192]:
很有意思,Color的类属性,是Color的实例,实例有两个描述符,name与value
通过继承的方式草写一遍
In [192]: class X(Enum): ...: A = 'a' ...: B = 100 ...: C = [1,2,3] ...: In [193]: X.C Out[193]: <X.C: [1, 2, 3]> In [194]: X['B'] Out[194]: <X.B: 100> In [196]: X('a') Out[196]: <X.A: 'a'> In [197]:
可以通过属性查找,也可以通过值key的形式查找,也可以通过实例化放入value的形式查找具体对象。
如果要避免相同的枚举定义,可用enum.unique装饰器。
这个枚举的类,我真心觉的没啥用,反正我基本没用到过,真要用,我宁可自己定义个更加好用的类。
内存
对于常用的小叔子,解释器会在初始化时进行预缓存。后续使用时,直接将名字关联到这些缓存既可。如此一来,无须创建实例对象,可提高性能,节约内存开销
Python3.6 预缓存范围是[-5, 256]
In [197]: a = -5 In [198]: b= -5 In [199]: a is b Out[199]: True In [200]: a= 256 In [201]: b= 256 In [202]: a is b Out[202]: True In [203]: a = 256 In [204]: a = 257 In [205]: b = 257 In [206]: a is b Out[206]: False In [207]:
Python2对回收后的整数复用不做收缩处理,会导致大量闲置内存驻留。而Python3则改进了不少。
from __future__ import print_function import psutil def rss(): m = psutil.Process().memory_info() print(m.rss >> 20, 'MB') if __name__ == '__main__': rss() x = list(range(10000000)) rss() del x rss()
运行结果
shijianzhongdeMacBook-Pro:第二章类型 shijianzhong$ python t2_2.py 7 MB 394 MB 394 MB shijianzhongdeMacBook-Pro:第二章类型 shijianzhong$ python3 t2_2.py 8 MB 394 MB 84 MB
浮点数
默认float类型存储双精度(double)浮点数,可表达16到17个小数位
从实现方式看,浮点数以二进制存储十进制数的近似值。这可能导致执行结果和编码预期不符,造成不一致缺陷,所以对精度有严格要求的地方,应选择固定精度类型。
------------恢复内容开始------------
我们将族群或类别称作类型(class),将个体叫做实例(instance)。类型持有同族个体的共同行为和共享状态,而实例仅保存私有特性而已。
上面这局话都类与实例的概括是确实太精准了。
任何类型都是其祖先类型的子类,同样对象也可以被判定为其祖先类型的实例。
Python中所有的类都是object的子类,同样也属于object的实例,但object哪里来,由type造出来,在Python中type就是造物主。
In [123]: isinstance(type,object) Out[123]: True In [124]: issubclass(type,object) Out[124]: True In [125]: isinstance(object,type) Out[125]: True In [126]:
简单理解,祖宗里面最高的是object,他是最高的,就好比树根。但祖宗确是造物主type创建,但type也是继承与祖宗,有点拗口。
单就类型对象而言,其本质就是用来存储方法和字段成员的特殊容器,用同一份设计来实现才是正常思路。
类型对象属于创建者这样的特殊存在。默认情况下,它们由解释器在首次载入时自动生成,生命周期与进程相同,且仅存在一个实例
In [135]: type('123') is 'abc'.__class__ is str Out[135]: True
名字
在通常认知里,变量是一段具有特定格式的内存,变量名则是内存别名。因为在编码阶段,无法确定内存的具体位置,故使用名称符号代替
静态编译和动态解释型语言对于变量名的处理方式也完全不同。静态编译器和链接器会以固定地址,或直接、间接寻址指令代替变量名。也就是说变量名
不参与执行过程,可被删除。但在解释型动态语言里,名字和对象通常史两个运行期实体。名字不但有自己的类型,还需分配内存,并介入执行过程。
甚至可以说,名字才是动态模型的基础。
赋值步骤:
1准备好右边值目标对象
2准好变量名
3在名字空间里为两者建立关联。
即便如此,名字与目标对象之间也仅是引用关联。名字只负责找人,但对于此人一无所知。
鉴于在运行期才能知道名字引用的目标类型,所以说Python是一种动态类型语言。
名字空间
名字空间默认使用字典(dict)数据结构,由多个键值对(key/value)组成。
内置函数globals和locals分别返回全局名字空间和本地名字空间字典
In [138]: globals() is locals() Out[138]: True In [139]:
在主模块中运行,locals()与glocals()是相等的
In [140]: def test(): ...: x = 'hello' ...: print(locals()) ...: print('local', id (locals())) ...: print('global', id(globals())) ...: In [141]: test() {'x': 'hello'} local 4547282256 global 4510296176 In [142]:
globals总是固定指向模块名字空间,而locals则指向当前作用域环境。
可以直接修改名字空间来建立关联引用。
In [142]: globals()['name'] = 'sidian' In [143]: name Out[143]: 'sidian' In [144]:
正因为名字空间的特性,赋值操作仅是名字在名字空间里重新关联,而非修改原对象。
命名习惯建议
1 类名称使用CapWords格式
2模块文件名、函数、方法成员等使用lower_case_with_underscores格式
3全局常量使用UPPER_CASE_WITH_UNDERSCORES格式
4避免与内置函数或标准库的常用类型同名,因为这样容易误导
模块与类还是由一些相同的地方的
模块成员以但下划线开头(_x),属私有成员,不会被*号导入
类型成员以双下划线揩油,但无结尾,属自动命名私有成员
以双下划线开头和结尾,通常是系统成员,应避免使用
在交互模式下,单下划线(_)返回最后一个表达式结果。
In [146]: 1+2 Out[146]: 3 In [147]: _ Out[147]: 3 In [148]:
内存
Python没有值类型、引用类型之分。事实上,每个对象都很重。即便是简单的数字,也由标准对象头,以及保存类型指针和引用计数等信息。
弱引用
如果说,名字与目标对象关联构成强引用关系,会增加引用计数,进而影响期生命周期,那么弱引用(weak reference)就是简配版,骑在保留引用前提下,不增加计数,也不阻止目标被回收
不是所有的类型支持弱引用,比如int,tuple,看有没有__weakref__属性
Python 3.7.4 (default, Jul 9 2019, 18:13:23) Type 'copyright', 'credits' or 'license' for more information IPython 7.7.0 -- An enhanced Interactive Python. Type '?' for help. PyDev console: using IPython 7.7.0 Python 3.7.4 (default, Jul 9 2019, 18:13:23) [Clang 10.0.1 (clang-1001.0.46.4)] on darwin class X: ...: def __del__(self): ...: print(id(self), 'dead') ...: a = X() import sys sys.getrefcount(a) Out[5]: 2 import weakref w = weakref.ref(a) id(w()) Out[8]: 4411485904 sys.getrefcount(a) Out[9]: 2 del a 4411485904 dead
weakref内置的一些方法
w() a = X() w = weakref.ref(a) weakref.getweakrefcount(a) Out[14]: 1 weakref.getweakrefs(a) Out[15]: [<weakref at 0x106f25bf0; to 'X' at 0x106f16f50>] hex(id(w)) Out[16]: '0x106f25bf0'
弱引用可用于一些特定场合,比较缓存,监控等。这类"外挂"场景不应该影响目标对象,不能阻止它们被回收。
弱引用的另外一个典型应用就是实现Finalizer,也就是在对象被回收时执行额外的"清理操作"(有点像回调函数)
a = X() w = weakref.ref(a, lambda x:print(x,x() is None)) del a 4377854224 dead <weakref at 0x107083770; dead> True
当删除a的时候,执行了red里面定义的函数。
书中说明了为什么不用__del__
因为析构方法作为目标成员,其用途是完成对象内部资源清理。它无法感知,也不应该处理与之无法的外部场景。
但在实际开发中,外部关联场景有很多,那么用Finalizer才是合理设计,因为这样只有一个不会侵入的观察员存在。
注意回调函数参数为弱引用而废目标对象。回调函数执行时,目标已无法访问。
使用weakref.proxy可以使弱引用对象的使用与原名字语法一致。
a = X() a.name = 'sidian' w = weakref.ref(a) w.name Traceback (most recent call last): File "/usr/local/lib/python3.7/site-packages/IPython/core/interactiveshell.py", line 3326, in run_code exec(code_obj, self.user_global_ns, self.user_ns) File "<ipython-input-32-df0c13a86467>", line 1, in <module> w.name AttributeError: 'weakref' object has no attribute 'name' w().name Out[33]: 'sidian' p = weakref.proxy(a) p Out[35]: <__main__.X at 0x104f256b0> p.name Out[36]: 'sidian' p.age=80 a.age Out[38]: 80 w().age Out[40]: 80 del a 4377856016 dead
对象复制
复制分浅拷贝(shallow copy)和深度拷贝(deep copy)两种。
对于对象内部成员,浅拷贝仅复制名字引用,而深拷贝会递归复制所有成员。
pick.dumps 与pick.loads可以实现深拷贝
循环引用垃圾回收
当两个或更多对象构成循环引用(reference cycle)时,该机制就会遭遇麻烦。因为彼此引用导致计数永不归零,从而无法触发回收操作,形成内存泄露。
为此,另有一套专门用来处理循环引用的垃圾回收器(gc)作为补充
单个对象也能构成循环引用,比如列表把自身引用作为元素存储。
In [184]: l = [1,2] In [185]: l[1] = l In [186]: l Out[186]: [1, [...]]
对垃圾回收器进行操作
class X: ...: def __del__(self): ...: print(id(self), 'dead') ...: gc.disable() a = X() b = X() a.x=b b.x=a del a del b gc.enable() 4595851024 dead 4595950928 dead gc.collect() Out[21]: 233
对于某些性能优先的算法,在确保没有循环引用的前提下,临时关闭gc可获得更好的性能。
甚至在某些极端优化策略里,会完全屏蔽垃圾回收,以重启进程来回收资源。
做性能测试(timeit)会关闭gc,避免垃圾回收对执行计时造成影响
编译
源码先编译成字节码,才能交由解释器以解释方式执行。这也时Python性能为人诟病的一个重要原因。
字节码(byte code)时中间代码,面向后端编译器或解释器。要么解释执行,要么二次编译成机器代码(native code)执行。
字节码指令通常基于栈式虚拟机(stack_based vm)实现,没有寄存器等复杂结构,实现简单。
且其具备重中立性,与硬件架构、操作系统等无关,便于将编译和平台实现分离,式跨平台语言的主流方案。
Python3使用专门的保存字节码缓存文件(__pycache__/*.pyc)
除了执行指令的字节码,还有很多数据,共同组成执行单元。
从这些元数据里,可以获得参数、闭包等诸多信息。
In [188]: def add(x, y): ...: return x + y ...: In [189]: add.__code__ Out[189]: <code object add at 0x10d1bba50, file "<ipython-input-188-5fcdd2924cd8>", line 1> In [190]: add.__code__.co_varnames Out[190]: ('x', 'y') In [191]: add.__code__.co_code Out[191]: b'|x00|x01x17x00Sx00' In [192]: In [192]: import dis In [193]: dis.dis(add) 2 0 LOAD_FAST 0 (x) 2 LOAD_FAST 1 (y) 4 BINARY_ADD 6 RETURN_VALUE In [194]:
上面简单的运行了dis进行了反汇编(disassembly)
某些时候,需要手工完成编译操作。
In [197]: source = ''' ...: print('hello, world') ...: print(1+2) ...: ''' In [198]: code = compile(source,'demo','exec') In [199]: dis.show_code(code) Name: <module> Filename: demo Argument count: 0 Kw-only arguments: 0 Number of locals: 0 Stack size: 2 Flags: NOFREE Constants: 0: 'hello, world' 1: 3 2: None Names: 0: print In [200]: dis.dis(code) 2 0 LOAD_NAME 0 (print) 2 LOAD_CONST 0 ('hello, world') 4 CALL_FUNCTION 1 6 POP_TOP 3 8 LOAD_NAME 0 (print) 10 LOAD_CONST 1 (3) 12 CALL_FUNCTION 1 14 POP_TOP 16 LOAD_CONST 2 (None) 18 RETURN_VALUE In [201]: exec(code) hello, world 3 In [202]:
除compile函数外,标准库还有编译原码文件的相关操作。
分别为py_compile,compileall,后续用到再学
执行
不管代码如何生成,最终要么以模块导入执行,要么调用eval,exec执行。这两个内置函数使用简单,eval执行单个表达式,exec执行代码块,接收字符串或已编译好的代码对象(code)作为参数。
如果是字符串,就会检查是否符合语法规则。
In [202]: eval('(1+2)+3') Out[202]: 6 In [203]: s = ''' ...: def test(): ...: print("hello,world") ...: test() ...: ''' In [204]: exec(s) hello,world In [205]:
无论选择那种方式执行,都必须由相应的上下文环境。默认直接使用当前全局和本地名字空间。
如同不同代码一样,从中读取目标对象,或写入新值。
In [205]: x= 100 In [206]: def test(): ...: y = 200 ...: print(eval('x+y')) ...: In [207]: test() 300 In [209]: def test(): ...: print('test:', id(globals()), id(locals())) ...: exec('print("exec", id(globals()),id(locals()))') ...: In [210]: test() test: 4510296176 4537806512 exec 4510296176 4537806512 In [211]:
有了操作上下文名字空间的能力,动态代码就可向外部环境注入新的成员,比如说构建新的类型,导入新的算法,最终达到将动态逻辑或其结果融入,成为当前体系组成部分的设计目标。
In [211]: s =''' ...: class My_X:... ...: def hello(): ...: print('hello, world') ...: ''' In [212]: exec(s) In [213]: My_X Out[213]: __main__.My_X In [214]: hello() hello, world In [215]:
某些时候,动态代码来源不确定,基于安全考虑,必须对执行过程进行隔离,阻止其直接读写环境。
如此,就需显式传入容器对象作为动态代码的专用名字空间,以类似建议沙箱(sandbox)方式执行。
根据需要,分别提供globals,locals参数,也可公用同一个空间字典。
为保证代码正确执行,解释器会自动导入__bultins__模块,以便调用内置函数。
In [215]: g = {'x':100} In [216]: l = {'y':200} In [217]: eval('x+y',g,l) Out[217]: 300 In [218]: In [218]: ns = {} In [219]: exec('class XXX:...',ns) In [220]: ns
同时提供两个名字空间参数时,默认总是在locals优先。
In [221]: s = ''' ...: print(x) ...: global y ...: y += 100 ...: z = x+y ...: ''' In [222]: g = {'x':10,'y':20} In [223]: l = {'x':1000} In [224]: exec(s,g,l) 1000
In [226]: l Out[226]: {'x': 1000, 'z': 1120}
前面的s定义中y定义为全局变量,所以没有输出到l里面
前面提及,在函数作用域内,locals函数总是返回执行栈帧(stack frame)名字空间。
因此就式显式提供locals名字空间,也无法将其注入倒台代码的函数内
In [227]: s = ''' ...: print(id(locals())) ...: def test(): ...: print(id(locals())) ...: test() ...: ''' In [228]: ns = {} In [229]: id(ns) Out[229]: 4536481344 In [230]: ns2 = {} In [231]: id(ns2) Out[231]: 4540089840 In [232]: exec(s,ns,ns2) 4540089840 4544788512
In [234]: ns2 Out[234]: {'test': <function test()>}
明显沙盒传入的,默认情况下,所有信息都保存在locals()里面,除非定义了global,会传入globals()
内置类型
与自定义类型(user-defined)相比,内置类型(built-in)算是特权阶层。除了它们是符合数据结构的基本构成单元以外,最重要的式被编译器和解释器特别对待。
比如核心级别的指令和性能优化,专门设计的高效缓存,等等。
内置类型主要的有int,float,str,bytes,bytearray,list,tuple,dict,set,frozenset,其中bytearray,list,dict,set为可变类型。
标准库collections.abc列出了相关类型的抽象基类,可据此判断其基本行为方式
In [10]: import collections.abc In [11]: issubclass(dict, collections.abc.Sequence) Out[11]: False In [12]: issubclass(dict, collections.abc.MutableSequence) Out[12]: False In [13]: issubclass(dict, collections.abc.Mapping) Out[13]: True In [14]:
整数
In [15]: import sys In [16]: x= 1 In [17]: sys.getsizeof(x) Out[17]: 28 In [18]: y=1<<10000 In [19]: sys.getsizeof(y) Out[19]: 1360 In [20]:
Python中int的变长结构允许我们创建超大的天文数字,理论上仅收可分配内存大小的限制。
对于长数字,可以用下划线当做分隔符,且不定位置。
In [21]: 2_3_4 Out[21]: 234 In [22]: 23_345_123 Out[22]: 23345123 In [23]:
另外进制的也可以用
In [23]: 0x23_34_12_2 Out[23]: 36913442 In [24]: 0b01_10 Out[24]: 6 In [25]:
0b,0x 0o分别代码2进制,16进制,8进制的数字
转换
In [31]: eval(bin(100)) Out[31]: 100 In [32]: bin(100) Out[32]: '0b1100100' In [33]: hex(100) Out[33]: '0x64' In [34]: oct(100) Out[34]: '0o144' In [35]: int(bin(100),2) Out[35]: 100 In [36]: int(hex(100),16) Out[36]: 100 In [37]: int(' 100 ') Out[37]: 100 In [38]: int(' 100 ') Out[38]: 100 In [39]:
通过一些命令,可以十进制数字转换为指定的进制字符字符串,也可以通过int还原,第二参数为第一输入参数的进制,输出都式10进制的。
当然也可以通过eval完成,单相比与直接用C实现的转换函数,其性能要差很多,毕竟动态运行需要额外编译和执行开销。
还有一种转换操作式将整数转换为字节数组,这常用于二进制网络协议和文件读写。在这里需要指定字节序,也就是常说的大小端。
目前使用较多的Intel x86 、AMD 64 采用小端。ARM则两种都支持,可自行设定。另外,TCP/IP网络字节,采用大端,这属于协议定义,与硬件结构与操作系统无法。
In [56]: x = 0x1234 In [57]: n = (x.bit_length() +8 -1) //8 In [58]: n Out[58]: 2 In [59]: x=0x1234 In [60]: n = (x.bit_length() + 8 -1)//8 In [61]: b = x.to_bytes(n, sys.byteorder) In [62]: b Out[62]: b'4x12' In [63]: b.hex() Out[63]: '3412' In [64]: hex(int.from_bytes(b,sys.byteorder)) Out[64]: '0x1234'
书中的代码执行完毕以后,我对b的输出其实卡住了一会而,为什么直接输出b是b'4x12',不是应该b'x34x12'吗?
在Python中对于字节码x的输出,如果x的字节码对应asci码有对应值,x就会自动转换成相应的ASCI字符.
In [79]: b'x34' Out[79]: b'4' In [80]:
在电脑数据中一个字节等于8个bit位,对应的16进制刚好为x12,其中的1为高位的4个bit,2对应为低位的4个bit
所以x12的一个数据刚好为一个字节,Python中显示器的为Unicode,对应2个字节,16位。这些计算机的基础,我薄弱啊。
所以在计算机中数据都喜欢用x的形式表示,因为计算机的最小单位为字节,用二进制表示需要8个占位00000000,但转换为16进制,刚好一个x表示。
In [94]: name = '中' In [95]: name Out[95]: '中' In [96]: name.encode('gbk') Out[96]: b'xd6xd0' In [97]: name.encode('utf8') Out[97]: b'xe4xb8xad' In [98]: ord(name) Out[98]: 20013 In [99]: hex(ord(name)) Out[99]: '0x4e2d' In [100]: 'u4e2d' Out[100]: '中' In [101]: chr(20013) Out[101]: '中' In [102]: chr(0x4e2d) Out[102]: '中' In [103]:
ord() 函数是 chr() 函数(对于8位的ASCII字符串)或 unichr() 函数(对于Unicode对象)的配对函数,它以一个字符(长度为1的字符串)作为参数,返回对应的 ASCII 数值,或者 Unicode 数值。
上面的列子中,很好的展示了解码与编码,Python编辑器,对于unicode字符是直接显示的。从字符解码也可以看出来,对于中文的解码,utf8需要三个字节,gbk两个字节,unicode也才两个字节。
In [103]: 0x123 Out[103]: 291 In [104]: 0b0101001 Out[104]: 41 In [105]: 0o5432 Out[105]: 2842 In [106]:
在终端中,无论你输入那种类型的数字,终端会自动转换成10进制的输出,这次学习让我对字符编码又有了更好的认识。
运算符
比较有意思的是一个
In [111]: divmod(5,2) Out[111]: (2, 1)
返回了一个元祖,一个是商,一个属余数。
布尔
布尔是整数的子类型,也就是说True和False可被当做数字直接使用
In [112]: True.__class__ Out[112]: bool In [113]: True.__class__.__mro__ Out[113]: (bool, int, object) In [114]:
In [114]: True == 1 Out[114]: True In [115]: False == 0 Out[115]: True In [117]: False + 1 Out[117]: 1 In [118]:
在进行布尔转换时,数字值、空值(None)、空序列,空字典、空集合,等都被视为False
对于自定义类型,可通过重写__boll__或__len__方法影响bool转换结果。
枚举
首相枚举操作的对象是一个类,是类,不时实例对象。Enum是一个类,继承元类EnumMeta
操作的对象,属于类属性。Enum()调用的是EnumMeta的__call__方法,整个模块有1000行代码,是在没信心看了。用的又是类元编程。
In [187]: Color = Enum('Color','BLACK, YELLOW BLUE RED') In [188]: black = Color.BLACK In [189]: isinstance(black, Color) Out[189]: True In [190]: black.name Out[190]: 'BLACK' In [191]: black.value Out[191]: 1 In [192]:
很有意思,Color的类属性,是Color的实例,实例有两个描述符,name与value
通过继承的方式草写一遍
In [192]: class X(Enum): ...: A = 'a' ...: B = 100 ...: C = [1,2,3] ...: In [193]: X.C Out[193]: <X.C: [1, 2, 3]> In [194]: X['B'] Out[194]: <X.B: 100> In [196]: X('a') Out[196]: <X.A: 'a'> In [197]:
可以通过属性查找,也可以通过值key的形式查找,也可以通过实例化放入value的形式查找具体对象。
如果要避免相同的枚举定义,可用enum.unique装饰器。
这个枚举的类,我真心觉的没啥用,反正我基本没用到过,真要用,我宁可自己定义个更加好用的类。
内存
对于常用的小叔子,解释器会在初始化时进行预缓存。后续使用时,直接将名字关联到这些缓存既可。如此一来,无须创建实例对象,可提高性能,节约内存开销
Python3.6 预缓存范围是[-5, 256]
In [197]: a = -5 In [198]: b= -5 In [199]: a is b Out[199]: True In [200]: a= 256 In [201]: b= 256 In [202]: a is b Out[202]: True In [203]: a = 256 In [204]: a = 257 In [205]: b = 257 In [206]: a is b Out[206]: False In [207]:
Python2对回收后的整数复用不做收缩处理,会导致大量闲置内存驻留。而Python3则改进了不少。
from __future__ import print_function import psutil def rss(): m = psutil.Process().memory_info() print(m.rss >> 20, 'MB') if __name__ == '__main__': rss() x = list(range(10000000)) rss() del x rss()
运行结果
shijianzhongdeMacBook-Pro:第二章类型 shijianzhong$ python t2_2.py 7 MB 394 MB 394 MB shijianzhongdeMacBook-Pro:第二章类型 shijianzhong$ python3 t2_2.py 8 MB 394 MB 84 MB
浮点数
默认float类型存储双精度(double)浮点数,可表达16到17个小数位
从实现方式看,浮点数以二进制存储十进制数的近似值。这可能导致执行结果和编码预期不符,造成不一致缺陷,所以对精度有严格要求的地方,应选择固定精度类型。
可以通过float.hex方式输出实际存储值的十六进制格式字符串,以检查执行的结果为何不同,还可以用该方式实现浮点数的精确传递,避免精度丢失。
In [9]: 0.1 * 3 == 0.3 Out[9]: False In [10]: (0.1 * 3).hex() Out[10]: '0x1.3333333333334p-2' In [11]: 0.3.hex() Out[11]: '0x1.3333333333333p-2' In [12]: In [12]: s = (1/3).hex() In [13]: s Out[13]: '0x1.5555555555555p-2' In [14]: float.fromhex(s) Out[14]: 0.3333333333333333 In [15]:
对于简单操作可以使用round进行精度控制
将数字或者字符串转换为浮点数,只要float函数一下就可以,能正确处理正负符号与空白符
通过math模块内的一部分函数处理小数
In [21]: from math import trunc, floor, ceil In [22]: trunc(2.6),trunc(-2.6) Out[22]: (2, -2) In [23]: floor(2.6),floor(-2.6) Out[23]: (2, -3) In [24]: ceil(2.6), ceil(-2.6) Out[24]: (3, -2) In [25]:
十进制浮点数
decaimail.Decimail是十进制实现,最高可提供28位有效精度。其能准确表达十进制数合运算,不存在二进制近似值问题。
In [25]: 1.1+2.2 Out[25]: 3.3000000000000003 In [26]: from decimal import Decimal In [27]: Decimal('1.1') + Decimal(2.2) Out[27]: Decimal('3.300000000000000177635683940') In [28]: Decimal('1.1') + Decimal('2.2') Out[28]: Decimal('3.3') In [29]:
在创建Decimail实例的时候,应该传入一个准确的值,比较整数或者字符串。如果是float类型的,那么在构建之前,其精度就已丢失。
通过设置上下文环境修改Decimail默认的28位精度
In [29]: from decimal import getcontext In [30]: getcontext() Out[30]: Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[Inexact, FloatOperation, Rounded], traps=[InvalidOperation, DivisionByZero, Overflow]) In [31]: getcontext().prec=2 In [32]: Decimal(1.99)/Decimal(9) Out[32]: Decimal('0.22') In [33]:
或者用localcontext限制指定区域的精度
In [33]: from decimal import localcontext In [34]: with localcontext() as ctx: ...: ctx.prec=2 ...: print(getcontext()) ...: print(Decimal(1)/Decimal(3)) ...: Context(prec=2, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[Inexact, FloatOperation, Rounded], traps=[InvalidOperation, DivisionByZero, Overflow]) 0.33
除非有明确需求,否则不要用Decimail替代float,要知道其运行速度会慢许多
四舍五入
Python中的round存咋很大的不确定性,特别是末尾是五
Python是按临近数字距离远近来考虑是否位,就最后一个进位数跟1对比,是否靠近1,所有除了5的尾数,另外都不存在问题
末尾是5的小数,返回整数,就取偶数位的整数
In [67]: round(0.5) Out[67]: 0 In [68]: round(1.5) Out[68]: 2 In [69]: round(2.5) Out[69]: 2 In [70]: round(4.5) Out[70]: 4 In [71]:
但如果返回的还是小数,就比较莫名其妙了
In [71]: round(1.25,1) Out[71]: 1.2 In [72]: round(1.245,1) Out[72]: 1.2 In [73]: round(1.275,2) Out[73]: 1.27 In [74]: round(1.375,2) Out[74]: 1.38 In [75]:
可以用Decimail来控制进位方案
def roundx(x, n): return Decimal(x).quantize(Decimal(n), ROUND_HALF_UP) print(roundx('1.245', '.01'))
字符串
Unicode是为整合全世界的所有语言文字而诞生的。任何文字在Unicode中都对应一个值,这个值称为代码点(code point)。代码点的值通常写成 U+ABCD 的格式。而文字和代码点之间的对应关系就是UCS-2(Universal Character Set coded in 2 octets)。顾名思义,UCS-2是用两个字节来表示代码点,其取值范围为 U+0000~U+FFFF。
为了能表示更多的文字,人们又提出了UCS-4,即用四个字节表示代码点。它的范围为 U+00000000~U+7FFFFFFF,其中 U+00000000~U+0000FFFF和UCS-2是一样的。
要注意,UCS-2和UCS-4只规定了代码点和文字之间的对应关系,并没有规定代码点在计算机中如何存储。规定存储方式的称为UTF(Unicode Transformation Format),其中应用较多的就是UTF-16和UTF-8了。
In [108]: ascii('你好') Out[108]: "'\u4f60\u597d'" In [109]: eval( "'\u4f60\u597d'") Out[109]: '你好'
In [105]: hex(ord('汉')) Out[105]: '0x6c49' In [107]: chr(0x6c49) Out[107]: '汉'
Unicode格式的大小写分别表示16位(u)和32位(U)整数,不能混用
In [112]: 'hx69,u6C49U00005B57' Out[112]: 'hi,汉字' In [113]:
字符串支持+与*运算,编译器在编译器直接计算出字面量拼接结果,可避免运行时开销
至于多个动态字符串串拼接,应优选join或format方式
相比与多次加法运算和多次内存分配(字符串是不可变对象),join这类函数(方法)可预先计算出总长度,一次性分配内存,随后直接复制内存数据参数。
另一方案,讲固定模板内容与变量分离的format更容易阅读.
书中有一个测试,该模块等后期用到,再回来使用。
字符串切片无论返回与原字符串不同的子串时,都可能会重新分配内存,并估值数据。
这个也是蛮有意思的一段示例。
In [121]: s = '-'*1024 In [122]: s1 = s[10:100] In [123]: s2= s[:] In [124]: s3 = s.split()[0] In [125]: s1 is s Out[125]: False In [126]: s2 is s Out[126]: True In [127]: s3 is s Out[127]: True In [128]:
格式化
Python的format格式化
标准格式说明符 的一般形式如下:
format_spec ::= [[fill
]align
][sign
][#][0][width
][grouping_option
][.precision
][type
] fill ::= <any character> align ::= "<" | ">" | "=" | "^" sign ::= "+" | "-" | " " width ::=digit
+ grouping_option ::= "_" | "," precision ::=digit
+ type ::= "b" | "c" | "d" | "e" | "E" | "f" | "F" | "g" | "G" | "n" | "o" | "
中文注释下吧,第一个参数填充字符对齐,sign数字符号#格式前缀 填充,width宽度千分位,小数长度,类型。
后续的Python,format用起来,%号格式化已经不行了。
池化
Python的做法是实现一个字符串池化
池负责管理实例,使用者只需引用既可。另一潜在的好处是,从池返回的字符串,只需比较指针就可知道内容是否相同,无须额外计算。
可以用池来提升哈希表等类似结构的查找性能。
除以常量方式出现的名字和字面量外,动态生成的字符串一样可假如池中。如此可保证每次都引用同一个对象。
普通的字符串,中间不加空格也会放入池中
In [136]: a = '__name__' In [137]: b = '__name__' In [138]: a is b Out[138]: True In [139]: a = '123' In [140]: b = '123' In [141]: a is b Out[141]: True In [142]:
默认的中文字符不会假如池中
In [142]: a = '你好' In [143]: b = '你好' In [144]: a is b Out[144]: False In [145]: a = 'hello, world' In [146]: b = 'hello, world' In [147]: a is b Out[147]: False In [148]: import sys In [152]: a = sys.intern('hello, world') In [153]: b = sys.intern('hello, world') In [154]: a is b Out[154]: True In [155]:
a = sys.intern('hello, world!') id(a) 4560821168 id(sys.intern('hello, world!')) 4560821168 del a id(sys.intern('hello, world!')) 4563113008
一旦失去所有外部引用,池内的字符串对象一样会被回收
字节数组
从底层实现来说,所有的数据都是二进制的字节序列。
In [157]: bytes('abc','utf8') Out[157]: b'abc' In [158]: bytes('中国', 'gbk') Out[158]: b'xd6xd0xb9xfa' In [159]:
bytes支持很多字符串的方法。
相比较于bytes的一次性内存分配,bytearray可按需扩张,更适合作为可读写缓冲区使用。如有必要,还可为其提前分配足够的内存,避免中途扩展造成额外消耗。
In [169]: b = bytearray(b'ab') In [170]: len(b) Out[170]: 2 In [171]: b.append(ord('c')) In [172]: b Out[172]: bytearray(b'abc') In [173]: b.append(b'c') --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-173-316c1313f7e0> in <module> ----> 1 b.append(b'c') TypeError: 'bytes' object cannot be interpreted as an integer In [174]: b.extend(b'lll') In [175]: b Out[175]: bytearray(b'abclll') In [176]:
从可变字节可以看出,需要添加字节还是蛮有意思的,append的时候,只能用过ord添加,字节的编码还是十进制的,通过extend扩展可变字节编码
同样还支持+*运算符。
内存视图
为什么要引用使用内存视图,引用某个片段,而不是整个对象?
以自定义网络协议位例,通常由标准头和数据体两部分组成。如要验证数据是否被修改,总不能将整个包作为参数交给验证函数。这势必要求该函数了解协议包结构,这显然不合理的设计。
而复制数据体又可能导致重大性能开销,同样得不偿失。
内存视图要求目标对象支持缓冲协议(Buffer Protocol)。它直接引用目标内存,没有额外复制行为。因此,可读取最新数据。在目标对象允许的情况下,还可执行写操作。
常见支持视图操作的又bytes,bytearray,array.array,以及NumPy的某些类型
In [194]: a = list('亲爱的'.encode()) In [195]: b = bytearray(a) In [196]: b Out[196]: bytearray(b'xe4xbaxb2xe7x88xb1xe7x9ax84') In [197]: v= memoryview(b) In [198]: x = v[2:5] In [199]: v Out[199]: <memory at 0x10c91a050> In [200]: x Out[200]: <memory at 0x10c91a120> In [201]: x.hex() Out[201]: 'b2e788' In [202]: b[3]=0x99 In [203]: x.hex() Out[203]: 'b29988' In [204]: x[1] = 0x88 In [205]: b Out[205]: bytearray(b'xe4xbaxb2x88x88xb1xe7x9ax84') In [207]: b[4]=0xaa In [208]: b Out[208]: bytearray(b'xe4xbaxb2x88xaaxb1xe7x9ax84') In [209]: x.hex() Out[209]: 'b288aa' In [210]:
如果要复制视图数据,可调用tobytes,tolist方法。复制后的数据与原对象无关,同样不会影响视图自身
In [213]: b Out[213]: bytearray(b'xe4xbaxb2x88xaaxb1xe7x9ax84') In [214]: v = memoryview(b) In [215]: x= v[2:5] In [216]: z = x.tobytes() In [217]: z.hex() Out[217]: 'b288aa' In [218]: z1=0xab In [219]: z.hex() Out[219]: 'b288aa' In [221]: z Out[221]: b'xb2x88xaa' In [222]: x Out[222]: <memory at 0x10c91a1f0> In [223]: x.tolist() Out[223]: [178, 136, 170] In [224]:
给自己脑子一点记心,对话模式下,输入0b,0x,0o都会默认转换为10进制输出。
列表
列表的内部结构由两部分组成,保存元素数量和内存分配计数的头部,以及存储元素指针的独立数组。
所有元素是有那个该数组保存指针引用,并不嵌入实际内容。
构建
要实现自定义的列表,建议基于collection.UserList包装
In [234]: list.__mro__ Out[234]: (list, object) In [235]: collections.UserList.__mro__ Out[235]: (collections.UserList, collections.abc.MutableSequence, collections.abc.Sequence, collections.abc.Reversible, collections.abc.Collection, collections.abc.Sized, collections.abc.Iterable, collections.abc.Container, object) In [236]:
In [239]: class A(list): ...: ... ...: In [240]: type(A('abc')+list('abc')) Out[240]: list In [241]: class B(collections.UserList): ...: ... ...: In [242]: type(B('abc')+list('abc')) Out[242]: __main__.B
最小设计接口是个基本原则,应慎重考虑列表这种功能丰富的类型是否适合作为基类。
列表的切片擦欧哦组,创建新列表对象,并复制相关指针数据到新数组。除所引用目标相同外,对列表自身的修改(插入、删除等)互不影响
In [244]: a = [0,2,4,6] In [245]: b = a[:2] In [246]: a[0] is b[0] Out[246]: True In [247]: a Out[247]: [0, 2, 4, 6] In [248]: b Out[248]: [0, 2] In [250]: b.insert(0,99) In [251]: a Out[251]: [0, 2, 4, 6] In [252]:
要注意,前面赋值属于浅拷贝。
In [260]: a = [[1,2],[3,4]] In [261]: b = a[::-1] In [262]: b Out[262]: [[3, 4], [1, 2]] In [263]: a is b Out[263]: False In [264]: a[0].append(99) In [265]: b Out[265]: [[3, 4], [1, 2, 99]] In [266]: a.insert(0,0) In [267]: a Out[267]: [0, [1, 2, 99], [3, 4]] In [268]: b Out[268]: [[3, 4], [1, 2, 99]] In [269]:
reveserd记住,跟[::-1]一样,返回的列表属于原数据的浅拷贝数据
利用bisect模块,可向有序列表插入元素。它使用二分查找适合位置,可用来实现优先级队列或一致性哈希算法
In [281]: d = [0,2,4] In [282]: from bisect import bisect In [283]: import bisect In [284]: bisect.insort_left(d,1) In [285]: d Out[285]: [0, 1, 2, 4] In [286]: bisect.insort_left(d,2) In [287]: d Out[287]: [0, 1, 2, 2, 4] In [288]: bisect.insort_left(d,3) In [289]: d Out[289]: [0, 1, 2, 2, 3, 4] In [290]:
元祖
没啥好些的,上一个概念
元祖是不可变类型,它的指针数组无须变动,故一次性完成内存分配。系统会缓存复用一定长度的元祖内存(含指针数组)。
创建时,按所需长度提取付用法,没有额外内存分配。从这点上来看,元祖的性能要好于列表
py3.6缓存复用长度在20以内的tuple内存,每种2000上限。
数组
数组和列表、元祖的本质区别在于:元素单一类型和内容嵌入
In [290]: import array In [291]: a = array.array('b',range(5)) In [292]: a Out[292]: array('b', [0, 1, 2, 3, 4]) In [293]: memoryview(a).hex() Out[293]: '0001020304' In [294]:
上面显示了内容嵌入
In [294]: a = array.array('i') In [295]: a.append(100) In [296]: a Out[296]: array('i', [100]) In [297]: a.append(1.23) --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-297-8779bdd8af29> in <module> ----> 1 a.append(1.23) TypeError: integer argument expected, got float In [298]:
可直接存储包括Unicode字符在内的各种数字
符合类型须用struct、marshal、pickle等转换为二进制字节后再行存储
与列表类似,数组长度不固定,按需扩张或收缩内存。
In [298]: a = array.array('i',range(1,4)) In [299]: a Out[299]: array('i', [1, 2, 3]) In [300]: a.buffer_info() Out[300]: (4512054016, 3) In [301]: a.extend(range(10000000)) In [302]: a.buffer_info() Out[302]: (4526223360, 10000003) In [303]:
由于可指定更紧凑的数字类型,故数组可节约更多内存。再者,内容嵌入也避免了对象的额外开销,减少了活跃的数量和内存分配的次数。
字典
字典是内置类型中唯一的映射(mapping)结构,基于哈希表存储键值对数据。
这里要注意,有些对象可能又__hash__方法,但无法执行
In [305]: callable([].__hash__) Out[305]: False In [306]:
自定义数据默认实现了__hash__和__eq__方法,用于哈希和相等比较操作。前者为每个实例返回随机值,后者除非与自己比较,否则总是返回False。这里可根据需要重载
Python3.6借鉴PyPy字典设计,采用更紧凑的存储结构。keys.entries和values用数组添加顺序存储主键和值引用。实际哈希表由keys.indices数组承担,通过计算主键哈希值找到合适的位置,然后在此存储主键在keys.entries的实际索引。如此一来,只要通过indices获取实际索引后,就可读取主键和值信息了。
构建
书中比较骚的构建方式吧
In [306]: dict(zip('abc',range(3))) Out[306]: {'a': 0, 'b': 1, 'c': 2} In [307]: dict(map(lambda k,v:(k,v + 10),'abc',range(3))) Out[307]: {'a': 10, 'b': 11, 'c': 12} In [308]: {k:v+10 for k,v in zip('abc', range(5))} Out[308]: {'a': 10, 'b': 11, 'c': 12} In [309]:
还有一些扩展以及初始化零值
In [309]: a = {'a':1} In [310]: b = dict(a,b='3') In [311]: b Out[311]: {'a': 1, 'b': '3'} In [312]: c = dict.fromkeys(range(3),0) In [313]: c Out[313]: {0: 0, 1: 0, 2: 0} In [314]:
操作还是比较常规的,没啥好些的
视图
Python3默认以视图关联字典内容,如此一来即能避免复制开销,还能同步观察字典变化。
In [314]: x = dict(a=1,b=2) In [315]: ks=x.keys() In [316]: 'b' in ks Out[316]: True In [317]: for k in ks: print(k,x[k]) a 1 b 2 In [318]: In [318]: x['b']=200 In [319]: x['c'] = 3 In [320]: for k in ks: print(k,x[k]) a 1 b 200 c 3 In [321]:
字典没有独立的只读版本,无论传递引用还是复制品,都存在弊端。直接引用有被接收方修改内容的风险,而复制品又仅是一次性快照,无法获知字典变化。
视图则不同,它能同步字典内容,却无法修改。且可选择不同粒度的内容进行传递,如果可讲接收方限定位指定模式下的观察员。
视图还支持集合运算,以弥补字典功能上的不足。
In [321]: a = dict(a = 1, b= 2) In [322]: b = dict(c = 3, b =2) In [323]: ka = a.keys() In [324]: kb = b.keys() In [325]: ka Out[325]: dict_keys(['a', 'b']) In [326]: kb Out[326]: dict_keys(['c', 'b']) In [327]: ka&kb Out[327]: {'b'} In [328]: ka|kb Out[328]: {'a', 'b', 'c'} In [329]: ka-kb Out[329]: {'a'} In [330]: ka^kb Out[330]: {'a', 'c'} In [331]:
利用视图集合愿算,可简化某些操作。列如,只更新,不新增
In [338]: a = dict(a=1,b=2) In [339]: b= dict(b=20,c=3) In [340]: ks = a.keys()&b.keys() In [341]: a.update({k:b[k] for k in ks}) In [342]: a Out[342]: {'a': 1, 'b': 20} In [343]:
扩展
defaultdict默认字典类似于setdefault包装。当主键不存在时,调用构造参数提供的工厂函数返回默认值。
In [343]: from collections import defaultdict In [344]: d = defaultdict(lambda:100) In [345]: d.get(100) In [346]: d Out[346]: defaultdict(<function __main__.<lambda>()>, {}) In [347]: d.get('a') In [348]: d Out[348]: defaultdict(<function __main__.<lambda>()>, {}) In [349]: d['a'] Out[349]: 100 In [350]: d['b'] +=1 In [351]: d Out[351]: defaultdict(<function __main__.<lambda>()>, {'a': 100, 'b': 101}) In [352]:
有序字典OrderedDict,Counter起作用的时__missing__,包括defaultdict
链式字典(ChainMap)以单一接口访问多个字典内容,其自身并不存储数据。读操作按参数顺序依次查找各字典,但修改操作(新增,更新,删除)仅针对第一字典
In [352]: a = dict(a=1,b=2) In [353]: b = dict(b=20,c=30) In [354]: x= collections.ChainMap(a,b) In [355]: x Out[355]: ChainMap({'a': 1, 'b': 2}, {'b': 20, 'c': 30}) In [356]: x['a'] Out[356]: 1 In [357]: x['c'] Out[357]: 30 In [358]: x['b'] Out[358]: 2 In [359]: for k,v in x.items():print(k,v) b 2 c 30 a 1 In [360]: x['b']=99 In [361]: x['z'] = 10 In [362]: x Out[362]: ChainMap({'a': 1, 'b': 99, 'z': 10}, {'b': 20, 'c': 30}) In [363]:
可利用链式字典设计多层次上下文(context)结构。
合理上下文类型,须具备两个基本特性。首先是继承,所有设置可被调用链的后续函数读取。其次是修改仅针对当前和后续逻辑,不应向无关的父级传递。如此,链式字典查找次序本身就是继承体现。
而修改操作被限制在当前第一字典中,自然也不会影响父级字典的同名主键设置。
In [363]: root = collections.ChainMap({'a':1}) In [365]: child= root.new_child({'b':200}) In [366]: child['a'] = 100 In [367]: root Out[367]: ChainMap({'a': 1}) In [368]: child.parents Out[368]: ChainMap({'a': 1}) In [369]: root['c'] = 5 In [370]: child.parents Out[370]: ChainMap({'a': 1, 'c': 5}) In [371]:
还是非常有意思的,多链可以两个相同key的错觉,或者child是后续的上下文,不影响前面的上下文
集合
集合存储非重复对象。
如果不是同一个对象,先比较哈希值,然后在比较内容。
实现都是用数组实现的哈希表存储元素对象引用,这也就要求元素必须位可哈希类型。
创建
比较简单不介绍
操作
支持大小,相等运算符号
In [372]: {1,2} >{2,1} Out[372]: False In [373]: {1,2} >{0,1} Out[373]: False In [375]: {1,2,3} == {3,1,2} Out[375]: True In [376]:
子集,超集的判断可以用<= 或者>=
In [376]: {1,2,3}<={1,2,3,4} Out[376]: True
交集,并集,差集 对称差集 & | - ^
集合愿算还可与更新操作一期使用
|=, &=这种
删除操作
remove,如果没有该元素会报错
用discard不会报错,pop也可以随机弹出对象
In [381]: {i for i in range(10)} Out[381]: {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} In [382]: a = {i for i in range(10)} In [383]: a.remove(11) --------------------------------------------------------------------------- KeyError Traceback (most recent call last) <ipython-input-383-2f6a2b387944> in <module> ----> 1 a.remove(11) KeyError: 11 In [384]: a.discard(11) In [385]: a.discard(18) In [386]: a.discard(8) In [387]: a Out[387]: {0, 1, 2, 3, 4, 5, 6, 7, 9} In [388]: a.pop() Out[388]: 0 In [389]:
自定义对象类型
由于默认继承object的__hash__每个对象的返回值不一样,所以要踢出类的不同实例,可以子集定义__hash__与__eq__
In [392]: u1 = User('1','user1') In [393]: u2 = User('1', 'user2') In [394]: u1 is u2 Out[394]: False In [395]: s = set() In [396]: s.add(u1) In [397]: s.add(u2) In [398]: s Out[398]: {<__main__.User at 0x1105f5410>} In [399]: id(u1) Out[399]: 4569650192 In [400]: id(u2) Out[400]: 4568741456 In [401]: u1 in s Out[401]: True In [402]: u2 in s Out[402]: True In [403]:
这里我有点把id也就是对象的内存地址,与__hash__差有点搞混,id要一样的话,要写单例模式了。
__hasn__可以在去重上面,当然id一样的化,__hash__跟__eq__肯定一样了。
当然__hash__跟__eq__主要用在去重上面,从上面可以看到由于两个对象的__hash__值相等,就可以直接在set()集合里面去重,只要重写好__hash__与__eq__的条件。
草草的学下来,Python内置的几大标准元素内部套路还是很多的,让我学到不到技巧。