Effective Python读书笔记
第二章 列表与字典
第11条 学会对序列做切片
凡是实现了getitem和setitem这这两个魔法方法的类都可以切割(参见第43条)
基本语法somelist[start:end],从start开始一直取到end这个位置,但不包含end本身的元素。注意正数是从零开始,复数是从-1开始,不是从零开始。
a[:] # ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
a[:5] # ['a', 'b', 'c', 'd', 'e']
a[:-1] # ['a', 'b', 'c', 'd', 'e', 'f', 'g']
a[4:] # ['e', 'f', 'g', 'h']
a[-3:] # ['f', 'g', 'h']
a[2:5] # ['c', 'd', 'e']
a[2:-1] # ['c', 'd', 'e', 'f', 'g']
a[-3:-1] # ['f', 'g']
切割时的下标和上标可以越界程序也不会报错,那样就会取出整个列表。切割会新建一个列表,这样的话修改切割的列表就不会影响原列表的值。但是如果在赋值符号=的左侧使用切割,代表对原列表指定位置进行修改。而且修改的内容与要修改的列表长度不用相同。
a = [1,2,3,4,5,6]
b = a[:3]
b[0] = 0
print(b) #[0, 2, 3]
print(a) #[1, 2, 3, 4, 5, 6]
print(id(b)) #1933360403648
print(id(a)) #1934043559488
a[:2] = [11,22,33,44,55]
print(a) #[11, 22, 33, 44, 55, 3, 4, 5, 6]
切片和单纯的赋值是不一样的,单纯的赋值两个变量仍然指向同一个对象,切片是新建一个对象
a = [1,2,3,4,5,6]
c = a[:]
d = a
print(id(d)) #2230032179328
print(id(c)) #2230715256000
print(id(a)) #2230032179328
第12条 不要在切片里同时指定起止下标与步进
python有一种特殊的步进切片方式,也就是somelist[start : end : stride]。注意这里stride不仅可以是整数,还可以是负数哦!如果是负数的话就代表从后往前取。负数的stride让程序变得不易读,所以尽量不要用负数形式的stride。
x = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
x[::2] # ['a', 'c', 'e', 'g']
x[::-2] # ['h', 'f', 'd', 'b']
x[2::2] # ['c', 'e', 'g']
x[-2::-2] # ['g', 'e', 'c', 'a']
x[-2:2:-2] # ['g', 'e']
x[2:2:-2] # []
第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, *others = car_ages_descending
print(oldest, second_oldest, others) #20 19 [15, 9, 8, 7, 6, 4, 1, 0]
oldest, *others, youngest = car_ages_descending
print(oldest, youngest, others) #20 0 [19, 15, 9, 8, 7, 6, 4, 1]
*others, second_youngest, youngest = car_ages_descending
print(youngest, second_youngest, others) #0 1 [20, 19, 15, 9, 8, 7, 6, 4]
拆分数据结构并把其中的数据赋给变量时,可以用带星号的表达式,将结构中无法与普通变量向匹配的内容捕获到一份列表里。
第14条 用sort方法的key参数来表示复杂的排序逻辑
对于一个整数列表对象,我们可以很容易的使用sort()方法进行排序,因为很显然我们只能根据数字的大小进行排序。但是对于一个复杂的对象,该对象中包含各种属性,我们又该怎么进行排序呢?我们依然使用sort()方法进行排序,但是使用sort()方法时要指定key参数,该参数表示我们要根据该对象的什么属性进行排序。
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),
]
print('Unsorted:', repr(tools)) #Unsorted: [Tool('level', 3.5), Tool('hammer', 1.25), Tool('screwdriver', 0.5), Tool('chisel', 0.25)]
tools.sort(key=lambda x: x.name)
print('\nSorted: ', tools) #Sorted: [Tool('chisel', 0.25), Tool('hammer', 1.25), Tool('level', 3.5), Tool('screwdriver', 0.5)]
上面的代码中创建了一个Tool类,Tool中包含了两个属性,所以我们在对包含该类的列表使用sort()方法进行排序时,需要使用key参数,我们将Tool类中的name属性传给这个参数,然后就可以对这个列表进行排序啦!如果我们不告诉它利用name属性进行排序,sort()方法自己不知道按照什么进行排序。注意:传给这个key的必须是一个可以比较的值,具备自然顺序的值,不然你传给它一个不能够比较的值,还是没有用啊!
利用这个方法,我们还可以实现简化代码的操作哦!例如对于一个包含大小写的字符串列表,如果直接对它进行排序,它就会把大写字母排在前面。但是我们想把该列表中的字符串全部按照小写字符串进行排序该怎么做呢?
places = ['home', 'work', 'New York', 'Paris']
places.sort()
print('Case sensitive: ', places) #Case sensitive: ['New York', 'Paris', 'home', 'work']
places.sort(key=lambda x: x.lower())
print('Case insensitive:', places) #Case insensitive:['home', 'New York', 'Paris', 'work']
可以看到我们把places中的元素传给key,但是传的过程中做一下变化,把字符串全部变成小写就可以啦!这样是不会改变原数组中字符的大小写哦!
如果对于一个包含多个属性的复杂对象,我们想利用不止一个属性进行排序,这时候我们把传给key的值写成元组就好了。
power_tools.sort(key=lambda x: (x.weight, x.name))
这个时候对于各种属性的排序都是按照升序排列的哦!我们想按照降序排列怎么做咧?写一个负号就行啦!
power_tools.sort(key=lambda x: (-x.weight, x.name))
第15条 不要过分依赖给字典添加条目时所用的顺序
从python3.7开始,python语言规范正式确立:迭代字典时会按照给字典添加元素时的顺序进行迭代。之前的迭代是没有顺序的。函数中的可变长参数也遵循这一规则,这样的规则也更利于程序调试。
def my_func(**kwargs):
for key, value in kwargs.items():
print(f'{key} = {value}')
my_func(goose='gosling', kangaroo='joey')
必须注意必须是标准的dict对象才遵循这一规则,不是标准的dict对象可能就不遵循这一规则,例如一些我们自己定义的对象。
第16条 用get处理键不在字典中的情况,不要使用in与KeyError
获取字典中存在的键,或给字典中不存在的键指定默认值,是两种很常见的操作。所以,python内置的字典类型提供了叫做get()的方法,可以通过第一个参数指定自己想查的键,如果存在就返回该键对应的值。并通过第二个参数指定这个键不存在时应返回的默认值。
counters = {
'pumpernickel': 2,
'sourdough': 1,
}
key = 'multigrain'
count = counters.get(key, 0)
counters[key] = count + 1
print(counters)
可以看出上面代码中,字典counters不存在键multigrain,由于我们设置了第二个参数为0,所以会返回0,即count=0。注意第二个参数默认为None。
对于更加复杂的字典处理可能就稍加麻烦了。
votes = {
'baguette': ['Bob', 'Alice'],
'ciabatta': ['Coco', 'Deb'],
}
key = 'brioche'
who = 'Elmer'
w = votes.get(key,[])
w.append(who)
votes[key] = w
print(votes) #{'baguette': ['Bob', 'Alice'], 'ciabatta': ['Coco', 'Deb'], 'brioche': ['Elmer']}
注意:列表的append()方法和sort()方法一样,都是直接在列表上做操作,都没有返回值,所以对于append()操作,我们要单独写一行,然后再进行赋值操作。
针对上面的情况,python提供了更加简单的方法,即setdefault()方法。
votes = {
'baguette': ['Bob', 'Alice'],
'ciabatta': ['Coco', 'Deb'],
}
key = 'brioche'
who = 'Elmer'
names = votes.setdefault(key, [])
names.append(who)
print(votes) #{'baguette': ['Bob', 'Alice'], 'ciabatta': ['Coco', 'Deb'], 'brioche': ['Elmer']}
这个方法会查询这个字典里有没有这个键,如果没有这个键,就会把这个键和用户提供的默认值(第二个参数)关联起来,然后插入字典中,最后返回这个用户提供的默认值。注意,此时这个默认值已经和这个键关联起来并插入字典中了哦!setdefault方法一般不推荐使用。
第17条 用defaultdict处理内部状态中缺失的元素,而不要用setdefault
class Visits:
def __init__(self):
self.data = {}
def add(self, country, city):
city_set = self.data.setdefault(country, set())
city_set.add(city)
visits = Visits()
visits.add('Russia', 'Yekaterinburg')
visits.add('Tanzania', 'Zanzibar')
print(visits.data)
上面这个类实现了使用setdefault()方法对字典插入数据,setdefaul()方法对于插入数据不够高效,因为每次调用add方法时,无论country参数所指定的国家名称是否在字典里,都必须构造新的set实例。我们可以利用python内置的collections模块提供的defaultdict类解决这一问题。
from collections import defaultdict
class 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(<class 'set'>, {'England': {'London', 'Bath'}})
上面这个类实现了使用defauldict类对字典插入数据。它会在键确实的情况下,自动添加这个键以及键所对应的默认值,这个默认值是在我们创建这个字典时所指定的。
第18条 学会利用 _ _ missing _ _ 构造依赖键的默认值
第三章 函数
第19条 不要把函数返回的多个数值拆分到三个以上的变量中
对于函数返回多个值,实际上会返回一个元组,元组里面的各个元素就是返回的值。在返回多个值时,可以使用带*的unpacking机制捕获接收那些没有被普通变量捕获的且我们用不到的值(参见13条)。如果确实需要接收很多变量,最好使用nametuple(参见37条)。
第20条 遇到意外情况时应该抛出异常,不要返回None
因为Python对于0,空列表,空元组,空字典,None在使用 if not 语句时都会判断为真,所以尽量不要返回None,因为这样会和输出的0,空列表,空元组,空字典搞混。
def careful_divide(a, b):
try:
return a / b
except ZeroDivisionError:
return None
x, y = 1, 0
result = careful_divide(x, y)
if result is None:
print('Invalid inputs')
else:
print('Result is %.1f' % result)
x, y = 0, 5
result = careful_divide(x, y)
if not result:
print('Invalid inputs') # This runs! But shouldn't
else:
assert False
我们直接利用python提供的捕获异常机制来捕获异常,并且捕获异常后不要返回None,而是直接输出错误信息。这样函数在执行异常后就没有返回值了。
def careful_divide(a: float, b: float) -> float:
"""Divides a by b.
Raises:
ValueError: When the inputs cannot be divided.
"""
try:
return a / b
except ZeroDivisionError as e:
raise ValueError('Invalid inputs')
第21条 了解如何在闭包里面使用外围作用域中的变量
先看一下什么是闭包:闭包就是外部函数中定义一个内部函数,内部函数引用外部函数中的变量,外部函数的返回值是内部函数。
创建一个闭包需要满足以下几点:
- 必须有一个内嵌函数
- 内嵌函数必须引用外部函数中的变量
- 外部函数的返回值必须是内嵌函数
# 闭包
def out(i): # 一个外层函数
def inner(j): # 内层函数
return i*j
return inner
def sort_priority(values, group):
def helper(x):
if x in group:
return (0, x)
return (1, x)
values.sort(key=helper)
numbers = [8, 3, 1, 2, 5, 4, 7, 6]
group = {2, 3, 5, 7}
sort_priority(numbers, group)
print(numbers) #[2, 3, 5, 7, 1, 4, 6, 8]
这里用到了前面的知识哦!参见第14条。将values中的值传给helper函数,helper函数处理后再进行排序。helper函数会返回一个元组,先按照元组的第一个元素进行排序,所以是0的排在前面,再按照元组的第二个元素进行排序,所以小的排在前面。
def sort_priority2(numbers, group):
found = False # Scope: 'sort_priority2'
def helper(x):
if x in group:
found = True # Scope: 'helper' -- Bad!
return (0, x)
return (1, x)
numbers.sort(key=helper)
return found
对于闭包来说,函数作用域和函数内部函数的作用域是两个作用域,他俩是不互通的。如果想函数内部函数想使用函数作用域,需要在函数内部函数的变量前加上nonlocal,以此进行声明这个变量是外部的变量,否则它会在函数的函数内部新建一个和外部变量名一样的变量。
def sort_priority3(numbers, group):
found = False
def helper(x):
nonlocal found # Added
if x in group:
found = True
return (0, x)
return (1, x)
numbers.sort(key=helper)
return found
第22条 用数量可变的位置参数给函数设计清晰的参数列表
在python里,可以给最后一个位置参数前加前缀 *,这样调用者就只需要提供不带 * 的那些参数,然后可以不再传最后饿位置参数,也可以传任意的数量参数。
def log(message, *values): # The only difference
if not values:
print(message)
else:
values_str = ', '.join(str(x) for x in values)
print(f'{message}: {values_str}')
log('My numbers are', 1, 2) #My numbers are: 1, 2
log('Hi there') # Hi there
使用 * 传数量可变的位置参数,可能导致两个问题。第一个问题是,程序总是必须把这些参数转化为一个元组,然后才能把它们当成可选的位置参数传给函数(相当于把那些参数给打包好,传给最后带 * 的位置参数,然后这个位置参数前面的 * 对传入的元组再进行解包后交给程序执行)。将参数打包成元组会造成内存浪费(例如我们传入的是一个生成器)。第二个问题是,一旦我们给函数添加了新的位置参数,那么之前写的程序就全部不对了。
def my_generator():
for i in range(10):
yield i
def my_func(*args):
print(args)
it = my_generator()
my_func(*it)
注意这里将生成器作为参数时,要在实参前加一个 * ,我猜测这里是要先对生成器解包,即迭代生成器,产生一个元组,再传递给这个函数。
第23条 用关键字参数来表示可选的行为
函数可以使用位置传参,关键字传参,带的任意数量位置参数,带 * * 的任意数量关键字参数。这四种形式的传参方式都是可以互相混用的,但是注意传参时,不要传给同一个形参。* * 会把字典里面的键值以关键字参数的形式传给函数。
第24条 用None和docstring来描述默认值会变的参数
from time import sleep
from datetime import datetime
def log(message, when=datetime.now()):
print(f'{when}: {message}')
log('Hi there!') #2022-08-03 20:32:22.980219: Hi there!
sleep(0.1)
log('Hello again!') #2022-08-03 20:32:22.980219: Hello again!
我们发现这两次时间是一样的,其实我们想要的是每一次执行程序的时间,这显然不是我们想要的。为什么会出现这样的情况呢?因为默认参数只会绑定一次。在第一次执行程序后执行datatime.now(),以后就不会再执行了。
我们可以把默认参数指定为None,然后在程序中执行datatime.now()。
def log(message, when=None):
"""Log a message with a timestamp.
Args:
message: Message to print.
when: datetime of when the message occurred.
Defaults to the present time.
"""
if when is None:
when = datetime.now()
print(f'{when}: {message}')
log('Hi there!') #2022-08-03 20:37:52.569318: Hi there!
sleep(0.1)
log('Hello again!') #2022-08-03 20:37:52.679071: Hello again!
第25条 用只能以关键字指定和只能按位置传入的参数来设计清晰的参数列表
def safe_division_c(number, divisor, *, # Changed
ignore_overflow=False,
ignore_zero_division=False):
参数中的 * 表示 * 之前的是位置参数,* 之后的是关键字参数且只能是关键字参数,传参时必须按照关键字参数的方法进行传参。
def safe_division_e(numerator, denominator, /,
ndigits=10, *, # Changed
ignore_overflow=False,
ignore_zero_division=False):
参数中的 / 表示 /之前的参数必须按照位置传参,否则将会传参失败。位于 / 和 * 之间的参数既可以使用位置传参,也可以使用关键字传参,这就是和普通参数是一样的。
第26条 用functools.wraps定义函数修饰器
python中有一个特殊的写法,可以用修饰器来封装某个函数,从而让程序在执行这个函数之前与执行完这个函数之后,分别运行某些代码。这意味着,调用者传给函数的参数值,函数返回给调用者的值,以及函数抛出的异常,都可以由修饰器访问并修改。