流畅的python学习笔记-第2章

第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

对于存放大量数据来说。我们选择用数组的形式要好很多。
因为数组存储的不是对象,而是数字的机器翻译。也就是字节表述。

流畅的python学习笔记-第2章_第1张图片

在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)

你可能感兴趣的:(python,程序员)