Python Cookbook(第3版)中文版

978-7-115-37959-7
作者: 【美】David Beazley Brian K.Jones
译者: 陈舸
编辑: 傅道坤
分类: Python

图书目录:

详情

本书介绍了Python应用在各个领域中的一些使用技巧和方法,其主题涵盖了数据结构和算法,字符串和文本,数字、日期和时间,迭代器和生成器,文件和I/O,数据编码与处理,函数,类与对象,元编程,模块和包,网络和Web编程,并发,实用脚本和系统管理,测试、调试以及异常,C语言扩展等。

图书摘要

版权信息

书名:Python Cookbook(第3版)中文版

ISBN:978-7-115-37959-7

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美] David Beazley Brian K.Jones

  译    陈 舸

  责任编辑 傅道坤

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Copyright © 2013 by O’Reilly Media.Inc.

Simplified Chinese Edition, jointly published by O’Reilly Media, Inc. and Posts & Telecom Press, 2015. Authorized translation of the English edition, 2011 O’Reilly Media, Inc., the owner of all rights to publish and sell the same.

All rights reserved including the rights of reproduction in whole or in part in any form.

本书中文简体字版由O’Reilly Media, Inc.授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式复制或抄袭。

版权所有,侵权必究。


本书介绍了Python应用在各个领域中的一些使用技巧和方法,其主题涵盖了数据结构和算法,字符串和文本,数字、日期和时间,迭代器和生成器,文件和I/O,数据编码与处理,函数,类与对象,元编程,模块和包,网络和Web编程,并发,实用脚本和系统管理,测试、调试以及异常,C语言扩展等。

本书覆盖了Python应用中的很多常见问题,并提出了通用的解决方案。书中包含了大量实用的编程技巧和示例代码,并在Python 3.3环境下进行了测试,可以很方便地应用到实际项目中去。此外,本书还详细讲解了解决方案是如何工作的,以及为什么能够工作。

本书非常适合具有一定编程基础的Python程序员阅读参考。


O’Reilly Media通过图书、杂志、在线服务、调查研究和会议等方式传播创新知识。自1978 年开始,O’Reilly一直都是前沿发展的见证者和推动者。超级极客们正在开创着未来,而我们关注真正重要的技术趋势—通过放大那些“细微的信号”来刺激社会对新科技的应用。作为技术社区中活跃的参与者,O’Reilly的发展充满了对创新的倡导、创造和发扬光大。

O’Reilly为软件开发人员带来革命性的“动物书”;创建第一个商业网站(GNN);组织了影响深远的开放源代码峰会,以至于开源软件运动以此命名;创立了Make杂志,从而成为DIY革命的主要先锋;公司一如既往地通过多种形式缔结信息与人的纽带。O’Reilly的会议和峰会集聚了众多超级极客和高瞻远瞩的商业领袖,共同描绘出开创新产业的革命性思想。作为技术人士获取信息的选择,O’Reilly现在还将先锋专家的知识传递给普通的计算机用户。无论是通过书籍出版,在线服务或者面授课程,每一项O’Reilly的产品都反映了公司不可动摇的理念——信息是激发创新的力量。

“O’Reilly Radar博客有口皆碑。”

——Wired

“O’Reilly 凭借一系列(真希望当初我也想到了)非凡想法建立了数百万美元的业务。”

——Business 2.0

“O’Reilly Conference是聚集关键思想领袖的绝对典范。”

——CRN

“一本O’Reilly的书就代表一个有用、有前途、需要学习的主题。”

——Irish Times

“Tim是位特立独行的商人,他不光放眼于最长远、最广阔的视野并且切实地按照Yogi Berra的建议去做了:‘如果你在路上遇到岔路口,走小路(岔路)。’回顾过去Tim似乎每一次都选择了小路,而且有几次都是一闪即逝的机会,尽管大路也不错。”

——Linux Journal


David Beazley是一位居住在芝加哥的独立软件开发者以及图书作者。他主要的工作在于编程工具,提供定制化的软件开发服务,以及为软件开发者、科学家和工程师教授编程实践课程。他最为人熟知的工作在于Python编程语言,他已为此创建了好几个开源的软件包(例如Swig和PLY),并且是备受赞誉的图书Python Essential Reference的作者。他也对C、C++以及汇编语言下的系统编程有着丰富的经验。

Brain K. Jones是普林斯顿大学计算机系的一位系统管理员。


自2008年以来,我们已经目睹了整个Python世界正缓慢向着Python 3进化的事实。众所周知,完全接纳Python 3要花很长的时间。事实上,就在写作本书时(2013年),大多数Python程序员仍然坚持在生产环境中使用Python 2。关于Python 3不能向后兼容的事实也已经做了许多努力来补救。的确,向后兼容性对于任何已经存在的代码库来说是个问题。但是,如果你着眼于未来,你会发现Python 3带来的好处绝非那么简单。

正因为Python 3是着眼于未来的,本书在之前的版本上做了很大程度的修改。首先也是最重要的一点,这是一本积极拥抱Python 3的书。所有的章节都采用Python 3.3来编写并进行了验证,没有考虑老的Python版本或者“老式”的实现方式。事实上,许多章节都只适用于Python 3.3甚至更高的版本。这么做可能会有风险,但是最终的目的是要编写一本Python 3的秘籍,尽可能基于最先进的工具和惯用法。我们希望本书可以指导人们用Python 3编写新的代码,或者帮助开发人员将已有的代码升级到Python 3。

无需赘言,以这种风格来编写本书给编辑工作带来了一定的挑战。只要在网络上搜索一下Python秘籍,立刻就能在ActiveState的Python版块或者Stack Overflow这样的站点上找到数以千计的使用心得和秘籍。但是,大部分这类资源已经沉浸在历史和过去中了。由于这些心得和秘籍几乎完全是针对Python 2所写的,其中常常包含有各种针对Python不同版本(例如2.3版对比2.4版)之间差异的变通方法和技巧。此外,这些网上资源常常使用过时的技术,而这些技术现在成了Python 3.3的内建功能。想寻找专门针对Python 3的资源会比较困难。

本书并非搜寻特定于Python 3方面的秘籍将其汇集而成,本书的主题都是在创作中由现有的代码和技术而产生出的灵感。我们将这些思想作为跳板,尽可能采用最现代化的Python编程技术来写作,因此本书的内容完全是原创性的。对于任何希望以现代化的风格来编写代码的人,本书都可以作为参考手册。

在选择应该包含哪些章节时,我们有一个共识。那就是根本不可能编写一本涵盖了每种Python用途的书。因此,我们在主题上优先考虑Python语言核心方面的内容,以及能够广泛适用于各种应用领域的常见任务。此外,有许多秘籍是用来说明在Python 3中新增的功能,这对许多人来说比较陌生,甚至对于那些使用老版Python经验丰富的程序员也是如此。我们也会优先选择普遍适用的编程技术(即,编程模式)作为主题,而不会选择那些试图解决一个非常具体的实际问题但适用范围太窄的内容。尽管在部分章节中也提到了特定的第三方软件包,但本书绝大多数章节都只关注语言核心和标准库。

本书的目标读者是希望加深对Python语言的理解以及学习现代化编程惯用法的有经验的程序员。本书许多内容把重点放在库、框架和应用中使用的高级技术上。本书假设读者已经有了理解本书主题的必要背景知识(例如对计算机科学的一般性知识、数据结构、复杂度计算、系统编程、并发、C语言编程等)。此外,本书中提到的秘籍往往只是一个框架,意在提供必要的信息让读者可以起步,但是需要读者自己做更多的研究来填补其中的细节。因此,我们假设读者知道如何使用搜索引擎以及优秀的Python在线文档。

有一些更加高级的章节将作为读者耐心阅读的奖励。这些章节对于理解Python底层的工作原理提供了深刻的见解。你将学到新的技巧和技术,可以将这些知识运用到自己的代码中去。

这不是一本用来给初学者首次学习Python编程而使用的书。事实上,本书已经假设读者通过Python教程或者入门书籍了解了基本知识。本书同样不能用来作为快速参考手册(即,快速查询特定模块中的某个函数)。相反,本书的目标是把重点放在特定的编程主题上,展示可能的解决方案并以此作为跳板引导读者学习更加高级的内容。这些内容你可能会在网上或者参考书中遇到过。

 提示

这个图标用来强调一个提示、建议或一般说明。

 警告

这个图标用来说明一个警告或注意事项。

本书中几乎所有的代码示例都可以在http://github.com/dabeaz/python-cookbook上找到。作者欢迎读者针对代码示例提供bug修正、改进以及评论。

本书的目的是为了帮助读者完成工作。一般而言,你可以在你的程序和文档中使用本书中的代码,而且也没有必要取得我们的许可。但是,如果你要复制的是核心代码,则需要和我们打个招呼。例如,你可以在无需获取我们许可的情况下,在程序中使用本书中的多个代码块。但是,销售或分发O’Reilly图书中的代码光盘则需要取得我们的许可。通过引用本书中的示例代码来回答问题时,不需要事先获得我们的许可。但是,如果你的产品文档中融合了本书中的大量示例代码,则需要取得我们的许可。

在引用本书中的代码示例时,如果能列出本书的属性信息是最好不过。一个属性信息通常包括书名、作者、出版社和ISBN。例如:Python Cookbook, 3rd edtion, by David Beazley and Brain K.Jones(O’Reilly)。Copyright 2013 David Beazley and Brain Jones, 978-1-449-34037-7。

在使用书中的代码时,如果不确定是否属于正常使用,或是否超出了我们的许可,请通过permissions@oreilly.com与我们联系。

如果你想就本书发表评论或有任何疑问,敬请联系出版社。

美国:

O’Reilly Media Inc.

1005 Gravenstein Highway North

Sebastopol, CA 95472

中国:

北京市西城区西直门南大街2号成铭大厦C座807室(100035)

奥莱利技术咨询(北京)有限公司

我们还为本书建立了一个网页,其中包含了勘误表、示例和其他额外的信息。你可以通过链接http://oreil.ly/python_cookbook_3e来访问页面。

关于本书的技术性问题或建议,请发邮件到:

bookquestions@oreilly.com

欢迎登录我们的网站(http://www.oreilly.com),查看更多我们的书籍、课程、会议和最新动态等信息。

Facebook: http://facebook.com/oreilly

Twitter: http://twitter.com/oreillymedia

YouTube: http://www.youtube.com/oreillymedia

我们要感谢本书的技术校审人员,他们是Jake Vanderplas、Robert Kern以及Andrea Crotti。感谢他们非常有用的评价,也要感谢整个Python社区的支持和鼓励。我们也要感谢本书第2版的编辑Alex Martelli、Anna Ravenscroft以及David Ascher。尽管本书的第3版是新创作的,但之前的版本为本书提供了挑选主题以及所感兴趣的秘籍的初始框架。最后也是最重要的是,我们要感谢本书早期版本的读者,感谢你们为本书的改进做出的评价和建议。

写一本书绝非易事。因此,我要感谢我的妻子Paula以及我的两个儿子,感谢你们的耐心以及支持。本书中的许多素材都来自于我过去6年里所教的与Python相关的训练课程。因此,我要感谢所有参加了我的课程的学生,正是你们最终促成了本书的问世。我也要感谢Ned Batchelder、Travis Oliphant、Peter Wang、Brain Van de Ven、Hugo Shi、Raymond Hettinger、Michael Foord以及Daniel Klein,感谢他们飞到世界各地去教学,而让我可以留在芝加哥的家中完成本书的写作。感谢来自O’Reilly的Meghan Blanchette以及Rachel Roumeliotis,你们见证了本书的创作过程,当然也经历了那些无法预料到的延期。最后也是最重要的是,我要感谢Python社区不间断的支持,以及容忍我那不着调的胡思乱想。

David M.Beazley

http://www.dabeaz.com

https://twitter.com/dabeaz

我要感谢我的合著者David Beazley以及O’Reilly的Meghan Blanchette和Rachel Roumeliotis,感谢你们和我一起完成了本书的创作。我也要感谢我的妻子Natasha,感谢你在我写作本书时给予的耐心和鼓励,也要谢谢你对于我所有追求的支持。我尤其要感谢Python社区。虽然我已经在多个开源项目和编程语言中有所贡献,但与Python社区长久以来所做的如此令人欣慰和富有意义的工作相比,我做的算不上什么。

Brain K.Jones

http://www.protocolostomy.com

https://twitter.com/bkjones


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

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

任何序列(或可迭代的对象)都可以通过一个简单的赋值操作来分解为单独的变量。唯一的要求是变量的总数和结构要与序列相吻合。例如:

>>> p = (4, 5)
>>> x, y = p
>>> x
4
>>> y
5
>>>

>>> data = [ 'ACME', 50, 91.1, (2012, 12, 21) ]
>>> name, shares, price, date = data
>>> name
'ACME'
>>> date
(2012, 12, 21)

>>> name, shares, price, (year, mon, day) = 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: need more than 2 values to unpack
>>>

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

>>> s = 'Hello'
>>> a, b, c, d, e = s
>>> a
'H'
>>> b
'e'
>>> e
'o'
>>>

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

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

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

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

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 = user_record
>>> name
'Dave'
>>> email
'dave@example.com'
>>> phone_numbers
['773-555-1212', '847-555-1212']
>>>

不管需要分解出多少个电话号码(甚至没有电话号码),变量phone_numbers都一直是列表,而这是毫无意义的。如此一来,对于任何用到了变量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)

当和某些特定的字符串处理操作相结合,比如做拆分(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的强项,这是因为其内在的递归限制所致。因此,最后一个例子在实践中没太大的意义,只不过是一点学术上的好奇罢了。

我们希望在迭代或是其他形式的处理过程中对最后几项记录做一个有限的历史记录统计。

保存有限的历史记录可算是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('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)

尽管可以在列表上手动完成这样的操作(append、del),但队列这种解决方案要优雅得多,运行速度也快得多。

更普遍的是,当需要一个简单的队列结构时,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)。

我们想在某个集合中找出最大或最小的N个元素。

heapq模块中有两个函数——nlargest()和nsmallest()——它们正是我们所需要的。例如:

import heapq

nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]
print(heapq.nlargest(3, nums)) # Prints [42, 37, 23]
print(heapq.nsmallest(3, nums)) # Prints [-4, 1, 2]

这两个函数都可以接受一个参数key,从而允许它们工作在更加复杂的数据结构之上。例如:

portfolio = [
   {'name': 'IBM', 'shares': 100, 'price': 91.1},
   {'name': 'AAPL', 'shares': 50, 'price': 543.22},
   {'name': 'FB', 'shares': 200, 'price': 21.09},
   {'name': 'HPQ', 'shares': 35, 'price': 31.75},
   {'name': 'YHOO', 'shares': 45, 'price': 16.35},
   {'name': 'ACME', 'shares': 75, 'price': 115.65}
]

cheap = heapq.nsmallest(3, portfolio, key=lambda s: s['price'])
expensive = heapq.nlargest(3, portfolio, key=lambda s: s['price'])

如果正在寻找最大或最小的N个元素,且同集合中元素的总数目相比,N很小,那么下面这些函数可以提供更好的性能。这些函数首先会在底层将数据转化成列表,且元素会以堆的顺序排列。例如:

>>> nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]
>>> import heapq
>>> heap = list(nums)
>>> heapq.heapify(heap)
>>> heap
[-4, 2, 1, 23, 7, 2, 18, 23, 42, 37, 8]
>>>

堆最重要的特性就是heap[0]总是最小那个的元素。此外,接下来的元素可依次通过heapq.heappop()方法轻松找到。该方法会将第一个元素(最小的)弹出,然后以第二小的元素取而代之(这个操作的复杂度是O(logN),N代表堆的大小)。例如,要找到第3小的元素,可以这样做:

>>> heapq.heappop(heap)
-4
>>> heapq.heappop(heap)
1
>>> heapq.heappop(heap)
2

当所要找的元素数量相对较小时,函数nlargest()和nsmallest()才是最适用的。如果只是简单地想找到最小或最大的元素(N=1时),那么用min()和max()会更加快。同样,如果N和集合本身的大小差不多大,通常更快的方法是先对集合排序,然后做切片操作(例如,使用sorted(items)[:N]或者sorted(items)[-N:])。应该要注意的是,nlargest()和nsmallest()的实际实现会根据使用它们的方式而有所不同,可能会相应作出一些优化措施(比如,当N的大小同输入大小很接近时,就会采用排序的方法)。

使用本节的代码片段并不需要知道如何实现堆数据结构,但这仍然是一个有趣也是值得去学习的主题。通常在优秀的算法和数据结构相关的书籍里都能找到堆数据结构的实现方法。在heapq模块的文档中也讨论了底层实现的细节。

我们想要实现一个队列,它能够以给定的优先级来对元素排序,且每次pop操作时都会返回优先级最高的那个元素。

下面的类利用heapq模块实现了一个简单的优先级队列:

import heapq
class PriorityQueue:

    def __init__(self):
        self._queue = []
        self._index = 0

def push(self, item, priority):
    heapq.heappush(self._queue, (-priority, self._index, item))
    self._index += 1

def pop(self):
    return heapq.heappop(self._queue)[-1]

下面是如何使用这个类的例子:

>>> class Item:
...      def __init__(self, name):
...           self.name = name
...      def __repr__(self):
...           return 'Item({!r})'.format(self.name)
...
>>> q = PriorityQueue()
>>> q.push(Item('foo'), 1)
>>> q.push(Item('bar'), 5)
>>> q.push(Item('spam'), 4)
>>> q.push(Item('grok'), 1)
>>> q.pop()
Item('bar')
>>> q.pop()
Item('spam')
>>> q.pop()
Item('foo')
>>> q.pop()
Item('grok')
>>>

请注意观察,第一次执行pop()操作时返回的元素具有最高的优先级。我们也观察到拥有相同优先级的两个元素(foo和grok)返回的顺序同它们插入到队列时的顺序相同。

上面的代码片段的核心在于heapq模块的使用。函数heapq.heappush()以及heapq.heappop()分别实现将元素从列表_queue中插入和移除,且保证列表中第一个元素的优先级最低(如1.4节所述)。heappop()方法总是返回“最小”的元素,因此这就是让队列能弹出正确元素的关键。此外,由于push和pop操作的复杂度都是O(logN),其中N代表堆中元素的数量,因此就算N的值很大,这些操作的效率也非常高。

在这段代码中,队列以元组(-priority, index, item)的形式组成。把priority取负值是为了让队列能够按元素的优先级从高到低的顺序排列。这和正常的堆排列顺序相反,一般情况下堆是按从小到大的顺序排序的。

变量index的作用是为了将具有相同优先级的元素以适当的顺序排列。通过维护一个不断递增的索引,元素将以它们入队列时的顺序来排列。但是,index在对具有相同优先级的元素间做比较操作时同样扮演了重要的角色。

为了说明Item实例是没法进行次序比较的,我们来看下面这个例子:

>>> a = Item('foo')
>>> b = Item('bar')
>>> a < b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unorderable types: Item() < Item()
>>>

如果以元组(priority, item)的形式来表示元素,那么只要优先级不同,它们就可以进行比较。但是,如果两个元组的优先级值相同,做比较操作时还是会像之前那样失败。例如:

>>> a = (1, Item('foo'))
>>> b = (5, Item('bar'))
>>> a < b
True
>>> c = (1, Item('grok'))
>>> a < c
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unorderable types: Item() < Item()
>>>

通过引入额外的索引值,以(prioroty, index, item)的方式建立元组,就可以完全避免这个问题。因为没有哪两个元组会有相同的index值(一旦比较操作的结果可以确定,Python就不会再去比较剩下的元组元素了):

>>> a = (1, 0, Item('foo'))
>>> b = (5, 1, Item('bar'))
>>> c = (1, 2, Item('grok'))
>>> a < b
True
>>> a < c
True
>>>

如果想将这个队列用于线程间通信,还需要增加适当的锁和信号机制。请参见12.3节的示例学习如何去做。

关于堆的理论和实现在heapq模块的文档中有着详细的示例和相关讨论。

我们想要一个能将键(key)映射到多个值的字典(即所谓的一键多值字典[multidict])。

字典是一种关联容器,每个键都映射到一个单独的值上。如果想让键映射到多个值,需要将这多个值保存到另一个容器如列表或集合中。例如,可能会像这样创建字典:

d = {
   'a' : [1, 2, 3],
   'b' : [4, 5]
}

e = {
   'a' : {1, 2, 3},
   'b' : {4, 5}
}

要使用列表还是集合完全取决于应用的意图。如果希望保留元素插入的顺序,就用列表。如果希望消除重复元素(且不在意它们的顺序),就用集合。

为了能方便地创建这样的字典,可以利用collections模块中的defaultdict类。defaultdict的一个特点就是它会自动初始化第一个值,这样只需关注添加元素即可。例如:

from collections import defaultdict

d = defaultdict(list)
d['a'].append(1)
d['a'].append(2)
d['b'].append(4)
...

d = defaultdict(set)
d['a'].add(1)
d['a'].add(2)
d['b'].add(4)
...

关于defaultdict,需要注意的一个地方是,它会自动创建字典表项以待稍后的访问(即使这些表项当前在字典中还没有找到)。如果不想要这个功能,可以在普通的字典上调用setdefault()方法来取代。例如:

d = {} # A regular dictionary
d.setdefault('a', []).append(1)
d.setdefault('a', []).append(2)
d.setdefault('b', []).append(4)
...

然而,许多程序员觉得使用setdefault()有点不自然——更别提每次调用它时都会创建一个初始值的新实例了(例子中的空列表[])。

原则上,构建一个一键多值字典是很容易的。但是如果试着自己对第一个值做初始化操作,这就会变得很杂乱。例如,可能会写下这样的代码:

d = {}
for key, value in pairs:
 if key not in d:
         d[key] = []
    d[key].append(value)

使用defaultdict后代码会清晰得多:

d = defaultdict(list)
for key, value in pairs:
    d[key].append(value)

这一节的内容同数据处理中的记录归组问题有很强的关联。请参见1.15节的示例。

我们想创建一个字典,同时当对字典做迭代或序列化操作时,也能控制其中元素的顺序。

要控制字典中元素的顺序,可以使用collections模块中的OrderedDict类。当对字典做迭代时,它会严格按照元素初始添加的顺序进行。例如:

from collections import OrderedDict

d = OrderedDict()
d['foo'] = 1
d['bar'] = 2
d['spam'] = 3
d['grok'] = 4

# Outputs "foo 1", "bar 2", "spam 3", "grok 4"
for key in d:
print(key, d[key])

当想构建一个映射结构以便稍后对其做序列化或编码成另一种格式时,OrderedDict就显得特别有用。例如,如果想在进行JSON编码时精确控制各字段的顺序,那么只要首先在OrderedDict中构建数据就可以了。

>>> import json
>>> json.dumps(d)
'{"foo": 1, "bar": 2, "spam": 3, "grok": 4}'
>>>

OrderedDict内部维护了一个双向链表,它会根据元素加入的顺序来排列键的位置。第一个新加入的元素被放置在链表的末尾。接下来对已存在的键做重新赋值不会改变键的顺序。

请注意OrderedDict的大小是普通字典的2倍多,这是由于它额外创建的链表所致。因此,如果打算构建一个涉及大量OrderedDict实例的数据结构(例如从CSV文件中读取100000行内容到OrderedDict列表中),那么需要认真对应用做需求分析,从而判断使用OrderedDict所带来的好处是否能超越因额外的内存开销所带来的缺点。

我们想在字典上对数据执行各式各样的计算(比如求最小值、最大值、排序等)。

假设有一个字典在股票名称和对应的价格间做了映射:

prices = {
   'ACME': 45.23,
   'AAPL': 612.78,
    'IBM': 205.55,
    'HPQ': 37.20,
    'FB': 10.75
}

为了能对字典内容做些有用的计算,通常会利用zip()将字典的键和值反转过来。例如,下面的代码会告诉我们如何找出价格最低和最高的股票。

min_price = min(zip(prices.values(), prices.keys()))
# min_price is (10.75, 'FB')

max_price = max(zip(prices.values(), prices.keys()))
# max_price is (612.78, 'AAPL')

同样,要对数据排序只要使用zip()再配合sorted()就可以了,比如:

prices_sorted = sorted(zip(prices.values(), prices.keys()))
# prices_sorted is [(10.75, 'FB'), (37.2, 'HPQ'),
# (45.23, 'ACME'), (205.55, 'IBM'),
# (612.78, 'AAPL')]

当进行这些计算时,请注意zip()创建了一个迭代器,它的内容只能被消费一次。例如下面的代码就是错误的:

prices_and_names = zip(prices.values(), prices.keys())
print(min(prices_and_names))    # OK
print(max(prices_and_names))    # ValueError: max() arg is an empty sequence

如果尝试在字典上执行常见的数据操作,将会发现它们只会处理键,而不是值。例如:

min(prices)     # Returns 'AAPL'
max(prices)     # Returns 'IBM'

这很可能不是我们所期望的,因为实际上我们是尝试对字典的值做计算。可以利用字典的values()方法来解决这个问题:

min(prices.values())    # Returns 10.75
max(prices.values())    # Returns 612.78

不幸的是,通常这也不是我们所期望的。比如,我们可能想知道相应的键所关联的信息是什么(例如哪支股票的价格最低?)

如果提供一个key参数传递给min()和max(),就能得到最大值和最小值所对应的键是什么。例如:

min(prices, key=lambda k: prices[k])    # Returns 'FB'
max(prices, key=lambda k: prices[k])    # Returns 'AAPL'

但是,要得到最小值的话,还需要额外执行一次查找。例如:

min_value = prices[min(prices, key=lambda k: prices[k])]

利用了zip()的解决方案是通过将字典的键-值对“反转”为值-键对序列来解决这个问题的。

当在这样的元组上执行比较操作时,值会先进行比较,然后才是键。这完全符合我们的期望,允许我们用一条单独的语句轻松的对字典里的内容做整理和排序。

应该要注意的是,当涉及(value, key)对的比较时,如果碰巧有多个条目拥有相同的value值,那么此时key将用来作为判定结果的依据。例如,在计算min()和max()时,如果碰巧value的值相同,则将返回拥有最小或最大key值的那个条目。示例如下:

>>> prices = { 'AAA' : 45.23, 'ZZZ': 45.23 }
>>> min(zip(prices.values(), prices.keys()))
(45.23, 'AAA')
>>> max(zip(prices.values(), prices.keys()))
(45.23, 'ZZZ')
>>>

有两个字典,我们想找出它们中间可能相同的地方(相同的键、相同的值等)。

考虑如下两个字典:

a = {
   'x' : 1,
   'y' : 2,
   'z' : 3
}

b = {
   'w' : 10,
   'x' : 11,
   'y' : 2
}

要找出这两个字典中的相同之处,只需通过keys()或者items()方法执行常见的集合操作即可。例如:

# Find keys in common
a.keys() & b.keys() # { 'x', 'y' }

# Find keys in a that are not in b
a.keys() - b.keys() # { 'z' }

# Find (key,value) pairs in common
a.items() & b.items() # { ('y', 2) }

这些类型的操作也可用来修改或过滤掉字典中的内容。例如,假设想创建一个新的字典,其中会去掉某些键。下面是使用了字典推导式的代码示例:

# Make a new dictionary with certain keys removed
c = {key:a[key] for key in a.keys() - {'z', 'w'}}
# c is {'x': 1, 'y': 2}

字典就是一系列键和值之间的映射集合。字典的keys()方法会返回keys-view对象,其中暴露了所有的键。关于字典的键有一个很少有人知道的特性,那就是它们也支持常见的集合操作,比如求并集、交集和差集。因此,如果需要对字典的键做常见的集合操作,那么就能直接使用keys-view对象而不必先将它们转化为集合。

字典的items()方法返回由(key,value)对组成的items-view对象。这个对象支持类似的集合操作,可用来完成找出两个字典间有哪些键值对有相同之处的操作。

尽管类似,但字典的values()方法并不支持集合操作。部分原因是因为在字典中键和值是不同的,从值的角度来看并不能保证所有的值都是唯一的。单这一条原因就使得某些特定的集合操作是有问题的。但是,如果必须执行这样的操作,还是可以先将值转化为集合来实现。

我们想去除序列中出现的重复元素,但仍然保持剩下的元素顺序不变。

如果序列中的值是可哈希(hashable)的,那么这个问题可以通过使用集合和生成器轻松解决。示例如下[1]

def dedupe(items):
    seen = set()
 for item in items:
 if item not in seen:
 yield item
            seen.add(item)

这里是如何使用这个函数的例子:

>>> a = [1, 5, 2, 1, 9, 1, 5, 10]
>>> list(dedupe(a))
[1, 5, 2, 9, 10]
>>>

只有当序列中的元素是可哈希的时候才能这么做。如果想在不可哈希的对象(比如列表)序列中去除重复项,需要对上述代码稍作修改:

def dedupe(items, key=None):
    seen = set()
 for item in items:
        val = item if key is None else key(item)
 if val not in seen:
 yield item
            seen.add(val)

这里参数key的作用是指定一个函数用来将序列中的元素转换为可哈希的类型,这么做的目的是为了检测重复项。它可以像这样工作:

>>> a = [ {'x':1, 'y':2}, {'x':1, 'y':3}, {'x':1, 'y':2}, {'x':2, 'y':4}]
>>> list(dedupe(a, key=lambda d: (d['x'],d['y'])))
[{'x': 1, 'y': 2}, {'x': 1, 'y': 3}, {'x': 2, 'y': 4}]
>>> list(dedupe(a, key=lambda d: d['x']))
[{'x': 1, 'y': 2}, {'x': 2, 'y': 4}]
>>>

如果希望在一个较复杂的数据结构中,只根据对象的某个字段或属性来去除重复项,那么后一种解决方案同样能完美工作。

如果想要做的只是去除重复项,那么通常足够简单的办法就是构建一个集合。例如:

>>> a
[1, 5, 2, 1, 9, 1, 5, 10]
>>> set(a)
{1, 2, 10, 5, 9}
>>>

但是这种方法不能保证元素间的顺序不变[2],因此得到的结果会被打乱。前面展示的解决方案可避免出现这个问题。

本节中对生成器的使用反映出一个事实,那就是我们可能会希望这个函数尽可能的通用——不必绑定在只能对列表进行处理。比如,如果想读一个文件,去除其中重复的文本行,可以只需这样处理:

with open(somefile,'r') as f:
 for line in dedupe(f):
        ...

我们的dedupe()函数也模仿了内置函数sorted()、min()以及max()对key函数的使用方式。例子可参考1.8节和1.13节。

我们的代码已经变得无法阅读,到处都是硬编码的切片索引,我们想将它们清理干净。

假设有一些代码用来从字符串的固定位置中取出具体的数据(比如从一个平面文件或类似的格式)[3]

######    0123456789012345678901234567890123456789012345678901234567890'
record = '....................100 .......513.25 ..........'
cost = int(record[20:32]) * float(record[40:48])

与其这样做,为什么不对切片命名呢?

SHARES = slice(20,32)
PRICE = slice(40,48)

cost = int(record[SHARES]) * float(record[PRICE])

在后一种版本中,由于避免了使用许多神秘难懂的硬编码索引,我们的代码就变得清晰了许多。

作为一条基本准则,代码中如果有很多硬编码的索引值,将导致可读性和可维护性都不佳。例如,如果一年以后再回过头来看代码,你会发现自己很想知道当初编写这些代码时自己在想些什么。前面展示的方法可以让我们对代码的功能有着更加清晰的认识。

一般来说,内置的slice()函数会创建一个切片对象,可以用在任何允许进行切片操作的地方。例如:

>>> items = [0, 1, 2, 3, 4, 5, 6]
>>> a = slice(2, 4)
>>> items[2:4]
[2, 3]
>>> items[a]
[2, 3]
>>> items[a] = [10,11]
>>> items
[0, 1, 10, 11, 4, 5, 6]
>>> del items[a]
>>> items
[0, 1, 4, 5, 6]

如果有一个slice对象的实例s,可以分别通过s.start、s.stop以及s.step属性来得到关于该对象的信息。例如:

>>> a = slice(
>>> a.start
10
>>> a.stop
50
>>> a.step
2
>>>

此外,可以通过使用indices(size)方法将切片映射到特定大小的序列上。这会返回一个(start, stop, step)元组,所有的值都已经恰当地限制在边界以内(当做索引操作时可避免出现IndexError异常)。例如:

>>> s = 'HelloWorld'
>>> a.indices(len(s))
(5, 10, 2)
>>> for i in range(*a.indices(len(s))):
... print(s[i])
...
w
r
d
>>>

我们有一个元素序列,想知道在序列中出现次数最多的元素是什么。

collections模块中的Counter类正是为此类问题所设计的。它甚至有一个非常方便的most_common()方法可以直接告诉我们答案。

为了说明用法,假设有一个列表,列表中是一系列的单词,我们想找出哪些单词出现的最为频繁。下面是我们的做法:

words = [
   'look', 'into', 'my', 'eyes', 'look', 'into', 'my', 'eyes',
   'the', 'eyes', 'the', 'eyes', 'the', 'eyes', 'not', 'around', 'the',
   'eyes', "don't", 'look', 'around', 'the', 'eyes', 'look', 'into',
   'my', 'eyes', "you're", 'under'
]

from collections import Counter
word_counts = Counter(words)
top_three = word_counts.most_common(3)
print(top_three)
# Outputs [('eyes', 8), ('the', 5), ('look', 4)]

可以给Counter对象提供任何可哈希的对象序列作为输入。在底层实现中,Counter是一个字典,在元素和它们出现的次数间做了映射。例如:

>>> word_counts['not']
1
>>> word_counts['eyes']
8
>>>

如果想手动增加计数,只需简单地自增即可:

>>> morewords = ['why','are','you','not','looking','in','my','eyes']
>>> for word in morewords:
... word_counts[word] += 1
...
>>> word_counts['eyes']
9
>>>

另一种方式是使用update()方法。

>>> word_counts.update(morewords)
>>>

关于Counter对象有一个不为人知的特性,那就是它们可以轻松地同各种数学运算操作结合起来使用。例如:

>>> a = Counter(words)
>>> b = Counter(morewords)
>>> a
Counter({'eyes': 8, 'the': 5, 'look': 4, 'into': 3, 'my': 3, 'around': 2,
         "you're": 1, "don't": 1, 'under': 1, 'not': 1})
>>> b
Counter({'eyes': 1, 'looking': 1, 'are': 1, 'in': 1, 'not': 1, 'you': 1,
         'my': 1, 'why': 1})

>>> # Combine counts
>>> c = a + b
>>> c
Counter({'eyes': 9, 'the': 5, 'look': 4, 'my': 4, 'into': 3, 'not': 2,
         'around': 2, "you're": 1, "don't": 1, 'in': 1, 'why': 1,
         'looking': 1, 'are': 1, 'under': 1, 'you': 1})

>>> # Subtract counts
>>> d = a - b
>>> d
Counter({'eyes': 7, 'the': 5, 'look': 4, 'into': 3, 'my': 2, 'around': 2,
         "you're": 1, "don't": 1, 'under': 1})
>>>

不用说,当面对任何需要对数据制表或计数的问题时,Counter对象都是你手边的得力工具。比起利用字典自己手写算法,更应该采用这种方式完成任务。

我们有一个字典列表,想根据一个或多个字典中的值来对列表排序。

利用operator模块中的itemgetter函数对这类结构进行排序是非常简单的。假设通过查询数据库表项获取网站上的成员列表,我们得到了如下的数据结构:

rows = [
    {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003},
    {'fname': 'David', 'lname': 'Beazley', 'uid': 1002},
    {'fname': 'John', 'lname': 'Cleese', 'uid': 1001},
    {'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
]

根据所有的字典中共有的字段来对这些记录排序是非常简单的,示例如下:

from operator import itemgetter

rows_by_fname = sorted(rows, key=itemgetter('fname'))
rows_by_uid = sorted(rows, key=itemgetter('uid'))

print(rows_by_fname)
print(rows_by_uid)

以上代码的输出为:

[{'fname': 'Big', 'uid': 1004, 'lname': 'Jones'},
 {'fname': 'Brian', 'uid': 1003, 'lname': 'Jones'},
 {'fname': 'David', 'uid': 1002, 'lname': 'Beazley'},
 {'fname': 'John', 'uid': 1001, 'lname': 'Cleese'}]

[{'fname': 'John', 'uid': 1001, 'lname': 'Cleese'},
 {'fname': 'David', 'uid': 1002, 'lname': 'Beazley'},
 {'fname': 'Brian', 'uid': 1003, 'lname': 'Jones'},
 {'fname': 'Big', 'uid': 1004, 'lname': 'Jones'}]

itemgetter()函数还可以接受多个键。例如下面这段代码:

rows_by_lfname = sorted(rows, key=itemgetter('lname','fname'))
print(rows_by_lfname)

这会产生如下的输出:

[{'fname': 'David', 'uid': 1002, 'lname': 'Beazley'},
 {'fname': 'John', 'uid': 1001, 'lname': 'Cleese'},
 {'fname': 'Big', 'uid': 1004, 'lname': 'Jones'},
 {'fname': 'Brian', 'uid': 1003, 'lname': 'Jones'}]

在这个例子中,rows被传递给内建的sorted()函数,该函数接受一个关键字参数key。这个参数应该代表一个可调用对象(callable),该对象从rows中接受一个单独的元素作为输入并返回一个用来做排序依据的值。itemgetter()函数创建的就是这样一个可调用对象。

函数operator.itemgetter()接受的参数可作为查询的标记,用来从rows的记录中提取出所需要的值。它可以是字典的键名称、用数字表示的列表元素或是任何可以传给对象的__getitem__()方法的值。如果传多个标记给itemgetter(),那么它产生的可调用对象将返回一个包含所有元素在内的元组,然后sorted()将根据对元组的排序结果来排列输出结果。如果想同时针对多个字段做排序(比如例子中的姓和名),那么这是非常有用的。

有时候会用lambda表达式来取代itemgetter()的功能。例如:

rows_by_fname = sorted(rows, key=lambda r: r['fname'])
rows_by_lfname = sorted(rows, key=lambda r: (r['lname'],r['fname']))

这种解决方案通常也能正常工作。但是用itemgetter()通常会运行得更快一些。因此如果需要考虑性能问题的话,应该使用itemgetter()。

最后不要忘了本节中所展示的技术同样适用于min()和max()这样的函数。例如:

>>> min(rows, key=itemgetter('uid'))
{'fname': 'John', 'lname': 'Cleese', 'uid': 1001}
>>> max(rows, key=itemgetter('uid'))
{'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
>>>

我们想在同一个类的实例之间做排序,但是它们并不原生支持比较操作。

内建的sorted()函数可接受一个用来传递可调用对象(callable)的参数key,而该可调用对象会返回待排序对象中的某些值,sorted则利用这些值来比较对象。例如,如果应用中有一系列的User对象实例,而我们想通过user_id属性来对它们排序,则可以提供一个可调用对象将User实例作为输入然后返回user_id。示例如下:

>>> class User:
...      def __init__(self, user_id):
...           self.user_id = user_id
...      def __repr__(self):
...           return 'User({})'.format(self.user_id)
...
>>> users = [User(23), User(3), User(99)]
>>> users
[User(23), User(3), User(99)]
>>> sorted(users, key=lambda u: u.user_id)
[User(3), User(23), User(99)]
>>>

除了可以用lambda表达式外,另一种方式是使用operator.attrgetter()。

>>> from operator import attrgetter
>>> sorted(users, key=attrgetter('user_id'))
[User(3), User(23), User(99)]
>>>

要使用lambda表达式还是attrgetter()或许只是一种个人喜好。但是通常来说,attrgetter()要更快一些,而且具有允许同时提取多个字段值的能力。这和针对字典的operator.itemgetter()的使用很类似(参见1.13节)。例如,如果User实例还有一个first_name和last_name属性的话,可以执行如下的排序操作:

by_name = sorted(users, key=attrgetter('last_name', 'first_name'))

同样值得一提的是,本节所用到的技术也适用于像min()和max()这样的函数。例如:

>>> min(users, key=attrgetter('user_id')
User(3)
>>> max(users, key=attrgetter('user_id')
User(99)
>>>

有一系列的字典或对象实例,我们想根据某个特定的字段(比如说日期)来分组迭代数据。

itertools.groupby()函数在对数据进行分组时特别有用。为了说明其用途,假设有如下的字典列表:

rows = [
    {'address': '5412 N CLARK', 'date': '07/01/2012'},
    {'address': '5148 N CLARK', 'date': '07/04/2012'},
    {'address': '5800 E 58TH', 'date': '07/02/2012'},
    {'address': '2122 N CLARK', 'date': '07/03/2012'},
    {'address': '5645 N RAVENSWOOD', 'date': '07/02/2012'},
    {'address': '1060 W ADDISON', 'date': '07/02/2012'},
    {'address': '4801 N BROADWAY', 'date': '07/01/2012'},
    {'address': '1039 W GRANVILLE', 'date': '07/04/2012'},
]

现在假设想根据日期以分组的方式迭代数据。要做到这些,首先以目标字段(在这个例子中是date)来对序列排序,然后再使用itertools.groupby()。

from operator import itemgetter
from itertools import groupby

# Sort by the desired field first
rows.sort(key=itemgetter('date'))

# Iterate in groups
for date, items in groupby(rows, key=itemgetter('date')):
    print(date)
    for i in items:
        print(' ', i)

这会产生如下的输出:

07/01/2012
     {'date': '07/01/2012', 'address': '5412 N CLARK'}
     {'date': '07/01/2012', 'address': '4801 N BROADWAY'}
07/02/2012
     {'date': '07/02/2012', 'address': '5800 E 58TH'}
     {'date': '07/02/2012', 'address': '5645 N RAVENSWOOD'}
     {'date': '07/02/2012', 'address': '1060 W ADDISON'}
07/03/2012
     {'date': '07/03/2012', 'address': '2122 N CLARK'}
07/04/2012
     {'date': '07/04/2012', 'address': '5148 N CLARK'}
     {'date': '07/04/2012', 'address': '1039 W GRANVILLE'}

函数groupby()通过扫描序列找出拥有相同值(或是由参数key指定的函数所返回的值)的序列项,并将它们分组。groupby()创建了一个迭代器,而在每次迭代时都会返回一个值(value)和一个子迭代器(sub_iterator),这个子迭代器可以产生所有在该分组内具有该值的项。

在这里重要的是首先要根据感兴趣的字段对数据进行排序。因为groupby()只能检查连续的项,不首先排序的话,将无法按所想的方式来对记录分组。

如果只是简单地根据日期将数据分组到一起,放进一个大的数据结构中以允许进行随机访问,那么利用defaultdict()构建一个一键多值字典(multidict,见1.6节)可能会更好。例如:

from collections import defaultdict
rows_by_date = defaultdict(list)
for row in rows:
    rows_by_date[row['date']].append(row)

这使得我们可以方便地访问每个日期的记录,如下所示:

>>> for r in rows_by_date['07/01/2012']:
... print(r)
...
{'date': '07/01/2012', 'address': '5412 N CLARK'}
{'date': '07/01/2012', 'address': '4801 N BROADWAY'}
>>>

对于后面这个例子,我们并不需要先对记录做排序。因此,如果不考虑内存方面的因素,这种方式会比先排序再用groupby()迭代要来的更快。

序列中含有一些数据,我们需要提取出其中的值或根据某些标准对序列做删减。

要筛选序列中的数据,通常最简单的方法是使用列表推导式(list comprehension)。例如:

>>> mylist = [1, 4, -5, 10, -7, 2, 3, -1]
>>> [n for n in mylist if n > 0]
[1, 4, 10, 2, 3]
>>> [n for n in mylist if n < 0]
[-5, -7, -1]
>>>

使用列表推导式的一个潜在缺点是如果原始输入非常大的话,这么做可能会产生一个庞大的结果。如果这是你需要考虑的问题,那么可以使用生成器表达式通过迭代的方式产生筛选的结果。例如:

>>> pos = (n for n in mylist if n > 0)
>>> pos
 at 0x1006a0eb0>
>>> for x in pos:
... print(x)
...
1
4
10
2
3
>>>

有时候筛选的标准没法简单地表示在列表推导式或生成器表达式中。比如,假设筛选过程涉及异常处理或者其他一些复杂的细节。基于此,可以将处理筛选逻辑的代码放到单独的函数中,然后使用内建的filter()函数处理。示例如下:

values = ['1', '2', '-3', '-', '4', 'N/A', '5']

def is_int(val):
 try:
        x = int(val)
 return True
 except ValueError:
 return False

ivals = list(filter(is_int, values))
print(ivals)
# Outputs ['1', '2', '-3', '4', '5']

filter()创建了一个迭代器,因此如果我们想要的是列表形式的结果,请确保加上了list(),就像示例中那样。

列表推导式和生成器表达式通常是用来筛选数据的最简单和最直接的方式。此外,它们也具有同时对数据做转换的能力。例如:

>>> mylist = [1, 4, -5, 10, -7, 2, 3, -1]
>>> import math
>>> [math.sqrt(n) for n in mylist if n > 0]
[1.0, 2.0, 3.1622776601683795, 1.4142135623730951, 1.7320508075688772]
>>>

关于筛选数据,有一种情况是用新值替换掉不满足标准的值,而不是丢弃它们。例如,除了要找到正整数之外,我们也许还希望在指定的范围内将不满足要求的值替换掉。通常,这可以通过将筛选条件移到一个条件表达式中来轻松实现。就像下面这样:

>>> clip_neg = [n if n > 0 else 0 for n in mylist]
>>> clip_neg
[1, 4, 0, 10, 0, 2, 3, 0]
>>> clip_pos = [n if n < 0 else 0 for n in mylist]
>>> clip_pos
[0, 0, -5, 0, -7, 0, 0, -1]
>>>

另一个值得一提的筛选工具是itertools.compress(),它接受一个可迭代对象以及一个布尔选择器序列作为输入。输出时,它会给出所有在相应的布尔选择器中为True的可迭代对象元素。如果想把对一个序列的筛选结果施加到另一个相关的序列上时,这就会非常有用。例如,假设有以下两列数据:

addresses = [
    '5412 N CLARK',
    '5148 N CLARK',
    '5800 E 58TH',
    '2122 N CLARK'
    '5645 N RAVENSWOOD',
    '1060 W ADDISON',
    '4801 N BROADWAY',
    '1039 W GRANVILLE',
]

counts = [ 0, 3, 10, 4, 1, 7, 6, 1]

现在我们想构建一个地址列表,其中相应的count值要大于5。下面是我们可以尝试的方法:

>>> from itertools import compress
>>> more5 = [n > 5 for n in counts]
>>> more5
[False, False, True, False, False, True, True, False]
>>> list(compress(addresses, more5))
['5800 E 58TH', '4801 N BROADWAY', '1039 W GRANVILLE']
>>>

这里的关键在于首先创建一个布尔序列,用来表示哪个元素可满足我们的条件。然后compress()函数挑选出满足布尔值为True的相应元素。

同filter()函数一样,正常情况下compress()会返回一个迭代器。因此,如果需要的话,得使用list()将结果转为列表。

我们想创建一个字典,其本身是另一个字典的子集。

利用字典推导式(dictionary comprehension)可轻松解决。例如:

prices = {
   'ACME': 45.23,
   'AAPL': 612.78,
   'IBM': 205.55,
   'HPQ': 37.20,
   'FB': 10.75
}

# Make a dictionary of all prices over 200
p1 = { key:value for key, value in prices.items() if value > 200 }

# Make a dictionary of tech stocks
tech_names = { 'AAPL', 'IBM', 'HPQ', 'MSFT' }
p2 = { key:value for key,value in prices.items() if key in tech_names }

大部分可以用字典推导式解决的问题也可以通过创建元组序列然后将它们传给dict()函数来完成。例如:

p1 = dict((key, value) for key, value in prices.items() if value > 200)

但是字典推导式的方案更加清晰,而且实际运行起来也要快很多(以本例中的字典prices来测试,效率要高2倍多)。

有时候会有多种方法来完成同一件事情。例如,第二个例子还可以重写成:

# Make a dictionary of tech stocks
tech_names = { 'AAPL', 'IBM', 'HPQ', 'MSFT' }
p2 = { key:prices[key] for key in prices.keys() & tech_names }

但是,计时测试表明这种解决方案几乎要比第一种慢上1.6倍。如果需要考虑性能因素,那么通常都需要花一点时间来研究它。有关计时和性能分析方面的信息,请参见14.13节。

我们的代码是通过位置(即索引,或下标)来访问列表或元组的,但有时候这会使代码变得有些难以阅读。我们希望可以通过名称来访问元素,以此减少结构中对位置的依赖性。

相比普通的元组,collections.namedtuple()(命名元组)只增加了极小的开销就提供了这些便利。实际上collections.namedtuple()是一个工厂方法,它返回的是Python中标准元组类型的子类。我们提供给它一个类型名称以及相应的字段,它就返回一个可实例化的类、为你已经定义好的字段传入值等。例如:

>>> from collections import namedtuple
>>> Subscriber = namedtuple('Subscriber', ['addr', 'joined'])
>>> sub = Subscriber('jonesy@example.com', '2012-10-19')
>>> sub
Subscriber(addr='jonesy@example.com', joined='2012-10-19')
>>> sub.addr
'jonesy@example.com'
>>> sub.joined
'2012-10-19'
>>>

尽管namedtuple的实例看起来就像一个普通的类实例,但它的实例与普通的元组是可互换的,而且支持所有普通元组所支持的操作,例如索引(indexing)和分解(unpacking)。比如:

>>> len(sub)
2
>>> addr, joined = sub
>>> addr
'jonesy@example.com'
>>> joined
'2012-10-19'
>>>

命名元组的主要作用在于将代码同它所控制的元素位置间解耦。所以,如果从数据库调用中得到一个大型的元组列表,而且通过元素的位置来访问数据,那么假如在表单中新增了一列数据,那么代码就会崩溃。但如果首先将返回的元组转型为命名元组,就不会出现问题。

为了说明这个问题,下面有一些使用普通元组的代码:

def compute_cost(records):
    total = 0.0
 for rec in records:
        total += rec[1] * rec[2]
 return total

通过位置来引用元素常常使得代码的表达力不够强,而且也很依赖于记录的具体结构。下面是使用命名元组的版本:

from collections import namedtuple

Stock = namedtuple('Stock', ['name', 'shares', 'price'])
def compute_cost(records):
    total = 0.0
    for rec in records:
        s = Stock(*rec)
        total += s.shares * s.price
    return total

当然,如果示例中的records序列已经包含了这样的实例,那么可以避免显式地将记录转换为Stock命名元组[4]

namedtuple的一种可能用法是作为字典的替代,后者需要更多的空间来存储。因此,如果要构建涉及字典的大型数据结构,使用namedtuple会更加高效。但是请注意,与字典不同的是,namedtuple是不可变的(immutable)。例如:

>>> s = Stock('ACME', 100, 123.45)
>>> s
Stock(name='ACME', shares=100, price=123.45)
>>> s.shares = 75
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>>

如果需要修改任何属性,可以通过使用namedtuple实例的_replace()方法来实现。该方法会创建一个全新的命名元组,并对相应的值做替换。示例如下:

>>> s = s._replace(shares=75)
>>> s
Stock(name='ACME', shares=75, price=123.45)
>>>

_replace()方法有一个微妙的用途,那就是它可以作为一种简便的方法填充具有可选或缺失字段的命名元组。要做到这点,首先创建一个包含默认值的“原型”元组,然后使用_replace()方法创建一个新的实例,把相应的值替换掉。示例如下:

from collections import namedtuple

Stock = namedtuple('Stock', ['name', 'shares', 'price', 'date', 'time'])

# Create a prototype instance
stock_prototype = Stock('', 0, 0.0, None, None)

# Function to convert a dictionary to a Stock
def dict_to_stock(s):
return stock_prototype._replace(**s)

让我们演示一下上面的代码是如何工作的:

>>> a = {'name': 'ACME', 'shares': 100, 'price': 123.45}
>>> dict_to_stock(a)
Stock(name='ACME', shares=100, price=123.45, date=None, time=None)
>>> b = {'name': 'ACME', 'shares': 100, 'price': 123.45, 'date': '12/17/2012'}
>>> dict_to_stock(b)
Stock(name='ACME', shares=100, price=123.45, date='12/17/2012', time=None)
>>>

最后,也是相当重要的是,应该要注意如果我们的目标是定义一个高效的数据结构,而且将来会修改各种实例属性,那么使用namedtuple并不是最佳选择。相反,可以考虑定义一个使用__slots__属性的类(参见8.4节)。

我们需要调用一个换算(reduction)函数(例如sum()、min()、max()),但首先得对数据做转换或筛选。

有一种非常优雅的方式能将数据换算和转换结合在一起——在函数参数中使用生成器表达式。例如,如果想计算平方和,可以像下面这样做:

nums = [1, 2, 3, 4, 5]
s = sum(x * x for x in nums)

这里还有一些其他的例子:

# Determine if any .py files exist in a directory
import os
files = os.listdir('dirname')
if any(name.endswith('.py') for name in files):
 print('There be python!')
else:
 print('Sorry, no python.')

# Output a tuple as CSV
s = ('ACME', 50, 123.45)
print(','.join(str(x) for x in s))

# Data reduction across fields of a data structure
portfolio = [
   {'name':'GOOG', 'shares': 50},
   {'name':'YHOO', 'shares': 75},
   {'name':'AOL', 'shares': 20},
   {'name':'SCOX', 'shares': 65}
]
min_shares = min(s['shares'] for s in portfolio)

这种解决方案展示了当把生成器表达式作为函数的单独参数时在语法上的一些微妙之处(即,不必重复使用括号)。比如,下面这两行代码表示的是同一个意思:

s = sum((x * x for x in nums))   # Pass generator-expr as argument
s = sum(x * x for x in nums)     # More elegant syntax

比起首先创建一个临时的列表,使用生成器做参数通常是更为高效和优雅的方式。例如,如果不使用生成器表达式,可能会考虑下面这种实现:

nums = [1, 2, 3, 4, 5]
s = sum([x * x for x in nums])

这也能工作,但这引入了一个额外的步骤而且创建了额外的列表。对于这么小的一个列表,这根本就无关紧要,但是如果nums非常巨大,那么就会创建一个庞大的临时数据结构,而且只用一次就要丢弃。基于生成器的解决方案可以以迭代的方式转换数据,因此在内存使用上要高效得多。

某些特定的换算函数比如min()和max()都可接受一个key参数,当可能倾向于使用生成器时会很有帮助。例如在portfolio的例子中,也许会考虑下面这种替代方案:

# Original: Returns 20
min_shares = min(s['shares'] for s in portfolio)

# Alternative: Returns {'name': 'AOL', 'shares': 20}
min_shares = min(portfolio, key=lambda s: s['shares'])

我们有多个字典或映射,想在逻辑上将它们合并为一个单独的映射结构,以此执行某些特定的操作,比如查找值或检查键是否存在。

假设有两个字典:

a = {'x': 1, 'z': 3 }
b = {'y': 2, 'z': 4 }

现在假设想执行查找操作,我们必须得检查这两个字典(例如,先在a中查找,如果没找到再去b中查找)。一种简单的方法是利用collections模块中的ChainMap类来解决这个问题。例如:

from collections import ChainMap
c = ChainMap(a,b)
print(c['x']) # Outputs 1 (from a)
print(c['y']) # Outputs 2 (from b)
print(c['z']) # Outputs 3 (from a)

ChainMap可接受多个映射然后在逻辑上使它们表现为一个单独的映射结构。但是,这些映射在字面上并不会合并在一起。相反,ChainMap只是简单地维护一个记录底层映射关系的列表,然后重定义常见的字典操作来扫描这个列表。大部分的操作都能正常工作。例如:

>>> len(c)
3
>>> list(c.keys())
['x', 'y', 'z']
>>> list(c.values())
[1, 2, 3]
>>>

如果有重复的键,那么这里会采用第一个映射中所对应的值。因此,例子中的c[‘z’]总是引用字典a中的值,而不是字典b中的值。

修改映射的操作总是会作用在列出的第一个映射结构上。例如:

>>> c['z'] = 10
>>> c['w'] = 40
>>> del c['x']
>>> a
{'w': 40, 'z': 10}
>>> del c['y']
Traceback (most recent call last):
...
KeyError: "Key not found in the first mapping: 'y'"
>>>

ChainMap与带有作用域的值,比如编程语言中的变量(即全局变量、局部变量等)一起工作时特别有用。实际上这里有一些方法使这个过程变得简单:

>>> values = ChainMap()
>>> values['x'] = 1
>>> # Add a new mapping
>>> values = values.new_child()
>>> values['x'] = 2
>>> # Add a new mapping
>>> values = values.new_child()
>>> values['x'] = 3
>>> values
ChainMap({'x': 3}, {'x': 2}, {'x': 1})
>>> values['x']
3
>>> # Discard last mapping
>>> values = values.parents
>>> values['x']
2
>>> # Discard last mapping
>>> values = values.parents
>>> values['x']
1
>>> values
ChainMap({'x': 1})
>>>

作为ChainMap的替代方案,我们可能会考虑利用字典的update()方法将多个字典合并在一起。例如:

>>> a = {'x': 1, 'z': 3 }
>>> b = {'y': 2, 'z': 4 }
>>> merged = dict(b)
>>> merged.update(a)
>>> merged['x']
1
>>> merged['y']
2
>>> merged['z']
3
>>>

这么做行得通,但这需要单独构建一个完整的字典对象(或者修改其中现有的一个字典,这就破坏了原始数据)。此外,如果其中任何一个原始字典做了修改,这个改变都不会反应到合并后的字典中。例如:

>>> a['x'] = 13
>>> merged['x']
1

而ChainMap使用的就是原始的字典,因此它不会产生这种令人不悦的行为。示例如下:

>>> a = {'x': 1, 'z': 3 }
>>> b = {'y': 2, 'z': 4 }
>>> merged = ChainMap(a, b)
>>> merged['x']
1
>>> a['x'] = 42
>>> merged['x']    # Notice change to merged dicts
42
>>>

[1] 如果一个对象是可哈希的,那么在它的生存期内必须是不可变的,它需要有一个__hash__()方法。整数、浮点数、字符串、元组都是不可变的。——译者注

[2] 集合的特点就是集合中的元素都是唯一的,但不保证它们之间的顺序。——译者注

[3] 平面文件(flat file)是一种包含没有相对关系结构的记录文件。——译者注

[4] 作者的意思是如果records中的元素是某个类的实例,且已经有了shares和price这样的属性,那就可以直接通过属性名来访问,不需要通过位置来引用,也就没有必要再转换成命名元组了。——译者注


本章主要关注的重点是利用Python来处理以各种常见编码形式所呈现出的数据,比如CSV文件、JSON、XML以及二进制形式的打包记录。与数据结构那章不同,本章不会把重点放在特定的算法之上,而是着重处理数据在程序中的输入和输出问题上。

我们想要读写CSV文件中的数据。

对于大部分类型的CSV数据,我们都可以用csv库来处理。比如,假设在名为stocks.csv的文件中包含有如下的股票市场数据:

    Symbol,Price,Date,Time,Change,Volume
    "AA",39.48,"6/11/2007","9:36am",-0.18,181800
    "AIG",71.38,"6/11/2007","9:36am",-0.15,195500
    "AXP",62.58,"6/11/2007","9:36am",-0.46,935000
    "BA",98.31,"6/11/2007","9:36am",+0.12,104800
    "C",53.08,"6/11/2007","9:36am",-0.25,360900
    "CAT",78.29,"6/11/2007","9:36am",-0.23,225400

下面的代码示例告诉我们如何将这些数据读取为元组序列:

import csv
with open('stocks.csv') as f:
    f_csv = csv.reader(f)
    headers = next(f_csv)
    for row in f_csv:
        # Process row
        ...

在上面的代码中,row将会是一个元组。因此,要访问特定的字段就需要用到索引,比如row[0](表示Symbol)和row[4](表示Change)。

由于这样的索引常常容易混淆,因此这里可以考虑使用命名元组。示例如下:

from collections import namedtuple
with open('stock.csv') as f:
    f_csv = csv.reader(f)
    headings = next(f_csv)
    Row = namedtuple('Row', headings)
    for r in f_csv:
        row = Row(*r)
        # Process row
        ...

这样就可以使用每一列的标头比如row.Symbol和row.Change来取代之前的索引了。应该要指出的是,这个方法只有在每一列的标头都是合法的Python标识符时才起作用。如果不是的话,就必须调整原始的标头(比如,把非标识符字符用下划线或其他类似的符号取代)。

另一种可行的方式是将数据读取为字典序列。可以用下面的代码实现:

import csv
with open('stocks.csv') as f:
    f_csv = csv.DictReader(f)
    for row in f_csv:
        # process row
        ...

在这个版本中,可以通过行标头来访问每行中的元素。比如,row['Symbol']或者row['Change']。

要写入CSV数据,也可以使用csv模块来完成,但是要创建一个写入对象。示例如下:

headers = ['Symbol','Price','Date','Time','Change','Volume']
rows = [('AA', 39.48, '6/11/2007', '9:36am', -0.18, 181800),
        ('AIG', 71.38, '6/11/2007', '9:36am', -0.15, 195500),
        ('AXP', 62.58, '6/11/2007', '9:36am', -0.46, 935000),
       ]
with open('stocks.csv','w') as f:
    f_csv = csv.writer(f)
    f_csv.writerow(headers)
    f_csv.writerows(rows)

如果数据是字典序列,那么可以这样处理:

headers = ['Symbol', 'Price', 'Date', 'Time', 'Change', 'Volume']
rows = [{'Symbol':'AA', 'Price':39.48, 'Date':'6/11/2007',
          'Time':'9:36am', 'Change':-0.18, 'Volume':181800},
        {'Symbol':'AIG', 'Price': 71.38, 'Date':'6/11/2007',
          'Time':'9:36am', 'Change':-0.15, 'Volume': 195500},
        {'Symbol':'AXP', 'Price': 62.58, 'Date':'6/11/2007',
          'Time':'9:36am', 'Change':-0.46, 'Volume': 935000},
        ]
with open('stocks.csv','w') as f:
    f_csv = csv.DictWriter(f, headers)
    f_csv.writeheader()
    f_csv.writerows(rows)

应该总是选择使用csv模块来处理,而不是自己手动分解和解析CSV数据。比如,我们可能会倾向于写出这样的代码:

with open('stocks.csv') as f:
    for line in f:
        row = line.split(',')
        # process row
        ...

这种方式的问题在于仍然需要自己处理一些令人厌烦的细节问题。比如说,如果有任何字段是被引号括起来的,那么就要自己去除引号。此外,如果被引用的字段中恰好包含有一个逗号,那么产生出的那一行会因为大小错误而使得代码崩溃(因为原始数据也是用逗号分隔的)。

默认情况下,csv库被实现为能够识别微软Excel所采用的CSV编码规则。这也许是最为常见的CSV编码规则了,能够带来最佳的兼容性。但是,如果查阅csv的文档,就会发现有几种方法可以将编码微调为其他的格式(例如,修改分隔字符等)。比方说,如果想读取以tab键分隔的数据,可以使用下面的代码:

# Example of reading tab-separated values
with open('stock.tsv') as f:
    f_tsv = csv.reader(f, delimiter='\t')
    for row in f_tsv:
        # Process row
        ...

如果正在读取CSV数据并将其转换为命名元组,那么在验证列标题时要小心。比如,某个CSV文件中可能在标题行中包含有非法的标识符字符,就像下面的示例这样[1]

Street Address,Num-Premises,Latitude,Longitude
5412 N CLARK,10,41.980262,-87.668452

这会使得创建命名元组的代码出现ValueError异常。要解决这个问题,应该首先整理标题。例如,可以对非法的标识符字符进行正则替换,示例如下:

import re
with open('stock.csv') as f:
    f_csv = csv.reader(f)
    headers = [ re.sub('[^a-zA-Z_]', '_', h) for h in next(f_csv) ]
    Row = namedtuple('Row', headers)
    for r in f_csv:
        row = Row(*r)
        # Process row
        ...

此外,还需要重点强调的是,csv模块不会尝试去解释数据或者将数据转换为除字符串之外的类型。如果这样的转换很重要,那么这就是我们需要自行处理的问题。下面这个例子演示了对CSV数据进行额外的类型转换:

col_types = [str, float, str, str, float, int]
with open('stocks.csv') as f:
    f_csv = csv.reader(f)
    headers = next(f_csv)
    for row in f_csv:
        # Apply conversions to the row items
        row = tuple(convert(value) for convert, value in zip(col_types, row))
        ...

作为另外一种选择,下面这个例子演示了将选中的字段转换为字典:

print('Reading as dicts with type conversion')
field_types = [ ('Price', float),
                ('Change', float),
                ('Volume', int) ]
with open('stocks.csv') as f:
    for row in csv.DictReader(f):
        row.update((key, conversion(row[key]))
                    for key, conversion in field_types)
        print(row)

一般来说,对于这样的转换都应该小心为上。在现实世界中,CSV文件可能会缺少某些值,或者数据损坏了,以及出现其他一些可能会使类型转换操作失败的情况,这都是很常见的。因此,除非可以保证数据不会出错,否则就需要考虑这些情况(也许需要加上适当的异常处理代码)。

最后,如果我们的目标是通过读取CSV数据来进行数据分析和统计,那么应该看看Pandas这个Python包(http://pandas.pydata.org))。Pandas中有一个方便的函数pandas.read_csv(),能够将CSV数据加载到DataFrame对象中。之后,就可以生成各种各样的统计摘要了,还可以对数据进行筛选并执行其他类型的高级操作。6.13节中给出了一个这样的例子。

我们想读写以JSON(JavaScript Object Notation)格式编码的数据。

json模块中提供了一种简单的方法来编码和解码JSON格式的数据。这两个主要的函数就是json.dumps()以及json.loads()。这两个函数在命名上借鉴了其他序列化处理库的接口,比如pickle。下面的示例展示了如何将Python数据结构转换为JSON:

import json
data = {
    'name' : 'ACME',
    'shares' : 100,
    'price' : 542.23
}
json_str = json.dumps(data)

而接下来的示例告诉我们如何把JSON编码的字符串再转换回Python数据结构:

data = json.loads(json_str)

如果要同文件而不是字符串打交道的话,可以选择使用json.dump()以及json.load()来编码和解码JSON数据。示例如下:

# Writing JSON data
with open('data.json', 'w') as f:
     json.dump(data, f)
# Reading data back
with open('data.json', 'r') as f:
     data = json.load(f)

JSON编码支持的基本类型有None、bool、int、float和str,当然还有包含了这些基本类型的列表、元组以及字典。对于字典,JSON会假设键(key)是字符串(字典中的任何非字符串键都会在编码时转换为字符串)。要符合JSON规范,应该只对Python列表和字典进行编码。此外,在Web应用中,把最顶层对象定义为字典是一种标准做法。

JSON编码的格式几乎与Python语法一致,只有几个小地方稍有不同。比如,True会被映射为true,False会被映射为false,而None会被映射为null。下面的示例展示了编码看起来是怎样的:

>>> json.dumps(False)
'false'
>>> d = {'a': True,
... 'b': 'Hello',
... 'c': None}
>>> json.dumps(d)
'{"b": "Hello", "c": null, "a": true}'
>>>

如果要检查从JSON中解码得到的数据,那么仅仅将其打印出来就想确定数据的结构通常是比较困难的——尤其是如果数据中包含了深层次的嵌套结构或者有许多字段时。为了帮助解决这个问题,考虑使用pprint模块中的pprint()函数。这么做会把键按照字母顺序排列,并且将字典以更加合理的方式进行输出。下面的示例展示了应该如何对Twitter上的搜索结果以漂亮的格式进行输出:

>>> from urllib.request import urlopen
>>> import json
>>> u = urlopen('http://search.twitter.com/search.json?q=python&rpp=5')
>>> resp = json.loads(u.read().decode('utf-8'))
>>> from pprint import pprint
>>> pprint(resp)
{'completed_in': 0.074,
 'max_id': 264043230692245504,
 'max_id_str': '264043230692245504',
 'next_page': '?page=2&max_id=264043230692245504&q=python&rpp=5',
 'page': 1,
 'query': 'python',
 'refresh_url': '?since_id=264043230692245504&q=python',
 'results': [{'created_at': 'Thu, 01 Nov 2012 16:36:26 +0000',
              'from_user': ...
             },
             {'created_at': 'Thu, 01 Nov 2012 16:36:14 +0000',
              'from_user': ...
            },
            {'created_at': 'Thu, 01 Nov 2012 16:36:13 +0000',
             'from_user': ...
            },
            {'created_at': 'Thu, 01 Nov 2012 16:36:07 +0000',
             'from_user': ...
            }
            {'created_at': 'Thu, 01 Nov 2012 16:36:04 +0000',
             'from_user': ...
            }],
 'results_per_page': 5,
 'since_id': 0,
 'since_id_str': '0'}
>>>

一般来说,JSON解码时会从所提供的数据中创建出字典或者列表。如果想创建其他类型的对象,可以为json.loads()方法提供object_pairs_hook或者object_hook参数。例如,下面的示例展示了我们应该如何将JSON数据解码为OrderedDict(有序字典),这样可以保持数据的顺序不变:

>>> s = '{"name": "ACME", "shares": 50, "price": 490.1}'
>>> from collections import OrderedDict
>>> data = json.loads(s, object_pairs_hook=OrderedDict)
>>> data
OrderedDict([('name', 'ACME'), ('shares', 50), ('price', 490.1)])
>>>

而下面的代码将JSON字典转变为Python对象:

>>> class JSONObject:
... def __init__(self, d):
... self.__dict__ = d
...
>>>
>>> data = json.loads(s, object_hook=JSONObject)
>>> data.name
'ACME'
>>> data.shares
50
>>> data.price
490.1
>>>

在上一个示例中,通过解码JSON数据而创建的字典作为单独的参数传递给了__init__()。之后就可以自由地根据需要来使用它了,比如直接将它当做对象的字典实例来用。

有几个选项对于编码JSON来说是很有用的。如果想让输出格式变得漂亮一些,可以在json.dumps()函数中使用indent参数。这会使得数据能够像pprint()函数那样以漂亮的格式打印出来。示例如下:

>>> print(json.dumps(data))
{"price": 542.23, "name": "ACME", "shares": 100}
>>> print(json.dumps(data, indent=4))
{
    "price": 542.23,
    "name": "ACME",
    "shares": 100
}
>>>

如果想在输出中对键进行排序处理,可以使用sort_keys参数:

>>> print(json.dumps(data, sort_keys=True))
{"name": "ACME", "price": 542.23, "shares": 100}
>>>

类实例一般是无法序列化为JSON的。比如说:

>>> class Point:
... def __init__(self, x, y):
... self.x = x
... self.y = y
...
>>> p = Point(2, 3)
>>> json.dumps(p)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.3/json/__init__.py", line 226, in dumps
    return _default_encoder.encode(obj)
  File "/usr/local/lib/python3.3/json/encoder.py", line 187, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/local/lib/python3.3/json/encoder.py", line 245, in iterencode
    return _iterencode(o, 0)
  File "/usr/local/lib/python3.3/json/encoder.py", line 169, in default
    raise TypeError(repr(o) + " is not JSON serializable")
TypeError: <__main__.Point object at 0x1006f2650> is not JSON serializable
>>>

如果想序列化类实例,可以提供一个函数将类实例作为输入并返回一个可以被序列化处理的字典。示例如下:

def serialize_instance(obj):
    d = { '__classname__' : type(obj).__name__ }
    d.update(vars(obj))
    return d

如果想取回一个实例,可以编写这样的代码来处理:

# Dictionary mapping names to known classes
classes = {
    'Point' : Point
}
def unserialize_object(d):
    clsname = d.pop('__classname__', None)
    if clsname:
        cls = classes[clsname]
        obj = cls.__new__(cls) # Make instance without calling __init__
        for key, value in d.items():
            setattr(obj, key, value)
            return obj
    else:
        return d

最后给出如何使用这些函数的示例:

>>> p = Point(2,3)
>>> s = json.dumps(p, default=serialize_instance)
>>> s
'{"__classname__": "Point", "y": 3, "x": 2}'
>>> a = json.loads(s, object_hook=unserialize_object)
>>> a
<__main__.Point object at 0x1017577d0>
>>> a.x
2
>>> a.y
3
>>>

json模块中还有许多其他的选项,这些选项可用来控制对数字、特殊值(比如NaN)等的底层解释行为。请参阅文档(http://docs.python.org/3/library/json.html)以获得进一步的细节。

我们想从一个简单的XML文档中提取出数据。

xml.etree.ElementTree模块可用来从简单的XML文档中提取出数据。为了说明,假设想对Planet Python(http://planet.python.org)上的RSS订阅做解析并生成一个总结报告。下面的脚本可以完成这个任务:

from urllib.request import urlopen
from xml.etree.ElementTree import parse
# Download the RSS feed and parse it
u = urlopen('http://planet.python.org/rss20.xml')
doc = parse(u)
# Extract and output tags of interest
for item in doc.iterfind('channel/item'):
    title = item.findtext('title')
    date = item.findtext('pubDate')
    link = item.findtext('link')

    print(title)
    print(date)
    print(link)
    print()

如果运行上面的脚本,会得到类似这样的输出:

Steve Holden: Python for Data Analysis
Mon, 19 Nov 2012 02:13:51 +0000
http://holdenweb.blogspot.com/2012/11/python-for-data-analysis.html
Vasudev Ram: The Python Data model (for v2 and v3)
Sun, 18 Nov 2012 22:06:47 +0000
http://jugad2.blogspot.com/2012/11/the-python-data-model.html
Python Diary: Been playing around with Object Databases
Sun, 18 Nov 2012 20:40:29 +0000
http://www.pythondiary.com/blog/Nov.18,2012/been-...-object-databases.html
Vasudev Ram: Wakari, Scientific Python in the cloud
Sun, 18 Nov 2012 20:19:41 +0000
http://jugad2.blogspot.com/2012/11/wakari-scientific-python-in-cloud.html
Jesse Jiryu Davis: Toro: synchronization primitives for Tornado coroutines
Sun, 18 Nov 2012 20:17:49 +0000
http://feedproxy.google.com/~r/EmptysquarePython/~3/_DOZT2Kd0hQ/

显然,如果想做更多的处理,就需要将print()函数替换为其他更加有趣的处理函数。

在许多应用中,同XML编码的数据打交道是很常见的事情。这不仅是因为XML作为一种数据交换格式在互联网中使用广泛,而且XML还是用来保存应用程序数据(例如文字处理、音乐库等)的常用格式。本节后面的讨论假设读者已经熟悉XML的基本概念。

在许多情况下,XML如果只是简单地用来保存数据,那么文档结构就是紧凑而直接的。例如,上面示例中的RSS订阅源看起来类似于如下的XML文档:

<?xml version="1.0"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
  <title>Planet Python</title>
  <link>http://planet.python.org/</link>
  <language>en</language>
  <description>Planet Python - http://planet.python.org/</description>
  <item>
    <title>Steve Holden: Python for Data Analysis</title>
      <guid>http://holdenweb.blogspot.com/...-data-analysis.html</guid>
      <link>http://holdenweb.blogspot.com/...-data-analysis.html</link>
      <description>...</description>
      <pubDate>Mon, 19 Nov 2012 02:13:51 +0000</pubDate>
  </item>
  <item>
    <title>Vasudev Ram: The Python Data model (for v2 and v3)</title>
    <guid>http://jugad2.blogspot.com/...-data-model.html</guid>
    <link>http://jugad2.blogspot.com/...-data-model.html</link>
    <description>...</description>
    <pubDate>Sun, 18 Nov 2012 22:06:47 +0000</pubDate>
    </item>
  <item>
    <title>Python Diary: Been playing around with Object Databases</title>
    <guid>http://www.pythondiary.com/...-object-databases.html</guid>
    <link>http://www.pythondiary.com/...-object-databases.html</link>
    <description>...</description>
    <pubDate>Sun, 18 Nov 2012 20:40:29 +0000</pubDate>
  </item>
    ...
</channel>
</rss>

xml.etree.ElementTree.parse()函数将整个XML文档解析为一个文档对象。之后,就可以利用find()、iterfind()以及findtext()方法查询特定的XML元素。这些函数的参数就是特定的标签名称,比如channel/item或者title。

当指定标签时,需要整体考虑文档的结构。每一个查找操作都是相对于一个起始元素来展开的。同样地,提供给每个操作的标签名也是相对于起始元素的。在示例代码中,对doc.iterfind('channel/item')的调用会查找所有在“channel”元素之下的“item”元素。doc代表着文档的顶层(顶层“rss”元素)。之后对item.findtext()的调用就相对于已找到的“item”元素来展开。

每个由ElementTree模块所表示的元素都有一些重要的属性和方法,它们对解析操作十分有用。tag属性中包含了标签的名称,text属性中包含有附着的文本,而get()方法可以用来提取出属性(如果有的话)。示例如下:

>>> doc
<xml.etree.ElementTree.ElementTree object at 0x101339510>
>>> e = doc.find('channel/title')
>>> e
<Element 'title' at 0x10135b310>
>>> e.tag
'title'
>>> e.text
'Planet Python'
>>> e.get('some_attribute')
>>>

应该要指出的是xml.etree.ElementTree并不是解析XML的唯一选择。对于更加高级的应用,应该考虑使用lxml。lxml采用的编程接口和ElementTree一样,因此本节中展示的示例能够以同样的方式用lxml实现。只需要将第一个导入语句修改为from lxml.etree import parse即可。lxml完全兼容于XML标准,这为我们提供了极大的好处。此外,lxml运行起来非常快速,还提供验证、XSLT以及XPath这样的功能支持。

我们需要从一个大型的XML文档中提取出数据,而且对内存的使用要尽可能少。

任何时候,当要面对以增量方式处理数据的问题时,都应该考虑使用迭代器和生成器。下面是一个简单的函数,可用来以增量方式处理大型的XML文件,它只用到了很少量的内存:

from xml.etree.ElementTree import iterparse
def parse_and_remove(filename, path):
    path_parts = path.split('/')
    doc = iterparse(filename, ('start', 'end'))
    # Skip the root element
    next(doc)
    tag_stack = []
    elem_stack = []
    for event, elem in doc:
        if event == 'start':
            tag_stack.append(elem.tag)
            elem_stack.append(elem)
        elif event == 'end':
            if tag_stack == path_parts:
                yield elem
                elem_stack[-2].remove(elem)
            try:
                tag_stack.pop()
                elem_stack.pop()
            except IndexError:
                pass

要测试这个函数,只需要找一个大型的XML文件来配合测试即可。这种大型的XML文件常常可以在政府以及数据公开的网站上找到。比如,可以下载芝加哥的坑洞数据库XML。在写作本书时,这个下载文件中有超过100000行的数据,它们按照如下的方式编码:

<response>
  <row>
    <row ...>
      <creation_date>2012-11-18T00:00:00</creation_date>
      <status>Completed</status>
      <completion_date>2012-11-18T00:00:00</completion_date>
      <service_request_number>12-01906549</service_request_number>
      <type_of_service_request>Pot Hole in Street</type_of_service_request>
      <current_activity>Final Outcome</current_activity>
      <most_recent_action>CDOT Street Cut ... Outcome</most_recent_action>
      <street_address>4714 S TALMAN AVE</street_address>
      <zip>60632</zip>
      <x_coordinate>1159494.68618856</x_coordinate>
      <y_coordinate>1873313.83503384</y_coordinate>
      <ward>14</ward>
      <police_district>9</police_district>
      <community_area>58</community_area>
      <latitude>41.808090232127896</latitude>
      <longitude>-87.69053684711305</longitude>
      <location latitude="41.808090232127896"
                       longitude="-87.69053684711305" />
    </row>
    <row ...>
      <creation_date>2012-11-18T00:00:00</creation_date>
      <status>Completed</status>
      <completion_date>2012-11-18T00:00:00</completion_date>
      <service_request_number>12-01906695</service_request_number>
      <type_of_service_request>Pot Hole in Street</type_of_service_request>
      <current_activity>Final Outcome</current_activity>
      <most_recent_action>CDOT Street Cut ... Outcome</most_recent_action>
      <street_address>3510 W NORTH AVE</street_address>
      <zip>60647</zip>
      <x_coordinate>1152732.14127696</x_coordinate>
      <y_coordinate>1910409.38979075</y_coordinate>
      <ward>26</ward>
      <police_district>14</police_district>
      <community_area>23</community_area>
      <latitude>41.91002084292946</latitude>
      <longitude>-87.71435952353961</longitude>
      <location latitude="41.91002084292946"
                       longitude="-87.71435952353961" />
    </row>
  </row>
</response>

假设我们想编写一个脚本来根据坑洞的数量对邮政编码(ZIP code)进行排序。可以编写如下的代码来实现:

from xml.etree.ElementTree import parse
from collections import Counter
potholes_by_zip = Counter()
doc = parse('potholes.xml')
for pothole in doc.iterfind('row/row'):
    potholes_by_zip[pothole.findtext('zip')] += 1
for zipcode, num in potholes_by_zip.most_common():
    print(zipcode, num)

这个脚本存在的唯一问题就是它将整个XML文件都读取到内存中后再做解析。在我们的机器上,运行这个脚本需要占据450 MB内存。但是如果使用下面这份代码,程序只做了微小的修改:

from collections import Counter
potholes_by_zip = Counter()
data = parse_and_remove('potholes.xml', 'row/row')
for pothole in data:
    potholes_by_zip[pothole.findtext('zip')] += 1
for zipcode, num in potholes_by_zip.most_common():
    print(zipcode, num)

这个版本的代码运行起来只用了7 MB内存——多么惊人的提升啊!

本节中的示例依赖于ElementTree模块中的两个核心功能。首先,iterparse()方法允许我们对XML文档做增量式的处理。要使用它,只需提供文件名以及一个事件列表即可。事件列表由1个或多个start/end,start-ns/end-ns组成。iterparse()创建出的迭代器产生出形式为(event,elem)的元组,这里的event是列出的事件,而elem是对应的XML元素。示例如下:

>>> data = iterparse('potholes.xml',('start','end'))
>>> next(data)
('start', <Element 'response' at 0x100771d60>)
>>> next(data)
('start', <Element 'row' at 0x100771e68>)
>>> next(data)
('start', <Element 'row' at 0x100771fc8>)
>>> next(data)
('start', <Element 'creation_date' at 0x100771f18>)
>>> next(data)
('end', <Element 'creation_date' at 0x100771f18>)
>>> next(data)
('start', <Element 'status' at 0x1006a7f18>)
>>> next(data)
('end', <Element 'status' at 0x1006a7f18>)
>>>

当某个元素首次被创建但是还没有填入任何其他数据时(比如子元素),会产生start事件,而end事件会在元素已经完成时产生。尽管没有在本节示例中出现,start-ns和end-ns事件是用来处理XML命名空间声明的。

在这个示例中,start和end事件是用来管理元素和标签栈的。这里的栈代表着文档结构中被解析的当前层次(current hierarchical),同时也用来判断元素是否匹配传递给parse_and_remove()函数的请求路径。如果有匹配满足,就通过yield将其发送给调用者。

紧跟在yield之后的语句就是使得ElementTree能够高效利用内存的关键所在:

elem_stack[-2].remove(elem)

这一行代码使得之前通过yield产生出的元素从它们的父节点中移除。因此可假设其再也没有任何其他的引用存在,因此该元素被销毁进而可以回收它所占用的内存。

这种迭代式的解析以及对节点的移除使得对整个文档的增量式扫描变得非常高效。在任何时刻都能构造出一棵完整的文档树。然而,我们仍然可以编写代码以直接的方式来处理XML数据。

这种技术的主要缺点就是运行时的性能。当进行测试时,将整个文档先读入内存的版本运行起来大约比增量式处理的版本快2倍。但是在内存的使用上,先读入内存的版本占用的内存量是增量式处理的60倍多。因此,如果内存使用量是更加需要关注的因素,那么显然增量式处理的版本才是大赢家。

我们想将Python字典中的数据转换为XML。

尽管xml.etree.ElementTree库通常用来解析XML文档,但它同样也可以用来创建XML文档。例如,考虑下面这个函数:

from xml.etree.ElementTree import Element
def dict_to_xml(tag, d):
    '''
    Turn a simple dict of key/value pairs into XML
    '''
    elem = Element(tag)
    for key, val in d.items():
        child = Element(key)
        child.text = str(val)
        elem.append(child)
    return elem

下面是使用这个函数的示例:

>>> s = { 'name': 'GOOG', 'shares': 100, 'price':490.1 }
>>> e = dict_to_xml('stock', s)
>>> e
<Element 'stock' at 0x1004b64c8>
>>>

转换的结果是一个Element实例。对于I/O操作来说,可以利用xml.etree.ElementTree中的tostring()函数将其转换为字节串。示例如下:

>>> from xml.etree.ElementTree import tostring
>>> tostring(e)
b'<stock><price>490.1</price><shares>100</shares><name>GOOG</name>'
>>>

如果想为元素附加上属性,可以使用set()方法实现:

>>> e.set('_id','1234')
>>> tostring(e)
b'<stock _id="1234"><price>490.1</price><shares>100</shares><name>GOOG</name>
</stock>'
>>>

如果需要考虑元素间的顺序,可以创建OrderedDict(有序字典)来取代普通的字典。参见1.7节中对有序字典的介绍。

当创建XML时,也许会倾向于只使用字符串来完成。比如:

def dict_to_xml_str(tag, d):
    '''
    Turn a simple dict of key/value pairs into XML
    '''
    parts = ['<{}>'.format(tag)]
    for key, val in d.items():
        parts.append('<{0}>{1}</{0}>'.format(key,val))
    parts.append('</{}>'.format(tag))
    return ''.join(parts)

问题在于如果尝试手工处理的话,那么这就是在自找麻烦。比如,如果字典中包含有特殊字符时会发生什么?

>>> d = { 'name' : '<spam>' }
>>> # String creation
>>> dict_to_xml_str('item',d)
'<item><name><spam></name></item>'
>>> # Proper XML creation
>>> e = dict_to_xml('item',d)
>>> tostring(e)
b'<item><name><spam></name></item>'
>>>

请注意在上面这个示例中,字符<和>分别被&lt;和&gt;取代了。

下面的提示仅供参考。如果需要手工对这些字符做转义处理,可以使用xml.sax.saxutils中的escape()和unescape()函数。示例如下:

>>> from xml.sax.saxutils import escape, unescape
>>> escape('<spam>')
'<spam>'
>>> unescape(_)
'<spam>'
>>>

为什么说创建Element实例要比使用字符串好?除了可以产生出正确的输出外,其他的原因在于这样可以更加方便地将Element实例组合在一起,创建出更大的XML文档。得到的Element实例也能够以各种方式进行处理,完全不必担心解析XML文本方面的问题。最重要的是,我们能够站在更高的层面上对数据进行各种处理,只在最后把结果作为字符串输出即可。

我们想读取一个XML文档,对它做些修改后再以XML的方式写回。

xml.etree.ElementTree模块可以轻松完成这样的任务。从本质上来说,开始时可以按照通常的方式来解析文档。例如,假设有一个名为pred.xml的文档,它看起来是这样的:

<?xml version="1.0"?>
<stop>
 <id>14791</id>
 <nm>Clark & Balmoral</nm>
 <sri>
 <rt>22</rt>
 <d>North Bound</d>
 <dd>North Bound</dd>
 </sri>
 <cr>22</cr>
 <pre>
 <pt>5 MIN</pt>
 <fd>Howard</fd>
 <v>1378</v>
 <rn>22</rn>
 </pre>
 <pre>
 <pt>15 MIN</pt>
 <fd>Howard</fd>
 <v>1867</v>
 <rn>22</rn>
 </pre>
</stop>

下面的示例采用ElementTree来读取这个文档,并对文档的结构作出修改:

>>> from xml.etree.ElementTree import parse, Element
>>> doc = parse('pred.xml')
>>> root = doc.getroot()
>>> root
<Element 'stop' at 0x100770cb0>
>>> # Remove a few elements
>>> root.remove(root.find('sri'))
>>> root.remove(root.find('cr'))
>>> # Insert a new element after <nm>...</nm>
>>> root.getchildren().index(root.find('nm'))
1
>>> e = Element('spam')
>>> e.text = 'This is a test'
>>> root.insert(2, e)
>>> # Write back to a file
>>> doc.write('newpred.xml', xml_declaration=True)
>>>

这些操作的结果产生了一个新的XML文档,看起来是这样的:

<?xml version='1.0' encoding='us-ascii'?>
<stop>
 <id>14791</id>
 <nm>Clark & Balmoral</nm>
 <spam>This is a test</spam><pre>
 <pt>5 MIN</pt>
 <fd>Howard</fd>
 <v>1378</v>
 <rn>22</rn>
 </pre>
 <pre>
 <pt>15 MIN</pt>
 <fd>Howard</fd>
 <v>1867</v>
 <rn>22</rn>
 </pre>
</stop>

修改XML文档的结构是简单直接的,但是必须记住所有的修改主要是对父元素进行的,我们把它当做是一个列表一样对待。比如说,如果移除某个元素,那么就利用它的直接父节点的remove()方法完成。如果插入或添加新的元素,同样要使用父节点的insert()和append()方法来完成。这些元素也可以使用索引和切片操作来进行操控,比如element[i]或者是element[i:j]。

如果需要创建新的元素,可以使用Element类来完成,我们本节给出的示例中已经这么做了。这在6.5节中有更进一步的描述。

我们要解析一个XML文档,但是需要使用XML命名空间来完成。

考虑使用了命名空间的如下XML文档:

<?xml version="1.0" encoding="utf-8"?>
<top>
 <author>David Beazley</author>
 <content>
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
 <title>Hello World</title>
 </head>
 <body>
 <h1>Hello World!</h1>
 </body>
 </html>
 </content>
</top>

如果解析这个文档并尝试执行普通的查询操作,就会发现没那么容易实现,因为所有的东西都变得特别冗长啰嗦:

>>> # Some queries that work
>>> doc.findtext('author')
'David Beazley'
>>> doc.find('content')
<Element 'content' at 0x100776ec0>
>>> # A query involving a namespace (doesn't work)
>>> doc.find('content/html')
>>> # Works if fully qualified
>>> doc.find('content/{http://www.w3.org/1999/xhtml}html')
<Element '{http://www.w3.org/1999/xhtml}html' at 0x1007767e0>
>>> # Doesn't work
>>> doc.findtext('content/{http://www.w3.org/1999/xhtml}html/head/title')
>>> # Fully qualified
>>> doc.findtext('content/{http://www.w3.org/1999/xhtml}html/'
... '{http://www.w3.org/1999/xhtml}head/{http://www.w3.org/1999/xhtml}title')
'Hello World'
>>>

通常可以将命名空间的处理包装到一个通用的类中,这样可以省去一些麻烦:

class XMLNamespaces:
 def __init__(self, **kwargs):
 self.namespaces = {}
 for name, uri in kwargs.items():
 self.register(name, uri)
 def register(self, name, uri):
 self.namespaces[name] = '{'+uri+'}'
 def __call__(self, path):
 return path.format_map(self.namespaces)

要使用这个类,可以按照下面的方式进行:

>>> ns = XMLNamespaces(html='http://www.w3.org/1999/xhtml')
>>> doc.find(ns('content/{html}html'))
<Element '{http://www.w3.org/1999/xhtml}html' at 0x1007767e0>
>>> doc.findtext(ns('content/{html}html/{html}head/{html}title'))
'Hello World'
>>>

对包含有命名空间的XML文档进行解析会非常繁琐。XMLNamespaces类的功能只是用来稍微简化一下这个过程,它允许在后序的操作中使用缩短的命名空间名称,而不必去使用完全限定的URI。

不幸的是,在基本的ElementTree解析器中不存在什么机制能获得有关命名空间的进一步信息。但是如果愿意使用iterparse()函数的话,还是可以获得一些有关正在处理的命名空间范围的信息。示例如下:

>>> from xml.etree.ElementTree import iterparse
>>> for evt, elem in iterparse('ns2.xml', ('end', 'start-ns', 'end-ns')):
... print(evt, elem)
...
end <Element 'author' at 0x10110de10>
start-ns ('', 'http://www.w3.org/1999/xhtml')
end <Element '{http://www.w3.org/1999/xhtml}title' at 0x1011131b0>
end <Element '{http://www.w3.org/1999/xhtml}head' at 0x1011130a8>
end <Element '{http://www.w3.org/1999/xhtml}h1' at 0x101113310>
end <Element '{http://www.w3.org/1999/xhtml}body' at 0x101113260>
end <Element '{http://www.w3.org/1999/xhtml}html' at 0x10110df70>
end-ns None
end <Element 'content' at 0x10110de68>
end <Element 'top' at 0x10110dd60>
>>> elem # This is the topmost element
<Element 'top' at 0x10110dd60>
>>>

最后要提到的是,如果正在解析的文本用到了除命名空间之外的其他高级XML特性,那么最好还是使用lxml库。比方说,lxml对文档的DTD验证、更加完整的XPath支持和其他的高级XML特性提供了更好的支持。本节提到的技术只是为解析操作做了一点修改,使得这个过程能够稍微简单一些。

我们需要选择、插入或者删除关系型数据库中的行数据。

在Python中,表达行数据的标准方式是采用元组序列。例如:

stocks = [
 ('GOOG', 100, 490.1),
    ('AAPL', 50, 545.75),
    ('FB', 150, 7.45),
    ('HPQ', 75, 33.2),
]

当数据以这种形式呈现时,通过Python标准的数据库API(在PEP 249中描述)来同关系型数据库进行交互相对来说就显得很直接了。该API的要点就是数据库上的所有操作都通过SQL查询来实现。每一行输入或输出数据都由一个元组来表示。

为了说明,我们可以使用Python自带的sqlite3模块。如果正在使用一个不同的数据库(如MySQL、Postgres或者ODBC),就需要安装一个第三方的模块来支持。但是,底层的编程接口即使不完全相同的话也几乎是一致的。

第一步是连接数据库。一般来说,要调用一个connect()函数,提供类似数据库名称、主机名、用户名、密码这样的参数以及一些其他需要的细节。示例如下:

>>> import sqlite3
>>> db = sqlite3.connect('database.db')
>>>

要操作数据的话,下一步就要创建一个游标(cursor)。一旦有了游标,就可以开始执行SQL查询了。示例如下:

>>> c = db.cursor()
>>> c.execute('create table portfolio (symbol text, shares integer, price real)')
<sqlite3.Cursor object at 0x10067a730>
>>> db.commit()
>>>

要在数据中插入行序列,可以采用这样的语句:

>>> c.executemany('insert into portfolio values (?,?,?)', stocks)
<sqlite3.Cursor object at 0x10067a730>
>>> db.commit()
>>>

要执行查询操作,可以使用下面这样的语句:

>>> for row in db.execute('select * from portfolio'):
... print(row)
...
('GOOG', 100, 490.1)
('AAPL', 50, 545.75)
('FB', 150, 7.45)
('HPQ', 75, 33.2)
>>>

如果想执行的查询操作需要接受用户提供的输入参数,请确保用?隔开参数,就像下面的示例这样:

>>> min_price = 100
>>> for row in db.execute('select * from portfolio where price >= ?',
                              (min_price,)):
... print(row)
...
('GOOG', 100, 490.1)
('AAPL', 50, 545.75)
>>>

从较低的层次来看,同数据库的交互其实是一件非常直截了当的事。只要组成SQL语句然后将它们传给底层的模块就可以更新数据库或者取出数据了。尽管如此,这里还是有一些比较棘手的细节问题需要针对每种情况逐项考虑。

其中一个比较复杂的问题就是将数据库中的数据映射到Python的类型中。对于像日期这样的条目,最常见的是使用datetime模块中的datetime实例,或者也可能是time模块中用到的系统时间戳(system timestamps)。对于数值型的数据,尤其是涉及小数的金融数据,这些数字可以用decimal模块中的Decimal实例来表示。不幸的是,确切的映射关系会因数据库后端的不同而有所区别,因此必须去阅读相关的文档。

另一个极其重要的问题是需要考虑组成SQL语句的字符串。我们绝对不应该用Python的字符串格式化操作符(即%)或者.format()方法来创建这种字符串。如果给这样的格式化操作符提供的值是来自于用户的输入,那么这就等于将你的程序敞开大门迎接SQL注入攻击(参见http://xkcd.com/327)。在查询操作中,特殊的?通配符会指示数据库后端启用自己的字符串替换机制,这样才能做到安全(希望如此)。

可悲的是,数据库后端对通配符的支持并不一致。有许多模块采用的是?或%s,而其他一些可能会使用不同的符号,比如用:0或者:1来代表参数。这里再次说明,必须查阅正在使用的数据库模块的文档资料。数据库模块的paramstyle属性中也包含有关于引用样式的相关信息。

对于简单地三趸将数据从数据库表项中取出和输入,使用数据库API通常足够了。如果要处理更加复杂的任务,那么使用一种更高层次的接口就显得很有意义了,比如那些提供有对象关系映射组件(object-relational mapper,ORM)的接口。像SQLAlchemy(http://www.sqlalchemy.org)这样的库允许数据库表项以Python类的形式来描述,在执行数据库操作时可隐藏大部分底层的SQL。

我们需要将十六进制数组成的字符串解码为字节流,或者将字节流编码为十六进制数。

如果需要编码或解码由十六进制数组成的原始字符串,可以使用binascii模块。示例如下:

>>> # Initial byte string
>>> s = b'hello'
>>> # Encode as hex
>>> import binascii
>>> h = binascii.b2a_hex(s)
>>> h
b'68656c6c6f'
>>> # Decode back to bytes
>>> binascii.a2b_hex(h)
b'hello'
>>>

同样的功能也可以在base64模块中找到。示例如下:

>>> import base64
>>> h = base64.b16encode(s)
>>> h
b'68656C6C6F'
>>> base64.b16decode(h)
b'hello'
>>>

对于大部分情况而言,采用上面给出的函数对十六进制数进行转换都是简单直接的。这两种技术的主要区别在于大写转换。base64.b16decode()和base64.b16encode()函数只能对大写形式的十六进制数进行操作,而binascii模块能够处理任意一种情况。

此外还需要重点提到的是编码函数产生的输出总是字节串。如果要将其强制转换为Unicode输出,可能需要增加一些额外的解码操作。示例如下:

>>> h = base64.b16encode(s)
>>> print(h)
b'68656C6C6F'
>>> print(h.decode('ascii'))
68656C6C6F
>>>

当解码十六进制数时,b16decode()和a2b_hex()函数可接受字节串或Unicode字符串作为输入。但是,这些字符串中必须只能包含ASCII编码的十六进制数字。

我们需要采用Base64编码来对二进制数据做编码解码操作。

base64模块中有两个函数——b64encode()和b64decode()——它们正是我们所需要的。示例如下:

>>> # Some byte data
>>> s = b'hello'
>>> import base64
>>> # Encode as Base64
>>> a = base64.b64encode(s)
>>> a
b'aGVsbG8='
>>> # Decode from Base64
>>> base64.b64decode(a)
b'hello'
>>>

Base64编码只能用在面向字节的数据上,比如字节串和字节数组。此外,编码过程的输出总是一个字节串。如果将Base64编码的数据同Unicode文本混在一起,那么可能需要执行一个额外的解码步骤。示例如下:

>>> a = base64.b64encode(s).decode('ascii')
>>> a
'aGVsbG8='
>>>

当解码Base64数据时,字节串和Unicode文本字符串都可以作为输入。但是,Unicode字符串中只能包含ASCII字符才行。

我们想将数据编码为统一结构的二进制数组,然后将这些数据读写到Python元组中去。

要同二进制数据打交道的话,我们可以使用struct模块。下面的示例将一列Python元组写入到一个二进制文件中,通过struct模块将每个元组编码为一个结构。

from struct import Struct
def write_records(records, format, f):
 '''
 Write a sequence of tuples to a binary file of structures.
 '''
 record_struct = Struct(format)
 for r in records:
 f.write(record_struct.pack(*r))

# Example
if __name__ == '__main__':
 records = [ (1, 2.3, 4.5),
 (6, 7.8, 9.0),
 (12, 13.4, 56.7) ]

with open('data.b', 'wb') as f:
     write_records(records, '<idd', f)

如果要将这个文件重新读取为一列Python元组的话,有好几种方法可以实现。首先,如果打算按块以增量式的方式读取文件的话,可以按照下面的示例来实现:

from struct import Struct
def read_records(format, f):
    record_struct = Struct(format)
    chunks = iter(lambda: f.read(record_struct.size), b'')
    return (record_struct.unpack(chunk) for chunk in chunks)

# Example
if __name__ == '__main__':
    with open('data.b','rb') as f:
        for rec in read_records('<idd', f):
            # Process rec
...

如果只想用一个read()调用将文件全部读取到一个字节串中,然后再一块一块的做转换,那么可以编写如下的代码:

from struct import Struct
def unpack_records(format, data):
    record_struct = Struct(format)
    return (record_struct.unpack_from(data, offset)
            for offset in range(0, len(data), record_struct.size))
# Example
if __name__ == '__main__':
    with open('data.b', 'rb') as f:
        data = f.read()
    for rec in unpack_records('<idd', data):
        # Process rec
        ...

在这两种情况下得到的结果都是一个可迭代对象,它能够产生出之前保存在文件中的那些元组。

对于那些必须对二进制数据编码和解码的程序,我们常会用到struct模块。要定义一个新的结构,只要简单地创建一个Struct实例即可:

# Little endian 32-bit integer, two double precision floats
record_struct = Struct('<idd')

结构总是通过一组结构化代码来定义,比如i、d、f等(参见Python的文档http://docs.python.org/3/library/struct.html)。这些代码同特定的二进制数据相对应,比如32位整数、64位浮点数、32位浮点数等。而第一个字符<指定了字节序。在这个例子中表示为“小端序”。将字符修改为>就表示为大端序,或者用!来表示网络字节序。

得到的Struct实例有着多种属性和方法,它们可用来操纵那种类型的结构。size属性包含了以字节为单位的结构体大小,这对于I/O操作来说是很有用的。pack()和unpack()方法是用来打包和解包数据的。示例如下:

>>> from struct import Struct
>>> record_struct = Struct('<idd')
>>> record_struct.size
20
>>> record_struct.pack(1, 2.0, 3.0)
b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08@'
>>> record_struct.unpack(_)
(1, 2.0, 3.0)
>>>

有时候我们会发现pack()和unpack()会以模块级函数(module-level functions)的形式调用,就像下面的示例这样:

>>> import struct
>>> struct.pack('<idd', 1, 2.0, 3.0)
b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08@'
>>> struct.unpack('<idd', _)
(1, 2.0, 3.0)
>>>

这么做行的通,但是比起创建一个单独的Struct实例来说还是显得不那么优雅,尤其是如果相同的结构会在代码中多处出现时。通过创建一个Struct实例,我们只用指定一次格式化代码,所有有用的操作都被漂亮地归组到了一起(通过实例方法来调用)。如果需要同结构打交道的话,这么做肯定会使得代码更容易维护(因为只需要修改一处即可)。

用来读取二进制结构的代码中涉及一些有趣而且优雅的编程惯用法(programming idioms)。在函数read_records()中,我们用iter()来创建一个迭代器,使其返回固定大小的数据块(参见5.8节)。这个迭代器会重复调用一个用户提供的可调用对象(即,lambda: f.read(record_struct.size))直到它返回一个指定值为止(即,b''),此时迭代过程结束。示例如下:

>>> f = open('data.b', 'rb')
>>> chunks = iter(lambda: f.read(20), b'')
>>> chunks
<callable_iterator object at 0x10069e6d0>
>>> for chk in chunks:
... print(chk)
...
b'\x01\x00\x00\x00ffffff\x02@\x00\x00\x00\x00\x00\x00\x12@'
b'\x06\x00\x00\x00333333\x1f@\x00\x00\x00\x00\x00\x00"@'
b'\x0c\x00\x00\x00\xcd\xcc\xcc\xcc\xcc\xcc*@\x9a\x99\x99\x99\x99YL@'
>>>

创建一个可迭代对象的原因之一在于这么做允许我们通过一个生成器表达式来创建records记录,就像解决方案中展示的那样。如果不采用这种方式,那么代码看起来就会像这样:

def read_records(format, f):
    record_struct = Struct(format)
    while True:
        chk = f.read(record_struct.size)
        if chk == b'':
            break
        yield record_struct.unpack(chk)
    return records

在函数unpack_records()中我们采用了另一种方法。这里使用的unpack_from()方法对于从大型的二进制数组中提取出二进制数据是非常有用的,因为它不会创建任何临时对象或者执行内存拷贝动作。我们只需提供一个字节串(或者任意的数组),再加上一个字节偏移量,它就能直接从那个位置上将字段解包出来。

如果用的是unpack()而不是unpack_from(),那么需要修改代码,创建许多小的切片对象并且还要计算偏移量。示例如下:

def unpack_records(format, data):
    record_struct = Struct(format)
    return (record_struct.unpack(data[offset:offset + record_struct.size])
            for offset in range(0, len(data), record_struct.size))

这个版本的实现除了读取数据变得更加复杂之外,还需要完成许多工作,因为它得计算很多偏移量,拷贝数据,创建小的切片对象。如果打算从已读取的大型字节串中解包出许多结构的话,那么unpack_from()是更加优雅的方案。

我们可能会想在解包记录时利用collections模块中的namedtuple对象。这么做允许我们在返回的元组上设定属性名。示例如下:

from collections import namedtuple
Record = namedtuple('Record', ['kind','x','y'])
with open('data.p', 'rb') as f:
    records = (Record(*r) for r in read_records('<idd', f))
for r in records:
    print(r.kind, r.x, r.y)

如果正在编写一个需要同大量的二进制数据打交道的程序,最好使用像numpy这样的库。比如,与其将二进制数据读取到元组列表中,不如直接将数据读入到结构化的数组中,就像这样:

>>> import numpy as np
>>> f = open('data.b', 'rb')
>>> records = np.fromfile(f, dtype='<i,<d,<d')
>>> records
array([(1, 2.3, 4.5), (6, 7.8, 9.0), (12, 13.4, 56.7)],
      dtype=[('f0', '<i4'), ('f1', '<f8'), ('f2', '<f8')])
>>> records[0]
(1, 2.3, 4.5)
>>> records[1]
(6, 7.8, 9.0)
>>>

最后但同样重要的是,如果面对的任务是从某种已知的文件结构中读取二进制数据(例如图像格式、shapefile、HDF5等),请先检查是否已有相应的Python模块可用。没必的话就别去重复发明轮子了。

我们需要读取复杂的二进制编码数据,这些数据中包含有一系列嵌套的或者大小可变的记录。这种数据包括图片、视频、shapefile(zh.wikipedia.org/zh-cn/Shapefile)等。

struct模块可用来编码和解码几乎任何类型的二进制数据结构。为了说明本节中提到的这种数据,假设我们有一个用Python数据结构表示的点的集合,这些点可用来组成一系列的三角形:

polys = [
          [ (1.0, 2.5), (3.5, 4.0), (2.5, 1.5) ],
          [ (7.0, 1.2), (5.1, 3.0), (0.5, 7.5), (0.8, 9.0) ],
          [ (3.4, 6.3), (1.2, 0.5), (4.6, 9.2) ],
        ]

现在假设要将这份数据编码为一个二进制文件,这个文件的文件头可以表示为如下的形式:

字  节

类  型

描  述

0

int

文件代码(0x1234,小端)

4

double

x的最小值(小端)

12

double

y的最小值(小端)

20

double

x的最大值(小端)

28

double

y的最大值(小端)

36

int

三角形数量(小端)

紧跟在这个文件头之后的是一系列的三角形记录,每条记录编码为如下的形式:

字  节

类  型

描  述

0

int

记录长度(N字节)

4-N

Points

(X,Y)坐标,以浮点数表示

要写入这个文件,可以使用如下的Python代码:

import struct
import itertools
def write_polys(filename, polys):
 # Determine bounding box
 flattened = list(itertools.chain(*polys))
 min_x = min(x for x, y in flattened)
 max_x = max(x for x, y in flattened)
 min_y = min(y for x, y in flattened)
 max_y = max(y for x, y in flattened)
with open(filename, 'wb') as f:
 f.write(struct.pack('<iddddi',
 0x1234,
 min_x, min_y,
 max_x, max_y,
 len(polys)))
 for poly in polys:
 size = len(poly) * struct.calcsize('<dd')
 f.write(struct.pack('<i', size+4))
 for pt in poly:
 f.write(struct.pack('<dd', *pt))
# Call it with our polygon data
write_polys('polys.bin', polys)

要将结果数据回读的话,可以利用struct.unpack()函数写出相似的代码,只是在编写的时候将所执行的操作反转即可(即,用unpack()取代之前的pack())。示例如下:

import struct
def read_polys(filename):
 with open(filename, 'rb') as f:
 # Read the header
 header = f.read(40)
 file_code, min_x, min_y, max_x, max_y, num_polys = \
 struct.unpack('<iddddi', header)
 polys = []
 for n in range(num_polys):
 pbytes, = struct.unpack('<i', f.read(4))
 poly = []
 for m in range(pbytes // 16):
 pt = struct.unpack('<dd', f.read(16))
 poly.append(pt)
 polys.append(poly)
 return polys

尽管这份代码能够工作,但是其中混杂了一些read调用、对结构的解包以及其他一些细节,因此代码比较杂乱。如果用这样的代码去处理一个真正的数据文件,很快就会变的更加混乱。因此,很明显需要寻求其他的解决方案来简化其中的一些步骤,将程序员解放出来,把精力集中在更加重要的问题上。

在本节剩余的部分中,我们将逐步构建出一个用来解释二进制数据的高级解决方案,目的是让程序员提供文件格式的高层规范,而将读取文件以及解包所有数据的细节部分都隐藏起来。先提前给读者预警,本节后面的代码可能是本书中最为高级的示例,运用了多种面向对象编程和元编程的技术。请确保仔细阅读本节的讨论部分,并且需要来回翻阅其他章节,交叉参考。

首先,当我们读取二进制数据时,文件中包含有文件头和其他的数据结构是非常常见的。尽管struct模块能够将数据解包为元组,但另一种表示这种信息的方式是通过类。下面的代码正是这么做的:

import struct
class StructField:
 '''
 Descriptor representing a simple structure field
 '''
 def __init__(self, format, offset):
 self.format = format
 self.offset = offset
 def __get__(self, instance, cls):
 if instance is None:
 return self
 else:
 r = struct.unpack_from(self.format,
 instance._buffer, self.offset)
 return r[0] if len(r) == 1 else r

class Structure:
 def __init__(self, bytedata):
 self._buffer = memoryview(bytedata)

代码中使用了描述符(descriptor)来代表每一个结构字段。每个描述符中都包含了一个struct模块可识别的格式代码(format)以及相对于底层内存缓冲区的字节偏移(offset)。在__get__()方法中,通过struct.unpack_from()函数从缓冲区中解包出对应的值,这样就不用创建额外的切片对象或者执行拷贝动作了。

Structure类只是用作基类,它接受一些字节数据并保存在由StructField描述符所使用的底层内存缓冲区中。这样一来,在Structure类中用到的memoryview(),意图就非常清楚了。

使用这份代码,现在就可以将结构定义为高层次的类,将前面表格中用来描述文件格式的信息都映射到类的定义中。示例如下:

class PolyHeader(Structure):
 file_code = StructField('<i', 0)
 min_x = StructField('<d', 4)
 min_y = StructField('<d', 12)
 max_x = StructField('<d', 20)
 max_y = StructField('<d', 28)
 num_polys = StructField('<i', 36)

下面的示例使用这个类来读取之前写入的三角形数据的文件头:

>>> f = open('polys.bin', 'rb')
>>> phead = PolyHeader(f.read(40))
>>> phead.file_code == 0x1234
True
>>> phead.min_x
0.5
>>> phead.min_y
0.5
>>> phead.max_x
7.0
>>> phead.max_y
9.2
>>> phead.num_polys
3
>>>

这么做挺有趣的,但是这种方法还存在许多问题。第一,尽管得到了便利的类接口,但代码比较冗长,需要用户指定许多底层的细节(比如,重复使用StructField、指定偏移量等)。得到的结果中,这个类也缺少一些常用的便捷方法,比如提供一种方式来计算结构的总大小。

任何时候当面对这种过于冗长的类定义时,都应该考虑使用类装饰器(class decorator)或者元类(metaclass)。元类的功能之一是它可用来填充许多底层的实现细节,把这份负担从用户身上拿走。举个例子,考虑下面这个元类和稍微修改过的Structure类:

class StructureMeta(type):
 '''
 Metaclass that automatically creates StructField descriptors
 '''
 def __init__(self, clsname, bases, clsdict):
 fields = getattr(self, '_fields_', [])
 byte_order = ''
 offset = 0
 for format, fieldname in fields:
 if format.startswith(('<','>','!','@')):
 byte_order = format[0]
 format = format[1:]
 format = byte_order + format
 setattr(self, fieldname, StructField(format, offset))
 offset += struct.calcsize(format)
 setattr(self, 'struct_size', offset)
class Structure(metaclass=StructureMeta):
 def __init__(self, bytedata):
 self._buffer = bytedata

 @classmethod
 def from_file(cls, f):
 return cls(f.read(cls.struct_size))

使用这个新的Structure类,现在就可以像这样编写结构的定义了:

class PolyHeader(Structure):
 _fields_ = [
    ('<i', 'file_code'),
    ('d', 'min_x'),
 ('d', 'min_y'),
 ('d', 'max_x'),
 ('d', 'max_y'),
 ('i', 'num_polys')
 ]

可以看到,现在的定义要简化得多。新增的类方法from_file()也使得从文件中读取数据变得更加简单,因为现在不需要了解数据的结构大小等细节问题了。比如,现在可以这么做:

>>> f = open('polys.bin', 'rb')
>>> phead = PolyHeader.from_file(f)
>>> phead.file_code == 0x1234
True
>>> phead.min_x
0.5
>>> phead.min_y
0.5
>>> phead.max_x
7.0
>>> phead.max_y
9.2
>>> phead.num_polys
3
>>>

一旦引入了元类,就可以为其构建更多智能化的操作。比如说,假设想对嵌套型的二进制结构提供支持。下面是对这个元类的修改,以及对新功能提供支持的描述符定义:

class NestedStruct:
 '''
 Descriptor representing a nested structure
 '''
 def __init__(self, name, struct_type, offset):
 self.name = name
 self.struct_type = struct_type
 self.offset = offset
 def __get__(self, instance, cls):
 if instance is None:
 return self
 else:
 data = instance._buffer[self.offset:
 self.offset+self.struct_type.struct_size]
 result = self.struct_type(data)
 # Save resulting structure back on instance to avoid
 # further recomputation of this step
 setattr(instance, self.name, result)
 return result
class StructureMeta(type):
 '''
 Metaclass that automatically creates StructField descriptors
 '''
 def __init__(self, clsname, bases, clsdict):
 fields = getattr(self, '_fields_', [])
 byte_order = ''
 offset = 0
 for format, fieldname in fields:
 if isinstance(format, StructureMeta):
 setattr(self, fieldname,
 NestedStruct(fieldname, format, offset))
 offset += format.struct_size
 else:
 if format.startswith(('<','>','!','@')):
 byte_order = format[0]
 format = format[1:]
 format = byte_order + format
 setattr(self, fieldname, StructField(format, offset))
 offset += struct.calcsize(format)
 setattr(self, 'struct_size', offset)

在这份代码中,NestedStruct描述符的作用是在一段内存区域上定义另一个结构[2]。这是通过在原内存缓冲区中取一个切片,然后在这个切片上实例化给定的结构类型来实现的。由于底层的内存缓冲区是由memoryview来初始化的,因此这个切片操作不会涉及任何额外的内存拷贝动作。相反,它只是在原来的内存中“覆盖”上新的结构实例。此外,要避免重复的实例化动作,这个描述符会利用8.10节中提到的技术将内层结构对象保存在该实例上。

使用这种新的技术,现在就可以像这样编写代码了:

class Point(Structure):
 _fields_ = [
 ('<d', 'x'),
 ('d', 'y')
 ]
class PolyHeader(Structure):
 _fields_ = [
   ('<i', 'file_code'),
   (Point, 'min'), # nested struct
   (Point, 'max'), # nested struct
  ('i', 'num_polys')
 ]

太神奇了,一切都还是按照所期望的方式正常运转。示例如下:

>>> f = open('polys.bin', 'rb')
>>> phead = PolyHeader.from_file(f)
>>> phead.file_code == 0x1234
True
>>> phead.min # Nested structure
<__main__.Point object at 0x1006a48d0>
>>> phead.min.x
0.5
>>> phead.min.y
0.5
>>> phead.max.x
7.0
>>> phead.max.y
9.2
>>> phead.num_polys
3
>>>

到目前为止,我们已经成功开发了一个用来处理固定大小记录的框架。但是对于大小可变的组件又该如何处理呢?比如说,这份三角形数据文件的剩余部分中包含有大小可变的区域。

一种处理方法是编写一个类来简单代表一块二进制数据,并附带一个通用函数来负责以不同的方式来解释数据的内容。这和6.11节中的代码关系紧密:

class SizedRecord:
 def __init__(self, bytedata):
 self._buffer = memoryview(bytedata)
 @classmethod
 def from_file(cls, f, size_fmt, includes_size=True):
 sz_nbytes = struct.calcsize(size_fmt)
 sz_bytes = f.read(sz_nbytes)
 sz, = struct.unpack(size_fmt, sz_bytes)
 buf = f.read(sz - includes_size * sz_nbytes)
 return cls(buf)

 def iter_as(self, code):
 if isinstance(code, str):
 s = struct.Struct(code)
 for off in range(0, len(self._buffer), s.size):
 yield s.unpack_from(self._buffer, off)
 elif isinstance(code, StructureMeta):
 size = code.struct_size
 for off in range(0, len(self._buffer), size):
 data = self._buffer[off:off+size]
 yield code(data)

这里的类方法SizedRecord.from_file()是一个通用的函数,用来从文件中读取大小预定好的数据块,这在许多文件格式中都是很常见的。对于输入参数,它可接受结构的格式代码,其中包含有编码的大小(以字节数表示)。可选参数includes_size用来指定字节数中是否要包含进文件头的大小。下面的示例展示如何使用这份代码来读取三角形数据文件中那些单独的三角形:

>>> f = open('polys.bin', 'rb')
>>> phead = PolyHeader.from_file(f)
>>> phead.num_polys
3
>>> polydata = [ SizedRecord.from_file(f, '<i')
... for n in range(phead.num_polys) ]
>>> polydata
[<__main__.SizedRecord object at 0x1006a4d50>,
 <__main__.SizedRecord object at 0x1006a4f50>,
 <__main__.SizedRecord object at 0x10070da90>]
>>>

可以看到,SizedRecord实例的内容还没有经过解释。要做到这一点,可以使用iter_as()方法。该方法可接受一个结构格式代码或者Structure类作为输入。这给了我们极大的自由来选择如何解释数据。比如:

>>> for n, poly in enumerate(polydata):
... print('Polygon', n)
... for p in poly.iter_as('<dd'):
... print(p)
...
Polygon 0
(1.0, 2.5)
(3.5, 4.0)
(2.5, 1.5)
Polygon 1
(7.0, 1.2)
(5.1, 3.0)
(0.5, 7.5)
(0.8, 9.0)
Polygon 2
(3.4, 6.3)
(1.2, 0.5)
(4.6, 9.2)
>>>
>>> for n, poly in enumerate(polydata):
... print('Polygon', n)
... for p in poly.iter_as(Point):
... print(p.x, p.y)
...
Polygon 0
1.0 2.5
3.5 4.0
2.5 1.5
Polygon 1
7.0 1.2
5.1 3.0
0.5 7.5
0.8 9.0
Polygon 2
3.4 6.3
1.2 0.5
4.6 9.2
>>>

现在我们把所有的东西结合起来。下面是read_polys()函数的另一种实现:

class Point(Structure):
    _fields_ = [
        ('<d', 'x'),
        ('d', 'y')
    ]
class PolyHeader(Structure):
    _fields_ = [
        ('<i', 'file_code'),
        (Point, 'min'),
        (Point, 'max'),
        ('i', 'num_polys')
    ]
def read_polys(filename):
    polys = []
    with open(filename, 'rb') as f:
        phead = PolyHeader.from_file(f)
        for n in range(phead.num_polys):
            rec = SizedRecord.from_file(f, '<i')
            poly = [ (p.x, p.y)
                      for p in rec.iter_as(Point) ]
            polys.append(poly)
    return polys

本节展示了多种高级编程技术的实际应用,这些技术包括描述符、惰性求值、元类、类变量以及memoryview。只是它们都用于一个非常具体的目的而已。

本节给出的实现中,一个非常重要的特性就是强烈基于惰性展开(lazy-unpacking)的思想。每当创建出一个Structure实例时,__init__()方法只是根据提供的字节数据创建出一个memoryview,除此之外别的什么都不做。具体而言就是这个时候不会进行任何的解包或其他与结构相关的操作。采用这种方法的一个动机是我们可能只对二进制记录中的某几个特定部分感兴趣。与其将整个文件解包展开,不如只对实际要访问到的那几个部分解包即可。

要实现惰性展开和对值进行打包,StructField描述符就派上用场了。用户在_fields_中列出的每个属性都会转换为一个StructField描述符,它保存着相关属性的结构化代码和相对于底层内存缓冲区的字节偏移量。当我们定义各种各样的结构化类型时,元类StructureMeta用来自动创建出这些描述符。使用元类的主要原因在于这么做能以高层次的描述来指定结构的格式,完全不用操心底层的细节问题,因而能极大地简化用户的操作。

元类StructureMeta中有一个微妙的方面需要注意,那就是它将字节序给规定死了。也就说,如果有任何属性指定了字节序(<指代小端序,而>指代大端序),那么这个字节序就适用于该属性之后的所有字段。这种行为可避免我们产生额外的键盘输入,同时也使得在定义字段时可以切换字节序。比如说,我们可能会碰到下面这样更加复杂的数据:

class ShapeFile(Structure):
    _fields_ = [ ('>i', 'file_code'), # Big endian 这里是大端,后两个属性都是大端
                 ('20s', 'unused'),
                 ('i', 'file_length'),
                 ('<i', 'version'), # Little endian 切换为小端,后续所有的属性都是小端
                 ('i', 'shape_type'),
                 ('d', 'min_x'),
                 ('d', 'min_y'),
                 ('d', 'max_x'),
                 ('d', 'max_y'),
                 ('d', 'min_z'),
                 ('d', 'max_z'),
                 ('d', 'min_m'),
                 ('d', 'max_m') ]

前文中提到,解决方案中对memoryview()的使用起到了避免内存拷贝的作用。当结构数据开始出现嵌套时,memoryview可用来在相同的内存区域中覆盖上不同的结构定义。这种行为十分微妙,它考虑到了切片操作在memoryview和普通的字节数组上的不同行为。如果对字节串或字节数组执行切片操作的话,通常都会得到一份数据的拷贝,但memoryview就不会这样——切片只是简单地覆盖在已有的内存之上。因此,这种方法更加高效。

还有一些相关的章节会帮助我们对解决方案中用到的技术进行扩展。8.13节中采用描述符构建了一个类型系统。8.10节中介绍了有关惰性计算的性质,这个和NestedStruct描述符的实现有一定的相关性。9.19节中有一个例子采用元类来初始化类的成员,这个和StructureMeta类采用的方式非常相似。我们可能也会对Python标准库中ctypes模块的源代码产生兴趣,因为它对定义数据结构、对数据结构的嵌套以及类似功能的支持和我们的解决方案比较相似。

我们需要在大型数据库中查询数据并由此生成汇总或者其他形式的统计数据。

对于任何涉及统计、时间序列以及其他相关技术的数据分析问题,都应该使用Pandas库(http://pandas.pydata.org)。

为了小试牛刀,下面这个例子使用Pandas来分析芝加哥的老鼠和啮齿动物数据库(https:// data.cityofchicago.org/Service-Requests/311-Service-Requests-Rodent-Baiting/97t6-zrhs)。在写作本书时,这个CSV文件中有大约74 000条数据:

>>> import pandas
>>> # Read a CSV file, skipping last line
>>> rats = pandas.read_csv('rats.csv', skip_footer=1)
>>> rats
<class 'pandas.core.frame.DataFrame'>
Int64Index: 74055 entries, 0 to 74054
Data columns:
Creation Date                 74055 non-null values
Status                     74055 non-null values
Completion Date                 72154 non-null values
Service Request Number         74055 non-null values
Type of Service Request         74055 non-null values
Number of Premises Baited         65804 non-null values
Number of Premises with Garbage    65600 non-null values
Number of Premises with Rats     65752 non-null values
Current Activity             66041 non-null values
Most Recent Action             66023 non-null values
Street Address                 74055 non-null values
ZIP Code                     73584 non-null values
X Coordinate                 74043 non-null values
Y Coordinate                 74043 non-null values
Ward                         74044 non-null values
Police District                 74044 non-null values
Community Area                 74044 non-null values
Latitude                     74043 non-null values
Longitude                     74043 non-null values
Location                     74043 non-null values
dtypes: float64(11), object(9)
>>> # Investigate range of values for a certain field
>>> rats['Current Activity'].unique()
array([nan, Dispatch Crew, Request Sanitation Inspector], dtype=object)
>>> # Filter the data
>>> crew_dispatched = rats[rats['Current Activity'] == 'Dispatch Crew']
>>> len(crew_dispatched)
65676
>>>
>>> # Find 10 most rat-infested ZIP codes in Chicago
>>> crew_dispatched['ZIP Code'].value_counts()[:10]
60647         3837
60618         3530
60614         3284
60629         3251
60636         2801
60657         2465
60641         2238
60609         2206
60651         2152
60632         2071
>>>
>>> # Group by completion date
>>> dates = crew_dispatched.groupby('Completion Date')
<pandas.core.groupby.DataFrameGroupBy object at 0x10d0a2a10>
>>> len(dates)
472
>>>
>>> # Determine counts on each day
>>> date_counts = dates.size()
>>> date_counts[0:10]
Completion Date
01/03/2011               4
01/03/2012             125
01/04/2011              54
01/04/2012              38
01/05/2011              78
01/05/2012             100
01/06/2011             100
01/06/2012              58
01/07/2011               1
01/09/2012              12
>>>

>>> # Sort the counts
>>> date_counts.sort()
>>> date_counts[-10:]
Completion Date
10/12/2012             313
10/21/2011             314
09/20/2011             316
10/26/2011             319
02/22/2011             325
10/26/2012             333
03/17/2011             336
10/13/2011             378
10/14/2011             391
10/07/2011             457
>>>

你没看错,2011年10月7号对于老鼠来说的确是非常忙碌的一天。

Pandas是一个庞大的库,它还有更多的功能,但我们无法在此一一描述。但是,如果需要分析大型的数据集、将数据归组、执行统计分析或者其他类似的任务,那么Pandas绝对值得一试。

由Wes McKinney所著的Python for Data Analysis(O’Reilly)一书中也包含了更多的内容。

[1] Num-Premises中的-不能用作Python的标识符字符。——译者注

[2] 类似C++中的placement new技法。——译者注


本面封面上的动物是一只跳鼠(跳兔),也被称为春兔。跳兔根本就不是野兔,而只是啮齿目跳兔科中唯一的成员。它们不是有袋类动物,但隐约有些袋鼠的样子,有着短小的前腿,强有力的后腿,适合于跳跃。除此之外还有一根长长的、强壮有力且毛发浓密(但不容易抓住)的尾巴,用来掌握平衡以及坐下来的时候作为支撑物。它们能长到14~18英寸,尾巴几乎和身体一样长,体重大约可达8磅。跳兔有着油腻且富有光泽的褐色或金色表皮,柔软的皮毛,它的腹部是白色的。它们的头部有着和身体不成比例的大小,耳朵则很长(耳朵底部有一块可翻动的皮肤,这样当它们在打洞时可以避免让沙子进入耳朵里),眼睛是深棕色的。

跳兔全年都可以交配,孕期在78~82天。雌性跳兔一般每窝只会产一只小跳兔(小跳兔会一直呆在它的妈妈身边,直到大约7周大为止),但每年会有3个或4个窝。刚生下的小跳兔是有牙齿的,而且毛发齐全,眼睛是闭上的,而耳朵是打开着的。

跳兔是陆生生物,非常适应于挖掘地洞。它们白天喜欢呆在自己构筑的洞穴和地道所交织成的网络中。跳兔是夜间活动生物,主要食草,可以吃鳞茎植物、根、谷物,偶尔也吃昆虫。当它们在觅食时会移动四肢,但每一次水平跳跃能移动10~25英尺,遇到危险时能够快速逃离。虽然常能在野外看见跳兔集体觅食,但它们并不会形成一个有组织的社会化单位,通常都是独自筑窝或者成对繁殖。跳兔在圈养下可以存活15年。可以在扎伊尔、肯尼亚以及南非的干燥沙漠或者半干旱地区见到跳兔的身影,它们也是南非最受喜爱的重要食物来源。


相关图书

深度学习的数学——使用Python语言
深度学习的数学——使用Python语言
动手学自然语言处理
动手学自然语言处理
Web应用安全
Web应用安全
Python高性能编程(第2版)
Python高性能编程(第2版)
图像处理与计算机视觉实践——基于OpenCV和Python
图像处理与计算机视觉实践——基于OpenCV和Python
Python数据科学实战
Python数据科学实战

相关文章

相关课程