python进阶书目串烧(三)

对序列使用+和*

Python 程序员会默认序列是支持 +* 操作的。通常 + 号两侧的序列由相同类型的数据所构成,在拼接的过程中,两个被操作的序列都不会被修改,Python 会新建一个包含同样类型数据的序列来作为拼接的结果。

把一个序列复制几份然后再拼接起来:

l = [1, 2, 3]
print(l * 5) # [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
print(5 * 'abcd') # abcdabcdabcdabcdabcd

建立由列表组成的列表

有时我们会需要初始化一个嵌套着几个列表的列表,譬如一个列表可能需要用来存放不同的学生名单,或者是一个井字游戏板 上的一行方块。想要达成这些目的,最好的选择是使用列表推导。

board = [['_'] * 3 for i in range(3)]
print(board)
board[1][2] = 'X'
print(board)

错误做法:

# 下面的列表其实包含 3 个指向同一个列表的引用。当我们不做修改的时候,看起来都还好
weird_board = [['_'] * 3] * 3
print(weird_board)
# 试图标记第 1 行第 2 列的元素,就立马暴露了列表内的3个引用指向同一个对象的事实
weird_board[1][2] = 'O'
print(weird_board)

序列的增量赋值

增量赋值运算符 +=*= 的表现取决于它们的第一个操作对象。+= 背后的特殊方法是 __iadd__ (用于“就地加法”)。但是如果一个类没有实现这个方法的话,Python 会退一步调用 __add__

以下面一个简单表达式为例:

 a += b

如果 a 实现了 __iadd__ 方法,就会调用这个方法。同时对可变序列(例如 listbytearrayarray.array)来说,a 会就地改动,就像调用了 a.extend(b) 一样。但是如果 a 没有实现 __iadd__ 的话,a+= b 这个表达式的效果就变得跟 a = a + b 一样了:首先计算 a +b,得到一个新的对象,然后赋值给 a。也就是说,在这个表达式中,变量名会不会被关联到新的对象,完全取决于这个类型有没有实现__iadd__ 这个方法。

总体来讲,可变序列一般都实现了 __iadd__ 方法,因此 += 是就地加法。上面所说的这些关于 += 的概念也适用于 *=,不同的是,后者相对应的是 __imul__

*= 在可变和不可变序列上的作用:

l = [1, 2, 3]
print(id(l))  # 185123464
l *= 2
print(l)  # [1, 2, 3, 1, 2, 3]
print(id(l))  # 185123464
t = (1, 2, 3)
print(id(t))  # 35891096
t *= 2
print(t)  # (1, 2, 3, 1, 2, 3)
print(id(t))  # 35453832

对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。1

list.sort方法和内置函数sorted2

list.sort 方法会就地排序列表,也就是说不会把原列表复制一份。这也是这个方法的返回值是 None 的原因,提醒你本方法不会新建一个列表。

python惯例:如果一个函数或者方法对对象进行的是就地改动,那它就应该返回 None3

list.sort 相反的是内置函数 sorted,它会新建一个列表作为返回值。这个方法可以接受任何形式的可迭代对象作为参数,甚至包括不可变序列或生成器。而不管 sorted 接受的是怎样的参数,它最后都会返回一个列表。

关键字参数list.sortsorted描述
reverse为 True,被排序的序列里的元素会以降序输出。默认值为False。
key4一个只有一个参数的函数,这个函数会被用在序列里的每一个元素上,所产生的结果将是排序算法依赖的对比关键字。例如,在对一些字符串排序时,可以用 key=str.lower 来实现忽略大小写的排序,或者是用 key=len 进行基于字符串长度的排序。这个参数的默认值是恒等函数(identity function),默认用元素自己的值来排序。

示例:

fruits = ['grape', 'raspberry', 'apple', 'banana']
print(sorted(fruits))  # ['apple', 'banana', 'grape', 'raspberry']
print(fruits)  # ['grape', 'raspberry', 'apple', 'banana']
print(sorted(fruits, reverse=True))  # ['raspberry', 'grape', 'banana', 'apple']

# 排序算法稳定,grape和apple的相对位置不变。
print(sorted(fruits, key=len))  # ['grape', 'apple', 'banana', 'raspberry']
print(sorted(fruits, key=len, reverse=True))  # ['raspberry', 'banana', 'grape', 'apple']

print(fruits)  # ['grape', 'raspberry', 'apple', 'banana']
fruits.sort()
print(fruits)  # ['apple', 'banana', 'grape', 'raspberry']

Python 的排序算法——Timsort5——是稳定的,意思是就算两个元素比不出大小,在每次排序的结果里它们的相对位置是固定的。

用bisect来管理已排序的序列

bisect 模块包含两个主要函数,bisectinsort两个函数都利用二分查找算法来在有序序列中查找或插入元素

用bisect来搜索

bisect(haystack, needle)haystack(干草垛)里搜索needle(针)的位置,该位置满足的条件是,把 needle 插入这个位置之后,haystack 还能保持升序。也就是在说这个函数返回的位置前面的值,都小于或等于 needle 的值。其中 haystack 必须是一个有序的序列。你可以先用 bisect(haystack, needle) 查找位置 index,再用 haystack.insert(index, needle) 来插入新值。但你也可用insort 来一步到位,并且后者的速度更快一些。

在有序序列中用 bisect 查找某个元素的插入位置:

bisect_demo.py

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)
$ python3 bisect_demo.py  # 命令行运行bisect_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可以用它的两个可选参数——lohi——来缩小搜寻的范围。lo的默认值是 0,hi 的默认值是序列的长度。

bisect 函数其实是 bisect_right 函数的别名,后者还有个姊妹函数叫 bisect_left。它们的区别在于,bisect_left 返回的插入位置是原序列中跟被插入元素相等的元素的位置,也就是新元素会被放置于它相等的元素的前面。

$ python3 bisect_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

bisect可以用来建立一个用数字作为索引的查询表格,比如对应分数和成绩评级6:

def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'):
"""根据一个分数,找到它所对应的等级"""
    i = bisect.bisect(breakpoints, score)
    return grades[i]


print([grade(score) for score in [33, 99, 77, 70, 89, 90, 100]])  
# ['F', 'A', 'C', 'C', 'B', 'A', 'A']

bisect.insort插入新元素

insort(seq, item) 把变量item插入到序列seq中,并能保持seq的升序顺序。

insort 可以保持有序序列的顺序:

import bisect
import random

SIZE = 7
random.seed(1729)  # 随机数的种子
my_list = []
for i in range(SIZE):
    new_item = random.randrange(SIZE * 2)  # 从 0-SIZE*2选取一个随机数
    bisect.insort(my_list, new_item)
    print('%2d ->' % new_item, my_list)

#-----------------结果----------------
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]

insortbisect 一样,有 lohi 两个可选参数用来控制查找的范围。它也有个变体叫 insort_left,这个变体在背后用的是bisect_left

目前所提到的内容都不仅仅是对列表或者元组有效,还可以应用于几乎所有的序列类型上。如果你只需要处理数字列表的话,数组可能是个更好的选择。

当列表不是首选时

数组

如果我们需要一个只包含数字的列表,那么 array.arraylist 更高效。数组支持所有跟可变序列有关的操作,包括 .pop.insert.extend。另外,数组还提供从文件读取和存入文件的更快的方法,如.frombytes.tofile

Python 数组跟 C 语言数组一样精简。创建数组需要一个类型码,这个类型码用来表示在底层的 C 语言应该存放怎样的数据类型。比如 b 类型码代表的是有符号的字符(signed char),因此 array('b') 创建出的数组就只能存放一个字节大小的整数,范围从 -128 到 127,这样在序列很大的时候,我们能节省很多空间。而且 Python 不会允许你在数组里存放除指定类型之外的数据。

创建一个有1000万个随机浮点数的数组,把这个数组存放到文件里,再从文件读取这个数组:

from array import array
from random import random

floats = array('d', (random() for i in range(10 ** 7))) # 建立一个双精度浮点数组(类型码是 'd')
print(floats[-1])  # 0.07802343889111107
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()
print(floats2[-1])  # 0.07802343889111107
print(floats2 == floats)  # True

array.fromfile 从一个二进制文件里读出 1000 万个双精度浮点数只需要 0.1 秒,这比从文本文件里读取的速度要快 60倍,因为后者会使用内置的 float 方法把每一行文字转换成浮点数。另外,使用 array.tofile 写入到二进制文件,比以每行一个浮点数的方式把所有数字写入到文本文件要快 7 倍。另外,1000 万个这样的数在二进制文件里只占用 80 000 000 个字节(每个浮点数占用 8 个字节,不需要任何额外空间),如果是文本文件的话,我们需要 181 515 739个字节。

另外一个快速序列化数字类型的方法pickle模块。pickle.dump 处理浮点数组的速度几乎跟 array.tofile 一样快。不过前者可以处理几乎所有的内置数字类型,包含复数、嵌套集合,甚至用户自定义的类。前提是这些类没有什么特别复杂的实现。

方法或属性列表数组详解
s.__add__(s2)s + s2,拼接
s.__iadd__(s2)s += s2,就地拼接
s.append(e)在尾部添加一个新元素
s.byteswap翻转数组内每个元素的字节序列,转换字节序
s.clear()删除所有元素
s.__contains__(e)s 是否包含 e
s.copy()列表的浅复制
s.__copy__()对 copy.copy 的支持
s.count(e)s 中 e 出现的次数
s.__deepcopy__()对 copy.deepcopy 的支持
s.__delitem__(p)删除位置 p 的元素
s.extend(it)将可迭代对象 it 里的元素添加到尾部
s.frombytes(b)将压缩成机器值的字节序列读出来添加到尾部
s.fromfile(f, n)将二进制文件 f 内含有机器值读出来添加到尾部,最多添加 n 项
s.fromlist(l)将列表里的元素添加到尾部,如果其中任何一个元素导致了 TypeError 异常,那么所有的添加都会取消
s.__getitem__(p)s[p],获取位置 p 的元素
s.index(e)在 s 中找到元素 e 第一次出现的位置
s.insert(p, e)在位置 p 之前插入元素e
s.itemsize数组中每个元素的长度是几个字节
s.__iter__()获取 s 的迭代器
s.__len__()len(s),元素的数量
s.__mul__(n)s * n,n 个 s 的重复拼接
s.__imul__(n)s *= n,就地重复拼接
s.__rmul__(n)n * s,反向拼接 *
s.pop([p])删除最后或者是(可选的)位于 p 的元素,并返回它的值
s.remove(e)删除 s 中的第一次出现的 e
s.reverse()就地把 s 的元素倒序排列
s.__reversed__()返回 s 的倒序迭代器
s.__setitem__(p,e)s[p] = e,把元素 e 放在位置p,替代已经在那个位置的元素
s.sort([key],[reverse])就地对 s 中的元素进行排序,可选的参数有键(key)和是否倒序(reverse)
s.tobytes()返回 s 的倒序迭代器
s.tofile(f)返回 s 的倒序迭代器
s.tolist()返回 s 的倒序迭代器
s.typecode返回只有一个字符的字符串,代表数组元素在 C 语言中的类型

从 Python 3.4 开始,数组类型不再支持诸如 list.sort() 这种就地排序方法。要给数组排序的话,得用 sorted 函数新建一个数组:

a = array.array(a.typecode, sorted(a))

想要在不打乱次序的情况下为数组添加新的元素,bisect.insort还是能派上用场。

python进阶书目串烧(一)—— 特殊方法、序列数组、列表推导、生成器表达

python进阶书目串烧(二)—— 元组拆包、具名元组、元组对比列表、切片

python进阶书目串烧(三)—— 序列、排序、列表对比数组

python进阶书目串烧(四)—— 内存视图、NumPy、列表对比双向队列

python进阶书目串烧(五)—— 泛映射类型、字典推导、映射的弹性键查询

python进阶书目串烧(六)—— 字典变种、不可变映射类型、集合推导

python进阶书目串烧(七)—— 字典原理、字典与集合特征对比


  1. str 是一个例外,因为对字符串做 += 实在是太普遍了,所以 CPython 对它做了优化。为 str初始化内存的时候,程序会为它留出额外的可扩展空间,因此进行增量操作的时候,并不会涉及复制原有字符串到新位置这类操作。 ↩︎

  2. B2中涉及到sorted排序的位置:1.14 排序不支持原生比较的对象。 ↩︎

  3. 用返回 None 来表示就地改动这个惯例有个弊端,那就是调用者无法将其串联起来。而返回一个新对象的方法(比如说 str 里的所有方法)则正好相反,它们可以串联起来调用,从而形成连贯接口(fluent interface)。 ↩︎

  4. 可选参数 key 还可以在内置函数 min()max() 中起作用。另外,还有些标准库里的函数也接受这个参数,像itertools.groupby()heapq.nlargest() 等。 ↩︎

  5. Timsort是结合了合并排序(merge sort)和插入排序(insertion sort)而得出的排序算法,它在现实中有很好的效率。Tim Peters在2002年设计了该算法并在Python中使用(TimSort 是 Python 中 list.sort 的默认实现)。该算法找到数据中已经排好序的块-分区,每一个分区叫一个run,然后按规则合并这些run。Pyhton自从2.3版以来一直采用Timsort算法排序,现在Java SE7和Android也采用Timsort算法对数组排序。 ↩︎

  6. 示例代码来自 bisect 模块的文档。文档里列举了一些利用bisect的函数,它们可以在很长的有序序列中作为 index 的替代,用来更快地查找一个元素的位置。 ↩︎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值