序列构成的数组

本章讨论的内容几乎可以应用到所有的序列类型上,从list, 到 Python3 特有的 str 和 bytes.

第二章 序列构成的数组

针对序列的分类:

  • 对序列类型提出了容器序列以及扁平序列的分类:
    • 容器序列:list , tuple, collections.deque 这些能存放不同类型数据的序列。它其实存放的是他们所包含的任意类型的对象的引用。
    • 扁平序列:str, bytes, bytearray, memoryview, array.array 这类只能容纳一种类型的序列。其本质就 是一段连续的内存空间。
  • 以及按照可变类型不可变类型的分类:
    • 可变类型:list, bytearray, array.array, collections.deque, memoryview 这类所存放的数据数量是可以变化的。
    • 不可变类型:str, tuple, bytes。

列表推导和生成器表达式

Python 会忽略代码里[], {}, ()中的换行。

Python2 中列表推导中的赋值操作可能会影响到推导上下文中的同名变量,Python3修复了。

请保持列表推导的简短,如果代码超过了两行,请考虑用for循环重写。

x = [ord(s) for s in somestr if ord(s) > 127]

# -->转成for
for s in somestr:
    if ord(s) > 127:
        return ord(s)
    
y = [(color, size) for color in colors for size in sizes]

# -->转出for
for color in colors:
    for size in sizes:
        return (color, size)
# 所以结果为[('black', 'L'), ('black', 'M')..]

列表推导的作用主要就是生成列表,如果想生成其他类型的序列,就可以用生成器表达式。

相对列表推导,生成器表达式背后遵循了迭代器协议,可以逐个的产生元素,更加节省内存。所以当生成的数据比较多时,用生成器表达式是明智的选择。

如果生成器表达式是一个函数调用过程中的唯一参数,则不需要额外再用括号括起来。

tuple(ord(s) for s in somestr)   # 无需括号
array.array('I', (ord(s) for s in somestr))  # 需要括号

更加合理的使用元组

元组不仅是可以用作不可变的列表,还可以用于记录

元组拆包 :如name, age = ('Wang', 20),这种方式可以应用到任何可迭代对象上,只要数量一致就行(又名平行赋值)。除非使用*来表示忽略多余的元素。

优雅的不使用中间变量来交换两个变量的值b, a = a, b

# divmod 接受 2 个参数
divmod(20, 8)

t = (20, 8)
# 将 * 放在可迭代对象前面可以将它拆开作为函数的参数
divmod(*t)  

元组拆包的另一个用法就是解析返回值为元组的函数:

# 在进行拆包时,我们不总是对元组里的所有数据都感兴趣,_ 占位符能帮助处理这种情况。
>>> _, filename = os.path.split('/home/xxx/test')
>>> filename
'test'

在 Python3 中,可以使用 * 来进行平行赋值:

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

>>> a, b, *rest = range(2)
>>> a, b, rest
>>> (0, 1, [])

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

具名元组

collections.namedtuple,用来构建一个带字段名的元组和一个有名字的

from collectiosn import namedtuple


# 建立一个具名元组需要两个参数,一个是类名,一个是类的各个字段的名字。
# 后者可以是由数个字符串组成的可迭代对象,或者是由空格分隔开的字段名组成的字符串。
City = namedtuple('City', ['name', 'country'])
City = namedtuple('City', 'name country')

beijing = City(name='Beijing', country='China')
>>> beijing.name
'Beijing'

具名元组除了继承普通元组的属性外,还有一些自己专有的属性,较为常用的有:_fields类属性,类方法_make(iterable)和实例方法 _asdict()

# _fields属性是一个包含这个类所有字段名称的元组
>>> City._fields
>>> ('name', 'country')

# _make()通过接受一个可迭代对象来生成这个类的一个实例
>>> shanghai_data = ('Shanghai', 'China')
>>> shanghai = City._make(shanghai_data)  <=> shanghai = City(*shanghai_data)

# _asdict()把具名元组以collections.OrderedDict的形式返回。
>>> shanghai._asdict()
>>> OrderedDict([('name', 'Shanghai'), ('country', 'China')])
>>> for k, v in shanghai._asdict().items():
   ...:     print k + ':' + v
   ...:     
name:Shanghai
country:China

切片

带名字的切片:因为切片的本质是slice()函数,所以对seq[start:stop:step]进行求值时,Python实际会调用seq.__getitem__(slice(start, stop, step))。所以可以用带名字的切片来增加可读性:

>>> strs = 'name age sex'
>>> NAME = slice(0, 4)
>>> AGE = slice(5, 9)
>>> SEX = slice(9, 14)

>>> strs[NAME]
'name'

给切片赋值:可以更加方便的修改列表,只是要注意赋的值必须为一个可迭代对象,即便只有单独一个值,也要转成可迭代的序列。

>>> nums = [0, 1, 2, 3, 4, 5]
>>> nums[1:2] = 20
TypeError: can only assign an iterable

>>> nums[1:2] = [20]
>>> nums
[0, 20, 2, 3, 4, 5]

>>> nums[1:2] = [20, 100, 150]
>>> nums
[0, 20, 100, 150, 2, 3, 4, 5]

>>> nums[1:2] = 'str'
>>> nums
[0, 's', 't', 'r', 100, 150, 2, 3, 4, 5]

对序列的*+操作

对于 * 和 + ,序列都会不修改原有的操作对象,而是构建一个全新的序列。但是如果序列中存的是可变对象的引用时,需要额外注意,因为对他的 * 会生成相同的引用。

假如我们想构建一个 3*3 的列表,[['-', '-', '-'], ['-', '-', '-'], ['-', '-', '-']]

# 我们可能会想到用这种办法
>>> list3 = [['-'] * 3] * 3
>>> list3
[['-', '-', '-'], ['-', '-', '-'], ['-', '-', '-']]

# 实际他是错的, 因为他生成了相同的引用
>>> list3[0][0] = 1
>>> list3
[[1, '-', '-'], [1, '-', '-'], [1, '-', '-']]

# 他类似下面的代码
>>> row = ['-'] * 3
>>> b = []
>>> for i in range(3):
    ...:     b.append(row)

# 正确做法是
b = [['-'] * 3 for i in range(3)]

# 他类似这种代码
>>> b = []
>>> for i in range(3):
    ...:     row = ['-'] * 3
    ...:     b.append(row)

所以当对序列做 a * n 这种操作时,需要注意序列中是否含有可变对象的引用。

序列的增量赋值

增量赋值运算符+=*=等的表现取决于它们第一个操作的对象,我们把讨论集中在+=上,因为这些概念对于 *= 和其他增量运算符来说都是一样的。

+= 背后的特殊方法是__iadd__,但是如果一个类没有实现这个方法,Python 则会退一步调用__add__。考虑如下这个简单的表达式:

>>> a += b
# 如果 a 实现了 __iadd__ 方法,就会调用这个方法,同时对可变序列(如 list, array.array 等)
# 来说

如果 a 实现了__iadd__方法,就会调用这个方法,同时对可变序列(如 list, array.array 等)来说,a 就会就地改动,就像调用了 a.extend(b)一样。如果 a 没有实现__iadd__方法的话,a += b就等同于a = a + b了:首先会计算 a + b,得到新对象,然后赋值给 a 。也就是说,变量名会不会关联到新对象,取决于是否实现__iadd__方法。

总体来说,可变序列一般都实现了__iadd__方法。而不可变序列本身就不支持该操作,所以没有实现该方法。

上述所说的 += 的概念也适合于 *= ,不同的是后者对应的是 __imul__

str例外,因为对字符串做 += 太普遍了,所以CPython对他进行了优化。

list.sort() 与 sorted()

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

Python惯例,如果一个函数或者方法本身进行的是就地改动,则它应该返回None。

而内置函数sorted()正相反,它会新建一个列表作为返回值,它可以接受任何形式的可迭代对象作为参数,甚至包括不可变序列和生成器。而且,无论他接受的是什么,最后都会返回一个列表。

list.sort 和 sorted 都有两个可选的关键字参数:

  • reverse,如果为True,则会降序输出,默认为False。
  • key,只能传函数,并且函数参数只有一个,这个函数会被应用到序列中的每一个元素上,并按照产生的结果进行排序。key默认用元素自己的值来排序。

双向队列

collections.deque类(双向队列),这是一个线程安全的,可以快速的从两端添加或删除元素的数据类型,适合场景:保存最近用到的几个元素。因为新建一个双向队列时,你可以指定这个队列的大小,如果队列满员了,可以从反向端删除过期的元素,然后在尾端添加新的元素。

推荐阅读更多精彩内容