第11条:学会对序列进行切片
python支持从序列里面切割出一部分内容,让我们能够放轻松地获取原序列地某个子集合,最简单的用法就是切割内置的list,str与bytes。其实,凡是实现了__getitem__
与__setitem__
这两个特殊方法的类都可以进行切割。
切片最基本的用法就是somelist[start:end]
这一形式来切割,也就是从start开始一直取到end这个位置,但不包含end本身的元素。如果从头开始切割列表,那就应该省略start,如果一直取到末尾,那就应该省略end。用负数作下标表示从倒数第k个。
1 | a[:] |
切片可以出现在赋值符号的左侧,表示用右侧那些元素把原列表中位于这个范围之内的元素换掉。与unpacking形式的赋值不同,这种赋值不要求等号两边所指定的元素个数必须相同,但是如果元素个数不同,列表的长度会发生变化。
1 | a[2:5] = [2, 3, 4] # list's length not change |
第12条:在切片中指定步进
python的切片还支持不仅切片形式,也就是somelist[start:end:stride]
。这种形式从start开始取,每n个元素里面选取一个。
1 | x = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'] |
当步进值设置为负数时,表示从start开始,从后往前取
1 | print(x[-3: 2]) |
设置步进值为负数的一个应用就是用于将列表进行反转
1 | print(x[::-1]) |
第13条:通过带星号的unpacking来捕获多个元素
python基本unpacking操作有一项限制,就是必须提前需要确定要拆解的序列的长度。但是如果不事先知道长度,而且想把一些元素仍然以list的形式保存,一种办法是通过获取长度,然后通过下标获取加切片的形式:
1 | car_ages = [0, 9, 4, 8, 7, 20, 19, 1, 6, 15] |
更好的方式是使用带*
的unpacking:
1 | oldest, *others = car_ages_desc |
这种带星号的表达式可以出现在任意位置,所以它能够获取序列中的任何一段元素:
1 | oldest, *others, youngest = car_ages_desc |
1 | *all = car_ages_desc # syntax error |
另外,如果要拆分的列表里以及没有元素留给带*
的变量,那么该变量会是一个长度为0的列表
1 | short_list = [1, 2] |
使用带星号的unpacking需要注意一点,带星号的这部分总是会形成一份列表,这有可能会耗尽计算机的全部内存并导致程序崩溃,尤其是在和生成器(yield方法)一起使用的时候。
第14条:用sort方法的key参数来表示复杂的排序逻辑
内置的列表类型提供了名叫sort的方法,可以按照多项指标给list实例中的元素进行排序。在默认情况下,sort方法总是按照自然升序排列列表内的元素。例如,如果列表中的元素都是整数,那么它就按数值从小到大排列
1 | numbers = [93, 86, 11, 68, 70] |
凡是具备自然顺序的内置类型几乎都可以用sort方法进行排列,例如字符串、浮点数等。但是一般的对象又该如何排序呢?比如,假如这里定义了一个People类:
1 | class People(object): |
如果仅仅这样写,那么这个由该类的对象所构成的列表是没办法用sort方法排序的,因为sort方法发现,排序所需要的特殊方法并没有在People类中实现
1 | perples.sort() |
虽然我们可以在People类中定义一些特殊的方法让我们在无须额外参数的情况下就能直接在这些类的实例所构成的列表上进行排序(参见第73条)。但是更为常见的情况是,很多对象需要在不同的情况下按照不同的标准排序,此时定义自然排序实际上没有意义。这些排序标准通常是针对对象中的某个属性,我们可以把这样的排序逻辑定义成函数,然后将这个函数传给sort方法的key参数。这个函数只有一个参数,用于指代列表中有待排序的对象,函数返回的应该是一个可比较的,具有自然顺序的值。
1 | peoples.sort(key=lambda p:p.name) |
有些时候我们可能需要用多个标准来排序。例如,在名字位首要标准的情况下,再按年龄进行排序,这种怎么实现呢?最简单的办法是利用元组类型来实现。两个元组是可以进行比较的,因为元组类型本身已经定义了自然顺序,也就是说,sort方法所要求的特殊方法(例如__it__
方法),它都已经定义好了。元组在实现这些特殊方法时会依次比较每个位置的那两个对应元素,直到能够确定大小为止,注意,对应位置的元素要能够比较大小,否则也会报异常。
利用元组的特性,我们来对peoples数组先按名称排序,再按年龄排序:
1 | peoples.sort(key=lambda p: (p.name, p.age)) |
1 | peoples.sort(key=lambda p: (p.name, -p.age)) |
1 | peoples.sort(key=lambda p: p.age, reverse=True) |
无论有多少项排序指标都可以按照这种思路来实现,而且每项指标可以分别按照各自的方向来排,也就是越主要的那项排序指标放在越后一轮处理。
尽管两种思路都能实现两种的效果,但是只调用一次sort,还是要比多次调用sort更为简单,所以,在实现多个指标按不同方向排序时,应该优先考虑让key函数返回元组,并按需对元组中的相应指标进行取反,只有在万不得已的时候,才考虑多次调用sort方法
第15条:不要过分依赖给字典添加条目时所用的顺序
S在python3.5与之前的版本中,迭代字典(dict)时所看到的顺序是任意的,即不一定与当初把这些键值对添加到字典时的顺序相同,而且每次迭代的顺序也不固定。
1 | # python 3.5 |
从Python3.6开始,字典会保留这些键值对在添加时所用的顺序,而且python3.7版本的语言规范正式确立了这条规则。于是在新版的python里,总是能够按照当初创建字段时的那套顺序来遍历这些键值对。
1 | # python 3.7 |
这项变化对Python中那些依赖字典类型及其实现细节的特性产生了很多影响:
函数的关键字参数(包括万能的**kwargs参数),以前是按照几乎随机的顺序出现的,现在,这些关键字参数总是能够保留嗲用函数时所指定的那套顺序
1
2
3def my_func(**kwargs):
for key, value in kwargs.items():
print(f'{key} = {value}' % (key, value))my_func(goose=’gosling’, kangaroo=’joey’)
goose = gosling
kangraoo = joey
1 | * 类也会利用字典来保存这个类的实例所具备的一些数据,在早前版本的Python中,遍历对象(object)中的`__dict__`也是按随机顺序出现的,同样,在新版的Python中,我们可以认为这些字段在`__dict__`中出现的顺序应该与当初赋值的顺序一样。 |
例如:
1 | def get_first(the_dict): |
在实际调用get_first函数时,我们不知道传入的是标准的dict类型,还是一个实现了items方法的类。解决这个问题有以下几种方法:
在函数开头判断是否是标准dict
1
2
3
4def get_first(the_dict):
if not isinstance(the_dict, dict):
raise TypeError('must provide a dict instance')
return the_dict.items[0]通过类型注解,在程序时,使用mypy模块进行静态分析
1
2
3from typing import Dict
def get_first(the_dict: Dict[str, int]):
return the_dict.items[0]1
python3 -m mypy --strict xxx.py
第16条:用get处理键不再字典中的情况,不要使用in与KeyError
字典有三种基本的交互操作:访问、赋值以及删除键值对。字典的内容经常变动,所以完全由可能会出现你想访问或删除的键以及不在字段中了,所以大多数情况我们访问字段都要先判断一下key是否还在dict中
1 | counter = { |
使用if表达式需要访问key两次,并且进行赋值操作一次,还有一种方法也可以实现相同的功能,就是利用KeyError异常:
1 | try: |
这种方式比用if表达要稍稍高效一点,因为只需要一次访问和一次赋值操作。更好的方法是使用dict的get方法,get方法第一参数指定自己想要查的键,第二个参数指定这个键不存在时返回的值:
1 | count = counter.get(key, 0) |
虽然这种方法也需要一次访问和一次赋值操作,但是这比捕获KeyError
的方式代码更简洁。
假设dict里面的value不是简单类型,而是例如列表list这样的复杂类型时,修改可能存在的key时应该怎么处理呢:
1 | votes = { |
在采用if表达式的实现方案里,如果键名已经存在,那么需要访问两次(一次是在if语句里,另外一次是在获取列表的语句里);如果键名不存在,那么就只需要在if语句中访问一次,然后再else语句中赋值一次。
在再采用捕获KeyError的方案里,如果键已经再字典中,那么只需要在try块里访问一次键名;如果不在字典中,那么要先在try块里访问一次键名,然后在except块中做一次赋值。
在使用get方法的方案里,由于get方法在key不存时,虽然会返回设置的默认返回值,但是不会将对应的值和字典关联起来,所以在操作复杂类型时,为了减少赋值操作,更好的方式是先将key和value关联起来,再对value进行操作
dict类型还提供了setdefault
方法,能够继续优化代码。这个方法会查询字典里有没有这个键,如果有,就返回对应的值,如果没有,就先把用户提供的默认值跟这个键关联起来并插入字典,然后返回这个值。总之,这个方法所返回的值肯定已经跟键关联起来。
1 | names = votes.setdefault(key, []) |
使用setdefault
方法可以达到预期的效果,并且代码也很简洁。但是代码读起来会有歧义,setdefault
的表现和它的名称似乎有点不相符:它实际上是在获取value,但是却叫做set。另外,当key不存在时,默认值会直接简单赋值给对应的key,而不是进行深拷贝,这样就可能存在问题。
1 | data = {} |
由于这个问题存在,就意味着必须保证每次调用setdefault
时,默认值参数都必须重新构造,这也导致不论key是否存在,都会进行一次默认值构造的开销。
但其实更好的解决方法是使用defaultdict
类,见下面的第17条。
第17条:用defaultdict处理内部状态中缺失的元素
deafultdict
类是collections包中内置的模块,相比于setdefault
要求提供默认值,它需要提供的是一个函数,注意,该函数不能有任何必填参数。
1 | from collections import defaultdict |
第18条:学会利用__missing__构造依赖键的默认值
前面介绍了dict的setdefault
方法和内置的defaultdict
类来解决key缺失的情况,但是还有些情况是这两个方法也不好解决的。
例如,有一个key为文件路径,value文件句柄的dict,用于文件的重复读写,当key在dict不存在时,需要打开文件并将句柄添加到dict中
1 | pictures = {} |
使用get
方法,如果字典中已经有这个handle了,那么这种写法只需要进行一次字典访问。如果没有,那么它会通过get方法访问一次字典,然后在try/except/else
结构的else分支中做一次赋值。
这套逻辑也能用in表达式或KeyError实现,但那两种方案的字典访问次数与代码嵌套层数都比较多。有人可能觉得,既然这套逻辑能用get
、in
与KeyError
这三种方案实现,那么也应该可以用第四种方案,也就是setdefault
方法来是实现:
1 | try: |
这样写有很多问题,因为即使图片的路径名已经在字典中了,程序还是得调用内置得open函数创建文件句柄,并且这个handle也没有显示地close。
如果考虑使用defaultdict
来实现,由于defaultdict
要求传入的构造函数不能有任何必填参数,所以在这种情况下,使用defaultdict
也是不太好的:
1 | from collections import defaultdict |
幸运的是,python还提供了一个内置的解决方法,那就是我们可以自定义一个类并继承自dict类型,并重写__missing__
方法来自定义key缺失的情况怎么处理。
1 | class Picture(dict): |
__missing__
方法必须给key创建一个default值,并插入到自身中,在调用self[key]
时是不会再次触发__missing__
方法的。