《Effective Python》笔记 第二章-列表与字典
阅读Effective Python(第二版)的一些笔记
第11条 学会对序列做切片
列表切片的格式:data[start:end]
,start和end可以理解为两个左右指针,需要满足一下条件:
- start和end可以是负数、0、整数,并且start、end的绝对值可以大于列表长度;
- start可以认为是左指针,end认为是右指针,左指针必须在右指针之前(可以重合);如果左指针在右指针的右边,始终返回空列表
- 截取时,会包含start指向的元素,忽略end所在的元素,也就是
[start, end)
切片的使用示例:
data = [2, 4, 6, 8, 10]
print(data) # [2, 4, 6, 8, 10]
print(data[:]) # [2, 4, 6, 8, 10]
print(data[0:]) # [2, 4, 6, 8, 10]
print(data[:len(data)]) # [2, 4, 6, 8, 10]
print(data[0:2]) # [2, 4]
print(data[2:4]) # [6, 8]
print(data[2: -1]) # [6, 8]
print(data[-4: -1]) # [4, 6, 8]
print(data[-9: 10]) # [2, 4, 6, 8, 10]
start跑到end后面的例子:
data = [2, 4, 6, 8, 10]
print(data[3: 2]) # []
print(data[-3: -4]) # []
切片生成的list与原list无关,修改切片生成的list不会影响原list,示例如下:
data = [2, 4, 6, 8, 10]
# 切片生成的数据与原列表无关
d = data[0: 2]
print(d) # [2, 4]
d[0] = 10
print(d) # [10, 4]
print(data) # [2, 4, 6, 8, 10]
第12条 不要在切片里同时指定起止下标与步进
data[start:end]
使用切片生成list时,默认是将start~end之间的元素挨个选出来;如果要中间隔一个选一个,也就是指定步长,切片也是支持的,也就是data[start:end:step]
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(data[0: len(data)]) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(data[0: len(data): 1]) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(data[0: len(data): 2]) # [1, 3, 5, 7, 9]
print(data[0: len(data): 3]) # [1, 4, 7, 10]
上面的代码比较好理解,但是当start不为0的时候,就稍微有点绕了,比如下面几个例子:
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(data[3:9:2]) # [4, 6, 8]
print(data[3:9:3]) # [4, 7]
print(data[-10:100:3]) # [1, 4, 7, 10]
print(data[-4:100:3]) # [7, 10]
从上面例子可以看出,如果指定的start为正数还好,但是如果是start是负数,那就有点绕了。
所以在既指定起止下标,且指定步进时,最好拆分为两步,先根据起止下标做一次拷贝生成list,然后再对生成的list做步长的切片,也就是下面这样:
d1 = data[3: 9]
d2 = d1[::3]
print(d2) # [4, 7]
当然拆分为两步会增加一下内存和时间的开销,因为需要生成一个中间的临时列表,如果内存够用且时间要求不高的情况下,尽量拆分为两步进行。
第13条 通过带星号的unpacking操作来捕获多个元素,不要用切片
unpacking时,需要左边的参数和右边的数据相匹配,如果不匹配就会异常,示例如下:
data = [1, 2]
a, b = data
print("a:{}, b:{}".format(a, b)) # a:1, b:2
data = [1, 2, 3]
# 接收的数量比提供的数量少,异常
# a, b = data # ValueError: too many values to unpack
# 接收数量比提供的数量多,异常
# a, b, c, d = data # ValueError: need more than 3 values to unpack
当左边的变量比右边提供的参数少的时候,要想让最后一个变量包含剩余的所有参数,可以使用切片来实现,比如下面这样:
data = [1, 2, 3, 4]
a, b = data[:2]
c = data[2:]
print("a:{}, b:{}, c:{}".format(a, b, c))
# a:1, b:2, c:[3, 4]
上面的做法,在Python3中有另外一种写法:带星号的unpacking,示例如下:
data = [1, 2, 3, 4]
a, b, *c = data
print("a:{}, b:{}, c:{}".format(a, b, c)) # a:1, b:2, c:[3, 4]
a, *b, c = data
print("a:{}, b:{}, c:{}".format(a, b, c)) # a:1, b:[2, 3], c:4
*a, b, c = data
print("a:{}, b:{}, c:{}".format(a, b, c)) # a:[1, 2], b:3, c:4
*a, b, c, d = data
print("a:{}, b:{}, c:{}, d:{}".format(a, b, c, d)) # a:[1], b:2, c:3, d:4
*a, b, c, d, e = data
print("a:{}, b:{}, c:{}, d:{}, e:{}".format(a, b, c, d, e)) # a:[], b:1, c:2, d:3, e:4
*a, b, c, d, e, f = data
print("a:{}, b:{}, c:{}, d:{}, e:{}, f:{}".format(a, b, c, d, e, f))
# ValueError: not enough values to unpack (expected at least 5, got 4)
使用带星号的unpacking时:
- 会先将右边的元素先分给左边不带星号的变量,然后将剩余的元素形成一份list给带星号的变量
- 这种形式不能同时有多个带星号的变量,因为剩余元素生成list,怎么分给多个带星号变量时不确定的。
第14条 用sort方法的key参数来表示复杂的排序逻辑
list列表的sort方法默认会将数字按照从小到大排序,示例如下;
data = [4, 2, 5, 1, 8, 7, 9]
data.sort()
print(data) # [1, 2, 4, 5, 7, 8, 9]
如果列表的元素不是数字,而是一些自定义的对象,那么调用sort方法是没有效果的,示例如下:
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return "Person={name:%s, age:%s}" % (self.name, self.age)
__repr__ = __str__
data = [
Person("abc", 66),
Person("xyz", 55),
Person("qwq", 99),
]
print(data[0])
data.sort()
print(data)
# [Person={name:abc, age:66}, Person={name:xyz, age:55}, Person={name:qwq, age:99}]
对于自定义的对象排序,就需要自定义排序的规则,比如比较对象时,怎么确定哪个对象排在前面,那个对象排在
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return "Person={name:%s, age:%s}" % (self.name, self.age)
__repr__ = __str__
data = [
Person("abc", 66),
Person("xyz", 55),
Person("qwq", 99),
]
print(data[0])
data.sort()
print(data)
# [Person={name:abc, age:66}, Person={name:xyz, age:55}, Person={name:qwq, age:99}]
# key指定比较的字段
data.sort(key=lambda p: p.age, reverse=True)
print(data)
# [Person={name:qwq, age:99}, Person={name:abc, age:66}, Person={name:xyz, age:55}]
第15条 不要过分依赖给字典添加条目时所用的顺序
Python3之前的dict,默认是不保证存取顺序的,也就是说放入的顺序和遍历dict时获取的元素不一定相同;
Python3.7及以后,dict会保证遍历的顺序和放入的顺序相同;
示例如下:
data_1 = {
"abc": "1111",
"xyz": "2222",
"qwq": "3333"
}
print(data_1)
# Python 2
# {'xyz': '2222', 'abc': '1111', 'qwq': '3333'}
# Python 3
# {'abc': '1111', 'xyz': '2222', 'qwq': '3333'}
需要注意的是,python3.8保证dict元素的顺序,这个顺序始终和放入的顺序相同,即使中间更新了value,顺序也不会变,示例如下:
data_1 = {
"abc": "1111",
"xyz": "2222",
"qwq": "3333"
}
print(data_1)
# Python 3
# {'abc': '1111', 'xyz': '2222', 'qwq': '3333'}
data_1["abc"] = "4444"
print(data_1)
# Python 3
# {'abc': '4444', 'xyz': '2222', 'qwq': '3333'}
Python的collections模块有一个OrderedDict类,这个类可以保证遍历顺序和放入的顺序相同
from collections import OrderedDict
ordered_dict = OrderedDict()
ordered_dict.setdefault("abc", "1111")
ordered_dict.setdefault("xyz", "2222")
ordered_dict.setdefault("qwq", "3333")
print(ordered_dict)
# OrderedDict([('abc', '1111'), ('xyz', '2222'), ('qwq', '3333')])
ordered_dict = OrderedDict()
# 传入的dict内部顺序由Python版本确定,python3.7之前无序,python3.7及以后有序
ordered_dict.update({"bbb": "222", "aaa": "111"})
ordered_dict.update({"ccc": "333", "ddd": "444"})
print(ordered_dict)
# python 2
# OrderedDict([('aaa', '111'), ('bbb', '222'), ('ccc', '333'), ('ddd', '444')])
# python 3
# OrderedDict([('bbb', '222'), ('aaa', '111'), ('ccc', '333'), ('ddd', '444')])
第16条 用get处理键不在字典中的情况,不要使用in与KeyError
python中,dict支持两种形式来访问,dict["key"] 和 dict.get("key")的形式,这两种形式有区别:
- 对于dict["key"]这种形式,如果dict中没有key,那么就会报KeyError;这个时候可以使用in或者捕获KeyError异常来处理,示例如下
data = {}
name = data["name"]
if name:
print("has name")
else:
print("empty name")
# 报错
# KeyError: 'name'
# 使用in判断dict中是否有key
if "name" in data:
print("has name")
else:
print("empty name")
# 利用捕获KeyError异常来看是否有key
name = None
try:
name = data["name"]
except KeyError:
pass
上面这几种形式都不推荐使用,而是推荐使用dict.get()的形式。
对于dict.get("key")这种形式,如果dict中没有key,则会返回None;这种形式可以支持设置默认值,也就是不存在key时,返回的默认值,示例如下:
data = {}
name = data.get("name")
print(name) # None
name = data.get("name", "default_name_val")
print(name) # default_name_val
第17条 用defaultdict处理内部状态中缺失的元素,而不要用setdefault
有时候会有这种操作:如果dict中没有key对应的值,就设置一个值;如果有key的value,就返回key对应的value,利用上面的get可以这么做:
data = {}
name = data.get("name")
if name:
print("name:{}".format(name))
else:
print("设置默认值")
data["name"] = "abc"
上面可以用下面两种方法进行优化:
data = {}
# 用赋值表达式来减少一行代码
if name := data.get("name"):
print("name:{}".format(name))
else:
data["name"] = "abc"
# setdefault(key, default_value),如果存在则直接返回key对应的value
# 如果key不存在,则将key-default_value加入dict,并返回default_val
name = data.setdefault("name", "abc")
print(name) # abc
print(data) # {'name': 'abc'}
上面如果使用setdefault方式时,如果dict的value是集合,比如list类型,那么传入的时候,使用不当的话,可能就会出现问题,比如下面的例子:
data = {}
empty_list = []
order_list = data.setdefault("order", empty_list)
order_list.append(1)
print(order_list) # [1]
index_list = data.setdefault("index", empty_list)
index_list.append(2)
print(index_list) # [1, 2]
order_list = data.get("order")
print(order_list) # [1, 2]
由于上面当key不存在的时候,设置的默认值是一个共用的列表变量,所以就会被多处修改,造成问题;当然可以在setdefault的时候设置默认值为空列表就可以了,比如下面这样:
data = {}
order_list = data.setdefault("order", [])
order_list.append(1)
print(order_list) # [1]
index_list = data.setdefault("index", [])
index_list.append(2)
print(index_list) # [2]
order_list = data.get("order")
print(order_list) # [1]
上面虽然可以解决问题,但是不太方便,因为有时候设置的默认只不是一个空的集合,而是一个有数据的集合或者对象,这样的话就需要在setdefault的时候手动写每个默认值的生成方式,如下:
data = {}
# 构建默认数据的函数
def build_default_list():
return [1, 2, 3]
order_list = data.setdefault("order", build_default_list())
print(order_list) # [1, 2, 3]
index_list = data.setdefault("index", build_default_list())
print(index_list) # [1, 2, 3]
order_list.append(4)
print(order_list) # [1, 2, 3, 4]
print(index_list) # [1, 2, 3]
而另外一种方式时使用defaultdict,示例如下:
from collections import defaultdict
data = defaultdict(list) # 传入list表示key不存在时,返回一个空list
# 使用defaultdict的话,不要使用get的形式,因为这样默认值不会生效,应该使用["key"]的形式
print(data.get("index"))
print(data.get("order"))
# 注意defaultdict使用["key"]访问时,如果key不存在,是不会报异常的
# print(data["index"]) # []
# print(data["order"]) # []
自定义生成默认值的例子如下:
from collections import defaultdict
# 生成默认值的方法
def build_default_value():
return [1, 2, 3]
data = defaultdict(build_default_value) # 设置key不存在时,生成value的func
index_list = data["index"]
print(index_list) # [1, 2, 3]
order_list = data["order"]
print(order_list) # [1, 2, 3]
order_list.append(4)
print(order_list) # [1, 2, 3, 4]
print(index_list) # [1, 2, 3]
第18条 学会利用__missing__构造依赖键的默认值
第17条,使用setdefault或者defaultdict都可以实现当key不存在时设置默认值,给出的示例是设置固定的默认值(所有key对应的value一样),如果要根据key进行动态的设置默认值也是可以的,比如下面这样:
data = {}
def build_default_by_key(k):
return k * 3
key = "abc"
value = data.setdefault(key, build_default_by_key(key))
print(value) # abcabcabc
但是使用defaultdict的时候,就会有问题了:
from collections import defaultdict
def build_default_by_key(k):
return k * 3
data = defaultdict(build_default_by_key)
value = data["abc"] # TypeError: build_default_by_key() takes exactly 1 argument (0 given)
print(value)
使用defaultdict报错了,这是defaultdict接受的是无参func(也就是说调用func时没有入参),但是执行的时候build_default_by_key却需要一个入参,所以导致异常。
所以defaultdict不适合这种场景,setdefault还是勉强可以使用的,另外一种方式可以实现这个目的,就是使用对象的__missing__()方法,
对于__missing__方法,官方手册的介绍:此方法由dict.__getitem__()在找不到字典中的键时调用以实现 dict 子类的 self[key]
。可能有点难理解,用下面的示例比较好理解:
# 继承自dict
class MyDict(dict):
# 重写__missing__方法,key就是dict[key],当key不在dict中时,就会执行__missing__
def __missing__(self, key):
print("key:{} missing".format(key))
value = self.build_default_by_key(key)
self[key] = value
return value
@classmethod
def build_default_by_key(cls, key):
return key * 3
d = MyDict()
print(d["abc"])
# key:abc missing
# abcabcabc
print(d["xyz"])
# key:xyz missing
# xyzxyzxyz
print(d["abc"])
# abcabcabc