发布于 2026-01-06 7 阅读
0

Python 数据结构常用技巧:使用列表;避免创建不必要的对象;合理使用字典;使用 collections 模块

Python 数据结构惯用法

使用列表

不要创建不必要的对象

惯用词典

使用集合模块

作为开发者,我们花费大量时间编写操作基本数据结构的代码:遍历列表、创建映射、筛选集合中的元素。因此,了解如何在 Python 中高效地实现这些操作,并使代码更易读、更高效至关重要。

使用列表

遍历列表

在 Python 中,遍历列表的方法有很多种。最简单的方法就是维护列表中的当前位置,并在每次迭代时递增它:

## SO WRONG
l = [1, 2, 3, 4, 5]
i = 0
while i < len(l):
    print l[i]
    i += 1

Enter fullscreen mode Exit fullscreen mode

这种方法可行,但 Python 提供了一种更便捷的方式,即使用range函数。range函数可以生成从 0 到 N 的数字,这可以看作是C 语言中for循环的类似功能:

## STILL WRONG
for i in range(len(l)):
    print l[i]

Enter fullscreen mode Exit fullscreen mode

虽然这种方法更简洁,但还有更好的方法,因为 Python 允许我们直接遍历列表,类似于其他语言中的foreach循环:

# RIGHT
for v in l:
    print v

Enter fullscreen mode Exit fullscreen mode

按相反顺序遍历列表

如何反向遍历列表?一种方法是使用一个可读性较差的三参数range函数,并提供列表中最后一个元素的位置(第一个参数)、列表中第一个元素之前元素的位置(第二个参数)以及反向遍历的负步长(第三个参数):

# WRONG
for i in range(len(l) - 1, -1, -1):
    print l[i]

Enter fullscreen mode Exit fullscreen mode

但正如你可能已经猜到的,Python 应该提供了一种更好的方法。我们可以直接在for循环中使用反向函数

# RIGHT
for i in reversed(l):
    print i

Enter fullscreen mode Exit fullscreen mode

访问最后一个元素

访问列表中最后一个元素的常用方法是:获取列表的长度,减去 1,将结果数字作为最后一个元素的位置:

# WRONG
l = [1, 2, 3, 4, 5]
>>> l[len(l) - 1]
5

Enter fullscreen mode Exit fullscreen mode

在 Python 中这样做比较麻烦,因为 Python 支持使用负索引来访问列表末尾的元素。所以 -1 是最后一个元素:

# RIGHT
>>> l[-1]
5
Enter fullscreen mode Exit fullscreen mode

负索引也可以用来访问倒数第二个元素,依此类推:

# RIGHT
>>> l[-2]
4
>>> l[-3]
3
Enter fullscreen mode Exit fullscreen mode

使用序列解包

在其他编程语言中,从列表中提取值并将其赋给多个变量的常用方法是使用索引:

# WRONG
l1 = l[0]
l2 = l[1]
l3 = l[2]
Enter fullscreen mode Exit fullscreen mode

但是 Python 支持序列解包,允许我们将列表中的值提取到多个变量中:

# RIGHT
l1, l2, l3 = [1, 2, 3]

>>> l1
1
>>> l2
2
>>> l3
3
Enter fullscreen mode Exit fullscreen mode

使用列表阅读理解

假设我们要筛选出所有由 18 岁及以下用户发布的电影评分。

你写过多少次这样的代码:

# WRONG
under_18_grades = []
for grade in grades:
    if grade.age <= 18:
        under_18_grades.append(grade)

Enter fullscreen mode Exit fullscreen mode

在 Python 中不要再这样做了,请改用列表推导式和if语句。

# RIGHT
under_18_grades = [grade for grade in grades if grade.age <= 18]
Enter fullscreen mode Exit fullscreen mode

使用枚举函数

有时你需要遍历一个列表并跟踪每个元素的位置。例如,如果你需要在 shell 中显示菜单项,你可以简单地使用range函数:

# WRONG
for i in range(len(menu_items)):
    menu_items = menu_items[i]
    print "{}. {}".format(i, menu_items)
Enter fullscreen mode Exit fullscreen mode

更好的方法是使用enumerate函数。它是一个迭代器,返回一个对,每个对包含元素的位置和元素本身:

# RIGHT
for i, menu_items in enumerate(menu_items):
    print "{}. {}".format(i, menu_items)
Enter fullscreen mode Exit fullscreen mode

使用键进行排序

在其他编程语言中,对元素进行排序的典型方法是提供一个函数,该函数会比较两个对象以及要排序的集合。在 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)]
Enter fullscreen mode Exit fullscreen mode

但这并非最佳方法。因为我们只需要比较两个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)]
Enter fullscreen mode Exit fullscreen mode

使用所有/任意功能

如果要检查集合中的所有值或其中任何一个值是否为 True,一种方法是遍历列表:

# WRONG
def all_true(lst):
    for v in lst:
        if not v:
            return False
    return True
Enter fullscreen mode Exit fullscreen mode

但 Python 已经有了`all``any`函数来实现这个功能。`all` 函数会在传入的可迭代对象中的所有值都为 True 时返回 True,而` any` 函数会在传入的值中至少有一个为 True 时返回 True。

# RIGHT
all([True, False])
>> False

any([True, False])
>> True
Enter fullscreen mode Exit fullscreen mode

要检查所有项目是否都符合某个条件,可以使用列表推导式将任意对象列表转换为布尔值列表:

all([person.age > 18 for person in people])
Enter fullscreen mode Exit fullscreen mode

或者你可以传递一个生成器(只需省略列表推导式周围的方括号):

all(person.age > 18 for person in people)
Enter fullscreen mode Exit fullscreen mode

这不仅可以节省两次击键,还可以省略创建中间列表的步骤(稍后会详细介绍)。

使用切片

您可以使用称为切片的技术来获取列表的一部分。访问列表时,您不必只在方括号中提供单个索引,而是可以提供以下三个值。

lst[start:end:step]
Enter fullscreen mode Exit fullscreen mode

所有这些参数都是可选的,省略某些参数可以得到列表的不同部分。如果只提供起始位置,则会返回从指定索引开始的所有元素:

# RIGHT
>>> lst = range(10)
>>> lst[3:]
[3, 4, 5, 6, 7, 8, 9]
Enter fullscreen mode Exit fullscreen mode

如果只提供结束位置,切片操作将返回直到指定位置的所有元素:

>>> lst[:-3]
[0, 1, 2, 3, 4, 5, 6]
Enter fullscreen mode Exit fullscreen mode

您还可以获取两个索引之间的列表部分:

>>> lst[3:6]
[3, 4, 5]
Enter fullscreen mode Exit fullscreen mode

切片操作的默认步长为 1,这意味着返回起始位置和结束位置之间的所有元素。如果您只想获取每隔一个元素或每隔两个元素,则需要提供一个步长值:

>>> lst[2:8:2]
[2, 4, 6]
Enter fullscreen mode Exit fullscreen mode

不要创建不必要的对象

使用 xrange

range 函数在需要生成一定范围内一致的整数值时非常有用,但它有一个缺点:它返回的是一个包含所有生成值的列表:

# WRONG
# Returns a too big list
for i in range(1000000000):
    ...
Enter fullscreen mode Exit fullscreen mode

解决方法是使用xrange函数。它会直接返回一个迭代器,而不是创建一个列表:

# RIGHT
# Returns an iterator
for i in xrange(1000000000):
    ...
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

使用 izip

如果需要从两个集合中的元素生成键值对,一种方法是使用zip函数:

# WRONG
names = ['Joe', 'Kate', 'Peter']
ages = [30, 28, 41]
# Creates a list
zip(names, ages)

[('Joe', 30), ('Kate', 28), ('Peter', 41)]
Enter fullscreen mode Exit fullscreen mode

我们可以使用izip函数,它会返回一个迭代器,而不是创建一个新列表:

# RIGHT
from itertools import izip
# Creates an iterator
it = izip(names, ages)
Enter fullscreen mode Exit fullscreen mode

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]

Enter fullscreen mode Exit fullscreen mode

避免这种情况的方法是使用生成器而不是列表推导式。语法上的区别很小:你应该使用圆括号而不是方括号,但这种区别至关重要。以下示例不会创建任何中间列表:

# 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)

Enter fullscreen mode Exit fullscreen mode

如果您只需要处理结果集合的一部分来获得结果,例如查找符合特定条件的第一个元素,这将特别方便。

惯用词典

避免使用 keys() 函数

如果需要遍历字典中的键,你可能会倾向于使用哈希映射的keys函数:

# WRONG
for k in d.keys():
    print k

Enter fullscreen mode Exit fullscreen mode

但还有更好的方法,你可以使用迭代器遍历字典,它会遍历字典的键,所以你可以简单地这样做:

# RIGHT
for k in d:
    print k
Enter fullscreen mode Exit fullscreen mode

它不仅可以节省你的一些输入,还可以防止像keys方法那样创建字典中所有键的副本。

遍历键和值

如果使用keys方法,就可以轻松地遍历字典中的键和值,如下所示:


#WRONG
for k in d:
    v = d[k]
    print k, v

Enter fullscreen mode Exit fullscreen mode

但还有更好的方法。您可以使用items函数,该函数可以从字典中返回键值对:

# RIGHT
for k, v in d.items():
    print k, v

Enter fullscreen mode Exit fullscreen mode

这种方法不仅更简洁,而且效率更高。

使用字典进行理解

创建字典的一种方法就是逐个为其赋值:

# WRONG

d = {}
for person in people:
    d[person.name] = person


Enter fullscreen mode Exit fullscreen mode

你可以使用字典理解功能将其简化为一行句子:

# RIGHT
d = {person.name: person for person in people}
Enter fullscreen mode Exit fullscreen mode

使用集合模块

使用命名元组

如果你需要类似结构体的类型,你可以定义一个包含初始化方法和多个字段的类:

# WRONG
class Point(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
Enter fullscreen mode Exit fullscreen mode

然而,Python 库中的collections模块提供了一个namedtuple类型,可以将此操作简化为一行代码:

# RIGHT
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
Enter fullscreen mode Exit fullscreen mode

此外,namedtuple还实现了__str____repr____eq__方法:

>>> Point(1, 2)
Point(x=1, y=2)
>>> Point(1, 2) == Point(1, 2)
True
Enter fullscreen mode Exit fullscreen mode

使用默认字典

如果我们需要统计某个元素在集合中出现的次数,我们可以使用一种常见的方法:

# WRONG
d = {}
for v in lst:
    if v not in d:
        d[v] = 1
    else:
        d[v] += 1
Enter fullscreen mode Exit fullscreen mode

collections模块为此提供了一个非常方便的类,名为defaultdict。它的构造函数接受一个函数,该函数将用于计算不存在的键的值:

>>> d = defaultdict(lambda: 42)
>>> d['key']
42
Enter fullscreen mode Exit fullscreen mode

为了重写计数示例,我们可以将int函数传递给defaultdict,该函数在不带参数调用时返回零:

# RIGHT
from collections import defaultdict
d = defaultdict(int)
for v in lst:
    d[v] += 1
Enter fullscreen mode Exit fullscreen mode

当您需要对集合中的项目进行任何类型的分组,但只需要获取元素数量时,defaultdict非常有用;您也可以使用Counter类来代替:

# RIGHT
from collections import Counter

>>> counter = Counter(lst)
>>> counter
Counter({4: 3, 1: 2, 2: 1, 3: 1, 5: 1})
Enter fullscreen mode Exit fullscreen mode

本文最初发表于Brewing Codes博客。

文章来源:https://dev.to/mushketyk/python-data-structs-idioms-6ae