流畅的Python (Fluent Python) —— 第二部分01
2.1 内置序列类型概览
Python 标准库用 C 实现了丰富的序列类型,列举如下。
容器序列
list、 tuple 和 collections.deque 这些序列能存放不同类型的数据。
扁平序列
str、 bytes、 bytearray、 memoryview 和 array.array,这类序列只能容纳一种类型。
序列类型还能按照能否被修改来分类。
可变序列
list、 bytearray、 array.array、 collections.deque 和 memoryview。
不可变序列
tuple、 str 和 bytes。
列表推导是构建列表(list)的快捷方式,而生成器表达式则可以用来创建其他任何类型的序列。
1 # 列表推导式,讲一个字符串变为Unicode码位的列表 2 # 列表推导式不会有变量泄露的问题 3 symbols = '!@#$%^&*' 4 codes = [ord(symbol) for symbol in symbols] 5 print(codes)
2.2.2 列表推导同filter和map的比较
filter 和 map 合起来能做的事情,列表推导也可以做,而且还不需要借助难以理解和阅读的 lambda 表达式。
1 # 功能:如果 ord(s) > 127 则 将 ord(s) 存入新列表 2 symbols = '$¢£¥€¤' 3 beyond_ascii1 = [ord(s) for s in symbols if ord(s) > 127] # 列表推导 4 print(beyond_ascii1) 5 6 beyond_ascii2 = list(filter(lambda c: c > 127, map(ord, symbols))) 7 print(beyond_ascii2) 8 ''' 9 map/filter实现同样的功能,但可能更慢 10 filter() 函数用于过滤序列,过滤掉不符合条件的元素,返回由符合条件元素组成的新列表。 11 该接收两个参数,第一个为函数,第二个为序列,序列的每个元素作为参数传递给函数进行判, 12 然后返回 True 或 False,最后将返回 True 的元素放到新列表中。 13 Python3.x 返回迭代器对象 14 map() 会根据提供的函数对指定序列做映射。 15 第一个参数 function 以参数序列中的每一个元素调用 function 函数, 16 返回包含每次 function 函数返回值的新列表。 17 lambda:匿名函数 18 '''
1 # 生成器表达式:遵守了迭代器协议,节约内存 2 # 特点:圆括号 3 symbols = '$¢£¥€¤' 4 t1 = tuple(ord(symbol) for symbol in symbols) 5 print(t1) 6 7 # 使用生成器表达式,初始化元组和数组 8 import array 9 # I 为参数 表明操作的是整数 C 为单个字符 10 a1 = array.array('I', (ord(symbol) for symbol in symbols)) 11 print(a1)
2.3.2 元组拆包
元组拆包可以应用到任何可迭代对象上,唯一的硬性要求是,被可迭代对象中的元素数量必须要跟接受这些元素的元组的空档数一致。除非我们用 * 来表示忽略多余的元素。
1 # 元组不仅仅是不可变列表 2 # 除了用作不可变的列表,它还可以用于没有字段名的记录。 3 lax_coordinates = (33.9425, -118.408056) # 存储经伟度 4 # 分别赋值(元组拆包) 5 city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014) 6 # 元组列表 7 traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'), ('ESP', 'XDA205856')] 8 for passport in sorted(traveler_ids): 9 print('%s/%s' % passport) 10 # 一个 % 就将 password 元组里的两个元素对应过去格式化字符串的空档中。
另外一个很优雅的写法当属不使用中间变量交换两个变量的值:
1 # 优雅写法,无需中间变量交换 2 a, b = 1, 2 3 b, a = a, b
还可以用 * 运算符把一个可迭代对象拆开作为函数的参数
1 print(divmod(20, 8)) 2 # 还可以用 * 运算符把一个可迭代对象拆开作为函数的参数 3 # 一点像指针? 4 t = (20, 8) 5 print(divmod(*t))
在 Python 中,函数用 *args 来获取不确定数量的参数算是一种经典写法了。于是 Python 3 里,这个概念被扩展到了平行赋值中:
1 # 在平行赋值中, * 前缀只能用在一个变量名前面,但是这个变量可以出现在赋值表达式的任意位置 2 a, *body, c, d = range(5) 3 print(a) 4 print(*body)
另外元组拆包还有个强大的功能,那就是可以应用在嵌套结构中。
2.3.3 嵌套元组拆包
1 metro_areas = [ 2 ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), # 每个元组内有 4 个元素,其中最后一个元素是一对坐标。 3 ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)), 4 ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)), 5 ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)), 6 ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)), 7 ] 8 print('{:15} | {:^9} | {:^9}'.format('', 'lat.', 'long.')) # {:^9}:中间对齐 (宽度为9) 9 fmt = '{:15} | {:9.4f} | {:9.4f}' # {:9.4f} 右对齐 (默认, 宽度为9),带符号保留小数点后四位 10 for name, cc, pop, (latitude, longitude) in metro_areas: # 我们把输入元组的最后一个元素拆包到由变量构成的元组里,这样就获取了坐标 11 if longitude <= 0: # if longitude <= 0: 这个条件判断把输出限制在西半球的城市 12 print(fmt.format(name, latitude, longitude))
2.3.4 具名元组
1 from collections import namedtuple 2 City = namedtuple('City', 'name country population coordinates') # 创建一个具名元组需要两个参数,一个是类名,另一个是类的各个字段的名字。后者可以是由数个字符串组成的可迭代对象,或者是由空格分隔开的字段名组成的字符串 3 tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667)) # 存放在对应字段里的数据要以一串参数的形式传入到构造函数中(注意,元组的构造函数却只接受单一的可迭代对象)。 4 print(tokyo) 5 print(tokyo.population) # 你可以通过字段名或者位置来获取一个字段的信息 6 print(tokyo.coordinates) 7 print(tokyo[0])
1 print(City._fields) # _fields 属性是一个包含这个类所有字段名称的元组。 2 LatLong = namedtuple('LatLong', 'lat long') 3 delhi_data = ('Delhi NCR', 'IN', 21.935, LatLong(28.613889, 77.208889)) 4 delhi = City._make(delhi_data) # 用 _make() 通过接受一个可迭代对象来生成这个类的一个实例,它的作用跟City(*delhi_data) 是一样的。 5 print(delhi._asdict()) # _asdict() 把具名元组以 collections.OrderedDict 的形式返回,我们可以利用它来把元组里的信息友好地呈现出来。 6 for key, value in delhi._asdict().items(): 7 print(key + ' : ', value)
现在我们知道了,元组是一种很强大的可以当作记录来用的数据类型。它的第二个角色则是充当一个不可变的列表。下面就来看看它的第二重功能。
除了跟增减元素相关的方法之外,元组支持列表的其他所有方法。还有一个例外,元组没有 __reversed__ 方法,但是这个方法只是个优化而已, reversed(my_tuple) 这个用法在没有 __reversed__ 的情况下也是合法的。
2.4 切片 :实际上切片操作比人们所想象的要强大很多。
2.4.1 为什么切片和区间会忽略最后一个元素
1. 当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素: range(3) 和 my_list[:3] 都返回 3 个元素。
2. 当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,用后一个数减去第一个下标(stop - start)即可。
3. 这样做也让我们可以利用任意一个下标来把序列分割成不重叠的两部分,只要写成my_list[:x] 和 my_list[x:] 就可以了。
2.4.2 对对象进行切片
我们还可以用 s[a:b:c] 的形式对 s 在 a 和 b 之间以 c 为间隔取值。 c 的值还可以为负,负值意味着反向取值。对 seq[start:stop:step] 进行求值的时候, Python 会调用seq.__getitem__(slice(start, stop, step))。
这时使用有名字的切片比用硬编码的数字区间要方便得多,注意示例里的 for 循环的可读性有多强。
1 invoice = """ 2 0.....6................................40........52...55........ 3 1909 Pimoroni PiBrella $17.50 3 $52.50 4 1489 6mm Tactile Switch x20 $4.95 2 $9.90 5 1510 Panavise Jr. - PV-201 $28.00 1 $28.00 6 1601 PiTFT Mini Kit 320x240 $34.95 1 $34.95 7 """ 8 SKU = slice(0, 6) 9 DESCRIPTION = slice(6, 40) 10 UNIT_PRICE = slice(40, 52) 11 QUANTITY = slice(52, 55) 12 ITEM_TOTAL = slice(55, None) 13 line_items = invoice.split('\n')[2:] 14 print(line_items) 15 for item in line_items: 16 print(item[UNIT_PRICE], item[DESCRIPTION]) 17 ''' 18 invoice.split('\n')[2:] 将 invoice 以 '\n' 来分隔成列表,[2:]表示除去前两行,一行是"""后面,一行是数字那行 19 执行 item[UNIT_PRICE] 时,读取 UNIT_PRICE ,则执行 slice(40, 52) 20 将 item 按照定义好的规则切片, 21 '''
如果把切片放在赋值语句的左边,或把它作为 del 操作的对象,我们就可以对序列进行嫁接、切除或就地修改操作。
2.5 对序列使用+和*
建立由列表组成的列表
有时我们会需要初始化一个嵌套着几个列表的列表,譬如一个列表可能需要用来存放不同的学生名单,或者是一个井字游戏板 上的一行方块。想要达成这些目的,最好的选择是使用列表推导。
1 board = [['_'] * 3 for i in range(3)] # 正确写法 2 print(board) 3 board[1][2] = 'X' 4 print(board) 5 6 # 这种方法是错误的 7 weird_board = [['_'] * 3] * 3 # 含有 3 个指向同一对象的引用的列表是毫无用处的 8 print(weird_board) 9 weird_board[0][1] = 'O' 10 print(weird_board) 11 # 错误就等同,追加同一个对象三次 12 row = ['_'] * 3 13 board = [] 14 for i in range(3): 15 board.append(row)
*= 在可变和不可变序列上的作用
1 l = [1, 2, 3] 2 print(id(l)) 3 l *= 2 4 print(id(l)) # 运用增量乘法后,列表的 ID 没变,新元素追加到列表上 5 # str 是个例外,因为字符串 += 太普遍了 所以CPython对其进行了优化,留出了可获扩展空间 6 t = (1, 2, 3) 7 print(id(t)) 8 t *= 2 9 print(id(t)) # 运用增量乘法后,新的元组被创建。
对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素
1 # 一个谜题 2 t = (1, 2, [30, 40]) 3 t[2] += [50, 60] 4 print(t) 5 # 1.不要把可变对象放在元组里 6 # 2.增量赋值不是一个原子操作。我们刚才也看到了,它虽然抛出了异常,但还是完成了操作。 7 # 3.查看 Python 的字节码并不难,而且它对我们了解代码背后的运行机制很有帮助