如何在Python中对一个字典进行排序

155 阅读9分钟

词典最好用于键值查找:我们提供一个键,词典非常迅速地返回相应的值

但是如果你既需要键值查找又需要迭代呢? 在 dictionary 上进行循环是可能的,当循环时,我们可能关心 dictionary 中项目的顺序

考虑到字典中项目的顺序,你可能会想,我们怎样才能对字典进行排序

字典是有顺序的

从 Python 3.6 开始,字典是有顺序的(技术上来说,这种排序在 3.7 中成为正式规定)。

字典的键值是按插入顺序存储的,这意味着每当一个新的键值被添加进来,它就会被添加到最后面。

1
2
3
4
>>> color_amounts = {"purple": 6, "green": 3, "blue": 2}
>>> color_amounts["pink"] = 4
>>> color_amounts
{'purple': 6, 'green': 3, 'blue': 2, 'pink': 4}

但是如果我们更新一个键值对,键就会保持在之前的位置。

1
2
3
>>> color_amounts["green"] = 5
>>> color_amounts
{'purple': 6, 'green': 5, 'blue': 2, 'pink': 4}

所以如果你打算用一些特定的数据填充一个字典,然后让这个字典保持原样,你需要做的就是确保原始数据是按你喜欢的顺序排列。

例如,如果我们有一个美国各州缩写的 CSV 文件,而我们的文件是按州名的字母顺序排列的,那么我们的字典也会以同样的方式排列。

1
2
3
4
5
6
7
>>> import csv
>>> state_abbreviations = {}
>>> for name, abbreviation in csv.reader("state-abbreviations.csv")
...     state_abbreviations[name] = abbreviation
...
>>> state_abbreviations
{'Alabama': 'AL', 'Alaska': 'AK', 'Arizona': 'AZ', 'Arkansas': 'AR', 'California': 'CA', ...}

如果我们的输入数据已经正确排序,我们的 dictionary 最终也会正确排序。

如何按键对 dictionary 进行排序

如果我们的数据还没有被排序呢?

假设我们有一个字典,它将会议室映射到它们相应的房间号。

1
>>> rooms = {"Pink": "Rm 403", "Space": "Rm 201", "Quail": "Rm 500", "Lime": "Rm 503"}

而我们想按键对这个字典进行排序。

我们可以在我们的 dictionary 上使用items 方法来获得键值图元的迭代表,然后使用sorted 函数来对这些图元进行排序。

1
2
3
4
>>> rooms.items()
dict_items([('Pink', 'Rm 403'), ('Space', 'Rm 201'), ('Quail', 'Rm 500'), ('Lime', 'Rm 503')])
>>> sorted(rooms.items())
[('Lime', 'Rm 503'), ('Pink', 'Rm 403'), ('Quail', 'Rm 500'), ('Space', 'Rm 201')]

sorted 函数使用< 操作符来比较给定迭代器中的许多项,并返回一个排序的列表。sorted 函数总是返回一个列表。

为了使这些键值对变成一个字典,我们可以直接把它们传递给dict 构造函数。

1
2
3
>>> sorted_rooms = dict(sorted(rooms.items()))
>>> sorted_rooms
{'Lime': 'Rm 503', 'Pink': 'Rm 403', 'Quail': 'Rm 500', 'Space': 'Rm 201'}

dict 构造函数将接受一个 2 项元组的列表(或任何 2 项元组的迭代器),并从中制作一个字典,用每个元组的第一项作为键,第二项作为相应的值。

键值对是按词典排序的......什么?

我们在把键值对做成字典之前对图元进行排序。 但是图元的排序是如何进行的呢?

1
2
3
>>> some_tuples = [(1, 3), (3, 1), (1, 9), (0, 3)]
>>> sorted(some_tuples)
[(0, 3), (1, 3), (1, 9), (3, 1)]

当对图元进行排序时,Python 使用了词法排序 (听起来比实际情况更复杂)。 比较一个 2 项图元基本上可以归结为这种算法。

1
2
3
4
5
6
def compare_two_item_tuples(a, b):
    """This is the same as a < b for two 2-item tuples."""
    if a[0] != b[0]:  # If the first item of each tuple is unequal
        return a[0] < b[0]  # Compare the first item from each tuple
    else:
        return a[1] < b[1]  # Compare the second item from each tuple

我已经写了一篇关于元组排序的文章,更详细地解释了这一点。

你可能会想:这似乎不仅仅是按键排序,而是按键值排序。 你是对的!但只是排序。

字典中的键应该总是被比较为不相等的(如果两个键相等,它们就被看作是同一个键)。所以只要键可以用小于运算符(<)相互比较,对键-值对的2项图元的排序应该总是按键排序。

字典不能被就地排序

如果我们已经把我们的项目放在一个字典,并且我们想对这个字典进行排序,该怎么办? 与列表不同,字典上没有sort 方法

我们不能对 dictionary 进行就地排序,但是我们可以从 dictionary 中获取项目,用我们之前使用的相同技术对这些项目进行排序,然后把这些项目变成一个新的 dictionary。

1
2
3
4
>>> rooms = {"Pink": "Rm 403", "Space": "Rm 201", "Quail": "Rm 500", "Lime": "Rm 503"}
>>> sorted_rooms = dict(sorted(rooms.items()))
>>> sorted_rooms
{'Lime': 'Rm 503', 'Pink': 'Rm 403', 'Quail': 'Rm 500', 'Space': 'Rm 201'}

如果我们真的想更新我们原来的 dictionary 对象,我们可以从 dictionary 中取出项目,对它们进行排序,清除 dictionary 中的所有项目,然后再把所有项目加回 dictionary 中。

1
2
3
4
>>> old_dictionary = {"Pink": "Rm 403", "Space": "Rm 201", "Quail": "Rm 500", "Lime": "Rm 503"}
>>> sorted_items = sorted(old_dictionary.items())
>>> old_dictionary.clear()
>>> old_dictionary.update(sorted_items)

我们通常不想在 Python 中对数据结构进行原地操作:我们倾向于制作一个新的数据结构,而不是重新使用一个旧的数据结构 (这种倾向部分归功于变量在 Python 中的工作方式)。

如何按值对一个字典进行排序

如果我们想通过它的值而不是它的键对 dictionary 进行排序呢?

我们可以建立一个新的值-键图元列表 (在我们下面的例子中实际上是一个生成器),对其进行排序,然后将其翻转为键-值图元并重新创建我们的 dictionary。

1
2
3
4
5
6
7
8
>>> rooms = {"Pink": "Rm 403", "Space": "Rm 201", "Quail": "Rm 500", "Lime": "Rm 503"}
>>> room_to_name = sorted((room, name) for (name, room) in rooms.items())
>>> sorted_rooms = {
...     name: room
...     for room, name in room_to_name
... }
>>> sorted_rooms
{'Space': 'Rm 201', 'Pink': 'Rm 403', 'Quail': 'Rm 500', 'Lime': 'Rm 503'}

这个方法很有效,但是有点长,而且这个技术实际上是对我们的值和键都进行了排序(在排序中,值优先)。

如果我们想按我们的字典的值排序,而完全忽略键的内容呢?Python 的sorted 函数接受一个key 参数,我们可以用它来做这件事。

1
2
3
4
5
6
7
8
>>> help(sorted)
Help on built-in function sorted in module builtins:

sorted(iterable, /, *, key=None, reverse=False)
    Return a new list containing all items from the iterable in ascending order.

    A custom key function can be supplied to customize the sort order, and the
    reverse flag can be set to request the result in descending order.

我们传递给 sorted 的 key 函数应该接受我们要排序的 iterable 中的一个项目,并返回要排序的key。 注意这里的 "key "这个词与 dictionary key 没有关系。 Dictionary key 用于查找 dictionary 的值,而这个 key 函数返回一个对象,决定如何对 iterable 中的项目排序。

如果我们想按字典的值来排序,我们可以做一个 key 函数,接受我们的 2 项图元列表中的每一项,只返回值

1
2
3
4
def value_from_item(item):
    """Return just the value from a given (key, value) tuple."""
    key, value = item
    return value

然后我们使用我们的 key 函数,把它传给sorted 函数 (在 Python 中函数可以传给其它函数),并把结果传给dict 来创建一个新的 dictionary。

1
2
3
>>> sorted_rooms = dict(sorted(rooms.items(), key=value_from_item))
>>> sorted_rooms
{'Space': 'Rm 201', 'Pink': 'Rm 403', 'Quail': 'Rm 500', 'Lime': 'Rm 503'}

如果你不想为了使用一次而创建一个自定义的 key 函数,你可以使用 lambda 函数 (我通常不推荐这样做)。

1
2
3
>>> sorted_rooms = dict(sorted(rooms.items(), key=lambda item: item[1]))
>>> sorted_rooms
{'Space': 'Rm 201', 'Pink': 'Rm 403', 'Quail': 'Rm 500', 'Lime': 'Rm 503'}

或者你可以使用operator.itemgetter 来制作一个 key 函数,从每个 key-value 元组中获取第二项。

1
2
3
4
>>> from operator import itemgetter
>>> sorted_rooms = dict(sorted(rooms.items(), key=itemgetter(1)))
>>> sorted_rooms
{'Space': 'Rm 201', 'Pink': 'Rm 403', 'Quail': 'Rm 500', 'Lime': 'Rm 503'}

在关于 lambda 函数的文章中讨论了我对itemgetter 的偏爱。

以其他方式对 dictionary 进行排序

如果我们需要按键或值以外的东西对我们的 dictionary 进行排序呢? 例如,如果我们的房间号字符串包括不总是相同长度的数字,怎么办?

1
2
3
4
5
6
7
8
rooms = {
    "Pink": "Rm 403",
    "Space": "Rm 201",
    "Quail": "Rm 500",
    "Lime": "Rm 503",
    "Ocean": "Rm 2000",
    "Big": "Rm 30",
}

如果我们按值对这些房间进行排序,这些字符串就不会按我们希望的数字方式进行排序。

1
2
3
4
>>> from operator import itemgetter
>>> sorted_rooms = dict(sorted(rooms.items(), key=itemgetter(1)))
>>> sorted_rooms
{'Ocean': 'Rm 2000', 'Space': 'Rm 201', 'Big': 'Rm 30', 'Pink': 'Rm 403', 'Quail': 'Rm 500', 'Lime': 'Rm 503'}

但是,我们在对字符串进行排序时,是根据每个字符的unicode值逐个排序的(我在关于元组排序的文章中指出了这一点)。

我们可以定制我们正在使用的key 函数来代替数字排序。

1
2
3
4
5
def by_room_number(item):
    """Return numerical room given a (name, room_number) tuple."""
    name, room = item
    _, number = room.split()
    return int(number)

当我们使用这个键函数对我们的字典进行排序时。

1
>>> sorted_rooms = dict(sorted(rooms.items(), key=by_room_number))

它将按整数房号排序,正如预期的那样。

1
2
>>> sorted_rooms
{'Big': 'Rm 30', 'Space': 'Rm 201', 'Pink': 'Rm 403', 'Quail': 'Rm 500', 'Lime': 'Rm 503', 'Ocean': 'Rm 2000'}

你应该对一个字典进行排序吗?

当你要对一个 dictionary 进行排序时,首先要问自己 "我需要这样做吗"? 事实上,当你考虑在一个 dictionary 上进行循环时,你可能会问 "我真的需要一个 dictionary 在这里吗"?

字典用于键值查找:你可以快速获得一个给定的键值。 它们在检索键值方面非常快。 但是字典比图元列表占用更多的空间。

如果你可以在你的代码中使用图元的列表(因为你实际上不需要键值查找),你可能应该使用图元的列表而不是字典。

但是如果键值查找是你所需要的,你不太可能也需要在你的 dictionary 上循环。

现在当然有可能,现在你确实有一个很好的使用案例来对 dictionary 进行排序 (例如,也许你正在对属性 dictionary 中的键进行排序),但是请记住,你很少需要对 dictionary 进行排序。

总结

字典是用来根据一个键快速查找一个值的。 字典中项目的顺序很少重要。

如果你关心你的 dictionary 项目的顺序,请记住 dictionary 是按照它们的键的插入顺序排序的 (从 Python 3.6 开始)。 所以你的 dictionary 中的键将保持它们被添加到 dictionary 的顺序。

如果你想按键来排序,你可以使用内置的sorted 函数和dict 构造函数。

1
>>> sorted_dictionary = dict(sorted(old_dictionary.items()))

如果你想按值对 dictionary 进行排序,你可以给key 传递一个自定义的函数 (一个返回每项值的函数) 到sorted

1
2
3
4
5
>>> def value_from_item(item):
...     key, value = item
...     return value
...
>>> sorted_dictionary = dict(sorted(old_dictionary.items(), key=value_from_item))

但是请记住,我们并不经常关心 dictionary 的顺序。 每当你对 dictionary 进行排序时,请记住问自己我真的需要对这个数据结构进行排序吗,在这里一个图元的列表比 dictionary 更合适吗?