java中的接口是类吗
263
2022-08-25
《Effective Python 2nd》 读书笔记——列表与字典(effective java)
引言
Python提供了一些特殊的语法和内置的模块,能够扩充列表与字典的能力,让我们可以用清晰的代码实现很多强大的功能。
#11 学会对序列做切片
Python可以从序列里切割(slice)出一部分内容。凡是实现了__getitem__与__setitem__这两个特殊方法的类都可以切割。
切割最基本的写法是somelist[start:end],从start开始取,不包括end。
a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']print('Middle two: ', a[3:5]) # Middle two: ['d', 'e']print('All but ens: ', a[1:7]) # All but ens: ['b', 'c', 'd', 'e', 'f', 'g']
Middle two: ['d', 'e']All but ens: ['b', 'c', 'd', 'e', 'f', 'g']
如果从头开始切割列表,可以省略冒号左侧的下标0。
assert a[:5] == a[0:5]
如果一直取到列表末尾,那就应该省略冒号右侧的下标。
assert a[5:] == a[5:len(a)]
用负数做下标表示从列表末尾往前算。下面看一些切割示例:
print(a[:]) # ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']print(a[:5]) # ['a', 'b', 'c', 'd', 'e']print(a[:-1]) # ['a', 'b', 'c', 'd', 'e', 'f', 'g'] # -1表示最后一个print(a[4:]) # ['e', 'f', 'g', 'h']print(a[-3:]) # ['f', 'g', 'h']print(a[2:5]) # ['c', 'd', 'e']print(a[2:-1]) # ['c', 'd', 'e', 'f', 'g']print(a[-3:-1]) # ['f', 'g']
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']['a', 'b', 'c', 'd', 'e']['a', 'b', 'c', 'd', 'e', 'f', 'g']['e', 'f', 'g', 'h']['f', 'g', 'h']['c', 'd', 'e']['c', 'd', 'e', 'f', 'g']['f', 'g']
如果起点与终点所确定的范围超出了列表的边界,那么系统会自动忽略不存在的元素。
first_twenty_items = a[:20] # 取a的前20个元素,但是a没有那么多元素,就取出a的所有元素last_twenty_items = a[-20:] # 取a的最后20个元素first_twenty_items # ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
切割出来的列表是一份全新的列表。即使把某个元素换掉,也不会影响原列表。
b = a[3:]print('Before ', b)b[1] = 99print('After ', b)print('No change:',a)
Before ['d', 'e', 'f', 'g', 'h']After ['d', 99, 'f', 'g', 'h']No change: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
切片还可以出现在赋值符号的左侧,表示用右侧那些元素把原列表中位于这个范围之内的元素换掉。 这种赋值不要求左右两侧所指定的元素个数相等。在原列表中,位于切片范围之前和之后的那些元素会予以保留,但是列表的长度可能有所变化。
例如,下面这个例子中,列表会变短,因为赋值符号的右侧只提供了3个值,但是左侧那个切片却涵盖了5个值,列表会比原来少两个元素。
print('Before ', a)a[2:7] = [99, 22, 14]print('After ', a)
Before ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']After ['a', 'b', 99, 22, 14, 'h']
而下面这段代码会使列表变长:
print('Before ', a)a[2:3] = [47, 11]print('After ', a)
Before ['a', 'b', 99, 22, 14, 'h']After ['a', 'b', 47, 11, 22, 14, 'h']
起止位置都留空的切片,出现在赋值右侧,表示给这个列表做副本。
b = a[:]assert b == a and b is not
把不带起止下标的切片放在赋值符号左边,表示是用右边那个列表的副本把左侧列表的全部内容替换掉。
b = aprint('Before a', a)print('Before b', b)a[:] = [101, 102, 103] #左侧列表的引用不变,值发生了改变。assert a is b print('After a ', a)print('After b ', b)
Before a ['a', 'b', 47, 11, 22, 14, 'h']Before b ['a', 'b', 47, 11, 22, 14, 'h']After a [101, 102, 103]After b [101, 102, 103]
#12 不要在切片里同时指定起止下标与步长
Python还有一种特殊的步长切片形式,即somelist[start:end:stride]。这种形式会在每n个元素里面选取一个,这样很容易就能把奇数位置上的元素与偶数位置上的元素分别通过x[::2]与x[1::2]选取出来。
x = ['red', 'orange', 'yellow', 'green', 'blue', 'purple']odds = x[::2] # 从下标0开始,每2个元素取一个(隔1个取1个)evens = x[1::2] # 从下标1开始,每2个元素取一个print(odds)print(evens)
['red', 'yellow', 'blue']['orange', 'green', 'purple']
但是,带有步长的切片经常会引发意外的效果,并使程序出现bug。列如,Python里面有个技巧,把-1当成步长对bytes类型的字符串做切片,这样就能将字符串反转过来。
x = b'mongoose'y = x[::-1]y # b'esoognom
b'esoognom'
Unicode形式的字符串也可以这样反转
x = '月饼'y = x[::-1]
'饼月'
但如果把这种字符串编码成UTF-8标准的字节数据,就不能用这个技巧来反转了。
w = '月饼'x = w.encode('utf-8')y = x[::-1]z = y.decode('utf-8')
---------------------------------------------------------------------------UnicodeDecodeError Traceback (most recent call last)
除了-1外,用其他负数做步长值,有没有意义呢?
x = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']print(x[::2]) # ['a', 'c', 'e', 'g'] 从头开始,每隔1个取一个print(x[::-2]) # ['h', 'f', 'd', 'b'] 从末尾开始往前
['a', 'c', 'e', 'g']['h', 'f', 'd', 'b']
那么2::2是什么意思?-2::2、-2:2:-2等又是什么意思?
print(x[2::2]) # ['c', 'e', 'g']print(x[-2::-2]) # ['g', 'e', 'c', 'a']print(x[-2:2:-2]) # ['g', 'e']print(x[2:2:-2]) # []
['c', 'e', 'g']['g', 'e', 'c', 'a']['g', 'e'][]
同时使用起止下标与步长会让切片很难懂。 为了避免这个问题,建议大家不要把起止下标和步长同时写在切片里。 如果必须指定步长,那么尽量采用正数,而且要把起止下标都留空。即便必须同时使用步长值与起止下标,也应该考虑分成两次来写。
y = x[::2] # ['a', 'c', 'e', 'g']z = y[1:-1] # ['c', 'e']
像上面这样先隔位选取然后再切割,会让程序做一次浅拷贝。如果程序没有那么多时间或内存取分两步操作,那么可以改用内置的itertools模块中的islice方法。
#13 通过带星号的unpacking操作来捕获多个元素,不要用切片
基本的unpacking操作有一项限制,就是必须提前确定需要拆解的序列的长度。 例如,销售汽车的时候,我们可能会把每辆车的年龄写到一份列表中,然后按照从大到小的顺序排好。如果试着通过基本的unpacking操作获取其中最旧的两辆车,那么程序运行时就会出现异常。
car_ages = [0, 9, 4, 8, 7, 20, 19, 1, 6, 15]car_ages_descending = sorted(car_ages, reverse=True)oldest, second_oldest =
---------------------------------------------------------------------------ValueError Traceback (most recent call last)
新手经常通过下标与切片来处理这个问题。例如,可以明确通过下标把最旧和第二旧的那两辆车取出来,然后把其余的车放到另一份列表中。
oldest = car_ages_descending[0]second_oldest = car_ages_descending[1]others = car_ages_descending[2:]print(oldest, second_oldest, others) # 20 19 [15, 9, 8, 7, 6, 4, 1, 0]
20 19 [15, 9, 8, 7, 6, 4, 1, 0]
下标与切片会让代码看起来很乱。而且,这样也很容易出错。 这个问题通过星号表达式(starred expresion)来解决更会更好一些,这也是一种unpacking操作,它可以把无法由普通变量接收的那些元素全部囊括进去。 下面用带星号的unpacking操作改写刚才的代码。
oldest, second_oldest, *others = car_ages_descendingprint(oldest, second_oldest, others) # 20 19 [15, 9, 8, 7, 6, 4, 1, 0]
20 19 [15, 9, 8, 7, 6, 4, 1, 0]
这样写简短易读,而且不容易出错。 这种星号表达式可以出现在任意位置,所以它能捕获序列中的任何一段元素。
oldest, *others, youngest = car_ages_descendingprint(oldest, youngest, others) # 20 0 [19, 15, 9, 8, 7, 6, 4, 1]*others, second_youngest, youngest = car_ages_descendingprint(youngest, second_youngest, others) # 0 1 [20, 19, 15, 9, 8, 7, 6, 4]
20 0 [19, 15, 9, 8, 7, 6, 4, 1]0 1 [20, 19, 15, 9, 8, 7, 6, 4]
不过,在使用这种写法时,至少要确保有一个普通的接收变量与它搭配,否则就会出错。例如不能像下面这样,只使用带星的表达式而不搭配普通变量。
*others =
File "
对于单层结构来说,同一级里面最多只能出现一次带星号的unpacking。
first, *middle, *second_middle, last = [1, 2, 3, 4]
File "
如果要拆解的结构有多层,那么同一级的不同部分里可以各自出现带星号的unpacking操作。 但是不推荐这种写法,这里举一个例子,让大家了解一下。
car_inventory = { 'Downtown': ('Silver Shadow', 'Pinto', 'DMC'), 'Airport': ('Skyline', 'Viper', 'Gremlin', 'Nova'),}((loc1, (best1, *rest1)), (loc2, (best2, *rest2))) = car_inventory.items()print(f'Best at {loc1} is {best1}, {len(rest1)} others')print(f'Best at {loc2} is {best2}, {len(rest2)} others')
Best at Downtown is Silver Shadow, 2 othersBest at Airport is Skyline, 3 others
星号表达式总会形成一份列表实例。如果要拆分的序列里已经没有元素留给它了,那么列表就是空白的。 如果能提前确定有待处理的序列里面至少会有N个元素,那么这项特性就相当有用。
short_list = [1, 2]first, second, *rest = short_listprint(first, second, rest)
1 2 []
unpacking操作也可以用在迭代器上,但是这样写与把数据拆分到多个变量里面的那种基本写法相比,并没有太大优势。
对迭代器做unpacking操作的好处,主要体现在带星号的用法上面,它使迭代器的拆分值更清晰。 例如,这里有个生成器,每次可以从含有整个一周的汽车订单的CSV文件中取出一行数据。
def generate_csv(): yield ('Date', 'Make' , 'Model', 'Year', 'Price') for i in range(100): yield ('2019-03-25', 'Honda', 'Fit' , '2010', '$3400') yield ('2019-03-26', 'Ford', 'F150' , '2008', '$2400')
我们可以用下标和切片来处理这个生成器所给出的结果,但是这样写需要很多行代码,而且可读性不好。
# Example 11all_csv_rows = list(generate_csv())header = all_csv_rows[0]rows = all_csv_rows[1:]print('CSV Header:', header)print('Row count: ', len(rows))
CSV Header: ('Date', 'Make', 'Model', 'Year', 'Price')Row count: 200
利用带星号的unpacking操作,我们可以把第一行单独放到header变量里,同时把迭代器所给出的其余内容合起来表示成rows变量。这样就很清晰了。
it = generate_csv()header, *rows = itprint('CSV Header:', header)print('Row count: ', len(rows))
CSV Header: ('Date', 'Make', 'Model', 'Year', 'Price')Row count: 200
带星号的这部分总是会形成一份列表,所以要注意,可能会耗尽计算机的全部内存。 所以,首先要确认系统有足够的内存可以存储拆分出来的结果数据。
#14 用sort方法的key参数来表示复杂的排序逻辑
列表类型提供了叫sort的方法,可以根据多项指标给list实例中的元素排序。默认按照升序排序。
numbers = [93, 86, 11, 68, 70]numbers.sort()print(numbers) # [11, 68, 70, 86, 93]
[11, 68, 70, 86, 93]
那么,一般对象该如何排序呢?比如定义以Tool类表示各种建筑工具,它带有__repr__方法:
class Tool: def __init__(self, name, weight): self.name = name self.weight = weight def __repr__(self): return f'Tool({self.name!r}, {self.weight})' tools = [ Tool('level', 3.5), Tool('hammer', 1.25), Tool('screwdriver', 0.5), Tool('chisel', 0.25),]
此时如果直接调用sort方法会报错的,因为排序所需要的特殊方法并没有定义在Tool类中。
tools.sort()
---------------------------------------------------------------------------TypeError Traceback (most recent call last)
很多对象需要在不同的情况下按照不同的标准排序。
这些排序标准通常是针对对象中的某个属性。我们可以把这样的排序逻辑定义成函数,然后将这个函数传给sort方法的key参数。key所表示的函数本身应该带有一个参数,这个参数指代列表中有待排序的对象,函数返回的应该是个可比较的值。以便sort方法以该值为标准给这些对象排序。
下面用lambda关键字定义这样一个函数,把它传给sort方法的key参数,让我们能按照name的字母顺序排列这些Tool对象。
print('Unsorted:', repr(tools))tools.sort(key=lambda x: x.name)print('\nSorted: ', tools)
Unsorted: [Tool('level', 3.5), Tool('hammer', 1.25), Tool('screwdriver', 0.5), Tool('chisel', 0.25)]Sorted: [Tool('chisel', 0.25), Tool('hammer', 1.25), Tool('level', 3.5), Tool('screwdriver', 0.5)]
如果想改用另一项标准,比如用weight来排序,那只需要再定义一个lambda函数:
tools.sort(key=lambda x: x.weight)print('By weight:', tools)
By weight: [Tool('chisel', 0.25), Tool('screwdriver', 0.5), Tool('hammer', 1.25), Tool('level', 3.5)]
在编写传给key参数的lambda函数时,可以像刚才那样返回对象的某个属性,如果对象时序列、元组或字典,那么还可以返回其中的某个元素。只要是有效的表达式,都可以充当lambda函数的返回值。
对于字符串这样的基本类型,我们可能需要通过key函数先对它的内容做一些变换,并根据变换之后的结果来排序。例如,下面这个places列表中存放着表示地点的字符串,如果想在排序的时候忽略大小写,那我们可以先用lower方法把待排序的字符串处理一下(因为默认的字典顺序,大写字母在小写字母之前)。
places = ['home', 'work', 'New York', 'Paris']places.sort()print('Case sensitive: ', places)places.sort(key=lambda x: x.lower())print('Case insensitive:', places)
Case sensitive: ['New York', 'Paris', 'home', 'work']Case insensitive: ['home', 'New York', 'Paris', 'work']
有时我们可能需要用多个标准来排序。例如,下面的列表里有一些电动工具,我们想以weight为首要指标来排序,在重量相同的情况下,再按name排序。
power_tools = [ Tool('drill', 4), Tool('circular saw', 5), Tool('jackhammer', 40), Tool('sander', 4),]
在Python里,最简单的方案是利用元组实现。两个元组之间是可比较的,因为这种类型本身已经定义了自然顺序,即,sort方法所要求的特殊方法(例如__lt__),它都已经定义好了。元组在实现这些特殊方法时会依次比较每个位置的那两个对应元素,直到能够确定大小为止。
下面,我们看看元组是如何比较重量的。
saw = (5, 'circular saw')jackhammer = (40, 'jackhammer')assert not (jackhammer < saw)
如果两个元组的首个元素相等,就比较第二个元素,如果仍然相等,就继续往下比较。
drill = (4, 'drill')sander = (4, 'sander')assert drill[0] == sander[0] # 重量相等assert drill[1] < sander[1] # 字母顺序d < sassert drill < sander # 因此,drill < sander
利用元组的这项特性,我们可以用工具的weight和name构造一个元组。下面就定义一个这样的lambda函数,让它返回这种元组,把首要指标(weight)写在前面。
power_tools.sort(key=lambda x: (x.weight, x.name)) # 代表元组的括号不能少print(power_tools)
[Tool('drill', 4), Tool('sander', 4), Tool('circular saw', 5), Tool('jackhammer', 40)]
这种做法有个缺点,就是key函数所构造的这个元组只能按同一个排序方法来对比它所表示的各项指标。 所以不太好实现weight按降序而name按升序的效果。sort方法可以指定reverse参数,这个参数会同时影响元组中的每项指标。
power_tools.sort(key=lambda x: (x.weight, x.name), reverse=True) # 使所有的指标变成降序print(power_tools)
[Tool('jackhammer', 40), Tool('circular saw', 5), Tool('sander', 4), Tool('drill', 4)]
如果其中一项是数字,那么可以在实现key函数时,利用一元减操作让两个指标按照不同的方向排序。即,key函数在返回这个元组时,可以单独对这项指标取相反数,并保持其他指标不变,这就相当于让排序算法单独在这项指标上采用逆序。下面演示怎样按照重量从大到小,名称从小到大的顺序排列。
power_tools.sort(key=lambda x: (-x.weight, x.name))print(power_tools)
[Tool('jackhammer', 40), Tool('circular saw', 5), Tool('drill', 4), Tool('sander', 4)]
显然,这个技巧并不适合所有的类型。比如,我们试着对name运用一元减操作。
power_tools.sort(key=lambda x: (x.weight, -x.name), reverse=True)
---------------------------------------------------------------------------TypeError Traceback (most recent call last)
可以看到,str类型不支持一元减操作。此时,可以考虑sort方法的一项特征,就是这个方法是个稳定的排序算法。意味着,如果key函数认定两个值相等,那么这两个值在排序结果中的先后顺序会与它们在排序前的顺序一致。 于是,我们可以在同一个列表上多次调用sort方法,每次指定不同的排序指标。 下面我们就利用这项特征实现刚才想要达成的那种效果,把首要指标(重量)降序放在第二轮,把次要指标(名称)升序放在第一轮。
power_tools.sort(key=lambda x: x.name) # Name 升序power_tools.sort(key=lambda x: x.weight, # Weight 降序 reverse=True)print(power_tools)
[Tool('jackhammer', 40), Tool('circular saw', 5), Tool('drill', 4), Tool('sander', 4)]
为什么这样可以呢? 我们拆开来看。先看第一轮,也就是按照名称升序排列:
power_tools.sort(key=lambda x: x.name)print(power_tools)
[Tool('circular saw', 5), Tool('drill', 4), Tool('jackhammer', 40), Tool('sander', 4)]
然后执行第二轮,即按重要降序排列。这时,由于’sander’与’drill’所对应的两个Tool重量相同,key函数会判定这两个对象相等,于是,在sort方法的排序结果中,它们之间的先后词序就跟第一轮结束时的次序相同。 所以,我们再实现了按重量降序排序的同时,保留了重量相同的对象在上一轮排序结果时的相对次序,而上一轮是按照名称升序排列的。
power_tools.sort(key=lambda x: x.weight, reverse=True)print(power_tools)
[Tool('jackhammer', 40), Tool('circular saw', 5), Tool('drill', 4), Tool('sander', 4)]
无论有多少项排序指标都可以按照这种思路来实现,而且每项指标可以分别按照各自的方向来排,不用全部都是升序或降序。只需要倒着写即可,即把最主要的那项排序指标放在最后一轮处理。
但是只调用一次sort,还是要比调用多次sort简单,不到万不得已,不要用多次排序的方式。
#15 不要过分依赖给字典添加条目时所用的顺序
从Python3.6开始,字典会保留键值对在添加时所用的顺序。
baby_names = { 'cat': 'kitten', 'dog': 'puppy',}print(baby_names)
{'cat': 'kitten', 'dog': 'puppy'}
在Python3.5之前的版本中,dict所提供的许多方法都不保证固定的顺序,所以让人觉得好像是随机处理的。在新版的Python中,这些方法可以按照当初添加键值对的顺序来处理了。
print(list(baby_names.keys()))print(list(baby_names.values()))print(list(baby_names.items()))print(baby_names.popitem()) # 最后添加的元素
['cat', 'dog']['kitten', 'puppy'][('cat', 'kitten'), ('dog', 'puppy')]('dog', 'puppy')
这项变化对Python中那些依赖字典类型及其实现细节的特性产生了很多影响。 函数的关键字参数,以前是按照近乎随机的顺序出现,现在,这些关键字参数总能保留调用函数时所指定的那套顺序。
def my_func(**kwargs): for key, value in kwargs.items(): print(f'{key} = {value}')my_func(goose='gosling', kangaroo='joey')
goose = goslingkangaroo = joey
另外,类也会利用字典来保存这个类的实例所具备的一些数据。在早前抱抱你的Python中,对象中的字段看上去好像是随机出现的。在新版中,我们就可以认为这些字段在__dict__中出现的顺序应该与当初赋值时的顺序一样。
class MyClass: def __init__(self): self.alligator = 'hatchling' self.elephant = 'calf'a = MyClass()for key, value in a.__dict__.items(): print(f'{key} = {value}')
alligator = hatchlingelephant = calf
所以,我们可以利用这样的特征来实现一些功能,而且可以把它融入自己给类和函数所设计的API中。
但处理字典的时候,不能总是假设所有的字典都能保留键值对插入时的顺序。在Python中,我们很容易就定义出特制的容器类型,并且让这些容器也像标准的list与dict等类型那样遵守相关的协议。 Python不是静态类型的语音,大多数代码都以鸭子类型机制运作(即对象支持什么样的行为,就可以当成什么样的数据使用,而不用执着于它在类体系中的地位)。这种特性可能会产生意想不到的问题。
例如,现在要写一个程序,统计各种小动物的受欢迎程度。我们可以设定一个字典,把每种动物和它得到的票数关联起来。
votes = { 'otter': 1281, 'polar bear': 587, 'fox': 863,}
现在定义一个函数来处理投票数据。用户可以把空的字典传给这个函数,这样的话,它就会把每个动物及其排名放到这个字典中。这种字典可以充当数据模型,给带有用户界面的元素提供数据。
def populate_ranks(votes, ranks): names = list(votes.keys()) names.sort(key=votes.get, reverse=True) for i, name in enumerate(names, 1): ranks[name] =
我们还需要写一个函数查出人气最高的动物。这个函数假定populate_ranks总是会按照升序向字典写入键值对,这样第一个出现在字典里的就应该是排名最靠前的动物。
def get_winner(ranks): return next(iter(ranks))
下面来验证刚才设计的函数,看它们能不能实现想要的结果。
ranks = {}populate_ranks(votes, ranks)print(ranks)winner = get_winner(ranks)print(winner)
{'otter': 1, 'fox': 2, 'polar bear': 3}otter
结果没有问题。但是,假设现在需求变了,我们想要按照字母顺序在UI中显示。为了实现这种效果,我们用内置的collections.abc模块定义这样一个类。这个类的功能和字典一样,而且会按照字母顺序迭代其中的内容。
from collections.abc import MutableMappingclass SortedDict(MutableMapping): def __init__(self): self.data = {} def __getitem__(self, key): return self.data[key] def __setitem__(self, key, value): self.data[key] = value def __delitem__(self, key): del self.data[key] def __iter__(self): keys = list(self.data.keys()) keys.sort() for key in keys: yield key def __len__(self): return len(self.data)
原来使用标准库dict的地方,现在可以改用这个类的实例。我们这个SortedDict类与标准的字典遵循同一套协议,因此程序不会出错。但是,我们并没有得到预期的效果。
sorted_ranks = SortedDict()populate_ranks(votes, sorted_ranks)print(sorted_ranks.data)winner = get_winner(sorted_ranks)print(winner)
{'otter': 1, 'fox': 2, 'polar bear': 3}fox
为什么会这样,因为get_winner总是假设, 迭代字典时的顺序应该跟populate_ranks函数当初向字典中插入数据时的顺序一样。但是这次,我们用的是SortedDict实例,而不是标准的dict实例,所以这项假设不成立。
因此,函数返回的数据是按照字母顺序排列时最先出现的那个数据,也就是’fox’。
这个问题有三种解决方法。第一种是重新实现get_winner函数,使它不再假设ranks字典总是按照固定的顺序来迭代。这是最保险、最稳妥的一种方法。
def get_winner(ranks): for name, rank in ranks.items(): if rank == 1: return namewinner = get_winner(sorted_ranks)print(winner)
otter
第二种方法是在函数开头先判断ranks是不是预期的那种标准字典。如果不是,就抛出异常。这个方法的运行性能要比刚才那个好。
def get_winner(ranks): if not isinstance(ranks, dict): raise TypeError('must provide a dict instance') return next(iter(ranks)) assert get_winner(ranks) == 'otter' get_winner(sorted_ranks)
---------------------------------------------------------------------------TypeError Traceback (most recent call last)
第三种方法是通过类型注解来保存传给get_winner函数的确是个真正的dict实例,而不是那种行为根标准字典类似的MutableMapping。下面就采用严格模式,针对含有注解的代码运行mypy工具。
example.py:
# python -m mypy
# 首先按照mypy$ pip install mypy$ python -m mypy --strict example.py example.py:6: error: Argument "key" to "sort" of "list" has incompatible type overloaded function; expected "Callable[[str], SupportsLessThan]"example.py:44: error: Argument 2 to "populate_ranks" has incompatible type "SortedDict"; expected "Dict[str, int]"example.py:46: error: Argument 1 to "get_winner" has incompatible type "SortedDict"; expected "Dict[str, int]"Found 3 errors in 1 file (checked 1 source file)
这样可以检查出类型不相符的问题,mypy会标出错误的用法。这个方案既能保证静态类型准确,又不会影响程序的运行效率。
#16 用get处理不在字典中的情况,不要使用in与KeyError
假设我们要给一家三明治店设计菜单,所以想先确定大家喜欢吃哪些类型的面包。我们定义一个字典,把每种款式的名字和它当前的得票数关联起来。
counters = { 'pumpernickel': 2, 'sourdough': 1,}
如果要记录新的一票。首先要判断对应的键在不在字典里。如果不在,那就把这个键的票数设成0,然后增加所得票数。这需要两次访问这个键,第一次是为了判断它是否在字典里,第二次为了用它来获取对应的值,而且还要做一次赋值。 下面我们用if语句来实现该逻辑。
key = 'wheat'if key in counters: count = counters[key]else: count = 0counters[key] = count + 1
这有个办法也能实现相同的功能,就是利用KeyError异常。如果程序抛出了这个异常,那说明要获取的键不在字典里。 这个写法比刚才的简单,因为只需要访问一次键名就可以了。
key = 'brioche'try: count = counters[key]except KeyError: count = 0counters[key] = count + 1
获取字典中存在的键,或给字典中不存在的键指定默认值,这两种操作非常常见。 Python的内置字典dict提供了get方法,可以指定键不存在时返回的默认值。 这种写法也只需要在查询键值时访问一次键名,然后做一次赋值操作,但要比刚才那种通过KeyError实现的方案简单得多。
count = counters.get(key, 0)counters[key] = count + 1
对于通过in表达式与KeyError实现的那两种方案来说,确实可以通过各种技巧来简化代码,但不管怎样简化,都无法完全消除重复赋值。所以,优先考虑用get方法来实现,因为in方案与KeyError方案无论如何读比它复杂。
if key not in counters: counters[key] = 0counters[key] += 1if key in counters: counters[key] += 1else: counters[key] = 1try: counters[key] += 1except KeyError: counters[key] = 1
如果字典里保存的数据比较复杂,比如列表,那该怎么办?例如,这次不仅要记录每种面包得的得票数,而且要记录投票的人。那可以像下面这样,把面包的名称(key)跟一份列表关联起来,而那份列表指的就是喜欢该面包的人。
votes = { 'baguette': ['Bob', 'Alice'], 'ciabatta': ['Coco', 'Deb'],}key = 'brioche'who = 'Elmer'if key in votes: names = votes[key]else: votes[key] = names = []names.append(who)
在采用in表达式实现的方案里,如果key已经存在,那么需要访问两次。一次在if语句里,另一次是在获取投票人列表的那条names = votes[key]语句里。
如果key不存在,那就只要在if语句中访问一次,然后在else分支中赋值一次值。这和上面那个单纯统计得票数的例子不同,这次如果发现键名不存在,那么只需要把空白的列表与这个键关联起来就行了。
votes[key] = names = []既可以把空白列表赋给names变量,又可以把这份列表与key相关联,这两项操作,只需要一行语句即可表达出来。 把空白列表(默认值)插入字典后,不需要再用另一条赋值语句给其中的某个元素赋值,一维可以直接在指向这份列表的names变量上调用append方法把投票人的名字添加进去。
还可以利用KeyError异常来实现。
key = 'rye'who = 'Felix'try: names = votes[key]except KeyError: votes[key] = names = []names.append(who)
同样,这个列子也能通过get方法改写。这样的话,如果键存在,只需要访问一次键名;如果不存在,那么还要在if块中用键名key作为下标赋一次值。
key = 'wheat'who = 'Gertrude'names = votes.get(key)if names is None: votes[key] = names = [] names.append(who)
这个方案中,无论votes.get(key)的结果是不是None,都要把这个结果赋给names变量,只不过在结果为None的时候,还需要在if块中做一些处理。这种逻辑用赋值表达式,参见第10条,改写可以再节省一行代码。
if (names := votes.get(key)) is None: votes[key] = names = []names.append(who)
dict类型提供了setdefault方法,能够继续简化代码。
key = 'cornbread'who = 'Kirk'names = votes.setdefault(key, [])names.append(who)
如果字典里本身有这个key,那么这个方法要做的,其实仅仅是返回相关的值而已,这时它不会set。
在字典里面没有这个键时,setdefault方法会把默认值直接放到字典里,而不是先给它做副本,然后把副本放到字典中。我们用下面这段代码演示一下默认值为列表时可能出现的问题。
data = {}key = 'foo'value = []data.setdefault(key, value)print('Before:', data)value.append('hello')print('After: ', data)
Before: {'foo': []}After: {'foo': ['hello']}
这意味着每次调用setdefault时都要构造一个新的默认值出来。这可能产生较大的性能开销。 回到之前那个只记录票数而不记录投票人的例子。那个例子为什么不用setdefault改写呢?比如,可以这样写:
key = 'dutch crunch'count = counters.setdefault(key, 0)counters[key] = count + 1
这样写的问题是,根本就没必要调用setdefault,因为不管字典里有没有这个键,我们都要递增它所对应的值。
count = counters.get(key, 0)counters[key] = count + 1
无论字典里有没有这个键,之前那种get方案只需要一次访问操作与一次赋值操作即可(如上代码,访问key,不存在即返回0,第二行赋值一次。),而目前的setdefault方案(在字典没有键的情况下)需要一次访问操作与两次赋值操作。
只有在少数几种情况下用setdefault处理缺失的键才是最简短的方式,例如:与键相关的默认值构造起来开销很低且可以变化,而且不用担心异常问题。在这种特殊的场合,可以用这个setdefault方案取代get方案。即便如此,一般也应该优先考虑用defaultdict取代dict。
# 17 用defaultdict处理内部状态中缺失的元素,而不要用setdefault
如果字典不是自己创建的,那么对其中缺失的键可以考虑用四种办法解决。在这四种办法中,get方法要胜过利用in表达式和KeyError异常来解决的那两种方法。对于某些用例,我们可能觉得setdefault应该是代码最简短的办法。
例如,要记录去过哪些国家,还要记录在每个国家中到过哪些城市。那可以用这样一个字典。
visits = { 'Mexico': {'Tulum', 'Puerto Vallarta'}, 'Japan': {'Hakone'},}
无论字典中有没有这个国家名,都可以用setdefault方案把新的城市添加到对应的集合里。
visits.setdefault('France', set()).add('Arles') # 代码简短if (japan := visits.get('Japan')) is None: # 这种代码就长多了 visits['Japan'] = japan = set()japan.add('Kyoto')
如果程序所访问的这个字典需要由你自己明确地创建,那又该怎么写?其实这种情况很常见,例如我们经常需要用字典实例来维护对象的内部状态。下面,我们写这样一个类,把刚才那个范例逻辑封装到辅助方法中,使用户可以调用该方法啦访问字典中保存的动态内部状态。
class Visits: def __init__(self): self.data = {} def add(self, country, city): city_set = self.data.setdefault(country, set()) city_set.add(city)
这个新类把刚才那套复杂的逻辑掩盖了起来,正确地调用了setdefault方法。
visits = Visits()visits.add('Russia', 'Yekaterinburg')visits.add('Tanzania', 'Zanzibar')print(visits.data)
{'Russia': {'Yekaterinburg'}, 'Tanzania': {'Zanzibar'}}
问题是,Visits.add方法还是写得不够理想,因为它还是调用了setdefault方法。这种写法也不够高效,因为每次调用add方法时,无论country参数所指定的国家名称是否存在,都必须构建新的set实例。
Python提供了defaultdict类,能轻松地实现出刚才那套逻辑。它会在键缺失的情况下,自动添加这个键以及键所对应的默认值。我们只需要在构造这种字典时提供一个函数即可。 每次发现键不存在时,该字典都会调用这个函数返回一份新的默认值。
from collections import defaultdictclass Visits: def __init__(self): self.data = defaultdict(set) def add(self, country, city): self.data[country].add(city)visits = Visits()visits.add('England', 'Bath')visits.add('England', 'London')print(visits.data)
defaultdict(
这次add方法相当简洁。
#18 学会利用__missing__构造依赖键的默认值
内置的dict类型提供了setdefault方法,在特殊场合可以用这个方法处理缺失的键。然后,对于一般情况,还是应该考虑使用defaultdict类型。当然,也有一些任务是这二者都处理不好的。
例如,我们要写一个程序,在文件系统里管理社交网络账号中的图片。这个程序应该用字典把这些图片的路径名跟相关的文件句柄关联起来,这样我们就能方便地读取并写入图像了。 下面先用普通的dict实例实现。
pictures = {}path = 'profile_1234.png'with open(path, 'wb') as f: f.write(b'image data here 1234')if (handle := pictures.get(path)) is None: try: handle = open(path, 'a+b') except OSError: print(f'Failed to open path {path}') raise else: pictures[path] = handlehandle.seek(0)image_data = handle.read()print(pictures)print(image_data)
{'profile_1234.png': <_io.BufferedRandom name='profile_1234.png'>}b'image data here 1234'
如果字典里已经有这个文件句柄,那么这种写法只需要进行一次字典访问。如果没有,那么它会通过get方法访问一次字典,然后在try/except/else结构的else分支中做一次赋值。读取数据的代码与打开文件并处理异常的代码可以分开写。
这套逻辑也能用in表达式或KeyError实现,但那两种方案的字典访问次数与代码嵌套层数都比较多。有人可能认为,既然这套逻辑能用get、in与KeyError这三种方案实现,那么也应该可以用setdefault方法来实现。
try: handle = pictures.setdefault(path, open(path, 'a+b'))except OSError: print(f'Failed to open path {path}') raiseelse: handle.seek(0) image_data = handle.read()
这样写有很多问题。首先,即使图片的路径已经在字典里了,程序还是的调用内置的open函数创建文件句柄。 另外,如果try块抛出异常,那我们可能无法判断这个异常是open函数导致的,还是setdefault方法导致的,因为这两次调用全部写在同一行代码里。
如果要把这套逻辑用作内部状态的管理,那么可能还会想到第五种方案,就是用defaultdict来记录跟踪这些图片。
= 'profile_4555.csv' with open(path, 'wb') as f: f.write(b'image data here 9239')from collections import defaultdictdef open_picture(profile_path): try: return open(profile_path, 'a+b') except OSError: print(f'Failed to open path {profile_path}') raisepictures = defaultdict(open_picture)handle = pictures[path]handle.seek(0)image_data = handle.read()
---------------------------------------------------------------------------TypeError Traceback (most recent call last)
出错了,原因在于,传给defaultdict的那个函数只能是不需要参数的函数,而我们写的辅助函数需要一个参数。defaultdict不知道当前要访问的这个键叫神马,所以无法给辅助函数传递这个参数。 此时,还有一种解决方案,通过继承dict类型并实现__missing__特殊方法来解决这个问题。我们可以把字典里不存在这个键时所要执行的逻辑写在这个方法中。
class Pictures(dict): def __missing__(self, key): value = open_picture(key) self[key] = value return valuepictures = Pictures()handle = pictures[path]handle.seek(0)image_data = handle.read()print(pictures)print(image_data)
{'profile_4555.csv': <_io.BufferedRandom name='profile_4555.csv'>}b'image data here 9239'
访问pictures[path]时,如果pictures字典里没有path这个键,那就调用__missing__方法。这个方法必须根据key参数创建一份新的默认值,系统会把这个默认值插入字典并返回给调用放。 以后再访问pictures[path],就不会调用__missing__了,因为字典里已经有了对应的键与值。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~