0%

流畅的python

流畅的python

第1章 Python数据模型

如何使用特殊方法

len getitem

python中所有集合类型都可以使用len()函数查看集合长度,使用len(obj)而不是obj.len(),也不是obj. len (),如果是自定义类的对象,python会自己调用类中实现的 len 方法

魔法方法/特殊方法/双下方法,使用len()的形式调用

字符串表示形式

repr 方法得到一个对象的字符串表示形式

字符串格式化语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> "{} {}".format("hello", "world") # 不设置指定位置,按默认顺序
'hello world'
>>> "{0} {1}".format('hello', 'world') # 设置指定位置
'hello world'
>>> "{1} {0} {1}".format('hello', 'world') # 设置指定位置
'world hello world'


print("姓名:{name}, 职业:{pos}".format(name = '阿离', pos = 'adc'))
# 通过字典设置参数
info_dict = {"name": "吕布", "pos": "top"}
print("姓名:{name}, 职业:{pos}".format(**info_dict))
# 通过列表索引设置参数, '0'是必须的
info_list = ["甄姬", "mid"]
print("姓名:{0[0]}, 职业:{0[1]}".format(info_list))
"""
姓名:阿离, 职业:adc
姓名:吕布, 职业:top
姓名:甄姬, 职业:mid
"""

字符串.join(‘分隔符’,字符串列表\元组)方法

使用指定的分隔符将字符串连接起来,如果分隔符指定为’’,则直接连接

1
2
3
4
5
6
7
8
9
10
11
12
a = "123"
b = "我爱你"
c = "abc"
d = "\n".join((a,b,c)) # 或者"\n".join([a,b,c])
e = ",".join((a,b,c))
f = "\n".join((d,e))
print(f)
####################################
123
我爱你
abc
123,我爱你,abc

变量的存在域

  • python能够改变变量作用域的代码段是def、class、lamda.

第2章 序列构成的数组

list可以容纳不同类型的元素,但如果列表里的元素不能比较大小,就不能进行排序,但如果是字符和数字可以利用key参数p53

列表推导和生成器表达式

列表推导

python3中列表推导不会再有变量泄露问题,列表推导和生成器表达式都有局部作用域,表达式内部的变量和赋值只起局部作用,不影响表达式上下文中的同名变量。

1
2
3
4
x = 'ABC'
dummy = [ord(x) for x in x]
>>> x # 'ABC'
>>> dummy # [65,66,67]

笛卡儿积

python会忽略代码里[],{},()中的换行

1
2
3
4
tshirts = [(color,size) for color in colors 
for size in sizes]
# 先以颜色排列,再以尺码排列
# 输出与for循环嵌套的结果一致(嵌套顺序一致时)

生成器表达式

初始化元组、数组或者其他序列类型,使用生成器表达式

语法与列表推导相似,把方括号换成圆括号

列表推导也可以用来初始化元组、数组或其他序列类型(先构造列表,然后把列表传递到构造函数中),但使用生成器表达式更好,因为生成器表达式遵守了迭代器协议,可以逐个产出元素,而不是先建立一个完整的列表,再将列表传递到构造函数中,这样可以节省内存

1
2
3
for tshirt in ('%s %s' % (c, s) for c in colors for s in sizes):
print(tshirt)
# 生成器表达式逐个产出元素,而不是一次性产出多个元素组成的列表,可以节省掉for循环的内存开销

列表乘法

对Python的列表使用乘法,如果列表的元素是不可变对象(如数字、字符串)则是复制值,如果是可变对象(如列表、字典)则是复制引用,因此对于包含可变对象的列表切莫使用列表乘法,可使用列表生成式代替。

用一个列表list1乘一个数字n 会得到一个新的列表list2, 这个列表的元素是list1的元素重复n次, 例如

1
2
list1 = [0]
list2 = list1 * 5 # list2 = [0, 0, 0, 0, 0]

但如果列表元素为可变对象如列表,乘法的结果是复制引用,也就是多个重复元素是共用一个地址id的

1
2
3
4
5
6
7
8
9
10
11
12
list1 = [[0, 0]]
list2 = list1 * 2 # list2 = [[0, 0], [0, 0]]
list2[0][0] = 1 # list2 = [[1, 0], [1, 0]]
id(list2[0]) == id(list2[1]) # True
# 列表内的元素是字典时,也是复制引用
>>> l = [{'a': 1}] * 4
>>> print(l)
>>> [{'a': 1}, {'a': 1}, {'a': 1}, {'a': 1}]

>>> l[0]['a'] = 2
>>> print(l)
>>> [{'a': 2}, {'a': 2}, {'a': 2}, {'a': 2}]

因此这种情况下,应该使用列表生成式

1
2
3
4
5
6
7
>>> l = [{'a': 1} for _ in range(4)]
>>> print(l)
>>> [{'a': 1}, {'a': 1}, {'a': 1}, {'a': 1}]

>>> l[0]['a'] = 2
>>> print(l)
>>> [{'a': 2}, {'a': 1}, {'a': 1}, {'a': 1}]

元组

  • 不可变的列表
  • 对数据的记录

对数据的记录

元组拆包

元组拆包可以应用在任何可迭代对象上,唯一硬性要求是元素数量要对应,除非使用*来忽略多余元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 应用1
city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014)
# 应用2
traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567')]
for passport in sorted(traveler_ids):
print('%s/%s' % passport)
# 应用3
a = (1,2)
b, c = a
# 应用4 交换两个变量的值
b, a = a, b
# 应用5 用*运算符把一个可迭代对象拆开作为函数参数
t = (20, 8)
divmod(*t)

使用*处理剩余元素

1
2
a, *rest, c, d = range(5)
0, [1,2], 3 ,4

嵌套元组拆包

如(a, b, (c, d))的形式

具名元组

使用collections.namedtuple构建一个带字段名的元组和一个有名字的类,目的是方便调试

1
2
3
4
5
6
7
8
from collection import namedtuple
# 创建一个具名元组需要两个参数,1.类名:City, 2.类的各个字段的名字:(空格分割的字符串或多个字符串组成的可迭代对象)
City = namedtuple('City', 'name country population coordinates')
# 创建一个对象
tokyo = City('Tokyo', 'JP', 36, (35.689, 139.691))
print(tokyo)
# City(name='Tokyo',country='JP', population=36, coordinates=(35.689, 139.691))
# 还可以通过字段名或者位置来访问一个字段的信息,如tokyo[1],tokyo.population

作为不可变的列表

列表和元组的常见属性和方法p27

切片

使用切片对象slice增加代码可读性

  1. 如果进行切片操作的时候,超出下标不会报错

  2. 如果切片操作的时候方向相互矛盾的话,不会报错,返回为空

  3. 在python中进行反向输出列表

多维切片和省略(…)

给切片赋值

list和numpy中的np.array的区别

np.array和list均支持切片操作,且切片范围超过实际大小时不会报错

np.array中arr_m[0, 0]与arr_m[0] [0]等价,而list中只能以list_m[0] [0]方式访问,而不能list_m[0, 0]

1
2
3
4
5
6
7
8
9
10
11
import numpy as np

arr_m = np.arange(12).reshape(2, 2, 3)
l, m, n = 2, 2, 3
list_m = [[[k*m*n + j*n + i for i in range(n)] for j in range(m)] for k in range(l)]
print(list_m)
print(list_m[0][0])
# print(list_m[0, 0]) # TypeError: list indices must be integers or slices, not tuple
# Indexing
print(arr_m[0:5])
print(arr_m[0, 0], arr_m[0][0])

对序列使用+和*(拼接)

不修改原有的操作对象,而是构建一个全新的序列

1
2
3
4
print([' ']*4)
[' ', ' ', ' ', ' ']
print([' '*4])
[' ']

序列的增量赋值(+=,*=)

a+=b

+=实际上调用了特殊方法 iadd,用于就地加法,与a.extend(b)效果一样,但如果类没有实现 _ _iadd _ _则调用 _ _add _ _,此时a+=b就和a=a+b一样,a+b产生一个新的对象,然后再把a指向a+b的地址.

可变序列如list都实现了_ _iadd _ _方法,也就是会就地改动,不可变序列不支持。

*=和+=类似

1
2
3
4
5
6
7
8
9
10
11
12
# 可变序列的重复拼接
l = [1,2,3]
print(id(l)) #4311953800
l*=2
print(l) #[1,2,3,1,2,3]
print(id(l)) #4311953800
# 不可变序列的重复拼接
t = (1,2,3)
print(id(t)) #4312681568
t*=2
# 执行完增量运算,创建了新的元组,对不可变序列进行重复拼接效率很低
print(id(t)) #4301348296

列表常用操作

1
2
3
4
5
6
7
8
9
s=[]
s.append(item) # 末尾添加元素
s.clear() # 删除所有元素
s.count(e) # e出现的次数
s.extend(it) # 把迭代器中的所有元素加入列表s
s.index(e) # e第一次出现的位置
s.pop([p]) # 删除并返回p位置的值,p默认为最后一个元素的位置,[]代表可选参数
s.reverse() # 就地调转序列中的元素位置
s.sort([key],[revers]) #[]代表可选参数

extend() 函数、append()函数、+ 与 += 功能比较:

  • append()是向列表尾部追加一个新元素,列表只占一个索引位,在原有列表上增加。
  • extend()向列表尾部追加一个列表,将列表中的每个元素都追加进来,在原有列表上增加。
  • +与extend()在效果上具有相同的功能,但是实际上生成了一个新的列表来存放这两个列表的和,只能用在两个列表相加上。
  • +=与extend()效果一样。

list.sort方法和内置函数sorted

list.sort就地排序,返回None,是为提醒本方法不会新建一个列表(会修改原数据

sorted会新建一个列表作为返回值,可以接受任何形式的可迭代对象作为参数,包括不可变序列和生成器(不会修改原数据

list.sort和sorted都有两个可选的关键字参数

  • reverse:默认为False,升序,True,降序
  • key:一个只有一个参数的函数,会被用在序列里的每个元素上,产生的结果将是排序算法依赖的对比关键字。如key = str.lower忽略大小写,key=len基于长度排序

p53,python中的key是一个单参数的函数,区别于其他语言中cmp函数是双参数的,好处是更快。

用bisect来管理已排序的序列

bisect模块的两个主要函数,bisect和insort,均利用二分查找在有序序列中查找或插入元素

1
2
3
4
5
import bisect
# 用bisect在有序序列haystack中查找needle要插入的位置
index = bisect.bisect(haystack, needle)
# 使用insert插入
haystack.insert(index, needle)

bisect函数是bisect_right的别名,返回跟他相等的元素之后的位置,相对的还有bisect_left,返回和插入元素相等的位置,也就是会被放在前面

用bisect.insort插入新元素,insort(seq, item)把变量item插入到序列seq中,且保持seq的升序

1
2
3
4
import bisect
mylist = [1,5,6]
bisect.insort(mylist,4)
#[1,4,5,6]

当列表不是首选时(某些情况下可以替换列表的数据类型)

数组

内存视图 memoryview

NumPy和SciPy

队列

利用list的.append和.pop方法,是可以把list当作栈或队列来用的,比如利用.append和.pop(0),就可以模拟队列的先进先出,但是删除第一个元素的操作是非常耗时的,因为会移动列表里的所有元素

collection.deque类,也就是双向队列,可以快速从两端添加或删除元素

1
2
3
4
5
6
7
8
9
10
from collection import deque
# 创建队列,maxlen设定了队列可容纳元素的数量
dq = deque(range(10), maxlen=10)
# 队列的旋转操作,旋转操作接受一个参数n,n>0时把队列右边的n个元素移到左边,n<0时把最左边的n个元素移动到右边
dq.rotate(3)
# 添加元素
dp.appendleft(9)
dp.entend([1,2,3])
# 注意是逐个添加,所以添加后的dp中元素顺序是2,1,0,逆序
dp.extendleft([0,1,2])

对一个已经满了的队列做添加操作时,会删除掉另外一侧的元素

队列的其他操作p48

其他标准库对队列的实现

  • queue
  • multiprocessing
  • asyncio
  • heapq

第3章 字典和集合

可散列的数据类型

散列表是字典类型性能出众的根本原因

标准库里所有映射类型都是利用dict来实现的,因此有个共同的限制:只有可散列的数据类型才能作为这些映射里的键(只有键有要求,值可以不是)。不可变的数据类型如str、bytes、数值类型都是可散列的,当元组包含的所有元素都是可散列类型的情况下也是散列的。可变类型list不是可散列的。

字典的构造与推导

常见构造方法

image-20220917105947825

字典推导dictcomp

从任何以键值对作为元素的可迭代对象中构建出字典

典型结构

1
key:value for var in iterable

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 从字典中提取特定键值对
D = {0: 'A', 1: 'B', 2: 'C', 3: 'D', 4: 'E', 5: 'F'}
selectedKeys = [0, 2, 5]
X = {k: D[k] for k in selectedKeys}
print(X) # {0: 'A', 2: 'C', 5: 'F'}
# 从字典中删除指定键值对来构建新字典
D = {0: 'A', 1: 'B', 2: 'C', 3: 'D', 4: 'E', 5: 'F'}
removeKeys = [0, 2, 5]
X = {k: D[k] for k in D.keys() - removeKeys}
print(X)
# 反转映射,键变为值,值变为键,注意当有重复值的时候,原本的键会被覆盖,假如2对应也是green,则
# 反转后green对应为2(或者1?此处有待探究)
D = {0: 'red', 1: 'green', 2: 'blue'}
R = {v: k for k,v in D.items()}
print(R)
# 带条件的字典推导
D = {x: x**2 for x in range(6) if x % 2 == 0}
print(D) # {0: 0, 2: 4, 4: 16}
# 嵌套for循环
D = {(k,v): k+v for k in range(2) for v in range(2)}
print(D) # {(0, 0): 0, (0, 1): 1, (1, 0): 1, (1, 1): 2}
# zip初始化
keys = ['name', 'age', 'job']
values = ['Bob', 25, 'Dev']

D = dict(zip(keys, values))
print(D) # {'name': 'Bob', 'age': 25, 'job': 'Dev'}

D = {k: v for (k, v) in zip(keys, values)}
print(D) # {'name': 'Bob', 'age': 25, 'job': 'Dev'}

映射类型的常见方法

p57

处理找不到的键

get()方法

dictname.get(key[,value])

  • key为指定键值

  • value是可选参数,若指定键值不存在时则返回value或者None

  • get()方法与dict[key]区别

    get(key) 方法在 key(键)不在字典中时,可以返回默认值 None 或者设置的默认值。而dict[key] 在 key(键)不在字典中时,会触发 KeyError 异常

setdefault

查找不到时,插入新的键值对

my_dict.setdefault(key,[])作用:如果key不存在则创建key和值为[]的键值对放入my_dict

1
2
3
4
5
my_dict.setdefault(key,[]).append(new_value)
# 效果与下面代码一致,但只需要一次查询操作,下方代码则需要2次或3次
if key not in my_dict:# 第一次查询
my_dict[key] = [] # 第二次查询
my_dict[key].append(new_value) #第三次查询

defaultdict

创建defaultdict对象时需要配置一个为找不到的键创建默认值的方法,提供一个可调用对象,如调用list()来创建一个新列表,作为值

1
2
my_dict = collections.defaultdict(list)
my_dict[key].append(new_value)

_ _missing _ _

字典的变种

OrderedDict

保持顺序

OrderedDict.popitem() 会移除字典里最先插入的元素(先进先出);同时这个方法还有一个可选的 last 参数,若为真,则会移除最后插入的元素(后进先出)。

collections.Counter

参考

这个映射类型会给键准备一个整数计数器。每次更新一个键的时候都会增加这个计数器。所以这个类型可以用来给可散列表对象计数,或者是当成多重集来用。多重集合就是集合里的元素可以出现不止一次。Counter 实现了 + 和 - 运算符用来合并记录,还有像 most_common([n]) 这类很有用的方法。most_common([n]) 会按照次序返回映射里最 常见的 n 个键和它们的计数

集合 set/frozenset

集合的本质是许多唯一对象的聚集。因此,集合可以用于去重

对于给定的两个集合a,b

a | b返回合集,a & b返回交集,a - b返回差集

1
found = len(set(set_a) & set(set_b))

常见操作

1
2
3
lookup = set()
lookup.remove(item)
lookup.add(item)

二维列表转集合,去重

数据结构

32位机器,一个整数占4字节,一个地址占4字节,最大内存为2^32-1约为4G

列表

列表中的元素是顺序存储的,在连续空间中存储,与数组区别在于,可以存储不同类型的元素,长度不固定

由于列表所存元素可能不是同一类型的,所以python在实现列表时,实际上存的是地址

长度固定的问题,python通常是开辟一片空间,不够了就会新开更大的空间,然后将原list拷贝过去

常用操作的时间复杂度

下标查找,append,不考虑拷贝,O(1)

插入insert和删除remove O(n),删了还需要移动元素

栈Stack

使用一般的列表结构即可实现栈

可以理解为只能在一端进行插入或者删除操作的列表,后进先出

队列Queue

在列表的一段进行插入(队尾rear),另一端进行删除(队首front),先进先出

没办法使用列表简单实现,需要通过环形队列实现的

环形队列:

  • 当队尾指针front == Maxsize + 1时,再前进一个位置就自动到0.
  • 队首指针前进1: front = (front + 1) % MaxSize
  • 队尾指针前进1: rear = (rear + 1) % MaxSize队空条件: rear == front
  • 队满条件:(rear + 1) % MaxSize == front,留了一个空间没有存数,用于区别队空和队满的情况

双向队列(两端都支持进队和出队)

python内置模块

1
2
3
4
5
6
7
8
9
from collections import deque
q = deque()
q = deque([1,2,3,4,5],5) #设置最大长度为5,超过长度加入时,队首自动出队
q.append(item) # 队尾进队
q.popleft() # 队首出队
# 用于双向队列
q.appendleft(item) # 队首进队
q.pop() # 队尾出队

哈希表

字典和集合就是使用哈希表实现的

支持高效操作:

  • insert(key, value):插入键值对(key,value)
  • get(key)∶如果存在键为key的键值对则返回其value,否则返回空值
  • delete(key):删除键为key的键值对