阅读笔记2:序列构成的数组

1.内置序列类型概览
 
Python标准库用C语言实现了丰富的序列类型:
容器序列:
list,tuple和collections.deque这些序列可以存放不同类型的数据
扁平序列:
str,bytes,bytearray,memoryview和array.array,这些序列只能容纳一种类型
 
容器序列存放的是它们所包含的任意类型的对象的引用;
扁平序列存放的是值而不是引用,扁平序列其实是一段连续的内存空间,但是它只能存放字符,字节和数值这种基础类型
 
序列类型还能按能否被修改来分类:
可变序列:
list,bytearray,array.array,collections.deque和memoryview
不可变序列:
tuple,str和bytes
 
可变序列(MutableSequence)和不可变序列(Sequence)的差异:
内置序列并不是直接从MutableSequence和Sequence继承来的,但是可以了解完整的序列类型包含哪些功能
 
2.列表推导和生成器表达式
列表推导(list comprehension)是构建列表list的快捷方式
生成器表达式(generator expression)则可以用来创建其它任何类型的序列
 
2.1 列表推导
2.1.1 基本介绍:
 
>>> symbols = 'abcdef'
>>> codes = [ord(symbol) for symbol in symbols]
>>> codes
[97, 98, 99, 100, 101, 102]
python会忽略代码里[] {} 和 ()中的换行,如果代码里有多行的列表,列表推导,生成器表达式,字典这一类的,可以省略不太好看的续行符 \
 
*****列表推导不会再有变量泄漏的问题
 
Python 2.x中,在列表推导中for关键词之后的赋值操作可能会影响列表推导上下文中的同名变量
 >>> x = "my prevalue"
>>> dummy = [x for x in 'ABC']
>>> x
'C'

在Python 3中,不会出现这样的情况;列表推导,生成器表达式,以及同它们很相似的集合set推导和字典推导,在python 3中都有了自己的局部作用域
>>> x = "my prevalue"
>>> dummy = [x for x in 'ABC']
>>> x
'my prevalue'
 
2.1.2. 列表推导和filter和map的比较:
 
filter和map合起来能做的事情,列表推导也可以做:
symbols = '$¢£¥€¤'
basic_ascii = [ord(s) for s in symbols if ord(s) > 127]
basic_ascii_another = list(filter(lambda c: c > 127, map(ord, symbols)))
 
2.1.3.利用列表推导计算笛卡尔积:

>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> tshirts = [(color, size) for color in colors
                                     for size in sizes ]
>>> tshirts
[('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'), ('white', 'M'), ('white', 'L')]

2.2 生成器表达式

生成器表达式背后遵守了迭代器协议,可以逐个产生出元素,可以节省内存
生成器表达式的语法和列表推导差不多,只不过把方括号换成圆括号而已

>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> for tshirt in ('%s %s' % (c, s) for c in colors for s in sizes):    
  print tshirt
    
black S
black M
black L
white S
white M
white L
>>>
3.元组

元组除用作不可变列表外,还可以用于记录没有字段名的记录

3.1 元组拆包

元组拆包可以应用到任意可迭代对象上,唯一的硬性要求,被可迭代对象中的元素数量必须要跟接受这些元素的元组的空档数一致

例1:平行赋值

>>> a_and_b = (1, 2)
>>> a,b = a_and_b
>>> a
1
>>> b
2

例2:不使用中间变量交换两个变量的值

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

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

>>> def my_add(a,b):
    print a+b
>>> my_add(1,2)
3
>>> t=(1,2)
>>> my_add(*t)
3

例4:函数用元组的形式返回多个值

>>> import os
>>> _, filename = os.path.split('/home/luciano/.ssh/idrsa.pub')
>>> filename
'idrsa.pub'
>>>

在进行拆包的时候,如果不是对元组里的所有数据感兴趣,可以用_占位符帮助处理这种情况

例5:用*来处理剩下的元素

python中,函数用*args来获取不确定数量的参数算是一种经典写法
python3开始,这个概念被扩展到了平行赋值中
>>> a, b, *rest = range(5)
>>> a, b, rest
(0, 1, [2, 3, 4])
>>> a, b, *rest = range(3)
>>> a, b, rest
(0, 1, [2])
>>> a, b, *rest = range(2)
>>> a, b, rest
(0, 1, [])
在平行赋值中, * 前缀只能用在一个变量名前面,但是这个变量可以出现在赋值表达式的任意位置
>>> a, *body, c, d = range(5)
>>> a, body, c, d
(0, [1, 2], 3, 4)
>>> *head, b, c, d = range(5)
>>> head, b, c, d
([0, 1], 2, 3, 4)

3.2 嵌套元组拆包

>>> mytest = [ ('record1', 1, (1,2)),
           ('record2', 2, (3,4)),
       ]
>>> for name, number, (a,b) in mytest:
    if a>1:
        print name,a,b

record2 3 4

3.3 具名元组

collections.namedtuple是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类

用namedtuple构建的类的实例所消耗的内存和元组是一样的,因为字段名都被存在对应的类里,这个实例跟普通的对象实例比起来也要小一些,因为python不会用__dict__来存放

>>> import collections
>>> Card = collections.namedtuple('Card', ['rank', 'suit'])
>>> card = Card('1', 'diamond')
>>> card
Card(rank='1', suit='diamond')
>>> card.rank
'1'
>>> card.suit
'diamond'
>>> card[1]
'diamond'

除了从普通元组继承来的属性之外,具名元组还有一些自己专有的属性
_fields 类属性
_make(iterable) 类方法
_asdict() 实例方法

>>> Card._fields
('rank', 'suit')
>>> card_data = ('2', 'diamond')
>>> card_2 = Card._make(card_data)
>>> card_2
Card(rank='2', suit='diamond')
>>> card_2._asdict()
OrderedDict([('rank', '2'), ('suit', 'diamond')])

4.切片

4.1 切片和区间忽略最后一个元素的原因

当只有最后一个位置信息时,可以快速看出切片和区间里有几个元素:
range(3)和mylist[:3]都返回3个元素
当起止位置信息都可见时,可以快速计算出切片和区间的长度,用后一个数减去第一个下标(stop-start)即可
可以利用任意一个下标把序列分割成不重叠的两部分 mylist[:x]和mylist[x:]

4.2 对对象进行切片

>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::-2]
'eccb'

对seq[start:sop:step]进行求值的时候,python会调用seq.__getitem__(slice(start, stop, step))

用有名字的切片比用硬编码的数字区间要方便的多:
>>> invoice = """
    1001     book1     $1.0     3     $3.0
    1002     book2     $2.0     4     $8.0
    """
>>> NUMBER = slice(0, 9)
>>> DESC = slice(9,19)
>>> line_items=invoice.split('\n')[1:]
>>> for item in line_items:
    print item[NUMBER], item[DESC]
    
    1001      book1
    1002      book2

4.3 多维切片和省略

[ ]运算符里还可使用以逗号分隔的多个索引或者切片
外部库 NumPy就用到了这个特性
a[m:n, k:l]

python内置的序列类型都是一维的。只支持单一索引

省略的 ... 它可以当作切片规范的一部分
x[i,...] 就是x[i, :, :, :]的缩写

4.4 给切片赋值

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

>>> l = list(range(10))
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[2:5] = [20,30]
>>> l
[0, 1, 20, 30, 5, 6, 7, 8, 9]
>>> del l[5:7]
>>> l
[0, 1, 20, 30, 5, 8, 9]
>>> l[3::2] =[11,22]
>>> l
[0, 1, 20, 11, 5, 22, 9]
>>> l[2:5]=100

Traceback (most recent call last):
  File "<pyshell#136>", line 1, in <module>
    l[2:5]=100
TypeError: can only assign an iterable
>>> l[2:5]=[100]
>>> l
[0, 1, 100, 22, 9]

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

5.对序列使用 + 和 *

>>> list_a = [1,2,3,4]
>>> list_b = [5,6,7,8]
>>> list_a + list_b
[1, 2, 3, 4, 5, 6, 7, 8]
>>> list_a * 5
[1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]
>>> 5 * list_a
[1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]
>>>

+ 和 *都遵循:不修改原有对象,构建一个全新的序列

建立由列表组成的列表:

正确的方法:

>>> board = [['_'] * 3 for i in range(3)]
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[1][2] = 'x'
>>> board
[['_', '_', '_'], ['_', '_', 'x'], ['_', '_', '_']]
>>>

错误的方法:

>>> wrong_board = [['_'] * 3 ] * 3
>>> wrong_board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> wrong_board[1][2] = '0'
>>> wrong_board
[['_', '_', '0'], ['_', '_', '0'], ['_', '_', '0']]
>>>

问题在于:外面的列表都指向同一个列表的引用。

6.序列的增量赋值

+= 背后的特殊方法是 __iadd__,但是如果一个类没有实现这个方法的话,python会退一步调用__add__

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

同样的概念,也适用于 *=,不同的是,后者对应的是__imul__

实例:
>>> l = [1,2,3]
>>> id(l)
58378376L
>>> l *= 2
>>> l
[1, 2, 3, 1, 2, 3]
>>> id(l)
58378376L
>>>
>>> t = (1,2,3)
>>> id(t)
56011512L
>>> t *= 2
>>> id(t)
50605032L

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

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

特殊例子:

>>> t = (1,2,[30,40])
>>> t[2] += [50,50]

Traceback (most recent call last):
  File "<pyshell#24>", line 1, in <module>
    t[2] += [50,50]
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 40, 50, 50])
>>>

t[2]被赋值,但是抛出异常

>>> t
(1, 2, [30, 40, 50, 50])
>>> t[2].append([10,20])
>>> t
(1, 2, [30, 40, 50, 50, [10, 20]])
>>>

用append,t[2]被赋值,也不抛出异常

总结:
不要把可变对象放在元组里
增量赋值不是一个原子操作,在例子中,它虽然抛出了异常,但还是完成了操作

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

list.sort  就地排序列表,不会产生新的对象,返回None值
sorted   新建一个列表做为返回值。可以接受任何形式的可迭代对象,甚至包括不可变序列或生成器

它们有两个可选的关键字参数:

reverse:  
如果被设定为True,被排序的序列元素会以降序输出,默认是False

key:
一个只有一个参数的函数,这个函数会被用在序列里的每一个元素上,所产生的结果是排序算法依赖的对比关键字; 这个参数的默认值是恒等函数,也就是默认用元素自己的值来排序

8. 用bisect来管理已排序的序列

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

bisect(haystack, needle) : 在haystack里搜索needle的位置,该位置满足的条件是,把needle插入这个位置之后,haystack还能保持升序 (其中,haystack必须是个有序的序列)

假设index是bisect找到的位置:
haystack.insert(index, needle)

或者也可以用insort来一步到位
# BEGIN BISECT_DEMO
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)  # <1>
        offset = position * '  |'  # <2>
        print(ROW_FMT.format(needle, position, offset))  # <3>

if __name__ == '__main__':

    if sys.argv[-1] == 'left':    # <4>
        bisect_fn = bisect.bisect_left
    else:
        bisect_fn = bisect.bisect

    print('DEMO:', bisect_fn.__name__)  # <5>
    print('haystack ->', ' '.join('%2d' % n for n in HAYSTACK))
    demo(bisect_fn)

# END BISECT_DEMO
9. 当列表不是首选时

数组的效率比较高,数组背后存的并不是float对象,而是数字的机器翻译,也就是字节表达,这和c语言中的数组一样
如果需要频繁对序列做先进先出的操作,deque(双端队列)的速度应该更快
如果包含操作的频率很高(比如检查一个元素是否出现在一个集合中),用set会更合适,set为检查元素是否存在做过优化,它并不是序列,它是无序的

9.1 数组
数组支持所有可变序列有关的操作,包括 pop, insert 和extend,另外,还提供从文件读取和存入的更快的方法,如 frombytes和tofile
python数组和c语言一样精简,创建数组需要一个类型码,这个类型码用来表示在底层的c语言应该存放怎样的数据类型,比如b类类型码代表的是有符号的字符,它创建出的数组就只能存放一个字节大小的整数,范围从-128到127
python不允许你在数组里存放除指定类型之外的数据

>>> from array import array
>>> from random import random
>>> floats = array('d', (random() for i in range(10**7)))
>>> floats[-1]
0.676530926941209
>>> fp = open('floats.bin','wb')
>>> floats.tofile(fp)
>>> fp.close()
>>> floats2 = array('d')
>>> fp = open('floats.bin','rb')
>>> floats2.fromfile(fp, 10**7)
>>> fp.close()
>>> floats2[-1]
0.676530926941209
>>> floats2 == floats
True

9.2 内存视图

内存视图其实是去泛化和去数学化的NumPy数组,它让你在不需要复制内容的前提下,在数据结构之间共享内存。其中数据结构可以是任何形式,比如PIL图片,SQLite数据库和NumPy的数组。这个功能在处理大型数据集合的时候非常需要

memoryview.cast的概念和数组模块类似,能用不同的方式读写同一块内存数据,而且内容字节不会随意移动,和c语言中类型转换的概念差不多。memoryview.cast会把同一块内存里的内容打包成一个全新的memoryview


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

deque:
collections.deque类是一个线程安全,可以快速从两端添加或者删除元素的数据类型。
如果想要有一种数据类型来存放最近用到的几个元素,deque也是一个很好的选择。因为在新建一个双向队列的时候,可以指定这个队列的大小,如果这个队列满员了,还可以从反向删除过期的元素,然后在尾端添加新的元素

双向队列实现了大部分列表所拥有的方法,也有一些额外的符合自身设计的方法,比如说popleft和rotate,但是为了实现这些方法,双向队列也付出了一些代价,从队列中删除元素的操作会慢一些,因为它只对头尾的操作进行了优化

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

其他形式的队列:

queue
提供了同步(线程安全)类Queue, LifoQueue和PriorityQueue,不同的线程可以利用这些数据类型来交换信息。这三个类的构造方法都有一个可选参数maxsize,它接受正整数作为输入值,用来限定队列的大小,但是在满员的时候,这些类不会扔掉旧的元素来腾出位置,相反,如果队列满了,它就会被锁住,直到另外的线程移除了某个元素而腾出了位置。这一特性让这些类很适合用来控制活跃线程的数量

multiprocessing

asyncio

heapq

posted on 2018-09-14 10:07  cherryjing0629  阅读(246)  评论(0编辑  收藏  举报

导航