第2章 序列构成的数组
[toc]
第二章开始介绍了列表这种数据结构,这个在python是经常用到的结构
列表推导
列表的推导,将一个字符串编程一个列表,有下面的2种方法。
其中第二种方法更简洁。可读性也比第一种要好。
str = 'abc'
string = []
# 第一种方法
for s in str:
print(string.append(s))
# 第二种方法
ret = [s for s in str]
print(ret)
用这种for…in的方法来推导列表,有个好处就是不会有变量泄露也就是越界的问题
列表的推导还有一种方式,称为生成器表达式。
表达式都差不多,不过是方括号编程了圆括号而已
生成器的好处是什么呢?
列表推导是首先生成一个组合的列表,这会占用到内存。
而生成式则是在每一个for循环运行时才生成一个组合,这样就不会预先占用内存
生成器表达式不会一次将整个列表加载到内存之中,而是生成一个生成器对象(Generator objector),所以一次只加载一个列表元素
举个例子来说明:
有20000个数组的列表。分别用列表推导法和生成式表达法来进行遍历。
并用memory_profiler
来监控代码占用的内存
列表推导法例子:
# 如果系统没有这个模块就执行pip install memory_profiler进行安装
from memory_profiler import profile
@profile
def fun_try():
test = []
for i in range(20000):
test.append(i)
for num in [t for t in test]:
print(num)
fun_try()
这个代码运行结果是:
Line # Mem usage Increment Line Contents
================================================
5 39.9 MiB 39.9 MiB @profile
6 def fun_try():
7 39.9 MiB 0.0 MiB test = []
8
9 40.8 MiB 0.0 MiB for i in range(20000):
10 40.8 MiB 0.1 MiB test.append(i)
11
12 41.2 MiB 0.2 MiB for num in [t for t in test]:
13 41.2 MiB 0.0 MiB print(num)
生成式例子:
# 如果系统没有这个模块就执行pip install memory_profiler进行安装
from memory_profiler import profile
@profile
def fun_try():
test = []
for i in range(20000):
test.append(i)
for num in (t for t in test):
print(num)
fun_try()
这个代码运行结果是:
Line # Mem usage Increment Line Contents
================================================
5 40.1 MiB 40.1 MiB @profile
6 def fun_try():
7 40.1 MiB 0.0 MiB test = []
8
9 41.1 MiB 0.0 MiB for i in range(20000):
10 41.1 MiB 0.1 MiB test.append(i)
11
12 41.1 MiB 0.0 MiB for num in (t for t in test):
13 41.1 MiB 0.0 MiB print(num)
结论:
`通过这两个结果可以看到列表推导法增加了0.2MB的内存
除非特殊的原因,应该经常在代码中使用生成器表达式。
但除非是面对非常大的列表,否则是不会看出明显区别的。
`
下面介绍下元组。说到元组,第一个反应就应该是不可变列表。
但作者同时还介绍了元组的很多其他特性。
首先来看下元组的拆包。
#元组拆包
t = (20, 8)
a, b = t
print(a, b)
如果在进行拆包的同时,并不是对所有的元组数据都感兴趣。
_占位符就能帮助处理这种情况
import os
_, filename = os.path.split("/home/jian/prj/demo.txt")
print(_, filename) #/home/jian/prj demo.txt
上面元组拆包的时候是对可迭代对象进行遍历,然后一一赋值到变量。
但是如果想通过给每个元组元素的命名来访问,则需用到命名元组namdtuple
# collections.namedtuple构建一个带有字段名的元组和一个有名字的类
from collections import namedtuple, OrderedDict
City = namedtuple('City', 'name country population')
tokyo = City('Tokyo', 'JP', '123')
tokyo_data = ('Tokyo', 'JP', '123')
print(tokyo) # City(name='Tokyo', country='JP', population='123')
print(tokyo.name, tokyo.country, tokyo.population) # Tokyo JP 123
print(City._fields) # ('name', 'country', 'population')
tokyo = City._make(tokyo_data)
print(tokyo) # City(name='Tokyo', country='JP', population='123')
OrderedDict([('name', 'Tokyo'), ('country', 'JP'), ('population', '123')])
print(tokyo._asdict())
# OrderedDict([('name', 'Tokyo'), ('country', 'JP'), ('population', '123')])
*符号用来处理剩下的元素
a, b, *rest = range(5)
print(a, b, rest) # 0 1 [2, 3, 4]
a, *b, rest = range(5)
print(a, b, rest) # 0 [1,2,3] 4
- 序列的操作:
增量赋值:
增量运算符+,*等其实是调用__iadd__/__add__/__mul__
方法。
对于可变对象来说,+调用的是__iadd__
,对于不可变对象来说对象调用的是__add__
。
两者有什么区别呢。
先看下面的例子
str = [1, 2, 3]
str1 = (1, 2, 3)
print("str:%d" % id(str))
print("str1:%d" % id(str1))
str += str
str1 += str1
print("str:%d" % id(str))
print("str1:%d" % id(str1))
得到的结果如下:
str:2256630301640
str1:2256630239736
str:2256630301640
str1:2256630606152
str是列表,str1是元组,列表是可变对象,元组是不可变对象。
在进行加法运算后,str的id没有改变,因此还是之前的对象,但是str1的id却发生了改变,不是之前的对象了。
这种的差别在于__iadd__
的方法类似调用a.extend(b)的方式,是在原有的对象上进行扩展操作。
但是__add__
的方式类似于a=a+b。
首先a+b得到一个新的的对象,然后复制给a,因此变量和之前的对象没有任何联系。
而是被关联到一个新的对象。同样的乘法__imul__/__mul__
也是类似的道理
列表组成的列表:
board = [['_'] * 3 for i in range(3)]
print(board)
board[1][2] = 'x'
print(board)
运行结果:
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
[['_', '_', '_'], ['_', '_', 'x'], ['_', '_', '_']]
输出三个列表的ID
board = [['_'] * 3 for i in range(3)]
print(board)
board[1][2] = 'x'
print(board)
# 输出3个列表的ID
print(id(board[0]))
print(id(board[1]))
print(id(board[2]))
结果是分别属于不同的ID:
3221578302664
3221578302536
3221578302600
我们再来看下另外一种用法。下面的代码对一个包含3个列表的列表进行*3的操作。
board = [['_'] * 3] * 3
print(board)
board[1][2] = 'x'
print(board)
print(id(board[0]))
print(id(board[1]))
print(id(board[2]))
运行结果是:
发现id都一样。说明了全部指向的是同一个对象。
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
[['_', '_', 'x'], ['_', '_', 'x'], ['_', '_', 'x']]
1195278432328
1195278432328
1195278432328
如果对不可变对象中的可变对象进行赋值会产生什么后果,比如下面的这段代码
t = (1, 2, [30, 40])
t[2] += [50, 60]
print(t)
运行结果直接报错:
TypeError: 'tuple' object does not support item assignment
我们可以把代码放在python在线调式网站进行调试
把代码稍微修改下然后再看看情况
t = (1, 2, [30, 40])
t[2].append([50, 60])
print(t)
为什么这两种实现会带来不同的结果呢?
原因在于t是一个元组属于不可变对象。但用t[2]+=[50,60]的时候是对一个元组进行赋值。
所以报错误。
但是同时t[2]又属于一个列表可变对象。因此数据也更新成功了
但是如果用t[2].append([50,60])的操作则是对一个列表进行操作,而并没有对一个元组进行赋值。因此能够更新成功且不会报错误。
这是一个很有趣的例子。
对于理解可变对象和不可变对象的操作很有帮助。
查看背后的字节码情况:
import dis
a = (1, 2, 3)
byte_info = dis.dis('a[2]+=1')
print(byte_info)
打印出反编译的结果(反编译后的代码与汇编语言接近)是:
1 0 LOAD_NAME 0 (a)
2 LOAD_CONST 0 (2)
4 DUP_TOP_TWO
6 BINARY_SUBSCR
8 LOAD_CONST 1 (1)
10 INPLACE_ADD
12 ROT_THREE
14 STORE_SUBSCR
16 LOAD_CONST 2 (None)
18 RETURN_VALUE
插播一个有趣的例子
python之交换变量值
import dis
def swap1():
x = 5
y = 6
x, y = y,x
def swap2():
x = 5
y = 6
tmp = x
x = y
y = tmp
if __name__ == "__main__":
print ("***SWAP1***")
print (dis.dis(swap1))
print ("***SWAP2***")
print (dis.dis(swap2))
查看结果:
*** SWAP1***
5 0 LOAD_CONST 1 (5)
3 STORE_FAST 0 (x)
6 6 LOAD_CONST 2 (6)
9 STORE_FAST 1 (y)
7 12 LOAD_FAST 1 (y)
15 LOAD_FAST 0 (x)
18 ROT_TWO
19 STORE_FAST 0 (x)
22 STORE_FAST 1 (y)
25 LOAD_CONST 0 (None)
28 RETURN_VALUE
***SWAP2***
10 0 LOAD_CONST 1 (5)
3 STORE_FAST 0 (x)
11 6 LOAD_CONST 2 (6)
9 STORE_FAST 1 (y)
12 12 LOAD_FAST 0 (x)
15 STORE_FAST 2 (tmp)
13 18 LOAD_FAST 1 (y)
21 STORE_FAST 0 (x)
14 24 LOAD_FAST 2 (tmp)
27 STORE_FAST 1 (y)
30 LOAD_CONST 0 (None)
33 RETURN_VALUE
得到结论
通过字节码可以看到,swap1和swap2最大的区别在于,swap1中通过ROT_TWO交换栈顶的两个元素实现x和y值的互换,swap2中引入了tmp变量,多了一次LOAD_FAST, STORE_FAST的操作。执行一个ROT_TWO指令比执行一个LOAD_FAST+STORE_FAST的指令快,
这也是为什么swap1比swap2性能更好的原因。
- 数组:
在列表和元组中,存放的是具体的对象,如整数对象,字符对象。
如下面的整数b。占据28个字节。因为存放的是整数对象,而非整数本身。
import sys
b = 1
print(sys.getsizeof(b)) # 28
对于存放大量数据来说。我们选择用数组的形式要好很多。
因为数组存储的不是对象,而是数字的机器翻译。也就是字节表述。
在array中需要规定各个字符的类型,如上表中的Type code。
定义好类型后则数组内的元素必须全是这个类型,否则会报错。
就像下面的代码。类型规定的是b也就是单字节的整数。
但是在插入的时候却是c字符,则报错:TypeError: an integer is required
import array
num = array.array('b')
num.append('c')
print(num)
在上表中,每个类型都有字节大小的限制。如果超出了字节大小的限制也是会报错的
还是b的这个类型,是有符号的单字节整数,那么范围是-128到127.
如果我们插入128.则报错:signed char is greater than maximum
提示超过了最大
import array
num = array.array('b')
num.append(128)
print(num)
对于数组这种结构体来说,由于占用的内存小,因此在读取和写入文件的时候的速度更快,相比于列表来说的话。
数组与列表效率对比
下面来对比下:
首先是用列表生成并写入txt文档的用法
import time
import struct
# struct 例子
# a = 20
# 'i'表示一个int
# # struct.pack把python值转化成字节流
# data = struct.pack('i', a)
# print(len(data))
# print(repr(data))
# print(data)
# 拆包
# a1 = struct.unpack('i', data)
# print("a1=", a1)
def arry_try_list():
floats = [float for float in range(10**7)]
fp = open('list.bin', 'wb')
start = time.clock()
for f in floats:
strdata = struct.pack('i', f)
fp.write(strdata)
fp.close()
end = time.clock()
print(end - start)
arry_try_list()
执行结果:5.8789385
再看数组的形式:
from array import array
from random import random
import time
def array_try():
floats = array('d', (random() for i in range(10**7)))
start = time.clock()
fp = open('floats.bin', 'wb')
floats.tofile(fp)
fp.close()
end = time.clock()
print(end - start)
array_try()
执行结果: 0.045192899999999994
可以看到速度明显提升了很多
再来对比读文件的速度
from array import array
import time
def array_try():
floats = array('d')
start = time.clock()
fp = open('floats.bin', 'rb')
# 比直接从文本文件里面读快,后者使用内置的float方法把每一行文字变成浮点数
floats.fromfile(fp, 10**7)
fp.close()
end = time.clock()
print(end - start)
array_try()
执行结果是:0.1172238
使用memoryview模块
精准地修改了一个数组的某个字节
#memoryview内存视图
#在不复制内容的情况下操作同一个数组的不同切片
from array import array
#5个短整型有符号整数的数组,类型码是h
numbers = array('h', [-2, -1, 0, 1, 2])
memv = memoryview(numbers)
print(len(memv))
#转成B类型,无符号字符
memv_oct = memv.cast('B')
tolist = memv_oct.tolist()
print(tolist)
memv_oct[5] = 4
print(numbers)
bisect模块用法
import bisect
# 以空格作为分隔打印S中所有元素再换行.
def print_all(S):
for x in S:
print(x, end = " ")
print("")
# 有序向量SV.
SV = [1, 3, 6, 6, 8, 9]
#查找元素.
key = int(input())
print(bisect.bisect_left(SV, key))
print(bisect.bisect_right(SV, key))
# 插入新元素.
key = 0
bisect.insort_right(SV, key)
# 删除重复元素的最后一个. 思考: 如何删除第一个?
key = 6
i = bisect.bisect_right(SV, key)
i -= 1
# 注意此时i < len(SV)必然成立. 如果确实有key这个元素则删除.
if (i > 0 and SV[i] == key): del(SV[i])
print_all(SV)
# 删除重复key所在区间: [bisect.bisect_left(SV, key):bisect.bisect_right(SV, key)).
del SV[bisect.bisect_left(SV, key):bisect.bisect_right(SV, key)]
print_all(SV)
# 无序向量USV.
USV = [9, 6, 1, 3, 8, 6]
# 插入新元素
key = 0
USV.append(key)
# 删除重复元素的最后一个.
key = 6
# 逆向遍历, 如果存在则删除.
i = len(USV) - 1
while i > 0:
if USV[i] == key:
USV[i] = USV[-1]
USV.pop()
break
i -= 1
print_all(USV)
# 删除重复元素的第一个.
try:
i = USV.index(key)
except:
pass
else:
USV[i] = USV[-1]
USV.pop()
print_all(USV)
pickle模块用法
pickle模块是将python值转成成byte
try:
import cPickle as pickle
except:
import pickle
data = [{'a': 'A', 'b': 2, 'c': 3.0}]
# 编码
data_string = pickle.dumps(data)
print("DATA:", data)
print("PICKLE:", data_string)
print(type(data_string))
# 解码
data_from_string = pickle.loads(data_string)
print(data_from_string)
双向队列
双向队列deque
import timeit
from collections import deque
def way1():
mylist = list(range(100000))
for i in range(100000):
mylist.pop()
def way2():
mydeque = deque(range(100000))
for i in range(100000):
mydeque.pop()
if __name__ == "__main__":
t1 = timeit.timeit("way1", setup="from __main__ import way1", number=10)
print(t1)
t2 = timeit.timeit("way2", setup="from __main__ import way2", number=10)
print(t2)
结果:
6e-07
1.0000000000001327e-06
deque是双向队列,如果你的业务逻辑里面需要大量的从队列的头或者尾部删除,添加,用deque的性能会大幅提高!如果只是小队列,并且对元素需要随机访问操作,那么list会快一些。
# 双向队列用法
from collections import deque
# 创建一个队列
q = deque([1])
print(q)
# 往队列中添加一个元素
q.append(2)
print(q)
# 往队列最左边添加一个元素
q.appendleft(3)
print(q)
# 同时入队多个元素
q.extend([4, 5, 6])
print(q)
# 在最左边同时入队多个元素
q.extendleft([7, 8, 9])
print(q)
# 剔除队列中最后一个
q.pop()
print(q)
# 删除队列最左边的一个元素
q.popleft()
print(q)
# 清空队列
# q.clear()
# 获取队列长度
# length = q.maxlen
# print(length)