在Python 3中使用Python 2的字典比较
我正在尝试把一些代码从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 个回答
你可以用 .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
如果你只是想要在不同的平台上多次运行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之前,先对加载对象的文件进行排序,来达到同样的效果。
有没有办法在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使用了排序后的(键,值)列表的字典序比较,但这在比较相等的常见情况下代价很高。”所以,这告诉你这是一个合理的规则——只要这确实是你想要的规则,并且你不介意性能成本。
在CPython2.x中,逻辑有点复杂,因为它的行为是由dict.__cmp__
决定的。你可以在这里找到一个Python的实现。
不过,如果你真的想要一个可靠的排序方式,你需要用比id
更好的关键字来排序。你可以使用functools.cmp_to_key
把比较函数转换成关键字函数,但实际上,这样的排序并不好,因为它完全是随意的。
最好的办法是根据某个字段的值(或者多个字段)来对所有字典进行排序。operator.itemgetter
可以很好地用于这个目的。使用这个作为关键字函数,应该能在任何相对现代的Python实现和版本中给你一致的结果。
如果你想在 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
)。