写在前面人生苦短,我用Python。
Python是一门简洁又强大的编程语言。比起C++/Java等语言,Python拥有简单易懂的语法,极易上手,专注于解决问题,而非语言本身。同时,Python拥有丰富的库,为各种工作提供支持,如网页开发(Django)、科学计算(Numpy)、软件开发(SCons)等[1]。虽然Python运行效率比不上C++/Java,但开发者会使用Python进行快速开发原型、试验想法,条件成熟后再迁移到C++/Java。总而言之,人生苦短,我用Python。
想要高效地使用Python解决问题,不仅需要学习基础语法,如条件、循环语句,还要知晓Python式的方式进行编程,如运用Python提供的语法糖(Syntactic sugar),充分利用Python的简洁、可读性强的语法。为此,David Beazley和Brian K. Jones著作了Python Cookbook,面向实际问题展示Python式的解决方案,并提供详实的讨论,是Python的“菜谱”大合集。
本系列学习笔记,旨在总结Python Cookbook每一章的重点难点,对每章讨论的问题进行归类,记录容易混淆和忽视的小技巧,方便学习和查询。
最后,本系列学习笔记不是Python语法的入门笔记,而是关于Python的进阶,适合在了解基础语法后想掌握更高效编程方式、或者熟悉Python2但对Python3不了解的同学。想学习Python基础语法的同学,可以看Learn Python the Hard Way,史诗般的入门书籍(笑)。以及,尽管本系列学习笔记将尽可能覆盖重点,但它不能代替原书,感兴趣的同学还是推荐阅读Python Cookbook,有免费的网页版/电子版。
本文总结Python Cookbook的第一章,数据结构和算法。希望这里的笔记能在大家学习Python的过程中提供一定帮助。
目录如何从序列解包(unpack)变量
可读性更强的切片(slice)
数据类型(一):高性能容器collections
数据类型(二):heapq
在min, max和sorted中使用key
正确使用生成器(generator)
一些高效的小技巧
1. 如何解包(unpack)变量
有时候,我们需要把一个含有若干元素的序列赋值给一些变量,即解包。序列可以是tuple,list,string,或任何iterable。根据序列的长度和变量的个数,解包可以分为两类:序列长度 = 变量个数
序列长度 > 变量个数
Python提供了非常简洁的解决方案:仅需要一次简单的赋值,就可以完成解包。
对于第一种情况的解包,我们只需要在将序列赋值给和序列长度相等个数的变量,序列中的元素按顺序赋值给变量:
>>> data = [ 'ACME', 50, 91.1, (2012, 12, 21) ]
>>> name, shares, price, date = data
对于第二种情况的解包,可以使用星号表达式*:
>>> record = ('Dave', 'dave@example.com', '773-555-1212', '847-555-1212')
>>> name, email, *phone_numbers = user_record
>>> phone_numbers
['773-555-1212', '847-555-1212']
有时候我们并不需要所有解包得到的变量,如我们不需要上个例子中的name,那么可以用下划线代替name,这是一种Python式的表示“不使用该变量”:
>>> _, email, *phone_numbers = user_record
2. 可读性更强的切片(slice)
有时我们需要从一个有固定格式的字符串的不同部分提取相应数据,并进行操作,如:
>>> res = '100-0.9'
>>> cost = float(res[0:2])*float(res[4:7])
>>> cost
9.0
上述方法虽然直接,但可读性差,不利于代码的维护,而Python内置的slice()函数则能解决这一不足。slice()能创建slice对象,用于任何需要切片的场合:
>>> NUMBER = slice(0, 2)
>>> PRICE = slice(4, 7)
>>> cost = float(res[NUMBER])*float(res[PRICE])
>>> cost
9.0
这样可读性是不是更强了呢。
此外,我们还可以改变slice()默认的step,并访问slice的start、stop和step:
>>> a = slice(10, 50, 2)
>>> a.start
10
>>> a.stop
50
>>> a.step
2
3. 数据类型(一):高性能容器collections
collections模块实现了几个专用数据类型,适用于一些特殊场合,可以作为Python内置的通用数据容器(dict、list、set和tuple)的替代。本章对collections的介绍涵盖了最常用的几种专用数据类型:deque、defaultdict、OrderedDict、Counter和namedtuple等。
deque
deque指double-ended queue,是栈(stack)和队列(queue)的推广,在deque两端进行append和pop只需要大约O(1)。固定长度的deque适用于保留固定长度的历史:
>>> from collections import deque
>>> dq = deque(maxlen=2)
>>> dq.append(0)
>>> dq
deque([0], maxlen=2)
>>> dq.append(1)
>>> dq
deque([0, 1], maxlen=2)
>>> dq.append(2)
>>> dq
deque([1, 2], maxlen=2)
可以看到,当新元素插入deque时,如果此时元素个数超过创建deque时设定的长度maxlen,旧的元素自动pop。
defaultdict
defaultdict不仅提供了和dict相同的功能,还能自动初始化初值,使程序员专注于元素的添加:
>>> s = 'mississippi'
>>> d = defaultdict(int)
>>> for k in s:
... d[k] += 1
...
>>> sorted(d.items())
[('i', 4), ('m', 1), ('p', 2), ('s', 4)]
关于如何指定初值的初始化,可以参见default_factory。
OrderedDict
当我们遍历Python内置的dict时,返回的元素并不按照添加的顺序。当我们需要控制元素的顺序时,可以使用OrderedDict:
from collections import OrderedDict
d = OrderedDict()
d['foo'] = 1
d['bar'] = 2
d['spam'] = 3
# Outputs "foo 1", "bar 2", "spam 3"
for key in d:
print(key, d[key])
需要注意的是,保持顺序有空间上的代价,OrderedDict通常占用dict两倍的空间。
Counter
当我们使用dict的目的是记录不同元素出现的次数时,可以使用专用数据类型Counter:
>>> from collections import Counter
>>> words = ['a', 'this', 'a', 'is', 'list', 'list', 'list']
>>> word_counts = Counter(words)
>>> word_counts
Counter({'list': 3, 'a': 2, 'this': 1, 'is': 1})
Counter还提供了most_common(n)方法,返回频率最高的n个元素:
>>> word_counts.most_common(2)
[('list', 3), ('a', 2)]
看到这里,Counter不就是一个将元素映射到次数的dict吗?Counter当然有它不一样的地方,最大的特点是Counter支持一些dict不支持的数学运算,特别适合频次的计算:
>>> morewords = ['another', 'list']
>>> moreword_counts = Counter(morewords)
>>> word_counts + moreword_counts
Counter({'list': 4, 'a': 2, 'this': 1, 'is': 1, 'another': 1})
>>> word_counts - moreword_counts
Counter({'a': 2, 'list': 2, 'this': 1, 'is': 1})
namedtuple
namedtuple是tuple的子类,通过namedtuple我们可以名字访问元组的元素,类似于C++的struct或简单的class。首先需要通过namedtuple()创建可以实例化的class,然后通过名字对field访问和赋值:
>>> 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是immutable的,一旦对某个field赋值,不能进行in-place的改变。
4. 数据类型(二):heapq
heapq模块实现了priority queue,它的nlargest()和nsmallest()方法适用于从一个数列中找到最大/最小的n个元素,且n相对数列长度较小时。
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]
我们还可以使用heapq.heapify()显式地将序列转化为heap:
>>> 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]
5. 在min, max和sorted中使用key
当我们有一系列字典,并且希望以字典的某个公共key为标准找到最小、最大的字典,或者对这些字典进行排序时,可以在min、max和sorted中使用key:
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'))
rows_by_fname按fname排序,rows_by_uid则按uid排序。
这里itemgetter('fname')相当于以下lambda表达式,根据key提取value:
lambda r: r['fname']
itemgetter()函数也接受多个key,如:
rows_by_lfname = sorted(rows, key=itemgetter('lname','fname'))
排序时优先按照lname排序,lname相同时再按fname排序。
同样的,我们在min和max中也可以用key:
>>> min(rows, key=itemgetter('uid'))
{'fname': 'John', 'lname': 'Cleese', 'uid': 1001}
>>> max(rows, key=itemgetter('uid'))
{'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
6. 正确使用生成器(generator)
在Python中,生成器是高效Python的重要组成部分,它提供了创建iterator的快捷方式。生成器的重要特点是,按需生成值,避免无谓地浪费内存,这点可以通过以下例子说明[2]:
def firstn(n):
num, nums = 0, []
while num < n:
nums.append(num)
num += 1
return nums
sum_of_first_n = sum(firstn(1000000))
firstn()返回一个list,当n很大时,list将占用较大空间,即使我们只需要这个list的元素和。而使用生成器时,firstn返回的只是一个lazy的iterator,只有当需要访问元素时才生成该元素,因此在求和前,firstn返回值占用远少于返回list时的内存;而求和时,sum依次累加生成的元素。在下面的代码中,我们使用yield构造生成器:
def firstn(n):
num = 0
while num < n:
yield num
num += 1
>>> g = firstn(1000000)
>>> g
sum_of_first_n = sum(g)
我们同样可以通过生成器表达式(generator expression),即一对圆括号(),构建生成器:
a = (i for i in range(1000000))
生成器在list compression中也特别有用:
>>> mylist = [1, 4, -5, 10, -7, 2, 3, -1]
>>> pos = (n for n in mylist if n > 0)
>>> pos
at 0x1006a0eb0>
>>> for x in pos:
... print(x)
生成器也适用于当我们需要先变换再reduce数据的场合,Python还提供了相关的语法糖。具体的,我们可以不用:
nums = [1, 2, 3, 4, 5]
s = sum((x * x for x in nums))
而用:
nums = [1, 2, 3, 4, 5]
s = sum(x * x for x in nums)
即当生成器作为函数唯一的输入时,可以省去重复的括号,生成器表达式的括号和函数的括号可以合并成一对括号。
此外,当我们希望函数的返回更通用,而非局限于list,也可以使用generator:
def general_purpose_iterable(n):
for i in range(n):
yield i
关于生成器更详细的介绍,可以参考Python Wiki。
7. 一些高效的小技巧
很多时候,我们有不同方式解决同一个问题。尽管达成的效果一样,但是效率千差万别。这里总结这章提到的技巧。
itemgetter()和attrgetter()
在对一系列dictionary按照公共key排序时,我们可以通过以下两种方式实现:
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}
]
# 使用itemgetter()
rows_by_fname = sorted(rows, key=itemgetter('fname'))
# 使用lambda表达式
rows_by_fname = sorted(rows, key=lambda r: r['fname']))
尽管两种方式的效果一样,但itemgetter()通常比lambda表达式更高效。类似的,在对一系列对象排序时,attrgetter()通常也比lambda表达式更快。
dictionary comprehension
处理字典时,dictionary comprehension比dict()更快:
p2 = { key:value for key,value in prices.items() if key in tech_names }
快过
p1 = dict((key, value) for key, value in prices.items() if value > 200)
除了本文总结的内容,这一章还提到一些数据结构和算法,如整合多个字典的collections.ChainMap,过滤iterable的itertools.compress(),还有寻找两个字典的公共key等,具体参见Python Cookbook。
参考