第2章 序列构成的数组
#《流畅的Python》读书笔记 # 第二部分 数据结构 # 第2章 序列构成的数组 # 我们把文本、列表和表格叫作数据火车……FOR命令通常能作用于数据火车上。 # 2.1 内置序列类型概览 # Python 标准库用 C 实现了丰富的序列类型,列举如下。 # 容器序列list、tuple 和 collections.deque这些序列能存放不同类型的数据。 # 扁平序列str、bytes、bytearray、memoryview和array.array,这类序列只能容纳一种类型。 # 容器序列存放的是它们所包含的任意类型的对象的引用,而扁平序列里存放的是值而不是引用。 # 序列类型还能按照能否被修改来分类。 # 可变序列:list、bytearray、array.array、collections.deque和memoryview。 # 不可变序列:tuple、str和bytes。 # 2.2 列表推导和生成器表达式 # 很多Python 程序员都把列表推导(list comprehension)简称为listcomps,生成式表达器(generator expression)则称为 genexps。 # 2.2.1 列表推导和可读性 #示例 2-1 把一个字符串变成 Unicode 码位的列表 >>> symbols = '$¢£¥€¤' >>> codes = [] >>> for symbol in symbols: codes.append(ord(symbol)) >>> codes [36, 162, 163, 165, 8364, 164] #示例 2-2 把字符串变成 Unicode 码位的另外一种写法 >>> symbols = '$¢£¥€¤' >>> codes = [ord(symbol) for symbol in symbols] >>> codes [36, 162, 163, 165, 8364, 164] # 列表推导、生成器表达式,以及同它们很相似的集合(set)推导和字典(dict)推导,在 Python 3 中都有了自己的局部作用域,就像函数似的。 # 表达式内部的变量和赋值只在局部起作用,表达式的上下文里的同名变量还可以被正常引用,局部变量并不会影响到它们。 #这是Python3代码: >>> x = 'ABC' >>> dummy = [ord(x) for x in x] >>> x # ➊ x 的值被保留了。 'ABC' >>> dummy # ➋ 列表推导也创建了正确的列表。 [65, 66, 67] # 2.2.2 列表推导同filter和map的比较 # filter 和 map 合起来能做的事情,列表推导也可以做,而且还不需要借助难以理解和阅读的 lambda 表达式。 #示例 2-3 用列表推导和 map/filter 组合来创建同样的表单 >>> symbols = '$¢£¥€¤' >>> beyond_ascii = [ord(s) for s in symbols if ord(s) > 127] >>> beyond_ascii [162, 163, 165, 8364, 164] >>> beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols))) >>> beyond_ascii [162, 163, 165, 8364, 164] #示例 2-4 使用列表推导计算笛卡儿积 >>> colors = ['black', 'white'] >>> sizes = ['S', 'M', 'L'] >>> tshirts = [(color, size) for color in colors for size in sizes] >>> tshirts [('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'), ('white', 'M'), ('white', 'L')] >>> for color in colors: for size in sizes: print((color, size)) ('black', 'S') ('black', 'M') ('black', 'L') ('white', 'S') ('white', 'M') ('white', 'L') >>> tshirts = [(color, size) for size in sizes for color in colors] >>> tshirts [('black', 'S'), ('white', 'S'), ('black', 'M'), ('white', 'M'), ('black', 'L'), ('white', 'L')] # 列表推导的作用只有一个:生成列表。如果想生成其他类型的序列,生成器表达式就派上了用场。 #示例 2-5 用生成器表达式初始化元组和数组 >>> symbols = '$¢£¥€¤' >>> tuple(ord(symbol) for symbol in symbols) (36, 162, 163, 165, 8364, 164) >>> import array >>> array.array('I', (ord(symbol) for symbol in symbols)) array('I', [36, 162, 163, 165, 8364, 164]) # array的构造方法需要两个参数,因此括号是必需的。array构造方法的第一个参数指定了数组中数字的存储方式。 #示例 2-6 使用生成器表达式计算笛卡儿积 >>> colors = ['black', 'white'] >>> sizes = ['S', 'M', 'L'] >>> for tshirt in ('%s %s' % (c, s) for c in colors for s in sizes): print(tshirt) black S black M black L white S white M white L # 2.3 元组不仅仅是不可变的列表 # 有些 Python 入门教程把元组称为“不可变列表”,然而这并没有完全概括元组的特点。 # 除了用作不可变的列表,它还可以用于没有字段名的记录。 # 鉴于后者常常被忽略,我们先来看看元组作为记录的功用。 # 2.3.1 元组和记录 # 元组其实是对数据的记录:元组中的每个元素都存放了记录中一个字段的数据,外加这个字段的位置。正是这个位置信息给数据赋予了意义。 # 如果只把元组理解为不可变的列表,那其他信息——它所含有的元素的总数和它们的位置——似乎就变得可有可无。但是如果把元组当作一些字段的集合,那么数量和位置信息就变得非常重要了。 #示例 2-7 把元组用作记录 >>> lax_coordinates = (33.9425, -118.408056) >>> city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014) >>> traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'),('ESP', 'XDA205856')] >>> for passport in sorted(traveler_ids): print('%s/%s' % passport) BRA/CE342567 ESP/XDA205856 USA/31195855 >>> for country, _ in traveler_ids: print(country) USA BRA ESP # ❶ 洛杉矶国际机场的经纬度。 # ❷ 东京市的一些信息: # ❸ 一个元组列表,元组的形式为 (country_code,passport_number)。 # ❹ 在迭代的过程中,passport 变量被绑定到每个元组上。 # ❺ % 格式运算符能被匹配到对应的元组元素上。 # ❻ for 循环可以分别提取元组里的元素,也叫作拆包(unpacking)。因为元组中第二个元素对我们没有什么用,所以它赋值给“_”占位符。 # 2.3.2 元组拆包 # 元组拆包可以应用到任何可迭代对象上,唯一的硬性要求是,被可迭代对象中的元素数量必须要跟接受这些元素的元组的空档数一致。 # 除非我们用 * 来表示忽略多余的元素,在“用 * 来处理多余的元素”一节里,我会讲到它的具体用法。 #最好辨认的元组拆包形式就是平行赋值,也就是说把一个可迭代对象里的元素,一并赋值到由对应的变量组成的元组中。 >>> lax_coordinates = (33.9425, -118.408056) >>> latitude, longitude = lax_coordinates # 元组拆包 >>> latitude 33.9425 >>> longitude -118.408056 # 另外一个很优雅的写法当属不使用中间变量交换两个变量的值: >>> b, a = a, b # 还可以用 * 运算符把一个可迭代对象拆开作为函数的参数: >>> divmod(20, 8) (2, 4) >>> t = (20, 8) >>> divmod(*t) (2, 4) >>> quotient, remainder = divmod(*t) >>> quotient, remainder (2, 4) #在进行拆包的时候,我们不总是对元组里所有的数据都感兴趣,_ 占位符能帮助处理这种情况,上面这段代码也展示了它的用法。 # os.path.split()函数就会返回以路径和最后一个文件名组成的元组(path, last_part): >>> import os >>> _, filename = os.path.split('/home/luciano/.ssh/idrsa.pub') >>> filename 'idrsa.pub' # 除此之外,在元组拆包中使用 * 也可以帮助我们把注意力集中在元组的部分元素上。 # 用*来处理剩下的元素 # 在 Python 中,函数用 *args 来获取不确定数量的参数算是一种经典写法了。 # 于是 Python 3 里,这个概念被扩展到了平行赋值中: >>> a, b, *rest = range(5) >>> a, b, rest (0, 1, [2, 3, 4]) >>> a, b, *rest = range(3) >>> a, b, *rest (0, 1, 2) >>> a, b, rest (0, 1, [2]) >>> a, b, *rest = range(2) >>> a, b, rest (0, 1, []) # 在平行赋值中,* 前缀只能用在一个变量名前面,但是这个变量可以出现在赋值表达式的任意位置: >>> a, *body, c, d = range(5) >>> a, body, c, d (0, [1, 2], 3, 4) >>> *head, b, c, d = range(5) >>> head, b, c, d ([0, 1], 2, 3, 4) # 2.3.3 嵌套元组拆包 # 外元组拆包还有个强大的功能,那就是可以应用在嵌套结构中。 # 接受表达式的元组可以是嵌套式的,例如 (a, b, (c, d))。 # 只要这个接受元组的嵌套结构符合表达式本身的嵌套结构,Python 就可以作出正确的对应。 # 示例 2-8 用嵌套元组来获取经度 metro_areas = [ ('Tokyo','JP',36.933,(35.689722,139.691667)), ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)), ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)), ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)), ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)), ] print('{:15} | {:^9} | {:^9}'.format('', 'lat.', 'long.')) fmt = '{:15} | {:9.4f} | {:9.4f}' for name, cc, pop, (latitude, longitude) in metro_areas: if longitude <= 0: print(fmt.format(name, latitude, longitude)) # ❶ 每个元组内有4个元素,其中最后一个元素是一对坐标。 # ❷ 我们把输入元组的最后一个元素拆包到由变量构成的元组里,这样就获取了坐标。 # ❸ if longitude <= 0: 这个条件判断把输出限制在西半球的城市。 # 示例 2-8 的输出是这样的: # | lat. | long. # Mexico City | 19.4333 | -99.1333 # New York-Newark | 40.8086 | -74.0204 # Sao Paulo | -23.5478 | -46.6358 # 2.3.4 具名元组 # collections.namedtuple 是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类——这个带名字的类对调试程序有很大帮助。 # 创建一个具名元组需要两个参数,一个是类名,另一个是类的各个字段的名字。 # 存放在对应字段里的数据要以一串参数的形式传入到构造函数中(注意,元组的构造函数却只接受单一的可迭代对象)。 #示例 2-9 定义和使用具名元组 >>> from collections import namedtuple >>> City = namedtuple('City', 'name country population coordinates') >>> tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667)) >>> tokyo City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722, 139.691667)) >>> tokyo.population 36.933 >>> tokyo.coordinates (35.689722, 139.691667) >>> tokyo[1] 'JP' # ❶ 创建一个具名元组需要两个参数,一个是类名,另一个是类的各个字段的名字。 # 后者可以是由数个字符串组成的可迭代对象,或者是由空格分隔开的字段名组成的字符串。 # ❷ 存放在对应字段里的数据要以一串参数的形式传入到构造函数中(注意,元组的构造函数却只接受单一的可迭代对象)。 # ❸ 你可以通过字段名或者位置来获取一个字段的信息。 # 除了从普通元组那里继承来的属性之外,具名元组还有一些自己专有的属性。 # 示例 2-10 中就展示了几个最有用的:_fields 类属性、类方法_make(iterable) 和实例方法 _asdict()。 # _fields 属性是一个包含这个类所有字段名称的元组。 #示例 2-10 具名元组的属性和方法(接续前一个示例) >>> City._fields #_fields 属性是一个包含这个类所有字段名称的元组。 ('name', 'country', 'population', 'coordinates') >>> LatLong = namedtuple('LatLong', 'lat long') >>> delhi_data = ('Delhi NCR', 'IN', 21.935, LatLong(28.613889, 77.208889)) >>> delhi = City._make(delhi_data) >>> delhi._asdict() OrderedDict([('name', 'Delhi NCR'), ('country', 'IN'), ('population', 21.935), ('coordinates', LatLong(lat=28.613889, long=77.208889))]) >>> for key, value in delhi._asdict().items(): print(key + ':', value) name: Delhi NCR country: IN population: 21.935 coordinates: LatLong(lat=28.613889, long=77.208889) # ❶ _fields 属性是一个包含这个类所有字段名称的元组。 # ❷ 用 _make() 通过接受一个可迭代对象来生成这个类的一个实例,它的作用跟 City(*delhi_data) 是一样的。 # ❸ _asdict() 把具名元组以 collections.OrderedDict 的形式返回,我们可以利用它来把元组里的信息友好地呈现出来。 # 2.3.5 作为不可变列表的元组 # 如果要把元组当作列表来用的话,最好先了解一下它们的相似度如何。 # 在表 2-1 中可以清楚地看到,除了跟增减元素相关的方法之外,元组支持列表的其他所有方法。 # 还有一个例外,元组没有 __reversed__ 方法,但是这个方法只是个优化而已,reversed(my_tuple) 这个用法在没有 __reversed__ 的情况下也是合法的。 # 每个 Python 程序员都知道序列可以用 s[a:b] 的形式切片,但是关于切片,我还想说说它的一些不太为人所知的方面。 # 2.4 切片 # 在 Python 里,像列表(list)、元组(tuple)和字符串(str)这类序列类型都支持切片操作,但是实际上切片操作比人们所想象的要强大很多。 # 这一节主要讨论的是这些高级切片形式的用法,它们的实现方法则会在第 10 章的一个自定义类里提到。 # 这么做主要是为了符合这本书的哲学:先讲用法,第四部分中再来讲如何创建新类。 #2.4.1 为什么切片和区间会忽略最后一个元素 # 在切片和区间操作里不包含区间范围的最后一个元素是 Python 的风格,这个习惯符合 Python、C 和其他语言里以 0 作为起始下标的传统。这样做带来的好处如下。 # 当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素:range(3) 和 my_list[:3] 都返回 3 个元素。 # 当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,用后一个数减去第一个下标(stop - start)即可。 # 这样做也让我们可以利用任意一个下标来把序列分割成不重叠的两部分,只要写成 my_list[:x] 和 my_list[x:] 就可以了,如下所示。 >>> l = [10, 20, 30, 40, 50, 60] >>> l[:2] # 在下标2的地方分割 [10, 20] >>> l[2:] [30, 40, 50, 60] >>> l[:3] # 在下标3的地方分割 [10, 20, 30] >>> l[3:] [40, 50, 60] #2.4.2 对对象进行切片 #我们还可以用s[a:b:c]的形式对s在a和b之间以c为间隔取值c的值还可以为负,负值意味着反向取值。 >>> s = 'bicycle' >>> s[::3] 'bye' >>> s[::-1] 'elcycib' >>> s[::-2] 'eccb' # a:b:c 这种用法只能作为索引或者下标用在 [] 中来返回一个切片对象:slice(a, b, c)。 # 在 10.4.1 节中会讲到,对seq[start:stop:step] 进行求值的时候,Python 会调用seq.__getitem__(slice(start, stop, step))。 #示例 2-11 纯文本文件形式的收据以一行字符串的形式被解析 invoice = """ ...0.....6.................................40........52...55........ ...1909 Pimoroni PiBrella $17.50 3 $52.50 ...1489 6mm Tactile Switch x20 $4.95 2 $9.90 ...1510 Panavise Jr. - PV-201 $28.00 1 $28.00 ...1601 PiTFT Mini Kit 320x240 $34.95 1 $34.95 ...""" SKU = slice(0, 6) DESCRIPTION = slice(6, 40) UNIT_PRICE = slice(40, 52) QUANTITY = slice(52, 55) ITEM_TOTAL = slice(55, None) line_items = invoice.split('\n')[2:] for item in line_items: print(item[UNIT_PRICE], item[DESCRIPTION]) # $17.50 Pimoroni PiBrella # $4.95 6mm Tactile Switch x20 # $28.00 Panavise Jr. - PV-201 # $34.95 PiTFT Mini Kit 320x240 # 在 10.4 节还有更多机会来了解切片(slice)对象。如果从 Python 用户的角度出发,切片还有个两个额外的功能:多维切片和省略表示法(...)。 # 2.4.3 多维切片和省略 # Python 内置的序列类型都是一维的,因此它们只支持单一的索引,成对出现的索引是没有用的。 # 省略(ellipsis)的正确书写方法是三个英语句号(...),而不是Unicdoe码位U+2026表示的半个省略号(...)。 #2.4.4 给切片赋值 >>> l = list(range(10)) >>> l [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> l[2:5] = [20, 30] >>> del l[5:7] >>> l [0, 1, 20, 30, 5, 8, 9] >>> l[3::2] = [11, 22] >>> l [0, 1, 20, 11, 5, 22, 9] >>> l[2:5] = 100 Traceback (most recent call last): File "<pyshell#148>", line 1, in <module> l[2:5] = 100 TypeError: can only assign an iterable >>> l[2:5] = [100] >>> l [0, 1, 100, 22, 9] # 2.5 对序列使用+和* # Python 程序员会默认序列是支持 + 和 * 操作的。 # 通常 + 号两侧的序列由相同类型的数据所构成,在拼接的过程中,两个被操作的序列都不会被修改,Python 会新建一个包含同样类型数据的序列来作为拼接的结果。 >>> l = [1, 2, 3] >>> l * 5 [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3] >>> 5 * 'abcd' 'abcdabcdabcdabcdabcd' # + 和 * 都遵循这个规律,不修改原有的操作对象,而是构建一个全新的序列。 # 比如,你想用my_list=[[]]*3来初始化一个由列表组成的列表,但是你得到的列表里包含的3个元素其实是3个引用,而且这3个引用指向的都是同一个列表。这可能不是你想要的效果 # 建立由列表组成的列表 # 有时我们会需要初始化一个嵌套着几个列表的列表,譬如一个列表可能需要用来存放不同的学生名单,或者是一个井字游戏板 上的一行方块。 # 想要达成这些目的,最好的选择是使用列表推导,见示例 2-12。 # 示例 2-12 一个包含 3 个列表的列表,嵌套的 3 个列表各自有 3 个元素来代表井字游戏的一行方块 # ➊ 建立一个包含 3 个列表的列表,被包含的 3 个列表各自有 3 个元素。打印出这个嵌套列表。 >>> board = [['_'] * 3 for i in range(3)] >>> board [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']] # ➋ 把第 1 行第 2 列的元素标记为 X,再打印出这个列表。 >>> board[1][2] = 'X' >>> board [['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']] # 示例 2-13 含有 3 个指向同一对象的引用的列表是毫无用处的 >>> weird_board = [['_'] * 3] * 3 >>> weird_board [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']] >>> weird_board[1][2] = 'O' >>> weird_board [['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']] #示例 2-13 犯的错误本质上跟下面的代码犯的错误一样: #追加同一个行对象(row)3 次到游戏板(board)。 >>> row=['_'] * 3 >>> board = [] >>> for i in range(3): board.append(row) #相反,示例 2-12 中的方法等同于这样做: >>> board = [] >>> for i in range(3): # ➊ 每次迭代中都新建了一个列表,作为新的一行(row)追加到游戏板(board)。 row=['_'] * 3 board.append(row) >>> board [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']] # ➋ 正如我们所期待的,只有第 2 行的元素被修改。 >>> board[2][0] = 'X' >>> board [['_', '_', '_'], ['_', '_', '_'], ['X', '_', '_']] # 2.6 序列的增量赋值 # 增量赋值运算符 += 和 *= 的表现取决于它们的第一个操作对象。 # += 背后的特殊方法是 __iadd__ (用于“就地加法”)。但是如果一个类没有实现这个方法的话,Python 会退一步调用 __add__ 。 # 考虑下面这个简单的表达式: # >>> a += b # 总体来讲,可变序列一般都实现了 __iadd__ 方法,因此 += 是就地加法。而不可变序列根本就不支持这个操作,对这个方法的实现也就无从谈起。 #接下来有个小例子,展示的是 *= 在可变和不可变序列上的作用: >>> l = [1, 2, 3] >>> id(l) # ❶ 刚开始时列表的 ID。 >>> l *= 2 >>> l [1, 2, 3, 1, 2, 3] >>> id(l) # ❷ 运用增量乘法后,列表的 ID 没变,新元素追加到列表上。 >>> t = (1, 2, 3) >>> id(t) # ❸ 元组最开始的 ID。 >>> t*=2 >>> id(t) # ❹ 运用增量乘法后,新的元组被创建。 # 对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。 # str 是一个例外,因为对字符串做 += 实在是太普遍了,所以 CPython 对它做了优化。为 str初始化内存的时候,程序会为它留出额外的可扩展空间,因此进行增量操作的时候,并不会涉及复制原有字符串到新位置这类操作。 # 一个关于+=的谜题 #示例 2-14 一个谜题 >>> t = (1, 2, [30, 40]) >>> t[2] += [50, 60] #到底会发生下面 4 种情况中的哪一种? # a. t 变成 (1, 2, [30, 40, 50, 60])。 # b. 因为 tuple 不支持对它的元素赋值,所以会抛出 TypeError 异常。 # c. 以上两个都不是。 # d. a 和 b 都是对的。 # 有读者提出,如果写成 t[2].extend([50, 60]) 就能避免这个异常。确实是这样,但这个例子是为了展示这种奇怪的现象而专门写的。 Traceback (most recent call last): File "<pyshell#190>", line 1, in <module> t[2] += [50, 60] TypeError: 'tuple' object does not support item assignment >>> t (1, 2, [30, 40, 50, 60]) # Python Tutor(http://www.pythontutor.com)是一个对 Python 运行原理进行可视化分析的工具。 # 至此我得到了 3 个教训。 # 不要把可变对象放在元组里面。 # 增量赋值不是一个原子操作。我们刚才也看到了,它虽然抛出了异常,但还是完成了操作。 # 查看 Python 的字节码并不难,而且它对我们了解代码背后的运行机制很有帮助。 #示例 2-16 s[a] = b 背后的字节码 >>> import dis >>> dis.dis('s[a] += b') LOAD_NAME 0 (s) LOAD_NAME 1 (a) DUP_TOP_TWO BINARY_SUBSCR LOAD_NAME 2 (b) INPLACE_ADD ROT_THREE STORE_SUBSCR LOAD_CONST 0 (None) RETURN_VALUE # 2.7 list.sort方法和内置函数sorted # list.sort 方法会就地排序列表,也就是说不会把原列表复制一份。 # 这也是这个方法的返回值是 None 的原因,提醒你本方法不会新建一个列表。 # 在这种情况下返回 None 其实是 Python 的一个惯例:如果一个函数或者方法对对象进行的是就地改动,那它就应该返回 None,好让调用者知道传入的参数发生了变动,而且并未产生新的对象。 # 例如,random.shuffle 函数也遵守了这个惯例。 # 与 list.sort 相反的是内置函数 sorted,它会新建一个列表作为返回值。 # 这个方法可以接受任何形式的可迭代对象作为参数,甚至包括不可变序列或生成器(见第 14 章)。 # 而不管 sorted 接受的是怎样的参数,它最后都会返回一个列表。 # 不管是 list.sort 方法还是 sorted 函数,都有两个可选的关键字参数。 # reverse如果被设定为 True,被排序的序列里的元素会以降序输出(也就是说把最大值当作最小值来排序)。这个参数的默认值是 False。 # key一个只有一个参数的函数,这个函数会被用在序列里的每一个元素上,所产生的结果将是排序算法依赖的对比关键字。 #下面通过几个小例子来看看这两个函数和它们的关键字参数: >>> fruits = ['grape', 'raspberry', 'apple', 'banana'] >>> sorted(fruits) ['apple', 'banana', 'grape', 'raspberry'] >>> fruits ['grape', 'raspberry', 'apple', 'banana'] >>> sorted(fruits, reverse=True) ['raspberry', 'grape', 'banana', 'apple'] >>> sorted(fruits, key=len) ['grape', 'apple', 'banana', 'raspberry'] >>> sorted(fruits, key=len, reverse=True) ['raspberry', 'banana', 'grape', 'apple'] >>> fruits ['grape', 'raspberry', 'apple', 'banana'] >>> fruits.sort() >>> fruits ['apple', 'banana', 'grape', 'raspberry'] # 2.8 用bisect来管理已排序的序列 # bisect 模块包含两个主要函数,bisect和insort,两个函数都利用二分查找算法来在有序序列中查找或插入元素 # 2.8.1 用bisect来搜索 # bisect(haystack, needle) 在haystack(干草垛)里搜索needle(针)的位置该位置满足的条件是,把 needle 插入这个位置之后,haystack 还能保持升序。 #示例 2-17 在有序序列中用 bisect 查找某个元素的插入位置 import bisect import sys HAYSTACK = [1, 4, 5, 6, 8, 12, 15, 20, 21, 23, 23, 26, 29, 30] NEEDLES = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31] ROW_FMT = '{0:2d} @ {1:2d} {2}{0:<2d}' def demo(bisect_fn): for needle in reversed(NEEDLES): position = bisect_fn(HAYSTACK, needle) # ❶ 用特定的 bisect 函数来计算元素应该出现的位置。 offset = position * ' |' # ❷利用该位置来算出需要几个分隔符号。 print(ROW_FMT.format(needle, position, offset)) # ❸ 把元素和其应该出现的位置打印出来。 if __name__ == '__main__': if sys.argv[-1] == 'left': # ❹ 根据命令上最后一个参数来选用 bisect 函数。 bisect_fn = bisect.bisect_left else: bisect_fn = bisect.bisect print('DEMO:', bisect_fn.__name__) # ❺ 把选定的函数在抬头打印出来。 print('haystack ->', ' '.join('%2d' % n for n in HAYSTACK)) demo(bisect_fn) # C:\Users\Administrator\AppData\Local\Programs\Python\Python37-32\python.exe D:/demo.py # DEMO: bisect_right # haystack -> 1 4 5 6 8 12 15 20 21 23 23 26 29 30 # 31 @ 14 | | | | | | | | | | | | | |31 # 30 @ 14 | | | | | | | | | | | | | |30 # 29 @ 13 | | | | | | | | | | | | |29 # 23 @ 11 | | | | | | | | | | |23 # 22 @ 9 | | | | | | | | |22 # 10 @ 5 | | | | |10 # 8 @ 5 | | | | |8 # 5 @ 3 | | |5 # 2 @ 1 |2 # 1 @ 1 |1 # 0 @ 0 0 # 其次,bisect函数其实是bisect_right函数的别名,后者还有个姊妹函数叫bisect_left。 # 它们的区别在于,bisect_left返回的插入位置是原序列中跟被插入元素相等的元素的位置,也就是新元素会被放置于它相等的元素的前面,而bisect_right返回的则是跟它相等的元素之后的位置。 # 这个细微的差别可能对于整数序列来讲没什么用,但是对于那些值相等但是形式不同的数据类型来讲,结果就不一样了。 # 比如说虽然 1 == 1.0 的返回值是 True,1 和 1.0 其实是两个不同的元素。 # C:\Users\Administrator\AppData\Local\Programs\Python\Python37-32\python.exe D:/demo.py left # DEMO: bisect_left # haystack -> 1 4 5 6 8 12 15 20 21 23 23 26 29 30 # 31 @ 14 | | | | | | | | | | | | | |31 # 30 @ 13 | | | | | | | | | | | | |30 # 29 @ 12 | | | | | | | | | | | |29 # 23 @ 9 | | | | | | | | |23 # 22 @ 9 | | | | | | | | |22 # 10 @ 5 | | | | |10 # 8 @ 4 | | | |8 # 5 @ 2 | |5 # 2 @ 1 |2 # 1 @ 0 1 # 0 @ 0 0 # 图 2-5:用 bisect_left 运行示例 2-17 得到的结果(跟图 2-4 对比可以发现,值 1、8、23、29 和 30 的插入位置变成了原序列中这些值的前面) #示例 2-18 根据一个分数,找到它所对应的成绩 >>> import bisect >>> def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'): i = bisect.bisect(breakpoints, score) return grades[i] >>> [grade(score) for score in [33, 99, 77, 70, 89, 90, 100]] ['F', 'A', 'C', 'C', 'B', 'A', 'A'] # 示例 2-18 里的代码来自 bisect 模块的文档(https://docs.python.org/3/library/bisect.html)。 # 2.8.2 用bisect.insort插入新元素 # 排序很耗时,因此在得到一个有序序列之后,我们最好能够保持它的有序。bisect.insort 就是为了这个而存在的。 # insort(seq, item)把变量item插入到序列seq中,并能保seq的升序顺序。 #示例 2-19 insort 可以保持有序序列的顺序 import bisect import random SIZE=7 random.seed(1729) my_list = [] for i in range(SIZE): new_item = random.randrange(SIZE*2) bisect.insort(my_list, new_item) print('%2d ->' % new_item, my_list) # C:\Users\Administrator\AppData\Local\Programs\Python\Python37-32\python.exe D:/demo.py # 10 -> [10] # 0 -> [0, 10] # 6 -> [0, 6, 10] # 8 -> [0, 6, 8, 10] # 7 -> [0, 6, 7, 8, 10] # 2 -> [0, 2, 6, 7, 8, 10] # 10 -> [0, 2, 6, 7, 8, 10, 10] # 2.9 当列表不是首选时 # 虽然列表既灵活又简单,但面对各类需求时,我们可能会有更好的选择。比如,要存放 1000 万个浮点数的话,数组(array)的效率要高 # 得多,因为数组在背后存的并不是 float 对象,而是数字的机器翻译,也就是字节表述。 # 这一点就跟 C 语言中的数组一样。再比如说,如果需要频繁对序列做先进先出的操作,deque(双端队列)的速度应该会更快。 # 2.9.1 数组 # 如果我们需要一个只包含数字的列表,那么 array.array 比 list 更高效。 # 数组支持所有跟可变序列有关的操作,包括 .pop、.insert 和.extend。 # 另外,数组还提供从文件读取和存入文件的更快的方法,如.frombytes 和 .tofile。 # Python 数组跟 C 语言数组一样精简。创建数组需要一个类型码,这个类型码用来表示在底层的 C 语言应该存放怎样的数据类型。 # 比如 b 类型码代表的是有符号的字符(signed char),因此 array('b') 创建出的数组就只能存放一个字节大小的整数,范围从 -128 到 127,这样在序列很大的时候,我们能节省很多空间。 # 而且 Python 不会允许你在数组里存放除指定类型之外的数据。 #示例 2-20 一个浮点型数组的创建、存入文件和从文件读取的过程 >>> from array import array # ❶ 引入 array 类型。 >>> from random import random # ❷ 利用一个可迭代对象来建立一个双精度浮点数组(类型码是 'd'),这里我们用的可迭代对象是一个生成器表达式。 >>> floats = array('d', (random() for i in range(10**7))) >>> floats[-1] # ❸ 查看数组的最后一个元素。 0.007768376737146143 >>> fp = open('floats.bin', 'wb') # ❹ 把数组存入一个二进制文件里。 >>> floats.tofile(fp) >>> fp.close() >>> floats2 = array('d') # ❺ 新建一个双精度浮点空数组。 >>> fp = open('floats.bin', 'rb') >>> floats2.fromfile(fp, 10**7) # ❻ 把 1000 万个浮点数从二进制文件里读取出来。 >>> fp.close() >>> floats2[-1] # ❼ 查看新数组的最后一个元素。 0.007768376737146143 >>> floats2 == floats # ❽ 检查两个数组的内容是不是完全一样。 True # 用array.fromfile从一个二进制文件里读出1000万个双精度浮点数只需要0.1秒,这比从文本文件里读取的速度要快60倍,因为后者会使用内置的float方法把每一行文字转换成浮点数。 # 另外,使用array.tofile写入到二进制文件,比以每行一个浮点数的方式把所有数字写入到文本文件要快7倍。 # 另外,1000万个这样的数在二进制文件里只占用80 000 000个字节(每个浮点数占用8个字节,不需要任何额外空间),如果是文本文件的话,我们需要181 515 739个字节。 # 另外一个快速序列化数字类型的方法是使用pickle(https://docs.python.org/3/library/pickle.html)模块。 # pickle.dump 处理浮点数组的速度几乎跟 array.tofile 一样快。 # 不过前者可以处理几乎所有的内置数字类型,包含复数、嵌套集合,甚至用户自定义的类。前提是这些类没有什么特别复杂的实现。 # 2.9.2 内存视图 # memoryview 是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片。 # memoryview 的概念受到了 NumPy 的启发(参见2.9.3 节)。 # 示例 2-21 通过改变数组中的一个字节来更新数组里某个元素的值 # memoryview是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片。 # memoryview.cast 的概念跟数组模块类似,能用不同的方式读写同一块内存数据,而且内容字节不会随意移动。 # 这听上去又跟 C 语言中类型转换的概念差不多。memoryview.cast 会把同一块内存里的内容打包成一个全新的 memoryview 对象给你。 # 在示例 2-21 里,我们利用 memoryview 精准地修改了一个数组的某个字节,这个数组的元素是 16 位二进制整数。 # 示例 2-21 通过改变数组中的一个字节来更新数组里某个元素的值 >>> import array >>> numbers = array.array('h', [-2, -1, 0, 1, 2]) # ❶ 利用含有 5 个短整型有符号整数的数组(类型码是 'h')创建一个memoryview。 >>> memv = memoryview(numbers) >>> len(memv) # ❷ memv 里的 5 个元素跟数组里的没有区别。 >>> memv[0] -2 # ❸ 创建一个 memv_oct,这一次是把 memv 里的内容转换成 'B' 类型,也就是无符号字符。 >>> memv_oct = memv.cast('B') # ❹ 以列表的形式查看 memv_oct 的内容。 >>> memv_oct.tolist() [254, 255, 255, 255, 0, 0, 1, 0, 2, 0] # ❺ 把位于位置 5 的字节赋值成 4。 >>> memv_oct[5] = 4 >>> numbers # ❻ 因为我们把占 2 个字节的整数的高位字节改成了 4,所以这个有符号整数的值就变成了 1024。 array('h', [-2, -1, 1024, 1, 2]) # 2.9.3 NumPy和SciPy # 凭借着 NumPy 和 SciPy 提供的高阶数组和矩阵操作,Python 成为科学计算应用的主流语言。 # SciPy 是基于 NumPy 的另一个库,它提供了很多跟科学计算有关的算法,专为线性代数、数值积分和统计学而设计。 # SciPy 的高效和可靠性归功于其背后的 C 和 Fortran 代码,而这些跟计算有关的部分都源自于# SciPy 的高效和可靠性归功于其背后的 C 和 Fortran 代码,而这些跟计算有关的部分都源自于 #示例 2-22 对 numpy.ndarray 的行和列进行基本操作 # ❶ 安装 NumPy 之后,导入它(NumPy 并不是 Python 标准库的一部分)。 >>> import numpy # ❷ 新建一个 0~11 的整数的 numpy.ndarry,然后把它打印出来。 >>> a = numpy.arange(12) >>> a array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) >>> type(a) <class 'numpy.ndarray'> # ❸ 看看数组的维度,它是一个一维的、有 12 个元素的数组。 >>> a.shape (12,) # ❹ 把数组变成二维的,然后把它打印出来看看。 >>> a.shape = 3, 4 >>> a array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]]) # ❺ 打印出第 2 行。 >>> a[2] array([ 8, 9, 10, 11]) # ❻ 打印第 2 行第 1 列的元素。 >>> a[2,1] # ❼ 把第 1 列打印出来。 >>> a[:, 1] array([1, 5, 9]) # ❽ 把行和列交换,就得到了一个新数组。 >>> a.transpose() array([[ 0, 4, 8], [ 1, 5, 9], [ 2, 6, 10], [ 3, 7, 11]]) # NumPy 也可以对 numpy.ndarray 中的元素进行抽象的读取、保存和其他操作: >>> import numpy # ❶ 从文本文件里读取 1000 万个浮点数。 >>> floats = numpy.loadtxt('floats-10M-lines.txt') # ❷ 利用序列切片来读取其中的最后 3 个数。 >>> floats[-3:] array([ 3016362.69195522, 535281.10514262, 4566560.44373946]) # ❸ 把数组里的每个数都乘以 0.5,然后再看看最后 3 个数。 >>> floats *= .5 >>> floats[-3:] array([ 1508181.34597761, 267640.55257131, 2283280.22186973]) # ❹ 导入精度和性能都比较高的计时器(Python 3.3 及更新的版本中都有这个库)。 >>> from time import perf_counter as pc # ❺ 把每个元素都除以 3,可以看到处理 1000 万个浮点数所需的时间还不足 40 毫秒。 >>> t0 = pc(); floats /= 3; pc() - t0 0.03690556302899495 # ❻ 把数组存入后缀为 .npy 的二进制文件。 >>> numpy.save('floats-10M', floats) # ❼ 将上面的数据导入到另外一个数组里,这次 load 方法利用了一种叫作内存映射的机制,它让我们在内存不足的情况下仍然可以对数组做切片。 >>> floats2 = numpy.load('floats-10M.npy', 'r+') >>> floats2 *= 6 # ❽ 把数组里每个数乘以 6 之后,再检视一下数组的最后 3 个数。 >>> floats2[-3:] memmap([3016362.69195522, 535281.10514262, 4566560.44373946]) # NumPy 和 SciPy 的安装可能会比较费劲。 # 在“Installing the SciPyStack”(http://www.scipy.org/install.html)页面,SciPy.org 建议找一个科学计算 Python 的分发渠道帮忙,比如 Anacoda、EnthoughtCanopy、WinPython,等等。 # 因此,要详细介绍 NumPy 和SciPy 的话,不写成几本书是不可能的。虽然本书不在此列,但是如果要对 Python 的序列类型做一个概览,恐怕没有人能忽略 NumPy。 # 2.9.4 双向队列和其他形式的队列 # 利用 .append 和 .pop 方法,我们可以把列表当作栈或者队列来用(比如,把 .append 和 .pop(0) 合起来用,就能模拟栈的“先进先出”的特点)。 # 但是删除列表的第一个元素(抑或是在第一个元素之前添加一个元素)之类的操作是很耗时的,因为这些操作会牵扯到移动列表里的所有元素。 # collections.deque 类(双向队列)是一个线程安全、可以快速从两端添加或者删除元素的数据类型。 # 而且如果想要有一种数据类型来存放“最近用到的几个元素”,deque 也是一个很好的选择。 # 这是因为在新建一个双向队列的时候,你可以指定这个队列的大小,如果这个队列满员了,还可以从反向端删除过期的元素,然后在尾端添加新的元素。 #示例 2-23 使用双向队列 >>> from collections import deque # ❶ maxlen 是一个可选参数,代表这个队列可以容纳的元素的数量,而且一旦设定,这个属性就不能修改了。 >>> dq = deque(range(10), maxlen=10) >>> dq deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10) # ❷ 队列的旋转操作接受一个参数 n,当 n > 0 时,队列的最右边的 n个元素会被移动到队列的左边。当 n < 0 时,最左边的 n 个元素会被移动到右边。 >>> dq.rotate(3) >>> dq deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10) >>> dq.rotate(-4) >>> dq deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10) # ❸ 当试图对一个已满(len(d) == d.maxlen)的队列做尾部添加操作的时候,它头部的元素会被删除掉。注意在下一行里,元素 0 被删除了。 >>> dq.appendleft(-1) >>> dq deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10) # ❹ 在尾部添加 3 个元素的操作会挤掉 -1、1 和 2。 >>> dq.extend([11, 22, 33]) >>> dq deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10) # ❺ extendleft(iter) 方法会把迭代器里的元素逐个添加到双向队列的左边,因此迭代器里的元素会逆序出现在队列里。 >>> dq.extendleft([10, 20, 30, 40]) >>> dq deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10) # 双向队列实现了大部分列表所拥有的方法,也有一些额外的符合自身设计的方法,比如说 popleft 和 rotate。 # 但是为了实现这些方法,双向队列也付出了一些代价,从队列中间删除元素的操作会慢一些,因为它只对在头尾的操作进行了优化。 # append 和 popleft 都是原子操作,也就说是 deque 可以在多线程程序中安全地当作先进先出的栈使用,而使用者不需要担心资源锁的问题。 # 除了 deque 之外,还有些其他的 Python 标准库也有对队列的实现。 # queue提供了同步(线程安全)类 Queue、LifoQueue 和PriorityQueue,不同的线程可以利用这些数据类型来交换信息。 # multiprocessing这个包实现了自己的 Queue,它跟 queue.Queue 类似,是设计给进程间通信用的。 # asyncioPython 3.4 新提供的包,里面有Queue、LifoQueue、PriorityQueue 和 JoinableQueue,这些类受到 queue 和 multiprocessing 模块的影响,但是为异步编程里的任务管理提供了专门的便利。 # heapq跟上面三个模块不同的是,heapq 没有队列类,而是提供了heappush 和 heappop 方法,让用户可以把可变序列当作堆队列或者优先队列来使用。 # 2.10 本章小结 # 列表推导和生成器表达式则提供了灵活构建和初始化序列的方式,这两个工具都异常强大。 # 如果你还不能熟练地使用它们,可以专门花时间练习一下。 # 它们其实不难,而且用起来让人上瘾。 # 本章末尾介绍了 collections.deque 这个类型,它具有灵活多用和线程安全的特性。 # 2.11 延伸阅读 #在 Python 3 中,如果列表里的东西不能比较大小,那么我们就不能对列表进行排序: >>> l = [28, 14, '28', 5, '9', '1', 0, 6, '23', 19] >>> sorted(l) Traceback (most recent call last): File "<pyshell#265>", line 1, in <module> sorted(l) TypeError: '<' not supported between instances of 'str' and 'int' # 元组则恰恰相反,它经常用来存放不同类型的的元素。这也符合它的本质,元组就是用作存放彼此之间没有关系的数据的记录。 #另外,key 参数也能让你对一个混有数字字符和数值的列表进行排序。你只需要决定到底是把字符看作数值,还是把数值看作字符: >>> l = [28, 14, '28', 5, '9', '1', 0, 6, '23', 19] >>> sorted(l, key=int) [0, '1', 5, 6, '9', 14, 19, '23', 28, '28'] >>> sorted(l, key=str) [0, '1', 14, 19, '23', 28, '28', 5, 6, '9']