流畅的python:3,字典和集合

本文介绍Python中序列的基础知识,包括如何使用特殊方法实现自定义序列,序列的常见操作如切片、迭代、比较等,以及介绍多种序列类型如列表、元组、数组、内存视图等的特点和适用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

import collections

 

 collections.namedtuple('Card', ['rank', 'suit'])

用以构建只有少数属性但是没有方法的对象(有名元组)

 

 def __len__(self):

 def __getitem__(self, position):  实现索引返回

 

from random import choice

 choice(deck):提供__getitem__ 即可随机返回

 

仅仅实现了 __getitem__ 方法,这一摞牌就变成可迭代的了

反向迭代也没关系:

 

因为它是 可迭代的, in 运算符就 会按顺序做一次迭代搜索。于是,in 运算符可以用在我们的 FrenchDeck 类上

 

通过实现 __len__ 和 __getitem__ 这两个特殊方法,FrenchDeck 就跟一个 Python 自有的序列数据类型一样,可以体现出 Python 的核心语言特性(例如迭 代和切片)。同时这个类还可以用于标准库中诸如 random.choice、reversed 和 sorted 这 些函数。

 

for i in x: 这个语句,背后其实用的是 iter(x),而这个函数的背后则是 x.__iter__() 方法。当然前提是这个方法在 x 中被实现了。

 

通常你的代码无需直接使用特殊方法。除非有大量的元编程存在,直接调用特殊方法的频 率应该远远低于你去实现它们的次数。唯一的例外可能是 __init__ 方法

 

abs 是一个内置函数,如果输入是整数或者浮点数,它返回的是输入值的绝对值;如果输 入是复数(complex number),那么返回这个复数的模

 

repr 就是通过 __repr__ 这个特殊方法来得到一个对象的字符串 表示形式的。如果没有实现 __repr__,当我们在控制台里打印一个向量的实例时,得到的 字符串可能会是 <Vector object at 0x10e100070>。在老的使用 % 符号的字符串格式中,这个函数返回的结果用来代替 %r 所代表的对象

 

__repr__ 和 __str__ 的区别在于,后者是在 str() 函数被使用,或是在用 print 函数打印 一个对象的时候才被调用的,并且它返回的字符串对终端用户更友好。

 

如果你只想实现这两个特殊方法中的一个,__repr__ 是更好的选择,因为如果一个对象没 有 __str__ 函数,而 Python 又需要调用它的时候,解释器会用 __repr__ 作为替代

 

为了判定一个值 x 为真还是为假,Python 会调用 bool(x),这个函数只能返回 True 或者 False。

 

默认情况下,我们自己定义的类的实例总被认为是真的,除非这个类对 __bool__ 或者 __ len__ 函数有自己的实现。bool(x) 的背后是调用x.__bool__() 的结果;如果不存在__ bool__ 方法,那么 bool(x) 会尝试调用 x.__len__()。若返回 0,则 bool 会返回 False;否 则返回 True。

 

 

 

 

 

len 之所以不是一个普通方法,是为了让 Python 自带的数据结构可以走后门, abs 也是同理。但是多亏了它是特殊方法,我们也可以把 len 用于自定义数据类型

 

Python 对象的一个基本要求就是它得有合理的字符串表示形式,我们可以通过 __repr__ 和 __str__ 来满足这个要求。前者方便我们调试和记录日志,后者则是给终端用户看的

 

对序列数据类型的模拟是特殊方法用得最多的地方

 

 

容器序列

list、tuple 和 collections.deque 这些序列能存放不同类型的数据。

扁平序列

str、bytes、bytearray、memoryview 和 array.array,这类序列只能容纳一种类型。

容器序列存放的是它们所包含的任意类型的对象的引用,而扁平序列里存放的是值而不是 引用。换句话说,扁平序列其实是一段连续的内存空间。由此可见扁平序列其实更加紧 凑,但是它里面只能存放诸如字符、字节和数值这种基础类型。

 

 

 

通常的原则是,只用列表推导来创建新的列表,并且尽量保持简短。如序列构成的数组,如果列表推导的代码超过了两行,你可能就要考虑是不是得用 for 循环重写了。

 

 

列表推导的作用只有一个:生成列表。如果想生成其他类型的序列,生成器表达式就派上 了用场

 

 for 循环可以分别提取元组里的元素,也叫作拆包(unpacking)。因为元组中第二个元 素对我们没有什么用,所以它赋值给“_”占位符

 

最好辨认的元组拆包形式就是平行赋值,也就是说把一个可迭代对象里的元素,一并赋值 到由对应的变量组成的元组中

 

另外一个很优雅的写法当属不使用中间变量交换两个变量的值:

>>> b, a = a, b

用 * 运算符把一个可迭代对象拆开作为函数的参数

 

这里元组拆包的用法则是让一个函数可以用元组的形式返回多个值, 然后调用函数的代码就能轻松地接受这些返回值。比如 os.path.split() 函数就会返回以 路径和最后一个文件名组成的元组 (path, last_part):

 

用*来处理剩下的元素 . 在 Python 中,函数用 *args 来获取不确定数量的参数算是一种经典写法了。

 

另外元组拆包还有个强大的功能,那就是可以应用在嵌套结构中。

 

2.3.3嵌套元组拆包:

 变量声明需要按照嵌套元组内部的变量声明方式

2.3.4具名元组

@collections.namedtuple 是一个工厂函数,它可以用来构建一个带字段名的元组和一个有 名字的类——这个带名字的类对调试程序有很大帮助。

@namedtuple 构建的类的实例所消耗的内存跟元组是一样的

@创建一个具名元组需要两个参数,一个是类名,另一个是类的各个字段的名字。后者可 以是由数个字符串组成的可迭代对象,或者是由空格分隔开的字段名组成的字符

@你可以通过字段名或者位置来获取一个字段的信息

@除了从普通元组那里继承来的属性之外,具名元组还有一些自己专有的属性。

➊ _fields 属性是一个包含这个类所有字段名称的元组。

 ➋ 用 _make() 通过接受一个可迭代对象来生成这个类的一个实例,它的作用跟 City(*delhi_data) 是一样的。

 ➌ _asdict() 把具名元组以 collections.OrderedDict 的形式返回,我们可以利用它来把元 组里的信息友好地呈现出来

2.3.5作为不可变列表的元组

@:列表或元组的方法和属性(那些由object类支持的方法没有列出来)

 

 

 

 

2.4切片

2.4.1为什么切片和区间会忽略最后一个元素

2.4.2对对象进行切片

@a:b:c 这种用法只能作为索引或者下标用在 [] 中来返回一个切片对象:slice(a, b, c)。

@在 10.4.1 节中会讲到,对seq[start:stop:step] 进行求值的时候,Python 会调用seq. __getitem__(slice(start, stop, step))。就算你还不会自定义序列类型,了解一下切片对 象也是有好处的。例如你可以给切片命名,就像电子表格软件里给单元格区域取名字一样。

比如,要解析示例 2-11 中所示的纯文本文件,这时使用有名字的切片比用硬编码的数字区 间要方便得多

@如果从 Python 用户的角度出发,切片 还有个两个额外的功能:多维切片和省略表示法(...)。

2.4.3多维切片和省略

@[] 运算符里还可以使用以逗号分开的多个索引或者是切片,外部库 NumPy 里就用到了这 个特性,二维的 numpy.ndarray 就可以用 a[i, j] 这种形式来获取,抑或是用 a[m:n, k:l] 的方式来得到二维切片。

@要正确处理这种 [] 运算符的 话,对象的特殊方法 __getitem__ 和 __setitem__ 需要以元组的形式来接收 a[i, j] 中的索 引。也就是说,如果要得到 a[i, j] 的值,Python 会调用 a.__getitem__((i, j))。

@Python 内置的序列类型都是一维的,因此它们只支持单一的索引,成对出现的索引是没有 用的。

@省略(ellipsis)的正确书写方法是三个英语句号(...),而不是Unicdoe 码位U+2026 表 示的半个省略号(...)。省略在 Python 解析器眼里是一个符号,而实际上它是 Ellipsis 对 象的别名,而 Ellipsis 对象又是 ellipsis 类的单一实例,主要用在NumPy

2.4.4 给切片赋值

@如果把切片放在赋值语句的左边,或把它作为 del 操作的对象,我们就可以对序列进行嫁 接、切除或就地修改操作。

@如果赋值的对象是一个切片,那么赋值语句的右侧必须是个可迭代对象。即便只有单独 一个值,也要把它转换成可迭代的序列。

2.5 对序列使用+和*

@你想用 my_list = [[]] * 3 来初始化一个由列表组成的列表,但是你得到的列表里 包含的 3 个元素其实是 3 个引用,而且这 3 个引用指向的都是同一个列表。 这可能不是你想要的效果。

@注意*对引用对象的复制的特别性

2.6 序列的增量赋值

@+= 背后的特殊方法是 __iadd__(用于“就地加法”)。但是如果一个类没有实现这个方法的 话,Python 会退一步调用 __add__。

@对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象

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

@不要把可变对象放在元组里面。

@ 增量赋值不是一个原子操作。

2.7 list.sort方法和内置函数sorted

@:如果一个函数或者方法对对象进行的是就地改动,那它就应该返回 None,好让调用者知道传入的参数发生了变动,而且并未产生新的对象。

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

@Key:一个只有一个参数的函数,这个函数会被用在序列里的每一个元素上,所产生的结果 将是排序算法依赖的对比关键字。

@已排序的序列可以用来进行快速搜索,而标准库的 bisect 模块给我们提供了二分查找算法。

2.8 用bisect来管理已排序的序列

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

@bisect 可以用它的两个可选参数——lo 和 hi——来缩小搜寻的范围。lo 的默认值是 0,hi 的默认值是序列的长度,即 len() 作用于该序列的返回值

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

@bisect 的函数可以在很长的有序序列中作为 index 的替代, 用来更快地查找一个元素的位置。

2.8.2 用bisect.insort插入新元素

@排序很耗时,因此在得到一个有序序列之后,我们最好能够保持它的有序。bisect.insort 就是为了这个而存在的

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

@insort 跟 bisect 一样,有 lo 和 hi 两个可选参数用来控制查找的范围。它也有个变体叫 insort_left,这个变体在背后用的是 bisect_left

@如果你只需要处理数字列表的话,数组可能是个更好的选择。下面就 来讨论一些可以替换列表的数据结构。

2.9 当列表不是首选

@要存放 1000 万个浮点数的话,数组(array)的效率要高得多,因为数组在背后存的并不是 float 对象,而是数字的机器翻译,也就是字节表述。这一点就跟 C 语言中的数组一样。再比如 说,如果需要频繁对序列做先进先出的操作,deque(双端队列)的速度应该会更快。

@下可以替换列表的数据类型

2.9.1 数组

@数组支持所有跟 可变序列有关的操作,包括 .pop、.insert 和 .extend。另外,数组还提供从文件读取和存 入文件的更快的方法,如 .frombytes 和 .tofile

@Python 数组跟 C 语言数组一样精简。创建数组需要一个类型码,这个类型码用来表示在 底层的 C 语言应该存放怎样的数据类型。比如 b 类型码代表的是有符号的字符(signed char)

@,array.tofile 和 array.fromfile 用起来很简单。把这段代 码跑一跑,你还会发现它的速度也很快。一个小试验告诉我,用 array.fromfile 从一个二 进制文件里读出 1000 万个双精度浮点数只需要 0.1 秒,这比从文本文件里读取的速度要快 60 倍,因为后者会使用内置的 float 方法把每一行文字转换成浮点数。

 

 

 

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

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

2.9.2 内存视图

@memoryview 是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切 片。

@memoryview.cast 的概念跟数组模块类似,能用不同的方式读写同一块内存数据,而且内容 字节不会随意移动。这听上去又跟 C 语言中类型转换的概念差不多。memoryview.cast 会 把同一块内存里的内容打包成一个全新的 memoryview 对象给你。

@如果利用数组来做高级的数字处理是你的日常工作,那么 NumPy 和 SciPy 应该是你 的常用武器。

2.9.3 NumPy和SciPy

@。NumPy 实现了多维同质数组(homogeneous array)和矩阵,这些数据结构不但能处理 数字,还能存放其他由用户定义的记录。通过 NumPy,用户能对这些数据结构里的元素进 行高效的操作。

@SciPy 是基于 NumPy 的另一个库,它提供了很多跟科学计算有关的算法,专为线性代数、 数值积分和统计学而设计

 

2.9.4 双向队列和其他形式的队列

@利用.append 和 .pop 方法,我们可以把列表当作栈或者队列来用(比如,把.append 和 .pop(0) 合起来用,就能模拟栈的“先进先出”的特点)。但是删除列表的第一个元素 (抑或是在第一个元素之前添加一个元素)之类的操作是很耗时的,因为这些操作会牵扯 到移动列表里的所有元素。

@collections.deque 类(双向队列)是一个线程安全、可以快速从两端添加或者删除元素的 数据类型。

@append 和 popleft 都是原子操作,也就说是 deque 可以在多线程程序中安全地当作先进先 出的栈使用,而使用者不需要担心资源锁的问题。

 

 

 

 

2.10 本章小结

@当元组被当作记录来用的时候,拆包是最安全可靠地从元组里提取不同字段信息 的方式。新引入的 * 句法让元组拆包的便利性更上一层楼,让用户可以选择性忽略不需要 的字段。

@就像普通元 组一样,具名元组的实例也很节省空间,但它同时提供了方便地通过名字来获取元组各个 字段信息的方式,另外还有个实用的 ._asdict() 方法来把记录变成 OrderedDict 类型。

@Python 里最受欢迎的一个语言特性就是序列切片,另外,对切片赋值是一个修改可变序列的捷径。

@增量赋值 += 和 *= 会区别对待可变和不可变序列。在遇到不可变序列时,这两个操作 会在背后生成新的序列。但如果被赋值的对象是可变的,那么这个序列会就地修改——然 而这也取决于序列本身对特殊方法的实现。

@。如果在插入新元素的同时还想保 持有序序列的顺序,那么需要用到 bisect.insort。bisect.bisect 的作用则是快速查找。

@虽然NumPy 和 SciPy 都不 是 Python 标准库的一部分,但稍微学习一下它们,会让你在处理大规模数值型数据时如 有神助。

@collections.deque 这个类型,它具有灵活多用和线程安全的特性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值