在Python 3中使用Python 2的字典比较

4 投票
5 回答
6381 浏览
提问于 2025-04-19 22:02

我正在尝试把一些代码从Python 2移植到Python 3。这些代码看起来很复杂,但我希望Python 3的结果能和Python 2的结果尽量一致。我有一段类似这样的代码:

import json

# Read a list of json dictionaries by line from file.

objs = []
with open('data.txt') as fptr:
    for line in fptr:
        objs.append(json.loads(line))

# Give the dictionaries a reliable order.

objs = sorted(objs)

# Do something externally visible with each dictionary:

for obj in objs:
    do_stuff(obj)

当我把这段代码从Python 2移植到Python 3时,出现了一个错误:

TypeError: unorderable types: dict() < dict()

于是我把排序的那一行改成了这样:

objs = sorted(objs, key=id)

但是字典的顺序在Python 2和Python 3之间还是发生了变化。

有没有办法在Python 3中复制Python 2的比较逻辑?是不是因为之前使用的id在不同的Python版本之间不可靠?

5 个回答

0

你可以用 .items() 来进行比较

d1 = {"key1": "value1"}
d2 = {"key1": "value1", "key2": "value2"}
d1.items() <= d2.items()
True

不过这不是递归的

d1 = {"key1": "value1", "key2": {"key11": "value11"}}
d2 = {"key1": "value1", "key2": {"key11": "value11", "key12": "value12"}}
d1.items() <= d2.items()
False
0

如果你只是想要在不同的平台上多次运行Python时保持一个一致的顺序,但实际上对顺序本身并不太在意,那么一个简单的解决办法就是在排序之前把字典转成JSON格式:

import json

def sort_as_json(dicts):
    return sorted(dicts, key=lambda d: json.dumps(d, sort_keys=True))

print(list(sort_as_json([{'foo': 'bar'}, {1: 2}])))
# Prints [{1: 2}, {'foo': 'bar'}]

显然,这个方法只有在你的字典可以转换成JSON格式时才有效,不过因为你本来就是从JSON中加载它们,所以这应该不是问题。在你的情况下,你可以通过在反序列化JSON之前,先对加载对象的文件进行排序,来达到同样的效果。

0

有没有办法在Python 3中复制Python 2的比较逻辑?是不是因为之前用的id在不同的Python版本之间不可靠?

id这个东西从来就不是“可靠”的。你得到的每个对象的id都是一个完全随意的值;即使在同一台机器上、同一个Python版本中,它也可能在不同的运行中是不同的。

Python 2.x其实并没有说明它是通过id来排序的。它只说

除了相等以外的结果是始终一致的,但其他方面并没有定义。

这其实更能说明问题:排序的顺序是明确规定为随意的(除了在某次运行中保持一致)。这和在Python 3.x中用key=id排序得到的保证是一样的,不管它是否真的以相同的方式工作。*

所以在3.x中你做的事情是一样的。两个随意的顺序不同,只是说明随意就是随意。


如果你想要根据字典的内容得到某种可重复的排序,你需要先决定这个顺序是什么,然后再去构建它。例如,你可以先对项目进行排序,然后进行比较(递归地传递相同的键函数,以防项目是字典或包含字典)。**

而且,设计并实现了一种合理的、非随意的排序后,它在2.7和3.x中当然会以相同的方式工作。


* 注意,这对于身份比较并不等价,仅仅是对于排序比较。如果你只是用它来做sorted,那么这会导致你的排序不再稳定。但反正它就是随意的顺序,所以这并不重要。

** 注意,Python 2.x以前使用过类似的规则。从上面的脚注:“早期版本的Python使用了排序后的(键,值)列表的字典序比较,但这在比较相等的常见情况下代价很高。”所以,这告诉你这是一个合理的规则——只要这确实是你想要的规则,并且你不介意性能成本。

0

在CPython2.x中,逻辑有点复杂,因为它的行为是由dict.__cmp__决定的。你可以在这里找到一个Python的实现。

不过,如果你真的想要一个可靠的排序方式,你需要用比id更好的关键字来排序。你可以使用functools.cmp_to_key把比较函数转换成关键字函数,但实际上,这样的排序并不好,因为它完全是随意的。

最好的办法是根据某个字段的值(或者多个字段)来对所有字典进行排序。operator.itemgetter可以很好地用于这个目的。使用这个作为关键字函数,应该能在任何相对现代的Python实现和版本中给你一致的结果。

4

如果你想在 Python 2.7(它使用任意的排序方式)和 3.x(它不允许对字典进行排序)中实现和早期版本 Python 2.x 一样的行为,Ned Batchelder 对字典排序的回答可以帮你解决一部分问题,但并不是全部。


首先,它给你的是一个旧式的 cmp 函数,而不是新式的 key 函数。幸运的是,2.7 和 3.x 都有 functools.cmp_to_key 来解决这个问题。(当然,你也可以把代码重写成一个 key 函数,但这样可能会让你更难看出发布的代码和你的代码之间的差异……)


更重要的是,它在 2.7 和 3.x 中不仅不做同样的事情,甚至在 2.7 和 3.x 中都不工作。要理解为什么,看看这段代码:

def smallest_diff_key(A, B):
    """return the smallest key adiff in A such that A[adiff] != B[bdiff]"""
    diff_keys = [k for k in A if A.get(k) != B.get(k)]
    return min(diff_keys)

def dict_cmp(A, B):
    if len(A) != len(B):
        return cmp(len(A), len(B))
    adiff = smallest_diff_key(A, B)
    bdiff = smallest_diff_key(B, A)
    if adiff != bdiff:
        return cmp(adiff, bdiff)
    return cmp(A[adiff], b[bdiff])

注意它是在对不匹配的值调用 cmp

如果字典中可以包含其他字典,这就依赖于 cmp(d1, d2) 最终会调用这个函数……而在更新的 Python 中,这显然不成立。

此外,在 3.x 中,cmp 甚至已经不存在了。

而且,这还依赖于任何值都可以和任何其他值进行比较——你可能会得到任意的结果,但不会抛出异常。在 2.x 中这是真的(除了少数几种特殊情况),但在 3.x 中就不再成立了。如果你不想用不可比较的值比较字典(例如,如果 {1: 2} < {1: 'b'} 抛出异常是可以接受的),那这可能对你来说不是问题,但如果不是,那就会有问题。

当然,如果你不想要字典比较的任意结果,你真的想要值比较的任意结果吗?

解决这三个问题的方法很简单:你必须替换掉 cmp,而不是调用它。所以,像这样:

def mycmp(A, B):
    if isinstance(A, dict) and isinstance(B, dict):
        return dict_cmp(A, B)
    try:
        return A < B
    except TypeError:
        # what goes here depends on how far you want to go for consistency

如果你想要 2.7 使用的不同类型对象比较的确切规则,它们是有文档说明的,你可以实现它们。但如果你不需要那么详细,你可以在这里写一些更简单的东西(或者如果上述提到的异常是可以接受的,甚至可以不捕获 TypeError)。

撰写回答