Raul2018

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

Python内置了许多非常有用的数据结构,比如列表(list),集合(set)以及字典(dictionary)。就绝大部分情况而言,我们可以直接使用这些数据结构。但是,我们通常还要考虑比如搜索,排序,排列以及筛选等这一类常见的问题。因此,本章的目的就是来讨论常见的数据结构和通数据相关的算法。此外,在collections模块中也包含了针对各种数据结构的解决方案。

1.1 将序列分解为单独的变量

1.1.1 问题

我们有一个包含N个元素的元组或序列,现在想将它分解为N个单独的变量。

1.1.2解决方案

任何序列(或可迭代的对象)都可以通过一个简单的赋值操作来分解为单独的变量。

唯一的要求是变量的总数和结构要与序列相吻合。列如:

>>> p = (4,5)

>>> x, y = p

>>> x

4

>>> y

5

>>> 

 

 

>>> data = ['ACNE', 50, 91.1, (2012, 12, 21)]

>>> name, shares, price, data = data

>>> name

'ACNE'

>>> data

(2012, 12, 21)

>>> 

 

>>> name, shares, price, data = data

>>> name

'ACME'

>>> year

2012

>>> mon

12

>>> day

21

>>>

 

如果元素的数量不匹配,将得到一个错误提示。列如

>>> p = (4,5)

>>> x,y,z = p

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

ValueError: not enough values to unpack (expected 3, got 2)

>>> 

 

1.1.3讨论

实际上不仅仅只是元素或列表,只要是对象恰好是可迭代的,那么就可以执行分解操作。这包括字符串,文件,迭代器以及生成器。比如:

>>> s = 'Hello'

>>> a,b,c,d,e = s

>>> a

'H'

>>> e

'o'

>>> b

'e'

>>> 

当做分解操作时,有时候可能想丢弃某些特定的值。Python并没有提供特殊的语法来实现这一点,但是通常可以选一个用不到的变量名,以此来作为要丢弃的值得名称。

列如:

>>> data = ['ACME',50,91.1,(2012,12,21)]

>>> _, shares, price, _ = data

>>> shares

50

>>> price

91.1

>>> 

但是请确保选择的变量名没有在其他地方用到过。

 

1.2 从任意长度的可迭代对象中分解元素

 

1.2.1 问题

需要从某个可迭代对象中分解出N个元素,但是这个可迭代对象的长度可能超过N,这回导致出现“分解的值过多(too many values to unpack)的异常”

1.2.2解决方案

Python的"*表达式,星号表达式"可以用来解决这个问题。列如,假设开设了一门课程,并决定在期末的作业成绩中去掉第一个和最后一个,只对中间剩下的成绩作平均分统计。如果只有4个成绩,也许可以简单地将4个都分解出来,但是如果有24个呢?*表达式使这一切都变得简单:

 

 def drop_first_last(grades):

     first, *middle, last = grades 

     return avg(middle)

 

另一个用例是假设有一些用户记录,记录由姓名和电子邮件地址组成,后面跟着任意数量的电话号码。则可以像这样分解记录;

>>> record = ('Dave', 'dave@example.com', '773-555-1212', '847-555-1212')
>>> name, email, *phone_numbers = record
>>> name
'Dave'
>>> email
'dave@example.com'
>>> phone_numbers
['773-555-1212', '847-555-1212']
>>>

值得注意的是上面解压出的 phone_numbers 变量永远都是列表类型,不管解压的电话号码数量是多少(包括 0 个)。 所以,任何使用到 phone_numbers 变量的代码就不需要做多余的类型检查去确认它是否是列表类型了。

 星号表达式也能用在列表的开始部分。比如,你有一个公司前 8 个月销售数据的序列, 但是你想看下最近一个月数据和前面 7 个月的平均值的对比。你可以这样做:

*trailing_qtrs, current_qtr = sales_record
trailing_avg = sum(trailing_qtrs) / len(trailing_qtrs)
return avg_comparison(trailing_avg, current_qtr)

下面是在 Python 解释器中执行的结果:
>>> *trailing, current = [10, 8, 7, 1, 9, 5, 10, 3]
>>> trailing
[10, 8, 7, 1, 9, 5, 10]
>>> current
3

讨论:

扩展的迭代解压语法是专门为解压不确定个数或任意个数元素的可迭代对象而设计的。 通常,这些可迭代对象的元素结构有确定的规则(比如第 1 个元素后面都是电话号码), 星号表达式让开发人员可以很容易的利用这些规则来解压出元素来。 而不是通过一些比较复杂的手段去获取这些关联的元素值。

值得注意的是,星号表达式在迭代元素为可变长元组的序列时是很有用的。 比如,下面是一个带有标签的元组序列:

records = [
    ('foo', 1, 2),
    ('bar', 'hello'),
    ('foo', 3, 4),
]

def do_foo(x, y):
    print('foo', x, y)

def do_bar(s):
    print('bar', s)

for tag, *args in records:
    if tag == 'foo':
        do_foo(*args)
    elif tag == 'bar':
        do_bar(*args)

foo 1 2
bar hello
foo 3 4

当和某些特定的字符串处理操作相结合,比如做做字符串分割(splitting)操作时,这种星号解压语法也很有用。

>>> line = 'nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false'
>>> uname, *fields, homedir, sh = line.split(':')
>>> uname
'nobody'
>>> homedir
'/var/empty'
>>> sh
'/usr/bin/false'
>>>
有时候可能想解压出某些值然后丢弃他们。在分解的时候,不能只是指定一个单独的*,但是可以使用几个常用来表示待丢弃值得变量名,比如_或者ign(ignored)。列如:

代码示例:

>>> record = ('ACME', 50, 123.45, (12, 18, 2012))
>>> name, *_, (*_, year) = record
>>> name
'ACME'
>>> year
2012
>>>

在很多函数式语言中,星号解压语法跟列表处理有许多相似之处。比如,如果你有一个列表, 你可以很容易的将它分割成前后两部分:

>>> items = [1, 10, 7, 4, 5, 9]
>>> head, *tail = items
>>> head
1
>>> tail
[10, 7, 4, 5, 9]
>>>

如果你够聪明的话,还能用这种分割语法去巧妙的实现递归算法。比如:

>>> def sum(items):
...     head, *tail = items
...     return head + sum(tail) if tail else head
...
>>> sum(items)
36
>>>

然后,由于语言层面的限制,递归并不是 Python 擅长的。 因此,最后那个递归演示仅仅是个好奇的探索罢了,对这个不要太认真了。

1.3  保存最后一个元素

1.3.1问题

 

在迭代操作或者其他操作的时候,怎样只保留最后有限几个元素的历史记录?

 

 

 1.3.2解决方案
保存有限的历史记录可算是collections.deque的完美应用场景了。列如:下面的代码对一系列文本行做简单的文本匹配操作,当发现有匹配时就输出以前的匹配行以及最后检查过的N行文本。

 

解决方案

保留有限历史记录正是 collections.deque 大显身手的时候。比如,下面的代码在多行上面做简单的文本匹配, 并返回匹配所在行的最后N行:

from collections import deque


def search(lines, pattern, history=5):
    previous_lines = deque(maxlen=history)
    for line in lines:
        if pattern in line:
            yield line, previous_lines
        previous_lines.append(line)

# Example use on a file
if __name__ == '__main__':
    with open(r'../../cookbook/somefile.txt') as f:
        for line, prevlines in search(f, 'python', 5):
            for pline in prevlines:
                print(pline, end='')
            print(line, end='')
            print('-' * 20)

讨论

我们在写查询元素的代码时,通常会使用包含 yield 表达式的生成器函数,也就是我们上面示例代码中的那样。 这样可以将搜索过程代码和使用搜索结果代码解耦。如果你还不清楚什么是生成器,请参看 4.3 节。

使用 deque(maxlen=N) 构造函数会新建一个固定大小的队列。当新的元素加入并且这个队列已满的时候, 最老的元素会自动被移除掉。

代码示例:

>>> q = deque(maxlen=3)
>>> q.append(1)
>>> q.append(2)
>>> q.append(3)
>>> q
deque([1, 2, 3], maxlen=3)
>>> q.append(4)
>>> q
deque([2, 3, 4], maxlen=3)
>>> q.append(5)
>>> q
deque([3, 4, 5], maxlen=3)

尽管你也可以手动在一个列表上实现这一的操作(比如增加、删除等等)。但是这里的队列方案会更加优雅并且运行得更快些。

更一般的, deque 类可以被用在任何你只需要一个简单队列数据结构的场合。 如果你不设置最大队列大小,那么就会得到一个无限大小队列,你可以在队列的两端执行添加和弹出元素的操作。

代码示例:

>>> q = deque()
>>> q.append(1)
>>> q.append(2)
>>> q.append(3)
>>> q
deque([1, 2, 3])
>>> q.appendleft(4)
>>> q
deque([4, 1, 2, 3])
>>> q.pop()
3
>>> q
deque([4, 1, 2])
>>> q.popleft()
4

在队列两端插入或删除元素时间复杂度都是 O(1) ,区别于列表,在列表的开头插入或删除元素的时间复杂度为 O(N) 。

posted on 2018-07-19 08:20  Raul2018  阅读(493)  评论(0编辑  收藏  举报