0%

Python_序列

Python中存在大量的序列类型,每种序列类型都具备自己的特点与使用场景,了解其机制有利于构造更加高效的代码与自制序列类。

优雅的Python第二章

内置序列概况

按照能否存储同一数据类型数据分为:

  • 容器序列:list,tuple,collections.deque,存储的为任意类型的引用,内存空间不连续
  • 扁平序列:str,bytes,bytearray,memoryview,array.array,存储的是同一类型的,内存空间连续

上段时间我总结了关于Python中值传递与引用传递问题时,有提到相关概念的区别。

按照序列类型能否修改:

  • 可变序列:list,bytearray.array.array,collection.deque,memoryview
  • 不可变序列:tuple,str,bytes

list序列

列表类型是Python中常见的序列类型,其使用已烂熟于心,不妨关于列表的创建方法:

列表推导与列表生成

列表推导的简单使用

列表推导就是用于生成列表的,其是一种易于阅读的列表生成方式。

  • 传统的列表构造方式:for + append

    1
    2
    3
    4
    5
    6
    7
    # 将一个字符串转换为Unicode码位列表
    symbols = "$#@%&*"
    codes = []
    for symbol in symbols:
    codes.append(ord(symbol))
    print(codes)
    # [36, 35, 64, 37, 38, 42]
  • 列表生成式:[ func(s) for s in strs]

    1
    2
    3
    4
    5
    # 将一个字符串转换为Unicode码位列表
    symbols = "$#@%&*"
    codes = [ord(symbol) for symbol in symbols ]
    print(codes)
    # [36, 35, 64, 37, 38, 42]

使用标准:

上面两种方式实现了相同的功能,使用哪一种取决于逻辑的复杂程度,逻辑复杂需要编写多行才能表示的使用传统for循环的方式更清晰,逻辑简单的使用列表推导方式则更加易读。

**TIPS:**Python会忽略代码中[],(),{}中的换行符号,方便逻辑展示。

列表推导与map,filter的关系

关系就是:map,filter能做的,利用列表推导同样可以实现。

示例:

1
2
3
4
5
# 得到所有ASCII大于36的结果,列表生成
symbols = "$#@%&*"
beyond_ascii = [ord(symbol) for symbol in symbols if ord(symbol) > 36]
print(beyond_ascii)
# [64, 37, 38, 42]
1
2
3
4
5
# 得到所有ASCII大于36的结果,map,filter,lambda
symbols = "$#@%&*"
beyond_ascii = list(filter(lambda c:c > 36 , map(ord , symbols)))
print(beyond_ascii)
# [64, 37, 38, 42]

列表推导与笛卡儿积

在列表推导中也可以生成笛卡儿积,直接看代码即可;

1
2
3
4
5
sizes = ["small" , "medium" , "large"]
colors = ["white" , "black"]
print([(size , color) for size in sizes
for color in colors ])
# [('small', 'white'), ('small', 'black'), ('medium', 'white'), ('medium', 'black'), ('large', 'white'), ('large', 'black')]

列表生成式

列表生成式也是生成列表的方式,其遵循了迭代器协议,逐个的产出元素,而不是建立完整的列表,显然可以节省内存。其形式与列表推导式大同小异,仅仅将方括号替换为圆括号

  • 生成式是唯一参数时,不需要添加圆括号。
1
2
3
4
symbols = "$#@%&*"
codes = list(ord(symbol) for symbol in symbols)
print(codes)
# [36, 35, 64, 37, 38, 42]
  • 生成式不是唯一参数时,需要添加圆括号。
1
2
3
4
5
import array
symbols = "$#@%&*"
codes = array.array("I",(ord(symbol) for symbol in symbols))
print(codes)
# array('I', [36, 35, 64, 37, 38, 42])
  • 生成式计算笛卡儿积
1
2
3
4
5
6
7
8
9
10
11
12
sizes = ["small" , "medium" , "large"]
colors = ["white" , "black"]
for tshirts in ("%s %s"%(size , color) for size in sizes for color in colors ):
print(tshirts)
"""
small white
small black
medium white
medium black
large white
large black
"""

Tuple序列

元组不仅仅是不可变的列表,如何使用也取决于如何看代元组,例如将元素视为用于记录的字段数据。那么其数量与位置信息则是非常重要的,

  • 元组用于记录:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
metro_areas = [
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), # <1>
('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: # <2>
if longitude <= 0: # <3>
print(fmt.format(name, latitude, longitude))
"""
| lat. | long.
Mexico City | 19.4333 | -99.1333
New York-Newark | 40.8086 | -74.0204
Sao Paulo | -23.5478 | -46.6358
"""
  1. 元组内部可嵌套,其内部嵌套标准这里不讨论。
  2. 元组可以进行拆包

元组拆包

拆包就是将元组中的各元素一次性提取到多变量中。平行赋值是元组拆包的形式之一,也是最好辨认的形式。

  • 平行赋值示例:
1
2
3
4
5
6
lax_coordinates = (33.9425 , -118.4080)
latitude , longitude = lax_coordinates
print(latitude)
print(longitude)
#33.9425
#-118.408
  • “*” 来表示剩余的元素
1
2
3
a , b , *rest = range(5)
print(a , b , rest)
# 0 1 [2, 3, 4]
  • 嵌套元组的拆包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
metro_areas = [
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), # <1>
('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: # <2>
if longitude <= 0: # <3>
print(fmt.format(name, latitude, longitude))
"""
| lat. | long.
Mexico City | 19.4333 | -99.1333
New York-Newark | 40.8086 | -74.0204
Sao Paulo | -23.5478 | -46.6358
"""

具名元组

具名元组是collection类的一个工厂函数,可以用来构建一个带字段名的元组和一个有名字的类。具体使用方式这里不表。

1
2
3
4
5
6
7
8
9
10
11
12
13
from collections import namedtuple
City = namedtuple("City" , "name country population coordinates")
tokyo = City("Tokyo" , "JP" , 36.933 , (35.68 , 139.69))
print(tokyo)
print(tokyo.name)
print(tokyo.coordinates)
print(tokyo[0])
"""
City(name='Tokyo', country='JP', population=36.933, coordinates=(35.68, 139.69))
Tokyo
(35.68, 139.69)
Tokyo
"""

序列中的 + 与 *

Python中的序列是支持的+与* 操作的,用于序列的拼接。而用于拼接的序列是不会被更改的,而是创建一个包含有同样类型的序列并返回。

“+”用于拼接序列时,一般要求左右元素的类型一致

1
2
3
4
5
6
7
8
9
10
11
12
l = [1,2,3]
# * 的应用
print(l*5)
print("abcd" * 5)
# [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
# abcdabcdabcdabcdabcd

# + 的应用
print(l + [4,5,6])
# [1, 2, 3, 4, 5, 6]
print(l + 4)
# TypeError: can only concatenate list (not "int") to list

注意:

在使用a * n语句时,序列中的a中的元素是对其他可变对象的引用时,得到的结果可能会与预期存在不一致。我自己在编写算法过程中也遇到类似的情形。比如:初始化列表中的列表时,得到的三个元素是三个引用,三个引用指向的地址一致,

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 正确的使用姿势
board = [["_"] * 3 for i in range(3) ]
board[0][1] = "x"
print(board)
# [['_', 'x', '_'], ['_', '_', '_'], ['_', '_', '_']]

# 错误的使用姿势
board = [["_"] * 3 ] * 3
board[0][1] = "x"
print(board)
# [['_', 'x', '_'], ['_', 'x', '_'], ['_', 'x', '_']]

# 上面的错误与接下来的写法一致
row = ["_"] * 3
board = []
for i in range(3):
board.append(row)
board[0][1] = "x"
print(board)
# [['_', 'x', '_'], ['_', 'x', '_'], ['_', 'x', '_']]

序列中的 += 与 *= 运算

序列中的增量赋值运算,大同小异,只介绍+= 即可。理解增量赋值运算,主要是其结果到底是就地加法还是构造新的对象并赋值。

Python遇到+=时

  • 首先会调用“__iadd__”(就地加法方法),__
  • 如果没有实现,则调用“__add__”,此时等价于a = a +b.

而上面是否实现了就地加法,却决于a是否是可变对象,比如列表就实现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 可变类型遇到增量赋值,一般为原地修改
l = [1,2,3]
print(id(l))
# 29050360
l += [4,5,6]
print(id(l))
# 29050360

# 不可变类型遇到增量复制,会重新构造对象
a = "abcd"
print(id(a))
# 29036000
a += "efgh"
print(id(a))
# 59961416

list.sort与内置的sorted方法

上面的两个方法作用一致,其差别就是list.sort方法会选择原地重建列表结构,地址不变。内置的sorted方法则是重构一个列表对象,并返回。

奇妙的key参数

在list.sort\sorted\min\max函数中均提供了key参数,用于进行比较大小的依据。在一般的语言中,甚至在python2中也采用了双参数函数用于返回“-1,0,1”,以表示大小。而Python3中的key参数支持单参数函数进行比较。单参数的优势在于其更加高效,每个key函数只会调用一次。在排序中,自然会进行两个键值的比较,但是其发生才C语言一层,这样保证了其高效性。

示例:实现字符与数字的序列排序

1
2
3
4
5
6
7
l = [24,14,"28" , 5,"9","1","0" , 6,"23" , 19]
print(sorted(l , key= int))
# ['0', '1', 5, 6, '9', 14, 19, '23', 24, '28']

l = [24,14,"28" , 5,"9","1","0" , 6,"23" , 19]
print(sorted(l , key= str))
# ['0', '1', 14, 19, '23', 24, '28', 5, 6, '9']

bisect来管理已排序序列

看理解就是利用二分查找,用来快速定位元素。支持左右二分查找,支持插入元素等。这里不一一贴代码。