Python深入-2-细说序列

Python内置序列类型

Python标准库用C语言实现了丰富的序列类型。分为如下

  • 容器序列: 可存放不同类型的项,其中包括嵌套容器。示例:list(列表)、tuple(元组) 和 collections.deque(队列)。

  • 扁平序列:可存放一种简单类型的项。示例:str(字符串)、bytes(字节) 和 array.array(数组)。

容器序列
容器序列存放的是所包含对象的引用,对象可以是任何类型。    

Python深入-2-细说序列
image.png

图中展示的是一个元组和一个数组的内存简图,它们各有 3 项。灰色方块(未按比例绘制)表示 各个 Python 对象的内存标头。元组中的每一项都是引用,引用的是不同的 Python 对象,对象中还可以 存放其他 Python 对象的引用,例如那个包含两个项的列表。

扁平序列
扁平序列在自己的内存空间中存储所含内容 的值,而不是各自不同的 Python 对象

Python深入-2-细说序列
image.png

Python 中的数组整体是一个对 象,存放一个 C 语言数组,包含 3 个双精度数因此,扁平序列更加紧凑,但是只能存放原始机器值,例如字节、整数和浮点数。

任何 Python 对象在内存中都有一个包含元数据的标头。最简单的 Python 对象,例如一个 float,内存标头中有一个值字段和两个元数据字段。

  • ob_refcnt:对象的引用计数。

  • ob_type:指向对象类型的指针。

  • ob_fval:一个 C 语言 double 类型值,存放 float 的值。

在 64 位设备中,每个字段占 8 字节。假如有一个浮点数数组和一个浮点数元组,显然前者比后者更紧凑,因为数组整体是一个对象,存放各个浮点数的原始值,而元组由多个对象构成:元组自身和存放的各个float对象  
另外,还可按可变性对序列类型分类。

  • 可变序列 例如 list、bytearray、array.array 和 collections.deque

  • 不可变序列 例如 tuple、str 和 bytes

可变序列继承不可变序列的所有方法,另外还多实现了几个方法,

列表推导式和生成器表达式

使用列表推导式(目标是列表)或生成器表达式(目标是其他序列类型)可以快速构建一个序列。使用这 两种句法写出的代码更易于理解,而且速度通常更快。

列表推导式与可读性

一个例子,基于一个字符串得到其 Unicode 的值。
简单的for循环方式

# 一个给定的字符串,需要把这个字符串给转换成Unicode,然后存入一个列表中
strs = "ABCDE"
# 定义空列表
codes = []

# 循环遍历字符串,然后把每一个字符转换成Unicode后,存入codes列表中
for s in strs:
    codes.append(ord(s))
# 输出转换后的列表
print(codes) # [65, 66, 67, 68, 69]

列表推导式方式

# 一个给定的字符串,需要把这个字符串给转换成Unicode,然后存入一个列表中
strs = "ABCDE"
# 通过列表推导式。直接生成一个新的列表
# 如果这部分代码不熟悉,可以去参考 Python基础-14-列表推导式
codes = [ord(s) for s in strs]
# 通过列表推导式得到的列表
print(codes)  # [65, 66, 67, 68, 69]

可以看到,列表推导式相对于for循环的方式,更加简单,且更加能简单明了。如果读者对列表推导式语法还不熟悉的花,可以去参考如下文章:Python基础-14-列表推导式

列表推导式写法说明:Python 会忽略 []、{} 和 () 内部的换行。因此,列表、列表推导式、元组、字典等结构完全可以分成几行来写,无须使用续行转义符 。如果不小心在续行转义符后面多输入一个空格,那反而不起作 用。另外,使用这 3 种括号定义字面量时,项与项之间使用逗号分隔,末尾的逗号将被忽略。因此, 跨多行定义列表字面量时,最好在最后一项后面添加一个逗号。这样不仅能方便其他程序员为列表添 加更多项,还可以减少代码差异给阅读带来的干扰。

上述推导式写法也可以这样写

# 一个给定的字符串,需要把这个字符串给转换成Unicode,然后存入一个列表中
strs = "ABCDE"
# 通过列表推导式。直接生成一个新的列表
# 如果这部分代码不熟悉,可以去参考 Python基础-14-列表推导式
codes = [
    ord(s)
    for s in strs
]
# 通过列表推导式得到的列表
print(codes)  # [65, 66, 67, 68, 69]

注意:虽然列表推导式功能强大,但也不不是必须要使用,且不能滥用列表推导式。相反,for循环的功能也非常强大且容易列表;代码不仅仅写给机器看,还要写给自己和项目协作者看。

Python 3 中的列表推导式、生成器表达式,以及类似的集合推导式和字典推导式,for 子句中赋值的 变量在局部作用域内。代码如下

# 一个给定的字符串,需要把这个字符串给转换成Unicode,然后存入一个列表中
strs = "ABCDE"
# 通过列表推导式。直接生成一个新的列表
# 如果这部分代码不熟悉,可以去参考 Python基础-14-列表推导式
codes = [
    ord(s)
    for s in strs
]
# 通过列表推导式得到的列表
print(codes)  # [65, 66, 67, 68, 69]
# 如下代码会报错,这里要使用列表推导式中的局部变量s,就会给出提示 NameError: name 's' is not defined
print(s)

然而,使用“海象运算符(即一个变量名后跟一个表达式或者一个值,这个和赋值运算符 = 类似,可以看作是一种新的赋值运算符。)” := 赋值的变量在推导式或生成器表达式返回后依然可以访问,这与函数内的局部变量行为不同。根据“PEP 572—Assignment Expressions”,:= 运算符赋值的变量,其作用域限定在函数内,除非目标变量使用globalnonlocal 声明。

# 一个给定的字符串,需要把这个字符串给转换成Unicode,然后存入一个列表中
strs = "ABCDE"
# 通过列表推导式。直接生成一个新的列表
# 如果这部分代码不熟悉,可以去参考 Python基础-14-列表推导式
codes = [
    # 这里使用海象运算符
    lastValue := ord(s)
    for s in strs
]
# 通过列表推导式得到的列表
print(codes)  # [65, 66, 67, 68, 69]
# 可以使用海象运算符赋值的变量
print(lastValue) # 69

列表推导式与 map 和 filter

回顾一下map和filter的用法
map() 会根据提供的函数对指定序列做映射。第一个参数 function 以参数序列中的每一个元素调用 function 函数,返回包含每次 function 函数返回值的新列表。

# 定义一个方法,实现平方计算
def square(x):
    return x ** 2


# map方法,把一个函数应用到指定可迭代的序列上。然后形成新的序列
result1 = map(square, [1234])  # 这里在Python3上,输出对象信息 <map object at 0x000001C41496BB80>
# 把结果转换成list后,在输出可以看到内容
print(list(result1))  # [1, 4, 9, 16]

# 还可以直接通过lambda表达式来代替第一个参数
print(list(map(lambda x: x ** 2, [1234])))  # [1, 4, 9, 16]

filter() 函数用于过滤序列,过滤掉不符合条件的元素,返回由符合条件元素组成的新列表。该接收两个参数,第一个为函数,第二个为序列,序列的每个元素作为参数传递给函数进行判断,然后返回 True 或 False,最后将返回 True 的元素放到新列表中。
注意: Python2.7 返回列表,Python3.x 返回迭代器对象

# 定义一个可以用于过滤的方法:返回奇数
def is_odd(n):
    return n % 2 == 1


newline = filter(is_odd, [12345678910])
print(list(newline))  # [1, 3, 5, 7, 9]

列表推导式涵盖 map 和 filter 两个函数的功能,写出的代码不像 Python 的 lambda 表达式那样晦涩难懂。

# 一个给定的字符串,需要把这个字符串给转换成Unicode,然后存入一个列表中
strs = "ABCDE"

# 实现过滤filter的功能,把字符串转换成Unicode时,判断大于67的才存入到新序列newline中
newline = [ord(s) for s in strs if ord(s) > 67]
print(newline)  # [68, 69]

# 实现上述功能,仔细读代码
# filter是过滤,大于67的
# map是映射,映射的方法是ord,而数据是strs
# 最后把结果通过list方法转换成list列表
newline2 = list(filter(lambda s: s > 67, map(ord, strs)))
print(newline2)  # [68, 69]

生成器表达式

虽然列表推导式也可以生成元组、数组或其他类型的序列,但是生成器表达式占用的内存更少,因为生成器表达式使用迭代器协议逐个产出项,而不是构建整个列表提供给其他构造函数。 生成器表达式的句法跟列表推导式几乎一样,只不过把方括号换成圆括号而已。  
**案例:使用生成器表达式构建一个元组和一个数组  **

# 一个给定的字符串,需要把这个字符串给转换成Unicode,然后存入一个列表中
strs = "ABCDE"
tuples = (ord(s) for s in strs)
print(tuples)  # <generator object <genexpr> at 0x00000243E17C9D20>
print(tuple(tuples)) # (65, 66, 67, 68, 69)

如果生成器表达式是函数唯一的参数,则不需要额外再使用圆括号括起来,

strs = "ABCDE"
print(tuple(ord(s) for s in strs))  # (65, 66, 67, 68, 69)

元组不仅仅是不可变列表

 元组有两个作用,除了 可以作为不可变列表使用之外,还可用作没有字段名称的记录 。

用作记录

 用元组存放记录,**元组中的一项对应一个字段的数据**,项的位置决定数据的意义。 如果只把元组当作不可变列表,那么项数和项的顺序就变得可有可无。**但是如果把元组当作字段的容器使用,那么项数通常是固定的,顺序也变得十分重要**。  
# 拆包
name, age, phone, address = ("zhangsan"201509999999"中国北京")
# 还可以通过_来接收不需要处理的
name, _, phone, address = ("zhangsan"201509999999"中国北京")

通常没必要为了给字段指定一个名称而创建一个类。如果不使用索引访问字段,而只使用拆包,那就更没有必要创建类。  
现在,我们把 tuple 类当作 list 类的一种不可变的变体

用作不可变列表

Python 解释器和标准库经常把元组当作不可变列表使用。这么做主要有两个好处。

  • 意图清晰 只要在源码中见到元组,你就知道它的长度永不可变。

  • 性能优越 长度相同的元组和列表,元组占用的内存更少,而且 Python 可对元组做些优化。

然而,你要知道,元组的不可变性仅针对元组中的引用而言。元组中的引用不可删除、不可替换。倘若引 用的是可变对象,改动对象之后,元组的值也会随之变化。下面的代码段通过创建两个元组(a 和 b)来演 示这一点。一开始,a 和 b 是相等的,此时 b 在内存中的初始布局如下

Python深入-2-细说序列
image.png


说明: 元组的内容自身是不可变的,但是这仅仅表明元组中存放的引用始终指向同一批对象。倘若引用 的是可变对象,例如一个列表,那么元组的值就可能发生变化。


list1 = ["篮球""足球""羽毛球"]
tup = ("zhangsan"201509999999"中国北京", list1)
print(tup)  # "zhangsan", 20, 1509999999, "中国北京"

list2 = ["抽烟""喝酒""烫头"]
list1 = list2
print(list1)  # ['抽烟', '喝酒', '烫头']
# 这里虽然list1变了,但是tup的值是固定了,且在list1变化之前就是固定的了
print(tup)  # ('zhangsan', 20, 1509999999, '中国北京', ['篮球', '足球', '羽毛球'])

# 修改元组的最后一项,tup的值就变化了
tup[-1].append(list2)
print(tup)  # ('zhangsan', 20, 1509999999, '中国北京', ['篮球', '足球', '羽毛球', ['抽烟', '喝酒', '烫头']])

总结

  • Python 编译器求解元组字面量时,经过一次操作即可生成元组常量的字节码。求解列表字面量时,生成的字节码将每个元素当作独立的常量推入数据栈,然后构建列表。

  • 给定一个元组 t,tuple(t) 直接返回 t 的引用,不涉及复制。相比之下,给定一个列表 l,list(l) 创建 l 的副本。

  • tuple 实例长度固定,分配的内存空间正好够用。而 list 实例的内存空间要富余一些,时刻准备追加元素。

  • 对元组中项的引用存储在元组结构体内的一个数组中,而列表把引用数组的指针存储在别处。二者不存储在同一个地方的原因是列表可以变长,一旦超出当前分配的空间,Python 就需要重新分配引用数组来腾出空间,而这会导致 CPU 缓存效率较低。  

列表和元组方法的比较

 元组支持所有不涉及增删项的列表方法,而且元组没有`__reversed__ `方法。其实,这正是一种优化措施。没有` __reversed__ `方法,`reversed(my_tuple)` 也能正常工作。  

序列和可迭代对象拆包

拆包的特点是不用我们自己动手通过索引从序列中提取元素,这样就减少了出错的可能。拆包的目标可以是任何可迭代对象,包括不支持索引表示法([])的迭代器。拆包对可迭代对象的唯一要求是,一次只能产出一项,提供给接收端变量。不过也有例外,可以使用星号(*)捕获余下的项,  
最明显的拆包形式是并行赋值(parallel assignment),即把可迭代对象中的项赋值给变量元组,代码如下:

name, age, phone, address = ("zhangsan"201509999999"中国北京")

利用拆包还可以轻松对调两个变量的值,省掉中间的临时变量 ,代码如下

a=1
b=2
a,b = b,a

调用函数时在参数前面加上一个 *,利用的也是拆包,代码如下

print(divmod(208))  # (2, 4)
t = (208)
# 这里就是拆包
print(divmod(*t))  # (2, 4)

使用 `*` 获取余下的项

定义函数时可以使用 *args 捕获余下的任意数量的参数,这是 Python 的一个经典特性。

a, b, *rest = range(5)
print(a)  # 0
print(b)  # 1
print(rest)  # [2, 3, 4]

Python 3 把这一思想延伸到了并行赋值上。 并行赋值时,* 前缀只能应用到一个变量上,不过可以是任何位置上的变量。

a, *body, c, d = range(5)
print(a)  # 0
print(body)  # [1, 2]
print(c)  # 3
print(d)  # 4

在函数调用和序列字面量中使用 `*` 拆包

在函数调用中可以多次使用 *,代码如下

def fun(a, b, c, d, *rest):
    print(a)  # 1
    print(b)  # 2
    print(c)  # 3
    print(d)  # 4
    print(rest)  # (5, 6)
    return a, b, c, d, rest


print(fun(*[12], 3, *range(47)))  # (1, 2, 3, 4, (5, 6))

定义列表、元组或集合字面量时,也可以使用*,代码如下

print(*range(4), 4)  # 0 1 2 3 4
print([*range(4), 4])  # [0, 1, 2, 3, 4]
print({*range(4), 4, *(567)})  # {0, 1, 2, 3, 4, 5, 6, 7}

嵌套拆包

拆包的对象可以嵌套,例如 (a, b, (c, d))。如果值的嵌套结构是相同的,则 Python 能正确处理。

# 每个元组是一个四字段记录,最后一个字段是坐标对
metro_areas = [
    ('Tokyo''JP'36.933, (35.689722139.691667)),
    ('Delhi NCR''IN'21.935, (28.61388977.208889)),
    ('Mexico City''MX'20.142, (19.433333-99.133333)),
    ('New York-Newark''US'20.104, (40.808611-74.020386)),
    ('São Paulo''BR'19.649, (-23.547778-46.635833)),
]


def main():
    print(f'{"":15} | {"latitude":>9} | {"longitude":>9}')
    for name, _, _, (lat, lon) in metro_areas: # 把最后一个字段赋值给一个嵌套元组,拆包坐标对。
        # lon <= 0: 测试条件只选取西半球的城市。
        if lon <= 0:
            print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')


if __name__ == '__main__':
    main()

序列模式匹配

def main():
    print(f'{"":15} | {"latitude":>9} | {"longitude":>9}')
    for record in metro_areas:
        # 这个 match 的匹配对象是 record,即 metro_areas 中的各个元组
        match record:
            # 一个 case 子句由两部分组成:一部分是模式,另一部分是使用 if 关键字指定的卫语句(guard clause,可选)
            case [name, _, _, (lat, lon)] if lon <= 0:
                print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')


if __name__ == '__main__':
    main()

序列模式可以写成元组或列表,或者任意形式的嵌套元组和列表,使用哪种句法都没有区别,因为在序列 模式中,方括号和圆括号的意思是一样的。上例中的模式写成列表形式,其中嵌套的序列则写成元组形式,这样做只是为了避免重复使用方括号或圆括号。

**注意: 在 ***match/case*** 上下文中,**str****bytes****bytearray** 实例不作为序列处理。**match** 把这 些类型视为“原子”值,就像整数 987 整体被视为一个值,而不是数字序列。**

** 与拆包不同,模式不析构序列以外的可迭代对象(例如迭代器) 。**
** **_ **符号在模式中有特殊意义:匹配相应位置上的任何一项,但不绑定匹配项的值。另外,**_ **是唯一可在模式中多次出现的变量。 模式中的任何一部分均可使用 **as** 关键字绑定到变量上。  **

case [name, _, _, (lat, lon) as coord]:

切片

在 Python 中,列表、元组、字符串等所有序列类型都支持切片操作 。

为什么切片和区间排除最后一项

切片和区间排除最后一项是一种 Python 风格约定,这与 Python、C 和很多其他语言中从零开始的索引相匹 配。排除最后一项可以带来以下好处。

  • 在仅指定停止位置时,容易判断切片或区间的长度。例如,range(3)my_list[:3] 都只产生 3 项。

  • 同时指定起始和停止位置时,容易计算切片或区间的长度,做个减法即可:stop - start

  • 方便在索引x 处把一个序列拆分成两部分而不产生重叠,直接使用 my_list[:x]my_list[x:] 即可。

list = [102030405060]

# 在索引位2处拆分
print(list[:2])  # [10, 20]
print(list[2:])  # [30, 40, 50, 60]

切片对象

 一个众所周知的秘密是,我们还可以使用`s[a:b:c]` 句法指定步距 c,让切片操作跳过部分项。步距也可 以是负数,反向返回项。代码如下
strs = "ABCDEFG"
# 从头切到未,步长为3
print(strs[::3])  # ADG
# 从头切到未,步长为负数,也就是倒序,步长为1
print(strs[::-1])  # GFEDCBA
# 从头切到未,步长为负数,也就是倒序,步长为2
print(strs[::-2])  # GECA

a:b:c 表示法只在 [] 内部有效,表示索引或下标运算符,得到的结果是一个切片对象:slice(a, b, c)

切片可以起别名

strs = "ABCDEFG"
# 切片起了别名
ONE = slice(12)
TWO = slice(35)
# 使用切片别名
print(strs[ONE])  # B
print(strs[TWO])  # DE

为切片赋值

在赋值语句的左侧使用切片表示法,或者作为 del 语句的目标,可以就地移植、切除或以其他方式修改可变序列。  代码如下

list = list(range(10))
print(list)  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# 为切片赋值,是动态修改的源列表
list[2:5] = [1122]
print(list)  # [0, 1, 11, 22, 5, 6, 7, 8, 9]

# 删除切片,动态修改的源列表
del list[5:7]
print(list) # [0, 1, 11, 22, 5, 8, 9]

如果赋值目标是一个切片,则右边必须是一个可迭代对象,即使只有一项。

使用 `+` 和 `*` 处理序列

Python 程序员预期序列支持 +*。通常,+ 的两个运算对象必须是同一种序列,而且都不可修改,拼接的结果是一个同类型的新序列。 如果想多次拼接同一个序列,可以乘以一个整数。同样,结果是一个新创建的序列。   **+**** 和 ***** 始终创建一个新对象,绝不更改操作数**。

list = [123]
print(list * 2)  # [1, 2, 3, 1, 2, 3]
print("abc" * 3)  # abcabcabc

list.sort 与内置函数 sorted

list.sort 方法就地排序列表,即不创建副本。返回值为 None,目的就是提醒我们,它更改了接收者, 没有创建新列表。这是 Python API 的一个重要约定:就地更改对象的函数或方法应该返回 None, 让调用方清楚地知道接收者已被更改,没有创建新对象。例如,random.shuffle(s)函数也有类似的行 为:就地混洗可变序列 s,返回 None。  
与之相反,内置函数 sorted 返回创建的新列表。该函数接受任何可迭代对象作为参数,包括不可变序列 和生成器。无论传入什么类型的可迭代对象,sorted 函数始终返回新创建的列表。
list.sort 和 sorted 均接受两个可选的关键字参数。

  • reverse 值为 True 时,降序返回项(即反向比较各项)。默认值为 False。

  • key 一个只接受一个参数的函数,应用到每一项上,作为排序依据。例如,排序字符串 时,key=str.lower 执行不区分大小写排序,而 key=len 按字符长度排序各个字符串。默认值是恒等函 数(即比较项本身)。  


原文始发于微信公众号(Python之家):Python深入-2-细说序列

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/198432.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!