Python 数据结构惯用法
使用列表
不要创建不必要的对象
惯用词典
使用集合模块
作为开发者,我们花费大量时间编写操作基本数据结构的代码:遍历列表、创建映射、筛选集合中的元素。因此,了解如何在 Python 中高效地实现这些操作,并使代码更易读、更高效至关重要。
使用列表
遍历列表
在 Python 中,遍历列表的方法有很多种。最简单的方法就是维护列表中的当前位置,并在每次迭代时递增它:
## SO WRONG
l = [1, 2, 3, 4, 5]
i = 0
while i < len(l):
print l[i]
i += 1
这种方法可行,但 Python 提供了一种更便捷的方式,即使用range函数。range函数可以生成从 0 到 N 的数字,这可以看作是C 语言中for循环的类似功能:
## STILL WRONG
for i in range(len(l)):
print l[i]
虽然这种方法更简洁,但还有更好的方法,因为 Python 允许我们直接遍历列表,类似于其他语言中的foreach循环:
# RIGHT
for v in l:
print v
按相反顺序遍历列表
如何反向遍历列表?一种方法是使用一个可读性较差的三参数range函数,并提供列表中最后一个元素的位置(第一个参数)、列表中第一个元素之前元素的位置(第二个参数)以及反向遍历的负步长(第三个参数):
# WRONG
for i in range(len(l) - 1, -1, -1):
print l[i]
但正如你可能已经猜到的,Python 应该提供了一种更好的方法。我们可以直接在for循环中使用反向函数:
# RIGHT
for i in reversed(l):
print i
访问最后一个元素
访问列表中最后一个元素的常用方法是:获取列表的长度,减去 1,将结果数字作为最后一个元素的位置:
# WRONG
l = [1, 2, 3, 4, 5]
>>> l[len(l) - 1]
5
在 Python 中这样做比较麻烦,因为 Python 支持使用负索引来访问列表末尾的元素。所以 -1 是最后一个元素:
# RIGHT
>>> l[-1]
5
负索引也可以用来访问倒数第二个元素,依此类推:
# RIGHT
>>> l[-2]
4
>>> l[-3]
3
使用序列解包
在其他编程语言中,从列表中提取值并将其赋给多个变量的常用方法是使用索引:
# WRONG
l1 = l[0]
l2 = l[1]
l3 = l[2]
但是 Python 支持序列解包,允许我们将列表中的值提取到多个变量中:
# RIGHT
l1, l2, l3 = [1, 2, 3]
>>> l1
1
>>> l2
2
>>> l3
3
使用列表阅读理解
假设我们要筛选出所有由 18 岁及以下用户发布的电影评分。
你写过多少次这样的代码:
# WRONG
under_18_grades = []
for grade in grades:
if grade.age <= 18:
under_18_grades.append(grade)
在 Python 中不要再这样做了,请改用列表推导式和if语句。
# RIGHT
under_18_grades = [grade for grade in grades if grade.age <= 18]
使用枚举函数
有时你需要遍历一个列表并跟踪每个元素的位置。例如,如果你需要在 shell 中显示菜单项,你可以简单地使用range函数:
# WRONG
for i in range(len(menu_items)):
menu_items = menu_items[i]
print "{}. {}".format(i, menu_items)
更好的方法是使用enumerate函数。它是一个迭代器,返回一个对,每个对包含元素的位置和元素本身:
# RIGHT
for i, menu_items in enumerate(menu_items):
print "{}. {}".format(i, menu_items)
使用键进行排序
在其他编程语言中,对元素进行排序的典型方法是提供一个函数,该函数会比较两个对象以及要排序的集合。在 Python 中,它看起来像这样:
people = [Person('John', 30), Person('Peter', 28), Person('Joe', 42)]
# WRONG
def compare_people(p1, p2):
if p1.age < p2.age:
return -1
if p1.age > p2.age:
return 1
return 0
sorted(people, cmp=compare_people)
[Person(name='Peter', age=28), Person(name='John', age=30), Person(name='Joe', age=42)]
但这并非最佳方法。因为我们只需要比较两个Person类实例的年龄字段值即可。为什么还要为此编写一个复杂的比较函数呢?
具体来说,在这种情况下,sorted函数接受一个 key函数,该函数用于提取一个键,该键将用于比较对象的两个实例:
# RIGHT
sorted(people, key=lambda p: p.age)
[Person(name='Peter', age=28), Person(name='John', age=30), Person(name='Joe', age=42)]
使用所有/任意功能
如果要检查集合中的所有值或其中任何一个值是否为 True,一种方法是遍历列表:
# WRONG
def all_true(lst):
for v in lst:
if not v:
return False
return True
但 Python 已经有了`all`和`any`函数来实现这个功能。`all` 函数会在传入的可迭代对象中的所有值都为 True 时返回 True,而` any` 函数会在传入的值中至少有一个为 True 时返回 True。
# RIGHT
all([True, False])
>> False
any([True, False])
>> True
要检查所有项目是否都符合某个条件,可以使用列表推导式将任意对象列表转换为布尔值列表:
all([person.age > 18 for person in people])
或者你可以传递一个生成器(只需省略列表推导式周围的方括号):
all(person.age > 18 for person in people)
这不仅可以节省两次击键,还可以省略创建中间列表的步骤(稍后会详细介绍)。
使用切片
您可以使用称为切片的技术来获取列表的一部分。访问列表时,您不必只在方括号中提供单个索引,而是可以提供以下三个值。
lst[start:end:step]
所有这些参数都是可选的,省略某些参数可以得到列表的不同部分。如果只提供起始位置,则会返回从指定索引开始的所有元素:
# RIGHT
>>> lst = range(10)
>>> lst[3:]
[3, 4, 5, 6, 7, 8, 9]
如果只提供结束位置,切片操作将返回直到指定位置的所有元素:
>>> lst[:-3]
[0, 1, 2, 3, 4, 5, 6]
您还可以获取两个索引之间的列表部分:
>>> lst[3:6]
[3, 4, 5]
切片操作的默认步长为 1,这意味着返回起始位置和结束位置之间的所有元素。如果您只想获取每隔一个元素或每隔两个元素,则需要提供一个步长值:
>>> lst[2:8:2]
[2, 4, 6]
不要创建不必要的对象
使用 xrange
range 函数在需要生成一定范围内一致的整数值时非常有用,但它有一个缺点:它返回的是一个包含所有生成值的列表:
# WRONG
# Returns a too big list
for i in range(1000000000):
...
解决方法是使用xrange函数。它会直接返回一个迭代器,而不是创建一个列表:
# RIGHT
# Returns an iterator
for i in xrange(1000000000):
...
与range函数相比, xrange的缺点是其输出只能迭代一次。
Python 3 的新特性
Python 3 中移除了xrange 函数, range函数的行为与 Python 2.x 中的xrange 函数类似。如果需要在 Python 3 中多次遍历range 函数的输出,可以将其输出转换为列表:
>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
使用 izip
如果需要从两个集合中的元素生成键值对,一种方法是使用zip函数:
# WRONG
names = ['Joe', 'Kate', 'Peter']
ages = [30, 28, 41]
# Creates a list
zip(names, ages)
[('Joe', 30), ('Kate', 28), ('Peter', 41)]
我们可以使用izip函数,它会返回一个迭代器,而不是创建一个新列表:
# RIGHT
from itertools import izip
# Creates an iterator
it = izip(names, ages)
Python 3 的新特性
在 Python 3 中,izip函数被移除,zip 函数的行为与 Python 2.x 中的izip函数类似。
使用生成器
列表推导式是 Python 中一个强大的工具,但由于每个列表推导式都会创建一个新列表,因此它会占用大量内存:
# WRONG
# Original list
lst = range(10)
# This will create a new list
lst_1 = [i + 1 for i in lst]
# This will create another list
lst_2 = [i ** 2 for i in lst_1]
避免这种情况的方法是使用生成器而不是列表推导式。语法上的区别很小:你应该使用圆括号而不是方括号,但这种区别至关重要。以下示例不会创建任何中间列表:
# RIGHT
# Original list
lst = range(10)
# Won't create a new list
lst_1 = (i + 1 for i in lst)
# Won't create another list
lst_2 = (i ** 2 for i in lst_1)
如果您只需要处理结果集合的一部分来获得结果,例如查找符合特定条件的第一个元素,这将特别方便。
惯用词典
避免使用 keys() 函数
如果需要遍历字典中的键,你可能会倾向于使用哈希映射的keys函数:
# WRONG
for k in d.keys():
print k
但还有更好的方法,你可以使用迭代器遍历字典,它会遍历字典的键,所以你可以简单地这样做:
# RIGHT
for k in d:
print k
它不仅可以节省你的一些输入,还可以防止像keys方法那样创建字典中所有键的副本。
遍历键和值
如果使用keys方法,就可以轻松地遍历字典中的键和值,如下所示:
#WRONG
for k in d:
v = d[k]
print k, v
但还有更好的方法。您可以使用items函数,该函数可以从字典中返回键值对:
# RIGHT
for k, v in d.items():
print k, v
这种方法不仅更简洁,而且效率更高。
使用字典进行理解
创建字典的一种方法就是逐个为其赋值:
# WRONG
d = {}
for person in people:
d[person.name] = person
你可以使用字典理解功能将其简化为一行句子:
# RIGHT
d = {person.name: person for person in people}
使用集合模块
使用命名元组
如果你需要类似结构体的类型,你可以定义一个包含初始化方法和多个字段的类:
# WRONG
class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y
然而,Python 库中的collections模块提供了一个namedtuple类型,可以将此操作简化为一行代码:
# RIGHT
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
此外,namedtuple还实现了__str__、__repr__和__eq__方法:
>>> Point(1, 2)
Point(x=1, y=2)
>>> Point(1, 2) == Point(1, 2)
True
使用默认字典
如果我们需要统计某个元素在集合中出现的次数,我们可以使用一种常见的方法:
# WRONG
d = {}
for v in lst:
if v not in d:
d[v] = 1
else:
d[v] += 1
collections模块为此提供了一个非常方便的类,名为defaultdict。它的构造函数接受一个函数,该函数将用于计算不存在的键的值:
>>> d = defaultdict(lambda: 42)
>>> d['key']
42
为了重写计数示例,我们可以将int函数传递给defaultdict,该函数在不带参数调用时返回零:
# RIGHT
from collections import defaultdict
d = defaultdict(int)
for v in lst:
d[v] += 1
当您需要对集合中的项目进行任何类型的分组,但只需要获取元素数量时,defaultdict非常有用;您也可以使用Counter类来代替:
# RIGHT
from collections import Counter
>>> counter = Counter(lst)
>>> counter
Counter({4: 3, 1: 2, 2: 1, 3: 1, 5: 1})
本文最初发表于Brewing Codes博客。
文章来源:https://dev.to/mushketyk/python-data-structs-idioms-6ae